Skip to content

Commit dcc12e4

Browse files
authored
Create AzureFunctionCalling.md
Updated sample to use uri and cleaned up code and made it consistent with other c# examples.
1 parent cae9ece commit dcc12e4

File tree

1 file changed

+277
-0
lines changed

1 file changed

+277
-0
lines changed
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
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

Comments
 (0)