|
| 1 | +using System.Text; |
| 2 | + |
| 3 | +namespace OpenAI.IntegrationTests.Examples; |
| 4 | + |
| 5 | +public partial class Examples |
| 6 | +{ |
| 7 | + [Test] |
| 8 | + [Explicit] |
| 9 | + public async Task FunctionCallingStreaming() |
| 10 | + { |
| 11 | + using var api = GetAuthenticatedClient(); |
| 12 | + |
| 13 | + List<ChatCompletionRequestMessage> messages = [ |
| 14 | + "What's the weather like today?", |
| 15 | + ]; |
| 16 | + |
| 17 | + var service = new FunctionCallingService(); |
| 18 | + IList<ChatCompletionTool> tools = service.AsTools(); |
| 19 | + |
| 20 | + bool requiresAction; |
| 21 | + |
| 22 | + do |
| 23 | + { |
| 24 | + requiresAction = false; |
| 25 | + Dictionary<int, string> indexToToolCallId = []; |
| 26 | + Dictionary<int, string> indexToFunctionName = []; |
| 27 | + Dictionary<int, StringBuilder> indexToFunctionArguments = []; |
| 28 | + StringBuilder contentBuilder = new(); |
| 29 | + IAsyncEnumerable<CreateChatCompletionStreamResponse> chatUpdates |
| 30 | + = api.Chat.CreateChatCompletionAsStreamAsync( |
| 31 | + messages, |
| 32 | + model: CreateChatCompletionRequestModel.Gpt4o20240806, |
| 33 | + tools: tools); |
| 34 | + |
| 35 | + await foreach (CreateChatCompletionStreamResponse chatUpdate in chatUpdates) |
| 36 | + { |
| 37 | + // Accumulate the text content as new updates arrive. |
| 38 | + if (!string.IsNullOrEmpty(chatUpdate.Choices[0].Delta.Content)) |
| 39 | + { |
| 40 | + contentBuilder.Append(chatUpdate.Choices[0].Delta.Content); |
| 41 | + } |
| 42 | + |
| 43 | + // Build the tool calls as new updates arrive. |
| 44 | + foreach (ChatCompletionMessageToolCallChunk toolCallUpdate in chatUpdate.Choices[0].Delta.ToolCalls ?? []) |
| 45 | + { |
| 46 | + // Keep track of which tool call ID belongs to this update index. |
| 47 | + if (toolCallUpdate.Id is not null) |
| 48 | + { |
| 49 | + indexToToolCallId[toolCallUpdate.Index] = toolCallUpdate.Id; |
| 50 | + } |
| 51 | + |
| 52 | + // Keep track of which function name belongs to this update index. |
| 53 | + if (toolCallUpdate.Function?.Name is {} functionName) |
| 54 | + { |
| 55 | + indexToFunctionName[toolCallUpdate.Index] = functionName; |
| 56 | + } |
| 57 | + |
| 58 | + // Keep track of which function arguments belong to this update index, |
| 59 | + // and accumulate the arguments string as new updates arrive. |
| 60 | + if (toolCallUpdate.Function?.Arguments is not null) |
| 61 | + { |
| 62 | + StringBuilder argumentsBuilder |
| 63 | + = indexToFunctionArguments.TryGetValue(toolCallUpdate.Index, out StringBuilder? existingBuilder) |
| 64 | + ? existingBuilder |
| 65 | + : new StringBuilder(); |
| 66 | + argumentsBuilder.Append(toolCallUpdate.Function?.Arguments); |
| 67 | + indexToFunctionArguments[toolCallUpdate.Index] = argumentsBuilder; |
| 68 | + } |
| 69 | + } |
| 70 | + |
| 71 | + switch (chatUpdate.Choices[0].FinishReason) |
| 72 | + { |
| 73 | + case CreateChatCompletionStreamResponseChoiceFinishReason.Stop: |
| 74 | + { |
| 75 | + // Add the assistant message to the conversation history. |
| 76 | + messages.Add(contentBuilder.ToString().AsAssistantMessage()); |
| 77 | + break; |
| 78 | + } |
| 79 | + |
| 80 | + case CreateChatCompletionStreamResponseChoiceFinishReason.ToolCalls: |
| 81 | + { |
| 82 | + // First, collect the accumulated function arguments into complete tool calls to be processed |
| 83 | + List<ChatCompletionMessageToolCall> toolCalls = []; |
| 84 | + foreach ((int index, string toolCallId) in indexToToolCallId) |
| 85 | + { |
| 86 | + toolCalls.Add(new ChatCompletionMessageToolCall |
| 87 | + { |
| 88 | + Id = toolCallId, |
| 89 | + Function = new ChatCompletionMessageToolCallFunction |
| 90 | + { |
| 91 | + Name = indexToFunctionName[index], |
| 92 | + Arguments = indexToFunctionArguments[index].ToString(), |
| 93 | + }, |
| 94 | + Type = ChatCompletionMessageToolCallType.Function, |
| 95 | + }); |
| 96 | + } |
| 97 | + |
| 98 | + // Next, add the assistant message with tool calls to the conversation history. |
| 99 | + var content = contentBuilder.Length > 0 |
| 100 | + ? new OneOf<string, IList<ChatCompletionRequestAssistantMessageContentPart>>(contentBuilder.ToString()) |
| 101 | + : (OneOf<string, IList<ChatCompletionRequestAssistantMessageContentPart>>?)null; |
| 102 | + messages.Add(new ChatCompletionRequestAssistantMessage |
| 103 | + { |
| 104 | + Content = content, |
| 105 | + Role = ChatCompletionRequestAssistantMessageRole.Assistant, |
| 106 | + ToolCalls = toolCalls, |
| 107 | + }); |
| 108 | + |
| 109 | + // Then, add a new tool message for each tool call to be resolved. |
| 110 | + foreach (ChatCompletionMessageToolCall toolCall in toolCalls) |
| 111 | + { |
| 112 | + var json = await service.CallAsync( |
| 113 | + functionName: toolCall.Function.Name, |
| 114 | + argumentsAsJson: toolCall.Function.Arguments); |
| 115 | + messages.Add(json.AsToolMessage(toolCall.Id)); |
| 116 | + } |
| 117 | + |
| 118 | + requiresAction = true; |
| 119 | + break; |
| 120 | + } |
| 121 | + |
| 122 | + case CreateChatCompletionStreamResponseChoiceFinishReason.Length: |
| 123 | + throw new NotImplementedException("Incomplete model output due to MaxTokens parameter or token limit exceeded."); |
| 124 | + |
| 125 | + case CreateChatCompletionStreamResponseChoiceFinishReason.ContentFilter: |
| 126 | + throw new NotImplementedException("Omitted content due to a content filter flag."); |
| 127 | + |
| 128 | + case CreateChatCompletionStreamResponseChoiceFinishReason.FunctionCall: |
| 129 | + throw new NotImplementedException("Deprecated in favor of tool calls."); |
| 130 | + |
| 131 | + case null: |
| 132 | + break; |
| 133 | + } |
| 134 | + } |
| 135 | + } while (requiresAction); |
| 136 | + |
| 137 | + foreach (ChatCompletionRequestMessage requestMessage in messages) |
| 138 | + { |
| 139 | + if (requestMessage.System is { } systemMessage) |
| 140 | + { |
| 141 | + Console.WriteLine($"[SYSTEM]:"); |
| 142 | + Console.WriteLine($"{systemMessage.Content.Value1}"); |
| 143 | + Console.WriteLine(); |
| 144 | + } |
| 145 | + else if (requestMessage.User is { } userMessage) |
| 146 | + { |
| 147 | + Console.WriteLine($"[USER]:"); |
| 148 | + Console.WriteLine($"{userMessage.Content.Value1}"); |
| 149 | + Console.WriteLine(); |
| 150 | + } |
| 151 | + else if (requestMessage.Assistant is { Content: not null } assistantMessage) |
| 152 | + { |
| 153 | + Console.WriteLine($"[ASSISTANT]:"); |
| 154 | + Console.WriteLine($"{assistantMessage.Content?.Value1}"); |
| 155 | + Console.WriteLine(); |
| 156 | + } |
| 157 | + else if (requestMessage.Tool is not null) |
| 158 | + { |
| 159 | + // Do not print any tool messages; let the assistant summarize the tool results instead. |
| 160 | + } |
| 161 | + } |
| 162 | + } |
| 163 | +} |
0 commit comments