openai/openai-dotnet

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
jsquire-patch-1

Branches

Tags

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

Clone

HTTPS

Download ZIP

examples/Chat/Example04_FunctionCallingStreaming.cs

290lines · modeblame

9f9f2936Jose Arriaga Maldonado2 years ago1using NUnit.Framework;
2using OpenAI.Chat;
3using System;
31c2ba63Jose Arriaga Maldonado1 years ago4using System.Buffers;
9f9f2936Jose Arriaga Maldonado2 years ago5using System.ClientModel;
6using System.Collections.Generic;
31c2ba63Jose Arriaga Maldonado1 years ago7using System.Diagnostics;
9f9f2936Jose Arriaga Maldonado2 years ago8using System.Text;
9using System.Text.Json;
10
8cc6643fJose Arriaga Maldonado2 years ago11namespace OpenAI.Examples;
9f9f2936Jose Arriaga Maldonado2 years ago12
8cc6643fJose Arriaga Maldonado2 years ago13public partial class ChatExamples
9f9f2936Jose Arriaga Maldonado2 years ago14{
8cc6643fJose Arriaga Maldonado2 years ago15// See Example03_FunctionCalling.cs for the tool and function definitions.
9f9f2936Jose Arriaga Maldonado2 years ago16
31c2ba63Jose Arriaga Maldonado1 years ago17#region
18public class StreamingChatToolCallsBuilder
19{
20private readonly Dictionary<int, string> _indexToToolCallId = [];
21private readonly Dictionary<int, string> _indexToFunctionName = [];
22private readonly Dictionary<int, SequenceBuilder<byte>> _indexToFunctionArguments = [];
23
24public void Append(StreamingChatToolCallUpdate toolCallUpdate)
25{
26// Keep track of which tool call ID belongs to this update index.
27if (toolCallUpdate.ToolCallId != null)
28{
29_indexToToolCallId[toolCallUpdate.Index] = toolCallUpdate.ToolCallId;
30}
31
32// Keep track of which function name belongs to this update index.
33if (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.
40if (toolCallUpdate.FunctionArgumentsUpdate != null && !toolCallUpdate.FunctionArgumentsUpdate.ToMemory().IsEmpty)
41{
42if (!_indexToFunctionArguments.TryGetValue(toolCallUpdate.Index, out SequenceBuilder<byte> argumentsBuilder))
43{
44argumentsBuilder = new SequenceBuilder<byte>();
45_indexToFunctionArguments[toolCallUpdate.Index] = argumentsBuilder;
46}
47
48argumentsBuilder.Append(toolCallUpdate.FunctionArgumentsUpdate);
49}
50}
51
52public IReadOnlyList<ChatToolCall> Build()
53{
54List<ChatToolCall> toolCalls = [];
55
56foreach ((int index, string toolCallId) in _indexToToolCallId)
57{
58ReadOnlySequence<byte> sequence = _indexToFunctionArguments[index].Build();
59
60ChatToolCall toolCall = ChatToolCall.CreateFunctionToolCall(
61id: toolCallId,
62functionName: _indexToFunctionName[index],
63functionArguments: BinaryData.FromBytes(sequence.ToArray()));
64
65toolCalls.Add(toolCall);
66}
67
68return toolCalls;
69}
70}
71#endregion
72
73#region
74public class SequenceBuilder<T>
75{
76Segment _first;
77Segment _last;
78
79public void Append(ReadOnlyMemory<T> data)
80{
81if (_first == null)
82{
83Debug.Assert(_last == null);
84_first = new Segment(data);
85_last = _first;
86}
87else
88{
89_last = _last!.Append(data);
90}
91}
92
93public ReadOnlySequence<T> Build()
94{
95if (_first == null)
96{
97Debug.Assert(_last == null);
98return ReadOnlySequence<T>.Empty;
99}
100
101if (_first == _last)
102{
103Debug.Assert(_first.Next == null);
104return new ReadOnlySequence<T>(_first.Memory);
105}
106
107return new ReadOnlySequence<T>(_first, 0, _last!, _last!.Memory.Length);
108}
109
110private sealed class Segment : ReadOnlySequenceSegment<T>
111{
112public Segment(ReadOnlyMemory<T> items) : this(items, 0)
113{
114}
115
116private Segment(ReadOnlyMemory<T> items, long runningIndex)
117{
118Debug.Assert(runningIndex >= 0);
119Memory = items;
120RunningIndex = runningIndex;
121}
122
123public Segment Append(ReadOnlyMemory<T> items)
124{
125long runningIndex;
126checked { runningIndex = RunningIndex + Memory.Length; }
127Segment segment = new(items, runningIndex);
128Next = segment;
129return segment;
130}
131}
132}
133#endregion
134
9f9f2936Jose Arriaga Maldonado2 years ago135[Test]
8cc6643fJose Arriaga Maldonado2 years ago136public void Example04_FunctionCallingStreaming()
9f9f2936Jose Arriaga Maldonado2 years ago137{
8cc6643fJose Arriaga Maldonado2 years ago138ChatClient client = new("gpt-4-turbo", Environment.GetEnvironmentVariable("OPENAI_API_KEY"));
9f9f2936Jose Arriaga Maldonado2 years ago139
140#region
31c2ba63Jose Arriaga Maldonado1 years ago141List<ChatMessage> messages =
142[
9f9f2936Jose Arriaga Maldonado2 years ago143new UserChatMessage("What's the weather like today?"),
144];
145
146ChatCompletionOptions options = new()
147{
148Tools = { getCurrentLocationTool, getCurrentWeatherTool },
149};
150#endregion
151
152#region
153bool requiresAction;
154
155do
156{
157requiresAction = false;
158StringBuilder contentBuilder = new();
31c2ba63Jose Arriaga Maldonado1 years ago159StreamingChatToolCallsBuilder toolCallsBuilder = new();
160
161CollectionResult<StreamingChatCompletionUpdate> completionUpdates = client.CompleteChatStreaming(messages, options);
9f9f2936Jose Arriaga Maldonado2 years ago162
31c2ba63Jose Arriaga Maldonado1 years ago163foreach (StreamingChatCompletionUpdate completionUpdate in completionUpdates)
9f9f2936Jose Arriaga Maldonado2 years ago164{
165// Accumulate the text content as new updates arrive.
31c2ba63Jose Arriaga Maldonado1 years ago166foreach (ChatMessageContentPart contentPart in completionUpdate.ContentUpdate)
9f9f2936Jose Arriaga Maldonado2 years ago167{
168contentBuilder.Append(contentPart.Text);
169}
170
171// Build the tool calls as new updates arrive.
31c2ba63Jose Arriaga Maldonado1 years ago172foreach (StreamingChatToolCallUpdate toolCallUpdate in completionUpdate.ToolCallUpdates)
9f9f2936Jose Arriaga Maldonado2 years ago173{
31c2ba63Jose Arriaga Maldonado1 years ago174toolCallsBuilder.Append(toolCallUpdate);
9f9f2936Jose Arriaga Maldonado2 years ago175}
176
31c2ba63Jose Arriaga Maldonado1 years ago177switch (completionUpdate.FinishReason)
9f9f2936Jose Arriaga Maldonado2 years ago178{
179case ChatFinishReason.Stop:
180{
181// Add the assistant message to the conversation history.
182messages.Add(new AssistantChatMessage(contentBuilder.ToString()));
183break;
184}
185
186case ChatFinishReason.ToolCalls:
187{
188// First, collect the accumulated function arguments into complete tool calls to be processed
31c2ba63Jose Arriaga Maldonado1 years ago189IReadOnlyList<ChatToolCall> toolCalls = toolCallsBuilder.Build();
9f9f2936Jose Arriaga Maldonado2 years ago190
191// Next, add the assistant message with tool calls to the conversation history.
31c2ba63Jose Arriaga Maldonado1 years ago192AssistantChatMessage assistantMessage = new(toolCalls);
193
194if (contentBuilder.Length > 0)
2ab1a942Jose Arriaga Maldonado1 years ago195{
31c2ba63Jose Arriaga Maldonado1 years ago196assistantMessage.Content.Add(ChatMessageContentPart.CreateTextPart(contentBuilder.ToString()));
2ab1a942Jose Arriaga Maldonado1 years ago197}
31c2ba63Jose Arriaga Maldonado1 years ago198
199messages.Add(assistantMessage);
9f9f2936Jose Arriaga Maldonado2 years ago200
201// Then, add a new tool message for each tool call to be resolved.
202foreach (ChatToolCall toolCall in toolCalls)
203{
204switch (toolCall.FunctionName)
205{
206case nameof(GetCurrentLocation):
207{
208string toolResult = GetCurrentLocation();
209messages.Add(new ToolChatMessage(toolCall.Id, toolResult));
210break;
211}
212
213case 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.
219using JsonDocument argumentsJson = JsonDocument.Parse(toolCall.FunctionArguments);
220bool hasLocation = argumentsJson.RootElement.TryGetProperty("location", out JsonElement location);
221bool hasUnit = argumentsJson.RootElement.TryGetProperty("unit", out JsonElement unit);
222
223if (!hasLocation)
224{
225throw new ArgumentNullException(nameof(location), "The location argument is required.");
226}
227
228string toolResult = hasUnit
229? GetCurrentWeather(location.GetString(), unit.GetString())
230: GetCurrentWeather(location.GetString());
231messages.Add(new ToolChatMessage(toolCall.Id, toolResult));
232break;
233}
234
235default:
236{
237// Handle other unexpected calls.
238throw new NotImplementedException();
239}
240}
241}
242
243requiresAction = true;
244break;
245}
246
247case ChatFinishReason.Length:
248throw new NotImplementedException("Incomplete model output due to MaxTokens parameter or token limit exceeded.");
249
250case ChatFinishReason.ContentFilter:
251throw new NotImplementedException("Omitted content due to a content filter flag.");
252
253case ChatFinishReason.FunctionCall:
254throw new NotImplementedException("Deprecated in favor of tool calls.");
255
256case null:
257break;
258}
259}
260} while (requiresAction);
261#endregion
262
263#region
7a8bc8beJose Arriaga Maldonado1 years ago264foreach (ChatMessage message in messages)
9f9f2936Jose Arriaga Maldonado2 years ago265{
7a8bc8beJose Arriaga Maldonado1 years ago266switch (message)
9f9f2936Jose Arriaga Maldonado2 years ago267{
268case UserChatMessage userMessage:
269Console.WriteLine($"[USER]:");
270Console.WriteLine($"{userMessage.Content[0].Text}");
271Console.WriteLine();
272break;
273
274case AssistantChatMessage assistantMessage when assistantMessage.Content.Count > 0:
275Console.WriteLine($"[ASSISTANT]:");
276Console.WriteLine($"{assistantMessage.Content[0].Text}");
277Console.WriteLine();
278break;
279
280case ToolChatMessage:
281// Do not print any tool messages; let the assistant summarize the tool results instead.
282break;
283
284default:
285break;
286}
287}
288#endregion
289}
290}