|
| 1 | +# Sample of Azure.AI.Agents using Azure Functions |
| 2 | + |
| 3 | +# Prerequisites |
| 4 | +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. |
| 5 | + |
| 6 | +```C# |
| 7 | +namespace FunctionProj |
| 8 | +{ |
| 9 | + public class Response |
| 10 | + { |
| 11 | + public required string Value { get; set; } |
| 12 | + public required string CorrelationId { get; set; } |
| 13 | + } |
| 14 | + |
| 15 | + public class Arguments |
| 16 | + { |
| 17 | + public required string OutputQueueUri { get; set; } |
| 18 | + public required string CorrelationId { get; set; } |
| 19 | + } |
| 20 | + |
| 21 | + public class Foo |
| 22 | + { |
| 23 | + private readonly ILogger<Foo> _logger; |
| 24 | + |
| 25 | + public Foo(ILogger<Foo> logger) |
| 26 | + { |
| 27 | + _logger = logger; |
| 28 | + } |
| 29 | + |
| 30 | + [Function("Foo")] |
| 31 | + public void Run([QueueTrigger("azure-function-foo-input")] Arguments input, FunctionContext executionContext) |
| 32 | + { |
| 33 | + var logger = executionContext.GetLogger("Foo"); |
| 34 | + logger.LogInformation("C# Queue function processed a request."); |
| 35 | + |
| 36 | + // We have to provide the Managed identity for function resource |
| 37 | + // and allow this identity a Queue Data Contributor role on the storage account. |
| 38 | + var cred = new DefaultAzureCredential(); |
| 39 | + var queueClient = new QueueClient(new Uri(input.OutputQueueUri), cred, |
| 40 | + new QueueClientOptions { MessageEncoding = QueueMessageEncoding.Base64 }); |
| 41 | + |
| 42 | + var response = new Response |
| 43 | + { |
| 44 | + Value = "Bar", |
| 45 | + // Important! Correlation ID must match the input correlation ID. |
| 46 | + CorrelationId = input.CorrelationId |
| 47 | + }; |
| 48 | + |
| 49 | + var jsonResponse = JsonSerializer.Serialize(response); |
| 50 | + queueClient.SendMessage(jsonResponse); |
| 51 | + } |
| 52 | + } |
| 53 | +} |
| 54 | +``` |
| 55 | + |
| 56 | +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. |
| 57 | + |
| 58 | +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: |
| 59 | + |
| 60 | +```shell |
| 61 | +pip install -U azure-cli |
| 62 | +az login |
| 63 | +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 |
| 64 | +``` |
| 65 | + |
| 66 | +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. |
| 67 | + |
| 68 | +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. |
| 69 | +``` |
| 70 | +func init FunctionProj --worker-runtime dotnet-isolated --target-framework net8.0 |
| 71 | +cd FunctionProj |
| 72 | +func new --name foo --template "HTTP trigger" --authlevel "anonymous" |
| 73 | +dotnet add package Azure.Identity |
| 74 | +dotnet add package Microsoft.Azure.Functions.Worker.Extensions.Storage.Queues --prerelease |
| 75 | +``` |
| 76 | + |
| 77 | +**Note:** There is a "Azure Queue Storage trigger", however the attempt to use it results in error for now. |
| 78 | +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. |
| 79 | +To deploy the function run the command from dotnet project folder: |
| 80 | + |
| 81 | +``` |
| 82 | +func azure functionapp publish function_name |
| 83 | +``` |
| 84 | + |
| 85 | +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. |
| 86 | +```json |
| 87 | +{ |
| 88 | + "OutputQueueUri": "https://storage_account_already_present_in_resource_group.queue.core.windows.net/azure-function-tool-output", |
| 89 | + "CorrelationId": "42" |
| 90 | +} |
| 91 | +``` |
| 92 | + |
| 93 | +Next, we will monitor the output queue or the message. You should receive the next message. |
| 94 | +```json |
| 95 | +{ |
| 96 | + "Value": "Bar", |
| 97 | + "CorrelationId": "42" |
| 98 | +} |
| 99 | +``` |
| 100 | +Please note that the input `CorrelationId` is the same as output. |
| 101 | +*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. |
| 102 | +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. |
| 103 | + |
| 104 | +In the example below we are calling function "foo", which responds "Bar". |
| 105 | +1. We create `AzureFunctionToolDefinition` object, with the function name, description, input and output queues, followed by function parameters. Plus we need to read in environment variables to get necessary parameters. |
| 106 | +```C# Snippet:AgentsAzureFunctionsDefineFunctionTools |
| 107 | +var projectEndpoint = new Uri(configuration["ProjectEndpoint"]); |
| 108 | +var modelDeploymentName = configuration["ModelDeploymentName"]; |
| 109 | +var storageQueueUri = configuration["StorageQueueURI"]; |
| 110 | + |
| 111 | +PersistentAgentsClient client = new(projectEndpoint, new DefaultAzureCredential()); |
| 112 | + |
| 113 | +AzureFunctionToolDefinition azureFnTool = new( |
| 114 | + name: "foo", |
| 115 | + description: "Get answers from the foo bot.", |
| 116 | + inputBinding: new AzureFunctionBinding( |
| 117 | + new AzureFunctionStorageQueue( |
| 118 | + queueName: "azure-function-foo-input", |
| 119 | + storageServiceEndpoint: storageQueueUri |
| 120 | + ) |
| 121 | + ), |
| 122 | + outputBinding: new AzureFunctionBinding( |
| 123 | + new AzureFunctionStorageQueue( |
| 124 | + queueName: "azure-function-tool-output", |
| 125 | + storageServiceEndpoint: storageQueueUri |
| 126 | + ) |
| 127 | + ), |
| 128 | + parameters: BinaryData.FromObjectAsJson( |
| 129 | + new |
| 130 | + { |
| 131 | + Type = "object", |
| 132 | + Properties = new |
| 133 | + { |
| 134 | + query = new |
| 135 | + { |
| 136 | + Type = "string", |
| 137 | + Description = "The question to ask.", |
| 138 | + }, |
| 139 | + outputqueueuri = new |
| 140 | + { |
| 141 | + Type = "string", |
| 142 | + Description = "The full output queue uri." |
| 143 | + } |
| 144 | + }, |
| 145 | + }, |
| 146 | + new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase } |
| 147 | + ) |
| 148 | +); |
| 149 | +``` |
| 150 | + |
| 151 | +2. Next we need to create an agent. In this scenario we are asking it to supply storage queue URI to the azure function whenever it is called. |
| 152 | + |
| 153 | +Synchronous sample: |
| 154 | +```C# Snippet:AgentsAzureFunctionsCreateAgentWithFunctionToolsSync |
| 155 | +PersistentAgent agent = client.CreateAgent( |
| 156 | + model: modelDeploymentName, |
| 157 | + name: "azure-function-agent-foo", |
| 158 | + instructions: "You are a helpful support agent. Use the provided function any " |
| 159 | + + "time the prompt contains the string 'What would foo say?'. When you invoke " |
| 160 | + + "the function, ALWAYS specify the output queue uri parameter as " |
| 161 | + + $"'{storageQueueUri}/azure-function-tool-output'. Always responds with " |
| 162 | + + "\"Foo says\" and then the response from the tool.", |
| 163 | + tools: [azureFnTool]); |
| 164 | +``` |
| 165 | + |
| 166 | +Asynchronous sample: |
| 167 | +```C# Snippet:AgentsAzureFunctionsCreateAgentWithFunctionTools |
| 168 | +PersistentAgent agent = await client.CreateAgentAsync( |
| 169 | + model: modelDeploymentName, |
| 170 | + name: "azure-function-agent-foo", |
| 171 | + instructions: "You are a helpful support agent. Use the provided function any " |
| 172 | + + "time the prompt contains the string 'What would foo say?'. When you invoke " |
| 173 | + + "the function, ALWAYS specify the output queue uri parameter as " |
| 174 | + + $"'{storageQueueUri}/azure-function-tool-output'. Always responds with " |
| 175 | + + "\"Foo says\" and then the response from the tool.", |
| 176 | + tools: [azureFnTool]); |
| 177 | +``` |
| 178 | + |
| 179 | +3. After we have created a message with request to ask "What would foo say?", we need to wait while the run is in queued, in progress or requires action states. |
| 180 | + |
| 181 | +Synchronous sample: |
| 182 | +```C# Snippet:AgentsAzureFunctionsHandlePollingWithRequiredActionSync |
| 183 | +PersistentAgentThread thread = client.CreateThread(); |
| 184 | + |
| 185 | +client.CreateMessage( |
| 186 | + thread.Id, |
| 187 | + MessageRole.User, |
| 188 | + "What is the most prevalent element in the universe? What would foo say?"); |
| 189 | + |
| 190 | +ThreadRun run = client.CreateRun(thread, agent); |
| 191 | + |
| 192 | +do |
| 193 | +{ |
| 194 | + Thread.Sleep(TimeSpan.FromMilliseconds(500)); |
| 195 | + run = client.GetRun(thread.Id, run.Id); |
| 196 | +} |
| 197 | +while (run.Status == RunStatus.Queued |
| 198 | + || run.Status == RunStatus.InProgress |
| 199 | + || run.Status == RunStatus.RequiresAction); |
| 200 | +``` |
| 201 | + |
| 202 | +Asynchronous sample: |
| 203 | +```C# Snippet:AgentsAzureFunctionsHandlePollingWithRequiredAction |
| 204 | +PersistentAgentThread thread = await client.CreateThreadAsync(); |
| 205 | + |
| 206 | +await client.CreateMessageAsync( |
| 207 | + thread.Id, |
| 208 | + MessageRole.User, |
| 209 | + "What is the most prevalent element in the universe? What would foo say?"); |
| 210 | + |
| 211 | +ThreadRun run = await client.CreateRunAsync(thread, agent); |
| 212 | + |
| 213 | +do |
| 214 | +{ |
| 215 | + await Task.Delay(TimeSpan.FromMilliseconds(500)); |
| 216 | + run = await client.GetRunAsync(thread.Id, run.Id); |
| 217 | +} |
| 218 | +while (run.Status == RunStatus.Queued |
| 219 | + || run.Status == RunStatus.InProgress |
| 220 | + || run.Status == RunStatus.RequiresAction); |
| 221 | +``` |
| 222 | + |
| 223 | +4. Finally, we will print out the messages to the console in chronological order. |
| 224 | + |
| 225 | +Synchronous sample: |
| 226 | +```C# Snippet:AgentsAzureFunctionsPrintSync |
| 227 | +PageableList<ThreadMessage> messages = client.GetMessages( |
| 228 | + threadId: thread.Id, |
| 229 | + order: ListSortOrder.Ascending |
| 230 | +); |
| 231 | + |
| 232 | +foreach (ThreadMessage threadMessage in messages) |
| 233 | +{ |
| 234 | + foreach (MessageContent contentItem in threadMessage.ContentItems) |
| 235 | + { |
| 236 | + if (contentItem is MessageTextContent textItem) |
| 237 | + { |
| 238 | + Console.Write($"{threadMessage.Role}: {textItem.Text}"); |
| 239 | + } |
| 240 | + Console.WriteLine(); |
| 241 | + } |
| 242 | +} |
| 243 | +``` |
| 244 | + |
| 245 | +Asynchronous sample: |
| 246 | +```C# Snippet:AgentsAzureFunctionsPrint |
| 247 | +PageableList<ThreadMessage> messages = await client.GetMessagesAsync( |
| 248 | + threadId: thread.Id, |
| 249 | + order: ListSortOrder.Ascending |
| 250 | +); |
| 251 | + |
| 252 | +foreach (ThreadMessage threadMessage in messages) |
| 253 | +{ |
| 254 | + foreach (MessageContent contentItem in threadMessage.ContentItems) |
| 255 | + { |
| 256 | + if (contentItem is MessageTextContent textItem) |
| 257 | + { |
| 258 | + Console.Write($"{threadMessage.Role}: {textItem.Text}"); |
| 259 | + } |
| 260 | + Console.WriteLine(); |
| 261 | + } |
| 262 | +} |
| 263 | +``` |
| 264 | + |
| 265 | +5. Finally, we delete all the resources, we have created in this sample. |
| 266 | + |
| 267 | +Synchronous sample: |
| 268 | +```C# Snippet:AgentsAzureFunctionsCleanupSync |
| 269 | +client.DeleteThread(thread.Id); |
| 270 | +client.DeleteAgent(agent.Id); |
| 271 | +``` |
| 272 | + |
| 273 | +Asynchronous sample: |
| 274 | +```C# Snippet:AgentsAzureFunctionsCleanup |
| 275 | +await client.DeleteThreadAsync(thread.Id); |
| 276 | +await client.DeleteAgentAsync(agent.Id); |
| 277 | +``` |
0 commit comments