Skip to content

Commit 4d1fbc8

Browse files
authored
Merge pull request #4722 from RobiladK/patch-2
Update azure-functions-samples.md
2 parents 4aed65b + b9b2bfb commit 4d1fbc8

File tree

2 files changed

+244
-2
lines changed

2 files changed

+244
-2
lines changed

articles/ai-services/agents/how-to/tools/azure-functions-samples.md

Lines changed: 241 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,4 +299,244 @@ In the sample below we create a client and an agent that has the AI tools defini
299299
For any issues with the TypeScript code, create an issue on the [sample code repository](https://github.com/Azure-Samples/azure-functions-ai-services-agent-javascript/issues)
300300

301301

302-
::: zone-end
302+
::: zone-end
303+
304+
::: zone pivot="csharp"
305+
306+
## Using the .NET SDK
307+
308+
### Prerequisites
309+
To make a function call, we need to create and deploy the Azure function. In the code snippet, we have an example of function on C# which can be used by the earlier code.
310+
311+
```csharp
312+
namespace FunctionProj
313+
{
314+
public class Response
315+
{
316+
public required string Value { get; set; }
317+
public required string CorrelationId { get; set; }
318+
}
319+
320+
public class Arguments
321+
{
322+
public required string OutputQueueUri { get; set; }
323+
public required string CorrelationId { get; set; }
324+
}
325+
326+
public class Foo
327+
{
328+
private readonly ILogger<Foo> _logger;
329+
330+
public Foo(ILogger<Foo> logger)
331+
{
332+
_logger = logger;
333+
}
334+
335+
[Function("Foo")]
336+
public void Run([QueueTrigger("azure-function-foo-input")] Arguments input, FunctionContext executionContext)
337+
{
338+
var logger = executionContext.GetLogger("Foo");
339+
logger.LogInformation("C# Queue function processed a request.");
340+
341+
// We have to provide the Managed identity for function resource
342+
// and allow this identity a Queue Data Contributor role on the storage account.
343+
var cred = new DefaultAzureCredential();
344+
var queueClient = new QueueClient(new Uri(input.OutputQueueUri), cred,
345+
new QueueClientOptions { MessageEncoding = QueueMessageEncoding.Base64 });
346+
347+
var response = new Response
348+
{
349+
Value = "Bar",
350+
// Important! Correlation ID must match the input correlation ID.
351+
CorrelationId = input.CorrelationId
352+
};
353+
354+
var jsonResponse = JsonSerializer.Serialize(response);
355+
queueClient.SendMessage(jsonResponse);
356+
}
357+
}
358+
}
359+
```
360+
361+
In this code we define function input and output class: `Arguments` and `Response` respectively. These two data classes are serialized in JSON. It's important that these both contain the `CorrelationId`, which is the same between input and output.
362+
363+
In our example the function is stored in the storage account, created with the AI hub. For that we need to allow key access to that storage. In the Azure portal, go to Storage account > Settings > Configuration and set "Allow storage account key access" to Enabled. If it isn't done, the error that is displayed is "The remote server returned an error: (403) Forbidden." To create the function resource that will host our function, install azure-cli python package and run the next command:
364+
365+
```shell
366+
pip install -U azure-cli
367+
az login
368+
az functionapp create --resource-group your-resource-group --consumption-plan-location region --runtime dotnet-isolated --functions-version 4 --name function_name --storage-account storage_account_already_present_in_resource_group --app-insights existing_or_new_application_insights_name
369+
```
370+
371+
This function writes data to the output queue and hence needs to be authenticated to Azure, so we'll need to assign the function system identity and provide it `Storage Queue Data Contributor`. To do that in Azure portal, select the function, located in `your-resource-group` resource group and in Settings > Identity, switch it on and select Save. After that assign the `Storage Queue Data Contributor` permission on storage account used by our function (`storage_account_already_present_in_resource_group` in the script above) for the assigned system managed identity.
372+
373+
Now we'll create the function itself. Install [.NET](https://dotnet.microsoft.com/download) and [Core Tools](https://go.microsoft.com/fwlink/?linkid=2174087) and create the function project using next commands.
374+
375+
```shell
376+
func init FunctionProj --worker-runtime dotnet-isolated --target-framework net8.0
377+
cd FunctionProj
378+
func new --name foo --template "HTTP trigger" --authlevel "anonymous"
379+
dotnet add package Azure.Identity
380+
dotnet add package Microsoft.Azure.Functions.Worker.Extensions.Storage.Queues --prerelease
381+
```
382+
383+
> [!NOTE]
384+
> There's an "Azure Queue Storage trigger," however the attempt to use it results in error for now.
385+
We have created a project, containing HTTP-triggered Azure function with the logic in `Foo.cs` file. As far as we need to trigger Azure function by a new message in the queue, we replace the content of a Foo.cs by the C# sample code above.
386+
To deploy the function, run the command from dotnet project folder:
387+
388+
```shell
389+
func azure functionapp publish function_name
390+
```
391+
392+
In the `storage_account_already_present_in_resource_group` select the `Queue service` and create two queues: `azure-function-foo-input` and `azure-function-tool-output`. The same queues are used in our sample. To check that the function is working, place the next message into the `azure-function-foo-input` and replace `storage_account_already_present_in_resource_group` by the actual resource group name, or just copy the output queue address.
393+
394+
```json
395+
{
396+
"OutputQueueUri": "https://storage_account_already_present_in_resource_group.queue.core.windows.net/azure-function-tool-output",
397+
"CorrelationId": "42"
398+
}
399+
```
400+
401+
Next, we monitor the output queue or the message. You should receive the next message.
402+
403+
```json
404+
{
405+
"Value": "Bar",
406+
"CorrelationId": "42"
407+
}
408+
```
409+
The input `CorrelationId` is the same as output.
410+
411+
> [!TIP]
412+
> Place multiple messages to input queue and keep second internet browser window with the output queue open and hit the refresh button on the portal user interface, so that you won't miss the message. If the message instead went to `azure-function-foo-input-poison` queue, the function completed with error, check your setup.
413+
After testing the function and made sure it works, make sure that the Azure AI Project has the following roles for the storage account: `Storage Account Contributor`, `Storage Blob Data Contributor`, `Storage File Data Privileged Contributor`, `Storage Queue Data Contributor` and `Storage Table Data Contributor`. Now the function is ready to be used by the agent.
414+
415+
In the example below we're calling function "foo," which responds "Bar."
416+
417+
### .NET Sample Code
418+
419+
1. First, we set up the necessary configuration, initialize the `PersistentAgentsClient`, define the `AzureFunctionToolDefinition` for our Azure Function, and then create the agent. This step includes all necessary `using` directives.
420+
421+
```csharp
422+
using Azure;
423+
using Azure.AI.Agents.Persistent;
424+
using Azure.Identity;
425+
using Microsoft.Extensions.Configuration;
426+
using System.Text.Json;
427+
428+
IConfigurationRoot configuration = new ConfigurationBuilder()
429+
.SetBasePath(AppContext.BaseDirectory)
430+
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
431+
.Build();
432+
433+
var projectEndpoint = configuration["ProjectEndpoint"];
434+
var modelDeploymentName = configuration["ModelDeploymentName"];
435+
var storageQueueUri = configuration["StorageQueueURI"];
436+
PersistentAgentsClient client = new(projectEndpoint, new DefaultAzureCredential());
437+
438+
AzureFunctionToolDefinition azureFnTool = new(
439+
name: "foo",
440+
description: "Get answers from the foo bot.",
441+
inputBinding: new AzureFunctionBinding(
442+
new AzureFunctionStorageQueue(
443+
queueName: "azure-function-foo-input",
444+
storageServiceEndpoint: storageQueueUri
445+
)
446+
),
447+
outputBinding: new AzureFunctionBinding(
448+
new AzureFunctionStorageQueue(
449+
queueName: "azure-function-tool-output",
450+
storageServiceEndpoint: storageQueueUri
451+
)
452+
),
453+
parameters: BinaryData.FromObjectAsJson(
454+
new
455+
{
456+
Type = "object",
457+
Properties = new
458+
{
459+
query = new
460+
{
461+
Type = "string",
462+
Description = "The question to ask.",
463+
},
464+
outputqueueuri = new
465+
{
466+
Type = "string",
467+
Description = "The full output queue uri."
468+
}
469+
},
470+
},
471+
new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }
472+
)
473+
);
474+
475+
PersistentAgent agent = client.Administration.CreateAgent(
476+
model: modelDeploymentName,
477+
name: "azure-function-agent-foo",
478+
instructions: "You are a helpful support agent. Use the provided function any "
479+
+ "time the prompt contains the string 'What would foo say?'. When you invoke "
480+
+ "the function, ALWAYS specify the output queue uri parameter as "
481+
+ $"'{storageQueueUri}/azure-function-tool-output'. Always responds with "
482+
+ "\"Foo says\" and then the response from the tool.",
483+
tools: [azureFnTool]
484+
);
485+
```
486+
487+
2. Next, we create a new persistent agent thread and add an initial user message to it.
488+
489+
```csharp
490+
PersistentAgentThread thread = client.Threads.CreateThread();
491+
492+
client.Messages.CreateMessage(
493+
thread.Id,
494+
MessageRole.User,
495+
"What is the most prevalent element in the universe? What would foo say?");
496+
```
497+
498+
4. Then, we create a run for the agent on the thread and poll its status until it completes or requires action.
499+
500+
```csharp
501+
ThreadRun run = client.Runs.CreateRun(thread.Id, agent.Id);
502+
503+
do
504+
{
505+
Thread.Sleep(TimeSpan.FromMilliseconds(500));
506+
run = client.Runs.GetRun(thread.Id, run.Id);
507+
}
508+
while (run.Status == RunStatus.Queued
509+
|| run.Status == RunStatus.InProgress
510+
|| run.Status == RunStatus.RequiresAction);
511+
```
512+
513+
5. After the run is complete, we retrieve and process the messages from the thread.
514+
515+
```csharp
516+
Pageable<ThreadMessage> messages = client.Messages.GetMessages(
517+
threadId: thread.Id,
518+
order: ListSortOrder.Ascending
519+
);
520+
521+
foreach (ThreadMessage threadMessage in messages)
522+
{
523+
foreach (MessageContent content in threadMessage.ContentItems)
524+
{
525+
switch (content)
526+
{
527+
case MessageTextContent textItem:
528+
Console.WriteLine($"[{threadMessage.Role}]: {textItem.Text}");
529+
break;
530+
}
531+
}
532+
}
533+
```
534+
535+
6. Finally, we clean up the created resources by deleting the thread and the agent.
536+
537+
```csharp
538+
client.Threads.DeleteThread(thread.Id);
539+
client.Administration.DeleteAgent(agent.Id);
540+
```
541+
542+
::: zone-end

zone-pivots/zone-pivot-groups.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1153,6 +1153,8 @@ groups:
11531153
title: Selections
11541154
prompt: What would you like to see?
11551155
pivots:
1156+
- id: csharp
1157+
title: C#
11561158
- id: python
11571159
title: Python
11581160
- id: typescript
@@ -1228,4 +1230,4 @@ groups:
12281230
- id: azure-portal
12291231
title: Azure portal
12301232
- id: cli
1231-
title: Azure CLI
1233+
title: Azure CLI

0 commit comments

Comments
 (0)