Skip to content

Commit 471dfe7

Browse files
authored
Update azure-functions-samples.md
Porting the Azure Functions sdk sample for c#.
1 parent 9315c8b commit 471dfe7

File tree

1 file changed

+320
-1
lines changed

1 file changed

+320
-1
lines changed

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

Lines changed: 320 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,4 +299,323 @@ 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+
# Sample for using Azure Functions with agents in Azure.AI.Agents
307+
308+
## Prerequisites
309+
To make a function call we need to create and deploy the Azure function. In the code snippet below, we have an example of function on C# which can be used by the code above.
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 will be serialized in JSON. It is important that these both contain field `CorrelationId`, which is the same between input and output.
362+
363+
In our example the function will be stored in the storage account, created with the AI hub. For that we need to allow key access to that storage. In Azure portal go to Storage account > Settings > Configuration and set "Allow storage account key access" to Enabled. If it is not done, the error will be displayed "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 will 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 click 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 just assigned System Managed identity.
372+
373+
Now we will 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+
func init FunctionProj --worker-runtime dotnet-isolated --target-framework net8.0
376+
cd FunctionProj
377+
func new --name foo --template "HTTP trigger" --authlevel "anonymous"
378+
dotnet add package Azure.Identity
379+
dotnet add package Microsoft.Azure.Functions.Worker.Extensions.Storage.Queues --prerelease
380+
```
381+
382+
**Note:** There is a "Azure Queue Storage trigger", however the attempt to use it results in error for now.
383+
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 will replace the content of a Foo.cs by the C# sample code above.
384+
To deploy the function run the command from dotnet project folder:
385+
386+
```
387+
func azure functionapp publish function_name
388+
```
389+
390+
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`. Note that 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.
391+
```json
392+
{
393+
"OutputQueueUri": "https://storage_account_already_present_in_resource_group.queue.core.windows.net/azure-function-tool-output",
394+
"CorrelationId": "42"
395+
}
396+
```
397+
398+
Next, we will monitor the output queue or the message. You should receive the next message.
399+
```json
400+
{
401+
"Value": "Bar",
402+
"CorrelationId": "42"
403+
}
404+
```
405+
Please note that the input `CorrelationId` is the same as output.
406+
*Hint:* 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 will not miss the message. If the message instead went to `azure-function-foo-input-poison` queue, the function completed with error, please check your setup.
407+
After we have tested the function and made sure it works, please make sure that the Azure AI Project have the next 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.
408+
409+
In the example below we are calling function "foo", which responds "Bar".
410+
411+
## Azure.AI.Agents Sample Code
412+
413+
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.
414+
415+
Common setup:
416+
417+
```csharp
418+
using Azure;
419+
using Azure.AI.Agents.Persistent;
420+
using Azure.Identity;
421+
using Microsoft.Extensions.Configuration;
422+
using System.Text.Json;
423+
424+
IConfigurationRoot configuration = new ConfigurationBuilder()
425+
.SetBasePath(AppContext.BaseDirectory)
426+
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
427+
.Build();
428+
429+
var projectEndpoint = configuration["ProjectEndpoint"];
430+
var modelDeploymentName = configuration["ModelDeploymentName"];
431+
var storageQueueUri = configuration["StorageQueueURI"];
432+
PersistentAgentsClient client = new(projectEndpoint, new DefaultAzureCredential());
433+
434+
AzureFunctionToolDefinition azureFnTool = new(
435+
name: "foo",
436+
description: "Get answers from the foo bot.",
437+
inputBinding: new AzureFunctionBinding(
438+
new AzureFunctionStorageQueue(
439+
queueName: "azure-function-foo-input",
440+
storageServiceEndpoint: storageQueueUri
441+
)
442+
),
443+
outputBinding: new AzureFunctionBinding(
444+
new AzureFunctionStorageQueue(
445+
queueName: "azure-function-tool-output",
446+
storageServiceEndpoint: storageQueueUri
447+
)
448+
),
449+
parameters: BinaryData.FromObjectAsJson(
450+
new
451+
{
452+
Type = "object",
453+
Properties = new
454+
{
455+
query = new
456+
{
457+
Type = "string",
458+
Description = "The question to ask.",
459+
},
460+
outputqueueuri = new
461+
{
462+
Type = "string",
463+
Description = "The full output queue uri."
464+
}
465+
},
466+
},
467+
new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }
468+
)
469+
);
470+
```
471+
472+
Synchronous sample:
473+
474+
```csharp
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+
Asynchronous sample:
488+
489+
```csharp
490+
PersistentAgent agent = await client.Administration.CreateAgentAsync(
491+
model: modelDeploymentName,
492+
name: "azure-function-agent-foo",
493+
instructions: "You are a helpful support agent. Use the provided function any "
494+
+ "time the prompt contains the string 'What would foo say?'. When you invoke "
495+
+ "the function, ALWAYS specify the output queue uri parameter as "
496+
+ $"'{storageQueueUri}/azure-function-tool-output'. Always responds with "
497+
+ "\"Foo says\" and then the response from the tool.",
498+
tools: [azureFnTool]
499+
);
500+
```
501+
502+
2. Next, we create a new persistent agent thread and add an initial user message to it.
503+
504+
Synchronous sample:
505+
506+
```csharp
507+
PersistentAgentThread thread = client.Threads.CreateThread();
508+
509+
client.Messages.CreateMessage(
510+
thread.Id,
511+
MessageRole.User,
512+
"What is the most prevalent element in the universe? What would foo say?");
513+
```
514+
515+
Asynchronous sample:
516+
517+
```csharp
518+
PersistentAgentThread thread = await client.Threads.CreateThreadAsync();
519+
520+
await client.Messages.CreateMessageAsync(
521+
thread.Id,
522+
MessageRole.User,
523+
"What is the most prevalent element in the universe? What would foo say?");
524+
```
525+
526+
3. Then, we create a run for the agent on the thread and poll its status until it completes or requires action.
527+
528+
Synchronous sample:
529+
530+
```csharp
531+
ThreadRun run = client.Runs.CreateRun(thread.Id, agent.Id);
532+
533+
do
534+
{
535+
Thread.Sleep(TimeSpan.FromMilliseconds(500));
536+
run = client.Runs.GetRun(thread.Id, run.Id);
537+
}
538+
while (run.Status == RunStatus.Queued
539+
|| run.Status == RunStatus.InProgress
540+
|| run.Status == RunStatus.RequiresAction);
541+
```
542+
543+
Asynchronous sample:
544+
545+
```csharp
546+
ThreadRun run = await client.Runs.CreateRunAsync(thread.Id, agent.Id);
547+
548+
do
549+
{
550+
await Task.Delay(TimeSpan.FromMilliseconds(500));
551+
run = await client.Runs.GetRunAsync(thread.Id, run.Id);
552+
}
553+
while (run.Status == RunStatus.Queued
554+
|| run.Status == RunStatus.InProgress
555+
|| run.Status == RunStatus.RequiresAction);
556+
```
557+
558+
4. After the run is complete, we retrieve and process the messages from the thread.
559+
560+
Synchronous sample:
561+
562+
```csharp
563+
Pageable<ThreadMessage> messages = client.Messages.GetMessages(
564+
threadId: thread.Id,
565+
order: ListSortOrder.Ascending
566+
);
567+
568+
foreach (ThreadMessage threadMessage in messages)
569+
{
570+
foreach (MessageContent content in threadMessage.ContentItems)
571+
{
572+
switch (content)
573+
{
574+
case MessageTextContent textItem:
575+
Console.WriteLine($"[{threadMessage.Role}]: {textItem.Text}");
576+
break;
577+
}
578+
}
579+
}
580+
```
581+
582+
Asynchronous sample:
583+
584+
```csharp
585+
AsyncPageable<ThreadMessage> messages = client.Messages.GetMessagesAsync(
586+
threadId: thread.Id,
587+
order: ListSortOrder.Ascending
588+
);
589+
590+
await foreach (ThreadMessage threadMessage in messages)
591+
{
592+
foreach (MessageContent content in threadMessage.ContentItems)
593+
{
594+
switch (content)
595+
{
596+
case MessageTextContent textItem:
597+
Console.WriteLine($"[{threadMessage.Role}]: {textItem.Text}");
598+
break;
599+
}
600+
}
601+
}
602+
```
603+
604+
5. Finally, we clean up the created resources by deleting the thread and the agent.
605+
606+
Synchronous sample:
607+
608+
```csharp
609+
client.Threads.DeleteThread(thread.Id);
610+
client.Administration.DeleteAgent(agent.Id);
611+
```
612+
613+
Asynchronous sample:
614+
615+
```csharp
616+
await client.Threads.DeleteThreadAsync(thread.Id);
617+
await client.Administration.DeleteAgentAsync(agent.Id);
618+
```
619+
620+
::: zone-end
621+

0 commit comments

Comments
 (0)