openai/openai-dotnet

Public

mirrored from https://github.com/openai/openai-dotnetAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
joseharriaga/README-version

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

examples/Chat/Example04_FunctionCallingStreaming.cs

290lines · modecode

1using NUnit.Framework;
2using OpenAI.Chat;
3using System;
4using System.Buffers;
5using System.ClientModel;
6using System.Collections.Generic;
7using System.Diagnostics;
8using System.Text;
9using System.Text.Json;
10
11namespace OpenAI.Examples;
12
13public partial class ChatExamples
14{
15 // See Example03_FunctionCalling.cs for the tool and function definitions.
16
17 #region
18 public class StreamingChatToolCallsBuilder
19 {
20 private readonly Dictionary<int, string> _indexToToolCallId = [];
21 private readonly Dictionary<int, string> _indexToFunctionName = [];
22 private readonly Dictionary<int, SequenceBuilder<byte>> _indexToFunctionArguments = [];
23
24 public void Append(StreamingChatToolCallUpdate toolCallUpdate)
25 {
26 // Keep track of which tool call ID belongs to this update index.
27 if (toolCallUpdate.ToolCallId != null)
28 {
29 _indexToToolCallId[toolCallUpdate.Index] = toolCallUpdate.ToolCallId;
30 }
31
32 // Keep track of which function name belongs to this update index.
33 if (toolCallUpdate.FunctionName != null)
34 {
35 _indexToFunctionName[toolCallUpdate.Index] = toolCallUpdate.FunctionName;
36 }
37
38 // Keep track of which function arguments belong to this update index,
39 // and accumulate the arguments as new updates arrive.
40 if (toolCallUpdate.FunctionArgumentsUpdate != null && !toolCallUpdate.FunctionArgumentsUpdate.ToMemory().IsEmpty)
41 {
42 if (!_indexToFunctionArguments.TryGetValue(toolCallUpdate.Index, out SequenceBuilder<byte> argumentsBuilder))
43 {
44 argumentsBuilder = new SequenceBuilder<byte>();
45 _indexToFunctionArguments[toolCallUpdate.Index] = argumentsBuilder;
46 }
47
48 argumentsBuilder.Append(toolCallUpdate.FunctionArgumentsUpdate);
49 }
50 }
51
52 public IReadOnlyList<ChatToolCall> Build()
53 {
54 List<ChatToolCall> toolCalls = [];
55
56 foreach ((int index, string toolCallId) in _indexToToolCallId)
57 {
58 ReadOnlySequence<byte> sequence = _indexToFunctionArguments[index].Build();
59
60 ChatToolCall toolCall = ChatToolCall.CreateFunctionToolCall(
61 id: toolCallId,
62 functionName: _indexToFunctionName[index],
63 functionArguments: BinaryData.FromBytes(sequence.ToArray()));
64
65 toolCalls.Add(toolCall);
66 }
67
68 return toolCalls;
69 }
70 }
71 #endregion
72
73 #region
74 public class SequenceBuilder<T>
75 {
76 Segment _first;
77 Segment _last;
78
79 public void Append(ReadOnlyMemory<T> data)
80 {
81 if (_first == null)
82 {
83 Debug.Assert(_last == null);
84 _first = new Segment(data);
85 _last = _first;
86 }
87 else
88 {
89 _last = _last!.Append(data);
90 }
91 }
92
93 public ReadOnlySequence<T> Build()
94 {
95 if (_first == null)
96 {
97 Debug.Assert(_last == null);
98 return ReadOnlySequence<T>.Empty;
99 }
100
101 if (_first == _last)
102 {
103 Debug.Assert(_first.Next == null);
104 return new ReadOnlySequence<T>(_first.Memory);
105 }
106
107 return new ReadOnlySequence<T>(_first, 0, _last!, _last!.Memory.Length);
108 }
109
110 private sealed class Segment : ReadOnlySequenceSegment<T>
111 {
112 public Segment(ReadOnlyMemory<T> items) : this(items, 0)
113 {
114 }
115
116 private Segment(ReadOnlyMemory<T> items, long runningIndex)
117 {
118 Debug.Assert(runningIndex >= 0);
119 Memory = items;
120 RunningIndex = runningIndex;
121 }
122
123 public Segment Append(ReadOnlyMemory<T> items)
124 {
125 long runningIndex;
126 checked { runningIndex = RunningIndex + Memory.Length; }
127 Segment segment = new(items, runningIndex);
128 Next = segment;
129 return segment;
130 }
131 }
132 }
133 #endregion
134
135 [Test]
136 public void Example04_FunctionCallingStreaming()
137 {
138 ChatClient client = new("gpt-4-turbo", Environment.GetEnvironmentVariable("OPENAI_API_KEY"));
139
140 #region
141 List<ChatMessage> messages =
142 [
143 new UserChatMessage("What's the weather like today?"),
144 ];
145
146 ChatCompletionOptions options = new()
147 {
148 Tools = { getCurrentLocationTool, getCurrentWeatherTool },
149 };
150 #endregion
151
152 #region
153 bool requiresAction;
154
155 do
156 {
157 requiresAction = false;
158 StringBuilder contentBuilder = new();
159 StreamingChatToolCallsBuilder toolCallsBuilder = new();
160
161 CollectionResult<StreamingChatCompletionUpdate> completionUpdates = client.CompleteChatStreaming(messages, options);
162
163 foreach (StreamingChatCompletionUpdate completionUpdate in completionUpdates)
164 {
165 // Accumulate the text content as new updates arrive.
166 foreach (ChatMessageContentPart contentPart in completionUpdate.ContentUpdate)
167 {
168 contentBuilder.Append(contentPart.Text);
169 }
170
171 // Build the tool calls as new updates arrive.
172 foreach (StreamingChatToolCallUpdate toolCallUpdate in completionUpdate.ToolCallUpdates)
173 {
174 toolCallsBuilder.Append(toolCallUpdate);
175 }
176
177 switch (completionUpdate.FinishReason)
178 {
179 case ChatFinishReason.Stop:
180 {
181 // Add the assistant message to the conversation history.
182 messages.Add(new AssistantChatMessage(contentBuilder.ToString()));
183 break;
184 }
185
186 case ChatFinishReason.ToolCalls:
187 {
188 // First, collect the accumulated function arguments into complete tool calls to be processed
189 IReadOnlyList<ChatToolCall> toolCalls = toolCallsBuilder.Build();
190
191 // Next, add the assistant message with tool calls to the conversation history.
192 AssistantChatMessage assistantMessage = new(toolCalls);
193
194 if (contentBuilder.Length > 0)
195 {
196 assistantMessage.Content.Add(ChatMessageContentPart.CreateTextPart(contentBuilder.ToString()));
197 }
198
199 messages.Add(assistantMessage);
200
201 // Then, add a new tool message for each tool call to be resolved.
202 foreach (ChatToolCall toolCall in toolCalls)
203 {
204 switch (toolCall.FunctionName)
205 {
206 case nameof(GetCurrentLocation):
207 {
208 string toolResult = GetCurrentLocation();
209 messages.Add(new ToolChatMessage(toolCall.Id, toolResult));
210 break;
211 }
212
213 case nameof(GetCurrentWeather):
214 {
215 // The arguments that the model wants to use to call the function are specified as a
216 // stringified JSON object based on the schema defined in the tool definition. Note that
217 // the model may hallucinate arguments too. Consequently, it is important to do the
218 // appropriate parsing and validation before calling the function.
219 using JsonDocument argumentsJson = JsonDocument.Parse(toolCall.FunctionArguments);
220 bool hasLocation = argumentsJson.RootElement.TryGetProperty("location", out JsonElement location);
221 bool hasUnit = argumentsJson.RootElement.TryGetProperty("unit", out JsonElement unit);
222
223 if (!hasLocation)
224 {
225 throw new ArgumentNullException(nameof(location), "The location argument is required.");
226 }
227
228 string toolResult = hasUnit
229 ? GetCurrentWeather(location.GetString(), unit.GetString())
230 : GetCurrentWeather(location.GetString());
231 messages.Add(new ToolChatMessage(toolCall.Id, toolResult));
232 break;
233 }
234
235 default:
236 {
237 // Handle other unexpected calls.
238 throw new NotImplementedException();
239 }
240 }
241 }
242
243 requiresAction = true;
244 break;
245 }
246
247 case ChatFinishReason.Length:
248 throw new NotImplementedException("Incomplete model output due to MaxTokens parameter or token limit exceeded.");
249
250 case ChatFinishReason.ContentFilter:
251 throw new NotImplementedException("Omitted content due to a content filter flag.");
252
253 case ChatFinishReason.FunctionCall:
254 throw new NotImplementedException("Deprecated in favor of tool calls.");
255
256 case null:
257 break;
258 }
259 }
260 } while (requiresAction);
261 #endregion
262
263 #region
264 foreach (ChatMessage message in messages)
265 {
266 switch (message)
267 {
268 case UserChatMessage userMessage:
269 Console.WriteLine($"[USER]:");
270 Console.WriteLine($"{userMessage.Content[0].Text}");
271 Console.WriteLine();
272 break;
273
274 case AssistantChatMessage assistantMessage when assistantMessage.Content.Count > 0:
275 Console.WriteLine($"[ASSISTANT]:");
276 Console.WriteLine($"{assistantMessage.Content[0].Text}");
277 Console.WriteLine();
278 break;
279
280 case ToolChatMessage:
281 // Do not print any tool messages; let the assistant summarize the tool results instead.
282 break;
283
284 default:
285 break;
286 }
287 }
288 #endregion
289 }
290}
291