From e39b28156dd933607b8e20d8555f42202f2a1a62 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Tue, 3 Dec 2024 18:28:32 +0000 Subject: [PATCH 1/2] Fix streaming function calling --- .../FunctionInvokingChatClient.cs | 2 +- .../FunctionInvokingChatClientTests.cs | 52 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs index e1e4542d5d0..20c70eb05d4 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs @@ -325,7 +325,7 @@ public override async IAsyncEnumerable CompleteSt // If there were any, remove them from the update. We do this before yielding the update so // that we're not modifying an instance already provided back to the caller. int addedFccs = functionCallContents.Count - preFccCount; - if (addedFccs > preFccCount) + if (addedFccs > 0) { update.Contents = addedFccs == update.Contents.Count ? [] : update.Contents.Where(c => c is not FunctionCallContent).ToList(); diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs index 1dc91797037..4c037d69a38 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs @@ -494,6 +494,58 @@ async Task InvokeAsync(Func work) } } + [Fact] + public async Task SupportsMultipleCallsInSameStreamingUpdate() + { + var options = new ChatOptions + { + Tools = [AIFunctionFactory.Create((string text) => $"Result for {text}", "Func1")] + }; + + var messages = new List + { + new(ChatRole.User, "Hello"), + }; + + using var innerClient = new TestChatClient + { + CompleteStreamingAsyncCallback = (chatContents, chatOptions, cancellationToken) => + { + // If the conversation is just starting, issue two calls in a single streaming update + // Otherwise just end the conversation + return chatContents.Last().Text == "Hello" + ? YieldAsync( + new StreamingChatCompletionUpdate { Contents = [new FunctionCallContent("callId1", "Func1", new Dictionary { ["text"] = "Input 1" })] }, + new StreamingChatCompletionUpdate { Contents = [new FunctionCallContent("callId2", "Func1", new Dictionary { ["text"] = "Input 2" })] }) + : YieldAsync( + new StreamingChatCompletionUpdate { Contents = [new TextContent("OK bye")] }); + } + }; + + using var client = new FunctionInvokingChatClient(innerClient); + + var updates = new List(); + await foreach (var update in client.CompleteStreamingAsync(messages, options, CancellationToken.None)) + { + updates.Add(update); + } + + // Message history should now include the FCCs and FRCs + Assert.Collection(messages, + m => Assert.Equal("Hello", Assert.IsType(Assert.Single(m.Contents)).Text), + m => Assert.Collection(m.Contents, + c => Assert.Equal("Input 1", Assert.IsType(c).Arguments!["text"]), + c => Assert.Equal("Input 2", Assert.IsType(c).Arguments!["text"])), + m => Assert.Collection(m.Contents, + c => Assert.Equal("Result for Input 1", Assert.IsType(c).Result?.ToString()), + c => Assert.Equal("Result for Input 2", Assert.IsType(c).Result?.ToString()))); + + // The returned updates should *not* include the FCCs and FRCs + var allUpdateContents = updates.SelectMany(updates => updates.Contents).ToList(); + var singleUpdateContent = Assert.IsType(Assert.Single(allUpdateContents)); + Assert.Equal("OK bye", singleUpdateContent.Text); + } + private static async Task> InvokeAndAssertAsync( ChatOptions options, List plan, From 89f15a6836c933aad9d14b46e18273a70f84b319 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Tue, 3 Dec 2024 18:33:28 +0000 Subject: [PATCH 2/2] Rename test --- .../ChatCompletion/FunctionInvokingChatClientTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs index 4c037d69a38..a274c6225a7 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs @@ -495,7 +495,7 @@ async Task InvokeAsync(Func work) } [Fact] - public async Task SupportsMultipleCallsInSameStreamingUpdate() + public async Task SupportsConsecutiveStreamingUpdatesWithFunctionCalls() { var options = new ChatOptions { @@ -511,7 +511,7 @@ public async Task SupportsMultipleCallsInSameStreamingUpdate() { CompleteStreamingAsyncCallback = (chatContents, chatOptions, cancellationToken) => { - // If the conversation is just starting, issue two calls in a single streaming update + // If the conversation is just starting, issue two consecutive updates with function calls // Otherwise just end the conversation return chatContents.Last().Text == "Hello" ? YieldAsync(