From a4f624a06bfcc21fa02f9a9029d5410c38247254 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Wed, 8 Jan 2025 18:05:09 +0000 Subject: [PATCH 1/7] add a request index to the streamed chat message content so that callers can group the messages by the request. --- .../Clients/GeminiChatCompletionClient.cs | 23 +++++++++++++------ .../Client/MistralClient.cs | 15 +++++++----- .../Core/ClientCore.ChatCompletion.cs | 4 ++-- .../Contents/StreamingChatMessageContent.cs | 5 ++++ 4 files changed, 32 insertions(+), 15 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.Google/Core/Gemini/Clients/GeminiChatCompletionClient.cs b/dotnet/src/Connectors/Connectors.Google/Core/Gemini/Clients/GeminiChatCompletionClient.cs index 39c357e28528..95122f05cdc4 100644 --- a/dotnet/src/Connectors/Connectors.Google/Core/Gemini/Clients/GeminiChatCompletionClient.cs +++ b/dotnet/src/Connectors/Connectors.Google/Core/Gemini/Clients/GeminiChatCompletionClient.cs @@ -340,10 +340,10 @@ private async IAsyncEnumerable GetStreamingChatMess // This scenario should not happen but I leave it as a precaution state.AutoInvoke = false; // We return the first message - yield return this.GetStreamingChatContentFromChatContent(messageContent); + yield return this.GetStreamingChatContentFromChatContent(messageContent, state.Iteration); // We return the second message messageContent = chatResponsesEnumerator.Current; - yield return this.GetStreamingChatContentFromChatContent(messageContent); + yield return this.GetStreamingChatContentFromChatContent(messageContent, state.Iteration); continue; } @@ -356,7 +356,7 @@ private async IAsyncEnumerable GetStreamingChatMess state.AutoInvoke = false; // If we don't want to attempt to invoke any functions, just return the result. - yield return this.GetStreamingChatContentFromChatContent(messageContent); + yield return this.GetStreamingChatContentFromChatContent(messageContent, state.Iteration); } } finally @@ -617,7 +617,7 @@ private static GeminiRequest CreateRequest( return geminiRequest; } - private GeminiStreamingChatMessageContent GetStreamingChatContentFromChatContent(GeminiChatMessageContent message) + private GeminiStreamingChatMessageContent GetStreamingChatContentFromChatContent(GeminiChatMessageContent message, int requestIndex) { if (message.CalledToolResult is not null) { @@ -627,7 +627,10 @@ private GeminiStreamingChatMessageContent GetStreamingChatContentFromChatContent modelId: this._modelId, calledToolResult: message.CalledToolResult, metadata: message.Metadata, - choiceIndex: message.Metadata?.Index ?? 0); + choiceIndex: message.Metadata?.Index ?? 0) + { + RequestIndex = requestIndex + }; } if (message.ToolCalls is not null) @@ -638,7 +641,10 @@ private GeminiStreamingChatMessageContent GetStreamingChatContentFromChatContent modelId: this._modelId, toolCalls: message.ToolCalls, metadata: message.Metadata, - choiceIndex: message.Metadata?.Index ?? 0); + choiceIndex: message.Metadata?.Index ?? 0) + { + RequestIndex = requestIndex + }; } return new GeminiStreamingChatMessageContent( @@ -646,7 +652,10 @@ private GeminiStreamingChatMessageContent GetStreamingChatContentFromChatContent content: message.Content, modelId: this._modelId, choiceIndex: message.Metadata?.Index ?? 0, - metadata: message.Metadata); + metadata: message.Metadata) + { + RequestIndex = requestIndex + }; } private static void ValidateAutoInvoke(bool autoInvoke, int resultsPerPrompt) diff --git a/dotnet/src/Connectors/Connectors.MistralAI/Client/MistralClient.cs b/dotnet/src/Connectors/Connectors.MistralAI/Client/MistralClient.cs index daff20926bdf..3b14bd9bfd2b 100644 --- a/dotnet/src/Connectors/Connectors.MistralAI/Client/MistralClient.cs +++ b/dotnet/src/Connectors/Connectors.MistralAI/Client/MistralClient.cs @@ -287,7 +287,7 @@ internal async IAsyncEnumerable GetStreamingChatMes IAsyncEnumerable response; try { - response = this.StreamChatMessageContentsAsync(chatHistory, mistralExecutionSettings, chatRequest, modelId, cancellationToken); + response = this.StreamChatMessageContentsAsync(chatHistory, mistralExecutionSettings, chatRequest, modelId, requestIndex, cancellationToken); } catch (Exception e) when (activity is not null) { @@ -459,7 +459,7 @@ internal async IAsyncEnumerable GetStreamingChatMes var lastChatMessage = chatHistory.Last(); - yield return new StreamingChatMessageContent(lastChatMessage.Role, lastChatMessage.Content); + yield return new StreamingChatMessageContent(lastChatMessage.Role, lastChatMessage.Content) { RequestIndex = requestIndex }; yield break; } } @@ -498,7 +498,7 @@ internal async IAsyncEnumerable GetStreamingChatMes } } - private async IAsyncEnumerable StreamChatMessageContentsAsync(ChatHistory chatHistory, MistralAIPromptExecutionSettings executionSettings, ChatCompletionRequest chatRequest, string modelId, [EnumeratorCancellation] CancellationToken cancellationToken) + private async IAsyncEnumerable StreamChatMessageContentsAsync(ChatHistory chatHistory, MistralAIPromptExecutionSettings executionSettings, ChatCompletionRequest chatRequest, string modelId, int requestIndex, [EnumeratorCancellation] CancellationToken cancellationToken) { this.ValidateChatHistory(chatHistory); @@ -506,13 +506,13 @@ private async IAsyncEnumerable StreamChatMessageCon using var httpRequestMessage = this.CreatePost(chatRequest, endpoint, this._apiKey, stream: true); using var response = await this.SendStreamingRequestAsync(httpRequestMessage, cancellationToken).ConfigureAwait(false); var responseStream = await response.Content.ReadAsStreamAndTranslateExceptionAsync(cancellationToken).ConfigureAwait(false); - await foreach (var streamingChatContent in this.ProcessChatResponseStreamAsync(responseStream, modelId, cancellationToken).ConfigureAwait(false)) + await foreach (var streamingChatContent in this.ProcessChatResponseStreamAsync(responseStream, modelId, requestIndex, cancellationToken).ConfigureAwait(false)) { yield return streamingChatContent; } } - private async IAsyncEnumerable ProcessChatResponseStreamAsync(Stream stream, string modelId, [EnumeratorCancellation] CancellationToken cancellationToken) + private async IAsyncEnumerable ProcessChatResponseStreamAsync(Stream stream, string modelId, int requestIndex, [EnumeratorCancellation] CancellationToken cancellationToken) { IAsyncEnumerator? responseEnumerator = null; @@ -536,7 +536,10 @@ private async IAsyncEnumerable ProcessChatResponseS modelId: modelId, encoding: chunk.GetEncoding(), innerContent: chunk, - metadata: chunk.GetMetadata()); + metadata: chunk.GetMetadata()) + { + RequestIndex = requestIndex + }; } } } diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs b/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs index a3adcbef798d..50660b3a0889 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs @@ -309,7 +309,7 @@ internal async IAsyncEnumerable GetStreamingC OpenAIFunctionToolCall.TrackStreamingToolingUpdate(chatCompletionUpdate.ToolCallUpdates, ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); } - var openAIStreamingChatMessageContent = new OpenAIStreamingChatMessageContent(chatCompletionUpdate, 0, targetModel, metadata); + var openAIStreamingChatMessageContent = new OpenAIStreamingChatMessageContent(chatCompletionUpdate, 0, targetModel, metadata) { RequestIndex = requestIndex }; if (openAIStreamingChatMessageContent.ToolCallUpdates is not null) { @@ -383,7 +383,7 @@ internal async IAsyncEnumerable GetStreamingC if (lastMessage != null) { - yield return new OpenAIStreamingChatMessageContent(lastMessage.Role, lastMessage.Content); + yield return new OpenAIStreamingChatMessageContent(lastMessage.Role, lastMessage.Content) { RequestIndex = requestIndex }; yield break; } diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingChatMessageContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingChatMessageContent.cs index 9e7325b771c2..5f4cf6cf1896 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingChatMessageContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingChatMessageContent.cs @@ -102,6 +102,11 @@ public Encoding Encoding } } + /// + /// Request sequence index of function invocation process. + /// + public int RequestIndex { get; init; } = 0; + /// /// Initializes a new instance of the class. /// From ded43b8069587d2a46ce8b23caa0e0ab25ca0aaa Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Wed, 8 Jan 2025 18:14:31 +0000 Subject: [PATCH 2/7] update property XML comment --- .../Contents/StreamingChatMessageContent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingChatMessageContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingChatMessageContent.cs index 5f4cf6cf1896..5faf156b4b24 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingChatMessageContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingChatMessageContent.cs @@ -103,7 +103,7 @@ public Encoding Encoding } /// - /// Request sequence index of function invocation process. + /// Index of the request that produced this message content /// public int RequestIndex { get; init; } = 0; From 1118e3d74e3c813332a695c96c109bd23db65dbc Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Mon, 13 Jan 2025 12:44:55 +0000 Subject: [PATCH 3/7] move request index to streaming function call update --- .../Clients/GeminiChatCompletionClient.cs | 23 ++++++------------- .../Client/MistralClient.cs | 15 +++++------- .../Core/ClientCore.ChatCompletion.cs | 9 +++++--- .../Contents/StreamingChatMessageContent.cs | 5 ---- .../StreamingFunctionCallUpdateContent.cs | 5 ++++ 5 files changed, 24 insertions(+), 33 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.Google/Core/Gemini/Clients/GeminiChatCompletionClient.cs b/dotnet/src/Connectors/Connectors.Google/Core/Gemini/Clients/GeminiChatCompletionClient.cs index 95122f05cdc4..39c357e28528 100644 --- a/dotnet/src/Connectors/Connectors.Google/Core/Gemini/Clients/GeminiChatCompletionClient.cs +++ b/dotnet/src/Connectors/Connectors.Google/Core/Gemini/Clients/GeminiChatCompletionClient.cs @@ -340,10 +340,10 @@ private async IAsyncEnumerable GetStreamingChatMess // This scenario should not happen but I leave it as a precaution state.AutoInvoke = false; // We return the first message - yield return this.GetStreamingChatContentFromChatContent(messageContent, state.Iteration); + yield return this.GetStreamingChatContentFromChatContent(messageContent); // We return the second message messageContent = chatResponsesEnumerator.Current; - yield return this.GetStreamingChatContentFromChatContent(messageContent, state.Iteration); + yield return this.GetStreamingChatContentFromChatContent(messageContent); continue; } @@ -356,7 +356,7 @@ private async IAsyncEnumerable GetStreamingChatMess state.AutoInvoke = false; // If we don't want to attempt to invoke any functions, just return the result. - yield return this.GetStreamingChatContentFromChatContent(messageContent, state.Iteration); + yield return this.GetStreamingChatContentFromChatContent(messageContent); } } finally @@ -617,7 +617,7 @@ private static GeminiRequest CreateRequest( return geminiRequest; } - private GeminiStreamingChatMessageContent GetStreamingChatContentFromChatContent(GeminiChatMessageContent message, int requestIndex) + private GeminiStreamingChatMessageContent GetStreamingChatContentFromChatContent(GeminiChatMessageContent message) { if (message.CalledToolResult is not null) { @@ -627,10 +627,7 @@ private GeminiStreamingChatMessageContent GetStreamingChatContentFromChatContent modelId: this._modelId, calledToolResult: message.CalledToolResult, metadata: message.Metadata, - choiceIndex: message.Metadata?.Index ?? 0) - { - RequestIndex = requestIndex - }; + choiceIndex: message.Metadata?.Index ?? 0); } if (message.ToolCalls is not null) @@ -641,10 +638,7 @@ private GeminiStreamingChatMessageContent GetStreamingChatContentFromChatContent modelId: this._modelId, toolCalls: message.ToolCalls, metadata: message.Metadata, - choiceIndex: message.Metadata?.Index ?? 0) - { - RequestIndex = requestIndex - }; + choiceIndex: message.Metadata?.Index ?? 0); } return new GeminiStreamingChatMessageContent( @@ -652,10 +646,7 @@ private GeminiStreamingChatMessageContent GetStreamingChatContentFromChatContent content: message.Content, modelId: this._modelId, choiceIndex: message.Metadata?.Index ?? 0, - metadata: message.Metadata) - { - RequestIndex = requestIndex - }; + metadata: message.Metadata); } private static void ValidateAutoInvoke(bool autoInvoke, int resultsPerPrompt) diff --git a/dotnet/src/Connectors/Connectors.MistralAI/Client/MistralClient.cs b/dotnet/src/Connectors/Connectors.MistralAI/Client/MistralClient.cs index 3b14bd9bfd2b..daff20926bdf 100644 --- a/dotnet/src/Connectors/Connectors.MistralAI/Client/MistralClient.cs +++ b/dotnet/src/Connectors/Connectors.MistralAI/Client/MistralClient.cs @@ -287,7 +287,7 @@ internal async IAsyncEnumerable GetStreamingChatMes IAsyncEnumerable response; try { - response = this.StreamChatMessageContentsAsync(chatHistory, mistralExecutionSettings, chatRequest, modelId, requestIndex, cancellationToken); + response = this.StreamChatMessageContentsAsync(chatHistory, mistralExecutionSettings, chatRequest, modelId, cancellationToken); } catch (Exception e) when (activity is not null) { @@ -459,7 +459,7 @@ internal async IAsyncEnumerable GetStreamingChatMes var lastChatMessage = chatHistory.Last(); - yield return new StreamingChatMessageContent(lastChatMessage.Role, lastChatMessage.Content) { RequestIndex = requestIndex }; + yield return new StreamingChatMessageContent(lastChatMessage.Role, lastChatMessage.Content); yield break; } } @@ -498,7 +498,7 @@ internal async IAsyncEnumerable GetStreamingChatMes } } - private async IAsyncEnumerable StreamChatMessageContentsAsync(ChatHistory chatHistory, MistralAIPromptExecutionSettings executionSettings, ChatCompletionRequest chatRequest, string modelId, int requestIndex, [EnumeratorCancellation] CancellationToken cancellationToken) + private async IAsyncEnumerable StreamChatMessageContentsAsync(ChatHistory chatHistory, MistralAIPromptExecutionSettings executionSettings, ChatCompletionRequest chatRequest, string modelId, [EnumeratorCancellation] CancellationToken cancellationToken) { this.ValidateChatHistory(chatHistory); @@ -506,13 +506,13 @@ private async IAsyncEnumerable StreamChatMessageCon using var httpRequestMessage = this.CreatePost(chatRequest, endpoint, this._apiKey, stream: true); using var response = await this.SendStreamingRequestAsync(httpRequestMessage, cancellationToken).ConfigureAwait(false); var responseStream = await response.Content.ReadAsStreamAndTranslateExceptionAsync(cancellationToken).ConfigureAwait(false); - await foreach (var streamingChatContent in this.ProcessChatResponseStreamAsync(responseStream, modelId, requestIndex, cancellationToken).ConfigureAwait(false)) + await foreach (var streamingChatContent in this.ProcessChatResponseStreamAsync(responseStream, modelId, cancellationToken).ConfigureAwait(false)) { yield return streamingChatContent; } } - private async IAsyncEnumerable ProcessChatResponseStreamAsync(Stream stream, string modelId, int requestIndex, [EnumeratorCancellation] CancellationToken cancellationToken) + private async IAsyncEnumerable ProcessChatResponseStreamAsync(Stream stream, string modelId, [EnumeratorCancellation] CancellationToken cancellationToken) { IAsyncEnumerator? responseEnumerator = null; @@ -536,10 +536,7 @@ private async IAsyncEnumerable ProcessChatResponseS modelId: modelId, encoding: chunk.GetEncoding(), innerContent: chunk, - metadata: chunk.GetMetadata()) - { - RequestIndex = requestIndex - }; + metadata: chunk.GetMetadata()); } } } diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs b/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs index 50660b3a0889..6b6a039a0acd 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs @@ -309,7 +309,7 @@ internal async IAsyncEnumerable GetStreamingC OpenAIFunctionToolCall.TrackStreamingToolingUpdate(chatCompletionUpdate.ToolCallUpdates, ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); } - var openAIStreamingChatMessageContent = new OpenAIStreamingChatMessageContent(chatCompletionUpdate, 0, targetModel, metadata) { RequestIndex = requestIndex }; + var openAIStreamingChatMessageContent = new OpenAIStreamingChatMessageContent(chatCompletionUpdate, 0, targetModel, metadata); if (openAIStreamingChatMessageContent.ToolCallUpdates is not null) { @@ -332,7 +332,10 @@ internal async IAsyncEnumerable GetStreamingC callId: functionCallUpdate.ToolCallId, name: functionCallUpdate.FunctionName, arguments: streamingArguments, - functionCallIndex: functionCallUpdate.Index)); + functionCallIndex: functionCallUpdate.Index) + { + RequestIndex = requestIndex, + }); } } streamedContents?.Add(openAIStreamingChatMessageContent); @@ -383,7 +386,7 @@ internal async IAsyncEnumerable GetStreamingC if (lastMessage != null) { - yield return new OpenAIStreamingChatMessageContent(lastMessage.Role, lastMessage.Content) { RequestIndex = requestIndex }; + yield return new OpenAIStreamingChatMessageContent(lastMessage.Role, lastMessage.Content); yield break; } diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingChatMessageContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingChatMessageContent.cs index 5faf156b4b24..9e7325b771c2 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingChatMessageContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingChatMessageContent.cs @@ -102,11 +102,6 @@ public Encoding Encoding } } - /// - /// Index of the request that produced this message content - /// - public int RequestIndex { get; init; } = 0; - /// /// Initializes a new instance of the class. /// diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingFunctionCallUpdateContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingFunctionCallUpdateContent.cs index 48280aad3bd2..b93aeaa0ada2 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingFunctionCallUpdateContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingFunctionCallUpdateContent.cs @@ -29,6 +29,11 @@ public class StreamingFunctionCallUpdateContent : StreamingKernelContent /// public int FunctionCallIndex { get; init; } + /// + /// Index of the request that produced this message content + /// + public int RequestIndex { get; init; } = 0; + /// /// Creates a new instance of the class. /// From fdc69dac71cc37b53e65db74da828f58fc6a9a8b Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Mon, 13 Jan 2025 19:05:09 +0000 Subject: [PATCH 4/7] extend the builder to support function building from function chunks from different requests --- .../FunctionCalling/FunctionCalling.cs | 24 ++++---- .../Contents/FunctionCallContentBuilder.cs | 25 +++++---- .../FunctionCallContentBuilderTests.cs | 55 ++++++++++++++++++- 3 files changed, 80 insertions(+), 24 deletions(-) diff --git a/dotnet/samples/Concepts/FunctionCalling/FunctionCalling.cs b/dotnet/samples/Concepts/FunctionCalling/FunctionCalling.cs index de6c65fa623c..1070f989a484 100644 --- a/dotnet/samples/Concepts/FunctionCalling/FunctionCalling.cs +++ b/dotnet/samples/Concepts/FunctionCalling/FunctionCalling.cs @@ -72,7 +72,7 @@ public async Task RunPromptWithAutoFunctionChoiceBehaviorAdvertisingAllKernelFun OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }; - Console.WriteLine(await kernel.InvokePromptAsync("Given the current time of day and weather, what is the likely color of the sky in Boston?", new(settings))); + Console.WriteLine(await kernel.InvokePromptAsync("What is the likely color of the sky in Boston today?", new(settings))); // Expected output: "Boston is currently experiencing a rainy day, hence, the likely color of the sky in Boston is grey." } @@ -104,7 +104,7 @@ public async Task RunPromptWithNoneFunctionChoiceBehaviorAdvertisingAllKernelFun OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.None() }; - Console.WriteLine(await kernel.InvokePromptAsync("Tell me which provided functions I would need to call to get the color of the sky in Boston on a specified date.", new(settings))); + Console.WriteLine(await kernel.InvokePromptAsync("Tell me which provided functions I would need to call to get the color of the sky in Boston for today.", new(settings))); // Expected output: "You would first call the `HelperFunctions-GetCurrentUtcDateTime` function to get the current date time in UTC. Then, you would use the `HelperFunctions-GetWeatherForCity` function, // passing in the city name as 'Boston' and the retrieved UTC date time. Note, however, that these functions won't directly tell you the color of the sky. @@ -122,7 +122,7 @@ public async Task RunPromptTemplateConfigWithAutoFunctionChoiceBehaviorAdvertisi // The `function_choice_behavior.functions` property is omitted which is equivalent to providing all kernel functions to the AI model. string promptTemplateConfig = """ template_format: semantic-kernel - template: Given the current time of day and weather, what is the likely color of the sky in Boston? + template: What is the likely color of the sky in Boston today? execution_settings: default: function_choice_behavior: @@ -177,7 +177,7 @@ public async Task RunNonStreamingChatCompletionApiWithAutomaticFunctionInvocatio IChatCompletionService chatCompletionService = kernel.GetRequiredService(); ChatMessageContent result = await chatCompletionService.GetChatMessageContentAsync( - "Given the current time of day and weather, what is the likely color of the sky in Boston?", + "What is the likely color of the sky in Boston today?", settings, kernel); @@ -204,7 +204,7 @@ public async Task RunStreamingChatCompletionApiWithAutomaticFunctionInvocationAs // Act await foreach (var update in chatCompletionService.GetStreamingChatMessageContentsAsync( - "Given the current time of day and weather, what is the likely color of the sky in Boston?", + "What is the likely color of the sky in Boston today?", settings, kernel)) { @@ -231,7 +231,7 @@ public async Task RunNonStreamingChatCompletionApiWithManualFunctionInvocationAs OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = Microsoft.SemanticKernel.FunctionChoiceBehavior.Auto(autoInvoke: false) }; ChatHistory chatHistory = []; - chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); + chatHistory.AddUserMessage("What is the likely color of the sky in Boston today?"); while (true) { @@ -293,7 +293,7 @@ public async Task RunStreamingChatCompletionApiWithManualFunctionCallingAsync() // Create chat history with the initial user message ChatHistory chatHistory = []; - chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); + chatHistory.AddUserMessage("What is the likely color of the sky in Boston today?"); while (true) { @@ -360,7 +360,7 @@ public async Task RunNonStreamingPromptWithSimulatedFunctionAsync() OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = Microsoft.SemanticKernel.FunctionChoiceBehavior.Auto(autoInvoke: false) }; ChatHistory chatHistory = []; - chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); + chatHistory.AddUserMessage("What is the likely color of the sky in Boston today?"); while (true) { @@ -411,7 +411,7 @@ public async Task DisableFunctionCallingAsync() // Alternatively, either omit assigning anything to the `FunctionChoiceBehavior` property or assign null to it to also disable function calling. OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(functions: []) }; - Console.WriteLine(await kernel.InvokePromptAsync("Given the current time of day and weather, what is the likely color of the sky in Boston?", new(settings))); + Console.WriteLine(await kernel.InvokePromptAsync("What is the likely color of the sky in Boston today?", new(settings))); // Expected output: "Sorry, I cannot answer this question as it requires real-time information which I, as a text-based model, cannot access." } @@ -538,8 +538,8 @@ private static Kernel CreateKernel() kernel.ImportPluginFromFunctions("HelperFunctions", [ kernel.CreateFunctionFromMethod(() => new List { "Squirrel Steals Show", "Dog Wins Lottery" }, "GetLatestNewsTitles", "Retrieves latest news titles."), - kernel.CreateFunctionFromMethod(() => DateTime.UtcNow.ToString("R"), "GetCurrentUtcDateTime", "Retrieves the current date time in UTC."), - kernel.CreateFunctionFromMethod((string cityName, string currentDateTime) => + kernel.CreateFunctionFromMethod(() => DateTime.UtcNow.ToString("R"), "GetCurrentDateTimeInUtc", "Retrieves the current date time in UTC."), + kernel.CreateFunctionFromMethod((string cityName, string currentDateTimeInUtc) => cityName switch { "Boston" => "61 and rainy", @@ -550,7 +550,7 @@ private static Kernel CreateKernel() "Sydney" => "75 and sunny", "Tel Aviv" => "80 and sunny", _ => "31 and snowing", - }, "GetWeatherForCity", "Gets the current weather for the specified city"), + }, "GetWeatherForCity", "Gets the current weather for the specified city and specified date time."), ]); return kernel; diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallContentBuilder.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallContentBuilder.cs index 756f6d8959bd..1586a06857f1 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallContentBuilder.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallContentBuilder.cs @@ -15,9 +15,9 @@ namespace Microsoft.SemanticKernel; /// public sealed class FunctionCallContentBuilder { - private Dictionary? _functionCallIdsByIndex = null; - private Dictionary? _functionNamesByIndex = null; - private Dictionary? _functionArgumentBuildersByIndex = null; + private Dictionary? _functionCallIdsByIndex = null; + private Dictionary? _functionNamesByIndex = null; + private Dictionary? _functionArgumentBuildersByIndex = null; private readonly JsonSerializerOptions? _jsonSerializerOptions; /// @@ -70,7 +70,7 @@ public IReadOnlyList Build() for (int i = 0; i < this._functionCallIdsByIndex.Count; i++) { - KeyValuePair functionCallIndexAndId = this._functionCallIdsByIndex.ElementAt(i); + KeyValuePair functionCallIndexAndId = this._functionCallIdsByIndex.ElementAt(i); string? pluginName = null; string functionName = string.Empty; @@ -96,7 +96,7 @@ public IReadOnlyList Build() [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "The warning is shown and should be addressed at the class creation site; there is no need to show it again at the function invocation sites.")] [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "The warning is shown and should be addressed at the class creation site; there is no need to show it again at the function invocation sites.")] - (KernelArguments? Arguments, Exception? Exception) GetFunctionArgumentsSafe(int functionCallIndex) + (KernelArguments? Arguments, Exception? Exception) GetFunctionArgumentsSafe(string functionCallIndex) { if (this._jsonSerializerOptions is not null) { @@ -118,7 +118,7 @@ public IReadOnlyList Build() /// A tuple containing the KernelArguments and an Exception if any. [RequiresUnreferencedCode("Uses reflection to deserialize function arguments if no JSOs are provided, making it incompatible with AOT scenarios.")] [RequiresDynamicCode("Uses reflection to deserialize function arguments if no JSOs are provided, making it incompatible with AOT scenarios.")] - private (KernelArguments? Arguments, Exception? Exception) GetFunctionArguments(int functionCallIndex, JsonSerializerOptions? jsonSerializerOptions = null) + private (KernelArguments? Arguments, Exception? Exception) GetFunctionArguments(string functionCallIndex, JsonSerializerOptions? jsonSerializerOptions = null) { if (this._functionArgumentBuildersByIndex is null || !this._functionArgumentBuildersByIndex.TryGetValue(functionCallIndex, out StringBuilder? functionArgumentsBuilder)) @@ -170,7 +170,7 @@ public IReadOnlyList Build() /// The dictionary of function call IDs by function call index. /// The dictionary of function names by function call index. /// The dictionary of function argument builders by function call index. - private static void TrackStreamingFunctionCallUpdate(StreamingFunctionCallUpdateContent update, ref Dictionary? functionCallIdsByIndex, ref Dictionary? functionNamesByIndex, ref Dictionary? functionArgumentBuildersByIndex) + private static void TrackStreamingFunctionCallUpdate(StreamingFunctionCallUpdateContent update, ref Dictionary? functionCallIdsByIndex, ref Dictionary? functionNamesByIndex, ref Dictionary? functionArgumentBuildersByIndex) { if (update is null) { @@ -178,25 +178,28 @@ private static void TrackStreamingFunctionCallUpdate(StreamingFunctionCallUpdate return; } + // Create index that is unique across many requests for a given streaming function call update content. + var functionCallIndex = $"{update.RequestIndex}{update.FunctionCallIndex}"; + // If we have an call id, ensure the index is being tracked. Even if it's not a function update, // we want to keep track of it so we can send back an error. if (update.CallId is string id && !string.IsNullOrEmpty(id)) { - (functionCallIdsByIndex ??= [])[update.FunctionCallIndex] = id; + (functionCallIdsByIndex ??= [])[functionCallIndex] = id; } // Ensure we're tracking the function's name. if (update.Name is string name && !string.IsNullOrEmpty(name)) { - (functionNamesByIndex ??= [])[update.FunctionCallIndex] = name; + (functionNamesByIndex ??= [])[functionCallIndex] = name; } // Ensure we're tracking the function's arguments. if (update.Arguments is string argumentsUpdate) { - if (!(functionArgumentBuildersByIndex ??= []).TryGetValue(update.FunctionCallIndex, out StringBuilder? arguments)) + if (!(functionArgumentBuildersByIndex ??= []).TryGetValue(functionCallIndex, out StringBuilder? arguments)) { - functionArgumentBuildersByIndex[update.FunctionCallIndex] = arguments = new(); + functionArgumentBuildersByIndex[functionCallIndex] = arguments = new(); } arguments.Append(argumentsUpdate); diff --git a/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallBuilder/FunctionCallContentBuilderTests.cs b/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallBuilder/FunctionCallContentBuilderTests.cs index 7857f7188780..e214478ce657 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallBuilder/FunctionCallContentBuilderTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Contents/FunctionCallBuilder/FunctionCallContentBuilderTests.cs @@ -130,6 +130,58 @@ public void ItShouldBuildFunctionCallContentForManyFunctions(JsonSerializerOptio Assert.Null(functionCall2.Exception); } + [Theory] + [ClassData(typeof(TestJsonSerializerOptionsForKernelArguments))] + public void ItShouldBuildFunctionCallContentForManyFunctionsCameInDifferentRequests(JsonSerializerOptions? jsos) + { + // Arrange + var sut = jsos is not null ? new FunctionCallContentBuilder(jsos) : new FunctionCallContentBuilder(); + + // Act + + // f1 call was streamed as part of the first request + var f1_update1 = CreateStreamingContentWithFunctionCallUpdate(choiceIndex: 0, functionCallIndex: 0, requestIndex: 0, callId: "f_1", name: "WeatherUtils-GetTemperature", arguments: null); + sut.Append(f1_update1); + + var f1_update2 = CreateStreamingContentWithFunctionCallUpdate(choiceIndex: 0, functionCallIndex: 0, requestIndex: 0, callId: null, name: null, arguments: "{\"city\":"); + sut.Append(f1_update2); + + var f1_update3 = CreateStreamingContentWithFunctionCallUpdate(choiceIndex: 0, functionCallIndex: 0, requestIndex: 0, callId: null, name: null, arguments: "\"Seattle\"}"); + sut.Append(f1_update3); + + // f2 call was streamed as part of the second request + var f2_update1 = CreateStreamingContentWithFunctionCallUpdate(choiceIndex: 0, functionCallIndex: 0, requestIndex: 1, callId: null, name: "WeatherUtils-GetHumidity", arguments: null); + sut.Append(f2_update1); + + var f2_update2 = CreateStreamingContentWithFunctionCallUpdate(choiceIndex: 0, functionCallIndex: 0, requestIndex: 1, callId: "f_2", name: null, arguments: null); + sut.Append(f2_update2); + + var f2_update3 = CreateStreamingContentWithFunctionCallUpdate(choiceIndex: 0, functionCallIndex: 0, requestIndex: 1, callId: null, name: null, arguments: "{\"city\":"); + sut.Append(f2_update3); + + var f2_update4 = CreateStreamingContentWithFunctionCallUpdate(choiceIndex: 0, functionCallIndex: 0, requestIndex: 1, callId: null, name: null, arguments: "\"Georgia\"}"); + sut.Append(f2_update4); + + var functionCalls = sut.Build(); + + // Assert + Assert.Equal(2, functionCalls.Count); + + var functionCall1 = functionCalls.ElementAt(0); + Assert.Equal("f_1", functionCall1.Id); + Assert.Equal("WeatherUtils", functionCall1.PluginName); + Assert.Equal("GetTemperature", functionCall1.FunctionName); + Assert.Equal("Seattle", functionCall1.Arguments?["city"]); + Assert.Null(functionCall1.Exception); + + var functionCall2 = functionCalls.ElementAt(1); + Assert.Equal("f_2", functionCall2.Id); + Assert.Equal("WeatherUtils", functionCall2.PluginName); + Assert.Equal("GetHumidity", functionCall2.FunctionName); + Assert.Equal("Georgia", functionCall2.Arguments?["city"]); + Assert.Null(functionCall2.Exception); + } + [Theory] [ClassData(typeof(TestJsonSerializerOptionsForKernelArguments))] public void ItShouldCaptureArgumentsDeserializationException(JsonSerializerOptions? jsos) @@ -160,7 +212,7 @@ public void ItShouldCaptureArgumentsDeserializationException(JsonSerializerOptio Assert.NotNull(functionCall.Exception); } - private static StreamingChatMessageContent CreateStreamingContentWithFunctionCallUpdate(int choiceIndex, int functionCallIndex, string? callId, string? name, string? arguments) + private static StreamingChatMessageContent CreateStreamingContentWithFunctionCallUpdate(int choiceIndex, int functionCallIndex, string? callId, string? name, string? arguments, int requestIndex = 0) { var content = new StreamingChatMessageContent(AuthorRole.Assistant, null); @@ -171,6 +223,7 @@ private static StreamingChatMessageContent CreateStreamingContentWithFunctionCal CallId = callId, Name = name, Arguments = arguments, + RequestIndex = requestIndex }); return content; From b90c3514a5edfab0c8b38f49bff42a5bf520fb78 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Mon, 13 Jan 2025 19:08:34 +0000 Subject: [PATCH 5/7] update comment --- .../Contents/FunctionCallContentBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallContentBuilder.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallContentBuilder.cs index 1586a06857f1..5d90000d9f64 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallContentBuilder.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallContentBuilder.cs @@ -178,7 +178,7 @@ private static void TrackStreamingFunctionCallUpdate(StreamingFunctionCallUpdate return; } - // Create index that is unique across many requests for a given streaming function call update content. + // Create index that is unique across many requests. var functionCallIndex = $"{update.RequestIndex}{update.FunctionCallIndex}"; // If we have an call id, ensure the index is being tracked. Even if it's not a function update, From 1afb20034edfff41e272514f59cc4f972f2dcd23 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Tue, 14 Jan 2025 10:41:52 +0000 Subject: [PATCH 6/7] add separator between request index and function callindex --- .../Contents/FunctionCallContentBuilder.cs | 2 +- .../Contents/StreamingFunctionCallUpdateContent.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallContentBuilder.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallContentBuilder.cs index 5d90000d9f64..ff4f6af246e5 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallContentBuilder.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/FunctionCallContentBuilder.cs @@ -179,7 +179,7 @@ private static void TrackStreamingFunctionCallUpdate(StreamingFunctionCallUpdate } // Create index that is unique across many requests. - var functionCallIndex = $"{update.RequestIndex}{update.FunctionCallIndex}"; + var functionCallIndex = $"{update.RequestIndex}-{update.FunctionCallIndex}"; // If we have an call id, ensure the index is being tracked. Even if it's not a function update, // we want to keep track of it so we can send back an error. diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingFunctionCallUpdateContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingFunctionCallUpdateContent.cs index b93aeaa0ada2..fccb4bd9eea3 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingFunctionCallUpdateContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingFunctionCallUpdateContent.cs @@ -30,7 +30,7 @@ public class StreamingFunctionCallUpdateContent : StreamingKernelContent public int FunctionCallIndex { get; init; } /// - /// Index of the request that produced this message content + /// Index of the request that produced this message content. /// public int RequestIndex { get; init; } = 0; From 3ef80ad964fd6538e1d91b65a0db9d2d9d5e8995 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Tue, 14 Jan 2025 10:54:42 +0000 Subject: [PATCH 7/7] annotate request index with the experimental attribute --- .../Contents/StreamingFunctionCallUpdateContent.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingFunctionCallUpdateContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingFunctionCallUpdateContent.cs index fccb4bd9eea3..d879fa983926 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingFunctionCallUpdateContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingFunctionCallUpdateContent.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Diagnostics.CodeAnalysis; using System.Text; namespace Microsoft.SemanticKernel; @@ -32,6 +33,7 @@ public class StreamingFunctionCallUpdateContent : StreamingKernelContent /// /// Index of the request that produced this message content. /// + [Experimental("SKEXP0001")] public int RequestIndex { get; init; } = 0; ///