From f6bf0bed2c91f0dd1cb3d0fffe9e4359b600e55b Mon Sep 17 00:00:00 2001 From: Mikita Slaunikau Date: Tue, 13 Oct 2026 16:58:52 +0400 Subject: [PATCH] .Net: Fix Gemini function-response message role Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Core/Gemini/AuthorRoleConverterTests.cs | 4 +- ...eminiChatGenerationFunctionCallingTests.cs | 47 ++++++++++++++++++- ...GeminiChatStreamingFunctionCallingTests.cs | 2 +- .../Core/Gemini/GeminiRequestTests.cs | 7 +++ .../Core/Gemini/AuthorRoleConverter.cs | 3 +- 5 files changed, 57 insertions(+), 6 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/AuthorRoleConverterTests.cs b/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/AuthorRoleConverterTests.cs index 03005b4fd01f..8d18d514d64a 100644 --- a/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/AuthorRoleConverterTests.cs +++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/AuthorRoleConverterTests.cs @@ -120,7 +120,7 @@ public void WriteWhenRoleIsAssistantReturnsModel() } [Fact] - public void WriteWhenRoleIsToolReturnsFunction() + public void WriteWhenRoleIsToolReturnsUser() { // Arrange var converter = new AuthorRoleConverter(); @@ -131,7 +131,7 @@ public void WriteWhenRoleIsToolReturnsFunction() converter.Write(writer, AuthorRole.Tool, JsonSerializerOptions.Default); // Assert - Assert.Equal("\"function\""u8, bufferWriter.GetSpan().Trim((byte)'\0')); + Assert.Equal("\"user\""u8, bufferWriter.GetSpan().Trim((byte)'\0')); } [Fact] diff --git a/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/Clients/GeminiChatGenerationFunctionCallingTests.cs b/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/Clients/GeminiChatGenerationFunctionCallingTests.cs index f4008897c790..21c1167e069f 100644 --- a/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/Clients/GeminiChatGenerationFunctionCallingTests.cs +++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/Clients/GeminiChatGenerationFunctionCallingTests.cs @@ -147,7 +147,7 @@ public async Task ShouldPassFunctionResponseToRequestAsync() GeminiRequest request = JsonSerializer.Deserialize(this._messageHandlerStub.RequestContent)!; var content = request.Contents.LastOrDefault(); Assert.NotNull(content); - Assert.Equal(AuthorRole.Tool, content.Role); + Assert.Equal(AuthorRole.User, content.Role); var functionResponse = content.Parts![0].FunctionResponse; Assert.NotNull(functionResponse); Assert.Equal(toolCallResponse.FullyQualifiedName, functionResponse.FunctionName); @@ -426,8 +426,9 @@ public async Task ShouldBatchMultipleToolResponsesIntoSingleMessageAsync() Assert.NotNull(request); // Find the content that represents the batched tool responses - var toolResponseContent = request.Contents.FirstOrDefault(c => c.Role == AuthorRole.Tool); + var toolResponseContent = request.Contents.FirstOrDefault(c => c.Parts!.Any(p => p.FunctionResponse != null)); Assert.NotNull(toolResponseContent); + Assert.Equal(AuthorRole.User, toolResponseContent.Role); Assert.NotNull(toolResponseContent.Parts); // Verify that all function responses are included as separate parts in the single message @@ -701,6 +702,48 @@ public async Task ThoughtSignatureIsIncludedInSubsequentRequestAsync() var secondRequestContent = handlerStub.GetRequestContentAsString(1); Assert.NotNull(secondRequestContent); Assert.Contains("test-thought-signature-abc123", secondRequestContent); + + using var requestJson = JsonDocument.Parse(secondRequestContent); + var contents = requestJson.RootElement.GetProperty("contents"); + Assert.Equal("model", contents[3].GetProperty("role").GetString()); + Assert.Equal("test-thought-signature-abc123", contents[3].GetProperty("parts")[0].GetProperty("thoughtSignature").GetString()); + Assert.Equal("user", contents[4].GetProperty("role").GetString()); + Assert.Equal(this._timePluginNow.FullyQualifiedName, contents[4].GetProperty("parts")[0].GetProperty("functionResponse").GetProperty("name").GetString()); + } + + [Fact] + public async Task AutoInvokeSerializesSuccessiveToolResponsesWithUserRoleAsync() + { + using var handlerStub = new MultipleHttpMessageHandlerStub(); + handlerStub.AddJsonResponse(this._responseContentWithFunction); + handlerStub.AddJsonResponse(this._responseContentWithFunction); + handlerStub.AddJsonResponse(this._responseContent); + + using var httpClient = new HttpClient(handlerStub, false); + var client = this.CreateChatCompletionClient(httpClient: httpClient); + var chatHistory = CreateSampleChatHistory(); + var executionSettings = new GeminiPromptExecutionSettings + { + ToolCallBehavior = GeminiToolCallBehavior.AutoInvokeKernelFunctions + }; + + var responses = await client.GenerateChatMessageAsync(chatHistory, executionSettings: executionSettings, kernel: this._kernelWithFunctions); + + Assert.NotEmpty(Assert.Single(responses).Content!); + Assert.Equal(2, chatHistory.Count(m => m.Role == AuthorRole.Tool)); + for (var requestIndex = 1; requestIndex <= 2; requestIndex++) + { + var requestContent = handlerStub.GetRequestContentAsString(requestIndex); + Assert.NotNull(requestContent); + using var requestJson = JsonDocument.Parse(requestContent); + var contents = requestJson.RootElement.GetProperty("contents").EnumerateArray().ToArray(); + var toolResponses = contents.Where(c => c.GetProperty("parts").EnumerateArray() + .Any(p => p.TryGetProperty("functionResponse", out _))).ToArray(); + + Assert.Equal(requestIndex, toolResponses.Length); + Assert.All(toolResponses, content => Assert.Equal("user", content.GetProperty("role").GetString())); + Assert.All(contents, content => Assert.Contains(content.GetProperty("role").GetString(), new[] { "user", "model" })); + } } private static ChatHistory CreateSampleChatHistory() diff --git a/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/Clients/GeminiChatStreamingFunctionCallingTests.cs b/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/Clients/GeminiChatStreamingFunctionCallingTests.cs index 7a6e478cbc4e..ce74c00b8ab1 100644 --- a/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/Clients/GeminiChatStreamingFunctionCallingTests.cs +++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/Clients/GeminiChatStreamingFunctionCallingTests.cs @@ -150,7 +150,7 @@ await client.StreamGenerateChatMessageAsync(chatHistory, executionSettings: exec GeminiRequest request = JsonSerializer.Deserialize(this._messageHandlerStub.RequestContent)!; var content = request.Contents.LastOrDefault(); Assert.NotNull(content); - Assert.Equal(AuthorRole.Tool, content.Role); + Assert.Equal(AuthorRole.User, content.Role); var functionResponse = content.Parts![0].FunctionResponse; Assert.NotNull(functionResponse); Assert.Equal(toolCallResponse.FullyQualifiedName, functionResponse.FunctionName); diff --git a/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/GeminiRequestTests.cs b/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/GeminiRequestTests.cs index fefd1ab0cb61..d46fa1239b8f 100644 --- a/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/GeminiRequestTests.cs +++ b/dotnet/src/Connectors/Connectors.Google.UnitTests/Core/Gemini/GeminiRequestTests.cs @@ -439,6 +439,13 @@ public void FromChatHistoryCalledToolNotNullAddsFunctionResponse() c => string.Equals(c.Parts![0].FunctionResponse!.FunctionName, toolCallResult.FullyQualifiedName, StringComparison.Ordinal)); var args = request.Contents[0].Parts![0].FunctionResponse!.Response.Arguments; Assert.Equal(expectedArgs.ToJsonString(), args.ToJsonString()); + + using var requestJson = JsonDocument.Parse(JsonSerializer.Serialize(request)); + var content = requestJson.RootElement.GetProperty("contents")[0]; + Assert.Equal("user", content.GetProperty("role").GetString()); + var functionResponse = content.GetProperty("parts")[0].GetProperty("functionResponse"); + Assert.Equal(toolCallResult.FullyQualifiedName, functionResponse.GetProperty("name").GetString()); + AssertDeepEquals(JsonSerializer.SerializeToElement(expectedArgs), functionResponse.GetProperty("response").GetProperty("content")); } [Fact] diff --git a/dotnet/src/Connectors/Connectors.Google/Core/Gemini/AuthorRoleConverter.cs b/dotnet/src/Connectors/Connectors.Google/Core/Gemini/AuthorRoleConverter.cs index b2aa0d959abd..a1091c88f15d 100644 --- a/dotnet/src/Connectors/Connectors.Google/Core/Gemini/AuthorRoleConverter.cs +++ b/dotnet/src/Connectors/Connectors.Google/Core/Gemini/AuthorRoleConverter.cs @@ -45,7 +45,8 @@ public override void Write(Utf8JsonWriter writer, AuthorRole? value, JsonSeriali if (value == AuthorRole.Tool) { - writer.WriteStringValue("function"); + // Gemini function responses are carried by user messages. + writer.WriteStringValue("user"); } else if (value == AuthorRole.Assistant) {