|
| 1 | +# Sample using agents with functions and streaming in Azure.AI.Agents |
| 2 | + |
| 3 | +In this example we are demonstrating how to use the local functions with the agents in streaming scenarios. The functions can be used to provide agent specific information in response to user question. |
| 4 | + |
| 5 | +1. First we need to create agent client and read the environment variables that will be used in the next steps. |
| 6 | +```C# Snippet:AgentsFunctionsWithStreaming_CreateClient |
| 7 | +var projectEndpoint = new Uri(configuration["ProjectEndpoint"]); |
| 8 | +var modelDeploymentName = configuration["ModelDeploymentName"]; |
| 9 | +PersistentAgentsClient client = new(projectEndpoint, new DefaultAzureCredential()); |
| 10 | +``` |
| 11 | + |
| 12 | +2 Define three toy functions: `GetUserFavoriteCity`that always returns "Seattle, WA" and `GetCityNickname`, which will handle only "Seattle, WA" and will throw exception in response to other city names. The last function `GetWeatherAtLocation` returns weather at Seattle, WA. For each function we need to create `FunctionToolDefinition`, which defines function name, description and parameters. |
| 13 | +```C# Snippet:AgentsFunctionsWithStreaming_DefineFunctionTools |
| 14 | +// Example of a function that defines no parameters |
| 15 | +string GetUserFavoriteCity() => "Seattle, WA"; |
| 16 | +FunctionToolDefinition getUserFavoriteCityTool = new("getUserFavoriteCity", "Gets the user's favorite city."); |
| 17 | +// Example of a function with a single required parameter |
| 18 | +string GetCityNickname(string location) => location switch |
| 19 | +{ |
| 20 | + "Seattle, WA" => "The Emerald City", |
| 21 | + _ => throw new NotImplementedException(), |
| 22 | +}; |
| 23 | +FunctionToolDefinition getCityNicknameTool = new( |
| 24 | + name: "getCityNickname", |
| 25 | + description: "Gets the nickname of a city, e.g. 'LA' for 'Los Angeles, CA'.", |
| 26 | + parameters: BinaryData.FromObjectAsJson( |
| 27 | + new |
| 28 | + { |
| 29 | + Type = "object", |
| 30 | + Properties = new |
| 31 | + { |
| 32 | + Location = new |
| 33 | + { |
| 34 | + Type = "string", |
| 35 | + Description = "The city and state, e.g. San Francisco, CA", |
| 36 | + }, |
| 37 | + }, |
| 38 | + Required = new[] { "location" }, |
| 39 | + }, |
| 40 | + new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })); |
| 41 | +// Example of a function with one required and one optional, enum parameter |
| 42 | +string GetWeatherAtLocation(string location, string temperatureUnit = "f") => location switch |
| 43 | +{ |
| 44 | + "Seattle, WA" => temperatureUnit == "f" ? "70f" : "21c", |
| 45 | + _ => throw new NotImplementedException() |
| 46 | +}; |
| 47 | +FunctionToolDefinition getCurrentWeatherAtLocationTool = new( |
| 48 | + name: "getCurrentWeatherAtLocation", |
| 49 | + description: "Gets the current weather at a provided location.", |
| 50 | + parameters: BinaryData.FromObjectAsJson( |
| 51 | + new |
| 52 | + { |
| 53 | + Type = "object", |
| 54 | + Properties = new |
| 55 | + { |
| 56 | + Location = new |
| 57 | + { |
| 58 | + Type = "string", |
| 59 | + Description = "The city and state, e.g. San Francisco, CA", |
| 60 | + }, |
| 61 | + Unit = new |
| 62 | + { |
| 63 | + Type = "string", |
| 64 | + Enum = new[] { "c", "f" }, |
| 65 | + }, |
| 66 | + }, |
| 67 | + Required = new[] { "location" }, |
| 68 | + }, |
| 69 | + new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })); |
| 70 | +``` |
| 71 | + |
| 72 | +3. We will create the function `GetResolvedToolOutput`. It runs the abovementioned functions and wraps their outouts in `ToolOutput` object. |
| 73 | +```C# Snippet:AgentsFunctionsWithStreamingUpdateHandling |
| 74 | +ToolOutput GetResolvedToolOutput(string functionName, string toolCallId, string functionArguments) |
| 75 | +{ |
| 76 | + if (functionName == getUserFavoriteCityTool.Name) |
| 77 | + { |
| 78 | + return new ToolOutput(toolCallId, GetUserFavoriteCity()); |
| 79 | + } |
| 80 | + using JsonDocument argumentsJson = JsonDocument.Parse(functionArguments); |
| 81 | + if (functionName == getCityNicknameTool.Name) |
| 82 | + { |
| 83 | + string locationArgument = argumentsJson.RootElement.GetProperty("location").GetString(); |
| 84 | + return new ToolOutput(toolCallId, GetCityNickname(locationArgument)); |
| 85 | + } |
| 86 | + if (functionName == getCurrentWeatherAtLocationTool.Name) |
| 87 | + { |
| 88 | + string locationArgument = argumentsJson.RootElement.GetProperty("location").GetString(); |
| 89 | + if (argumentsJson.RootElement.TryGetProperty("unit", out JsonElement unitElement)) |
| 90 | + { |
| 91 | + string unitArgument = unitElement.GetString(); |
| 92 | + return new ToolOutput(toolCallId, GetWeatherAtLocation(locationArgument, unitArgument)); |
| 93 | + } |
| 94 | + return new ToolOutput(toolCallId, GetWeatherAtLocation(locationArgument)); |
| 95 | + } |
| 96 | + return null; |
| 97 | +} |
| 98 | +``` |
| 99 | + |
| 100 | +4. Create agent with the `FunctionToolDefinitions` we have created in step 2. |
| 101 | + |
| 102 | +Synchronous sample: |
| 103 | +```C# Snippet:AgentsFunctionsWithStreamingSync_CreateAgent |
| 104 | +PersistentAgent agent = client.CreateAgent( |
| 105 | + model: modelDeploymentName, |
| 106 | + name: "SDK Test Agent - Functions", |
| 107 | + instructions: "You are a weather bot. Use the provided functions to help answer questions. " |
| 108 | + + "Customize your responses to the user's preferences as much as possible and use friendly " |
| 109 | + + "nicknames for cities whenever possible.", |
| 110 | + tools: [getUserFavoriteCityTool, getCityNicknameTool, getCurrentWeatherAtLocationTool] |
| 111 | +); |
| 112 | +``` |
| 113 | + |
| 114 | +Asynchronous sample: |
| 115 | +```C# Snippet:AgentsFunctionsWithStreaming_CreateAgent |
| 116 | +PersistentAgent agent = await client.CreateAgentAsync( |
| 117 | + model: modelDeploymentName, |
| 118 | + name: "SDK Test Agent - Functions", |
| 119 | + instructions: "You are a weather bot. Use the provided functions to help answer questions. " |
| 120 | + + "Customize your responses to the user's preferences as much as possible and use friendly " |
| 121 | + + "nicknames for cities whenever possible.", |
| 122 | + tools: [ getUserFavoriteCityTool, getCityNicknameTool, getCurrentWeatherAtLocationTool ] |
| 123 | +); |
| 124 | +``` |
| 125 | + |
| 126 | +5. Create `Thread` with the message. |
| 127 | + |
| 128 | +Synchronous sample: |
| 129 | +```C# Snippet:AgentsFunctionsWithStreamingSync_CreateThread |
| 130 | +PersistentAgentThread thread = client.CreateThread(); |
| 131 | + |
| 132 | +ThreadMessage message = client.CreateMessage( |
| 133 | + thread.Id, |
| 134 | + MessageRole.User, |
| 135 | + "What's the weather like in my favorite city?"); |
| 136 | +``` |
| 137 | + |
| 138 | +Asynchronous sample: |
| 139 | +```C# Snippet:AgentsFunctionsWithStreaming_CreateThread |
| 140 | +PersistentAgentThread thread = await client.CreateThreadAsync(); |
| 141 | + |
| 142 | +ThreadMessage message = await client.CreateMessageAsync( |
| 143 | + thread.Id, |
| 144 | + MessageRole.User, |
| 145 | + "What's the weather like in my favorite city?"); |
| 146 | +``` |
| 147 | + |
| 148 | +6. Create a stream and wait for the stream update of the `RequiredActionUpdate` type. This update will mark the point, when we need to submit tool outputs to the stream. We will submit outputs in the inner cycle. Please note that `RequiredActionUpdate` keeps only one required action, while our run may require multiple function calls, this case is handled in the inner cycle, so that we can add tool output to the existing array of outputs. After all required actions were submitted we clean up the array of required actions. |
| 149 | + |
| 150 | +Synchronous sample: |
| 151 | +```C# Snippet:AgentsFunctionsWithStreamingSyncUpdateCycle |
| 152 | +List<ToolOutput> toolOutputs = []; |
| 153 | +ThreadRun streamRun = null; |
| 154 | +CollectionResult<StreamingUpdate> stream = client.CreateRunStreaming(thread.Id, agent.Id); |
| 155 | +do |
| 156 | +{ |
| 157 | + toolOutputs.Clear(); |
| 158 | + foreach (StreamingUpdate streamingUpdate in stream) |
| 159 | + { |
| 160 | + if (streamingUpdate is RequiredActionUpdate submitToolOutputsUpdate) |
| 161 | + { |
| 162 | + RequiredActionUpdate newActionUpdate = submitToolOutputsUpdate; |
| 163 | + toolOutputs.Add( |
| 164 | + GetResolvedToolOutput( |
| 165 | + newActionUpdate.FunctionName, |
| 166 | + newActionUpdate.ToolCallId, |
| 167 | + newActionUpdate.FunctionArguments |
| 168 | + )); |
| 169 | + streamRun = submitToolOutputsUpdate.Value; |
| 170 | + } |
| 171 | + else if (streamingUpdate is MessageContentUpdate contentUpdate) |
| 172 | + { |
| 173 | + Console.Write($"{contentUpdate?.Text}"); |
| 174 | + } |
| 175 | + } |
| 176 | + if (toolOutputs.Count > 0) |
| 177 | + { |
| 178 | + stream = client.SubmitToolOutputsToStream(streamRun, toolOutputs); |
| 179 | + } |
| 180 | +} |
| 181 | +while (toolOutputs.Count > 0); |
| 182 | +``` |
| 183 | + |
| 184 | +Asynchronous sample: |
| 185 | +```C# Snippet:AgentsFunctionsWithStreamingUpdateCycle |
| 186 | +List<ToolOutput> toolOutputs = []; |
| 187 | +ThreadRun streamRun = null; |
| 188 | +AsyncCollectionResult<StreamingUpdate> stream = client.CreateRunStreamingAsync(thread.Id, agent.Id); |
| 189 | +do |
| 190 | +{ |
| 191 | + toolOutputs.Clear(); |
| 192 | + await foreach (StreamingUpdate streamingUpdate in stream) |
| 193 | + { |
| 194 | + if (streamingUpdate is RequiredActionUpdate submitToolOutputsUpdate) |
| 195 | + { |
| 196 | + RequiredActionUpdate newActionUpdate = submitToolOutputsUpdate; |
| 197 | + toolOutputs.Add( |
| 198 | + GetResolvedToolOutput( |
| 199 | + newActionUpdate.FunctionName, |
| 200 | + newActionUpdate.ToolCallId, |
| 201 | + newActionUpdate.FunctionArguments |
| 202 | + )); |
| 203 | + streamRun = submitToolOutputsUpdate.Value; |
| 204 | + } |
| 205 | + else if (streamingUpdate is MessageContentUpdate contentUpdate) |
| 206 | + { |
| 207 | + Console.Write($"{contentUpdate?.Text}"); |
| 208 | + } |
| 209 | + } |
| 210 | + if (toolOutputs.Count > 0) |
| 211 | + { |
| 212 | + stream = client.SubmitToolOutputsToStreamAsync(streamRun, toolOutputs); |
| 213 | + } |
| 214 | +} |
| 215 | +while (toolOutputs.Count > 0); |
| 216 | +``` |
| 217 | + |
| 218 | +7. Finally, we delete all the resources, we have created in this sample. |
| 219 | + |
| 220 | +Synchronous sample: |
| 221 | +```C# Snippet:AgentsFunctionsWithStreamingSync_Cleanup |
| 222 | +client.DeleteThread(thread.Id); |
| 223 | +client.DeleteAgent(agent.Id); |
| 224 | +``` |
| 225 | + |
| 226 | +Asynchronous sample: |
| 227 | +```C# Snippet:AgentsFunctionsWithStreaming_Cleanup |
| 228 | +await client.DeleteThreadAsync(thread.Id); |
| 229 | +await client.DeleteAgentAsync(agent.Id); |
| 230 | +``` |
0 commit comments