diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatClientChatCompletionService.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatClientChatCompletionService.cs index f447c38b0c99..1aa413918415 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatClientChatCompletionService.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatCompletion/ChatClientChatCompletionService.cs @@ -91,6 +91,9 @@ public async IAsyncEnumerable GetStreamingChatMessa ChatRole? role = null; + List functionCalls = []; + Dictionary functionResults = []; + await foreach (var update in this._chatClient.GetStreamingResponseAsync( chatHistory.ToChatMessageList(), executionSettings.ToChatOptions(kernel), @@ -103,17 +106,27 @@ public async IAsyncEnumerable GetStreamingChatMessa { if (fcc is Microsoft.Extensions.AI.FunctionCallContent functionCallContent) { - chatHistory.Add(new ChatMessage(ChatRole.Assistant, [functionCallContent]).ToChatMessageContent()); + functionCalls.Add(functionCallContent); continue; } if (fcc is Microsoft.Extensions.AI.FunctionResultContent functionResultContent) { - chatHistory.Add(new ChatMessage(ChatRole.Tool, [functionResultContent]).ToChatMessageContent()); + functionResults[functionResultContent.CallId] = functionResultContent; } } yield return update.ToStreamingChatMessageContent(); } + + // Tool result messages must be added immediately after the corresponding tool call to preserve correct conversation order. + foreach (var functionCallContent in functionCalls) + { + chatHistory.Add(new ChatMessage(ChatRole.Assistant, [functionCallContent]).ToChatMessageContent()); + if (functionResults.TryGetValue(functionCallContent.CallId, out var functionResultContent)) + { + chatHistory.Add(new ChatMessage(ChatRole.Tool, [functionResultContent]).ToChatMessageContent()); + } + } } } diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatClientChatCompletionServiceConversionTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatClientChatCompletionServiceConversionTests.cs index 8ef515b416b1..2bc7f4e67eab 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatClientChatCompletionServiceConversionTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatClientChatCompletionServiceConversionTests.cs @@ -647,6 +647,68 @@ public async Task GetStreamingChatMessageContentsAsyncWithMutatingPrepareChatHis Assert.Equal("Original message", originalChatHistory[1].Content); } + [Fact] + public async Task GetStreamingChatMessageContentsAsyncShouldMaintainFunctionCallOrdering() + { + // Arrange + var chatHistory = new ChatHistory(); + + using var chatClient = new TestChatClient + { + CompleteStreamingAsyncDelegate = (messages, options, cancellationToken) => + { + // Simulate calls coming first, then results + return new[] + { + new ChatResponseUpdate(ChatRole.Assistant, [new Microsoft.Extensions.AI.FunctionCallContent("call-1", "Func1", null)]), + new ChatResponseUpdate(ChatRole.Assistant, [new Microsoft.Extensions.AI.FunctionCallContent("call-2", "Func2", null)]), + new ChatResponseUpdate(ChatRole.Tool, [new Microsoft.Extensions.AI.FunctionResultContent("call-1", "result1")]), + new ChatResponseUpdate(ChatRole.Tool, [new Microsoft.Extensions.AI.FunctionResultContent("call-2", "result2")]) + }.ToAsyncEnumerable(); + } + }; + + var service = chatClient.AsChatCompletionService(); + + // Act + await service.GetStreamingChatMessageContentsAsync(chatHistory).ToListAsync(); + + // Assert + Assert.Collection(chatHistory, + call1 => + { + Assert.Equal(AuthorRole.Assistant, call1.Role); + var callContent = Assert.Single(call1.Items); + var functionCall = Assert.IsType(callContent); + Assert.Equal("call-1", functionCall.Id); + Assert.Equal("Func1", functionCall.FunctionName); + }, + result1 => + { + Assert.Equal(AuthorRole.Tool, result1.Role); + var resultContent = Assert.Single(result1.Items); + var functionResult = Assert.IsType(resultContent); + Assert.Equal("call-1", functionResult.CallId); + Assert.Equal("result1", functionResult.Result); + }, + call2 => + { + Assert.Equal(AuthorRole.Assistant, call2.Role); + var callContent = Assert.Single(call2.Items); + var functionCall = Assert.IsType(callContent); + Assert.Equal("call-2", functionCall.Id); + Assert.Equal("Func2", functionCall.FunctionName); + }, + result2 => + { + Assert.Equal(AuthorRole.Tool, result2.Role); + var resultContent = Assert.Single(result2.Items); + var functionResult = Assert.IsType(resultContent); + Assert.Equal("call-2", functionResult.CallId); + Assert.Equal("result2", functionResult.Result); + }); + } + /// /// Test implementation of PromptExecutionSettings that overrides PrepareChatHistoryToRequestAsync. ///