From 9691c9c27100bb9de5ff721d78dc9e7473f6060e Mon Sep 17 00:00:00 2001 From: AkiKurisu <2683987717@qq.com> Date: Fri, 27 Mar 2026 00:13:00 +0800 Subject: [PATCH] .NET: Fix role assignment in ChatMessage construction (#4290) * Use actual message role when creating ChatMessage Replace hard-coded ChatRole.User with a ChatRole constructed from the message's Role. The change ensures ToChatMessage and FunctionMessage use the original role (new ChatRole(this.Role)) for both text and contents branches, fixing incorrect role assignment when constructing ChatMessage instances. * Update changes * Fix formatting in ToChatMessage tests --------- Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> --- .../Models/ChatCompletionRequestMessage.cs | 10 +- ...pletionRequestMessageToChatMessageTests.cs | 115 ++++++++++++++++++ 2 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ChatCompletionRequestMessageToChatMessageTests.cs diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ChatCompletions/Models/ChatCompletionRequestMessage.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ChatCompletions/Models/ChatCompletionRequestMessage.cs index 3e9483c616..2433eacbe0 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ChatCompletions/Models/ChatCompletionRequestMessage.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ChatCompletions/Models/ChatCompletionRequestMessage.cs @@ -39,14 +39,16 @@ internal abstract record ChatCompletionRequestMessage /// Thrown when the content is neither text nor AI contents. public virtual ChatMessage ToChatMessage() { + var role = new ChatRole(this.Role); + if (this.Content.IsText) { - return new(ChatRole.User, this.Content.Text); + return new(role, this.Content.Text); } else if (this.Content.IsContents) { var aiContents = this.Content.Contents.Select(MessageContentPartConverter.ToAIContent).Where(c => c is not null).ToList(); - return new ChatMessage(ChatRole.User, aiContents!); + return new ChatMessage(role, aiContents!); } throw new InvalidOperationException("MessageContent has no value"); @@ -165,9 +167,11 @@ internal sealed record FunctionMessage : ChatCompletionRequestMessage /// Thrown when the content is not text. public override ChatMessage ToChatMessage() { + var role = new ChatRole(this.Role); + if (this.Content.IsText) { - return new(ChatRole.User, this.Content.Text); + return new(role, this.Content.Text); } throw new InvalidOperationException("FunctionMessage Content must be text"); diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ChatCompletionRequestMessageToChatMessageTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ChatCompletionRequestMessageToChatMessageTests.cs new file mode 100644 index 0000000000..406f0a32e1 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ChatCompletionRequestMessageToChatMessageTests.cs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Linq; +using System.Text.Json; +using Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions.Models; +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.AI.Hosting.OpenAI.UnitTests; + +/// +/// Tests for ChatCompletionRequestMessage.ToChatMessage() role preservation. +/// Verifies that each message type correctly maps its role to the corresponding ChatRole. +/// +public sealed class ChatCompletionRequestMessageToChatMessageTests +{ + [Theory] + [InlineData("system", """{"role":"system","content":"You are a helpful assistant."}""")] + [InlineData("developer", """{"role":"developer","content":"Follow these rules."}""")] + [InlineData("user", """{"role":"user","content":"Hello!"}""")] + [InlineData("assistant", """{"role":"assistant","content":"Hi there!"}""")] + [InlineData("tool", """{"role":"tool","content":"result","tool_call_id":"call_123"}""")] + public void ToChatMessage_PreservesRole_ForTextContent(string expectedRole, string json) + { + // Arrange + ChatCompletionRequestMessage message = JsonSerializer.Deserialize( + json, ChatCompletions.ChatCompletionsJsonContext.Default.ChatCompletionRequestMessage)!; + + // Act + ChatMessage chatMessage = message.ToChatMessage(); + + // Assert + Assert.Equal(expectedRole, message.Role); + Assert.Equal(new ChatRole(expectedRole), chatMessage.Role); + } + + [Fact] + public void ToChatMessage_FunctionMessage_PreservesRole() + { + // Arrange + const string Json = """{"role":"function","name":"get_weather","content":"sunny"}"""; + ChatCompletionRequestMessage message = JsonSerializer.Deserialize( + Json, ChatCompletions.ChatCompletionsJsonContext.Default.ChatCompletionRequestMessage)!; + + // Act + ChatMessage chatMessage = message.ToChatMessage(); + + // Assert + Assert.Equal("function", message.Role); + Assert.Equal(new ChatRole("function"), chatMessage.Role); + } + + [Theory] + [InlineData("system")] + [InlineData("developer")] + [InlineData("user")] + [InlineData("assistant")] + public void ToChatMessage_PreservesRole_ForMultiPartContent(string expectedRole) + { + // Arrange + string json = $$"""{"role":"{{expectedRole}}","content":[{"type":"text","text":"Hello!"}]}"""; + ChatCompletionRequestMessage message = JsonSerializer.Deserialize( + json, ChatCompletions.ChatCompletionsJsonContext.Default.ChatCompletionRequestMessage)!; + + // Act + ChatMessage chatMessage = message.ToChatMessage(); + + // Assert + Assert.Equal(expectedRole, message.Role); + Assert.Equal(new ChatRole(expectedRole), chatMessage.Role); + } + + [Fact] + public void ToChatMessage_MultiTurnConversation_PreservesAllRoles() + { + // Arrange - simulate a multi-turn conversation + string[] jsons = + [ + """{"role":"system","content":"You are a helpful assistant."}""", + """{"role":"user","content":"Hello!"}""", + """{"role":"assistant","content":"Hi there! How can I help?"}""", + """{"role":"user","content":"What did I just say?"}""" + ]; + + string[] expectedRoles = ["system", "user", "assistant", "user"]; + + // Act + ChatMessage[] chatMessages = jsons + .Select(j => JsonSerializer.Deserialize( + j, ChatCompletions.ChatCompletionsJsonContext.Default.ChatCompletionRequestMessage)!) + .Select(m => m.ToChatMessage()) + .ToArray(); + + // Assert + Assert.Equal(expectedRoles.Length, chatMessages.Length); + for (int i = 0; i < expectedRoles.Length; i++) + { + Assert.Equal(new ChatRole(expectedRoles[i]), chatMessages[i].Role); + } + } + + [Fact] + public void ToChatMessage_PreservesTextContent() + { + // Arrange + const string Json = """{"role":"system","content":"You are a helpful assistant."}"""; + ChatCompletionRequestMessage message = JsonSerializer.Deserialize( + Json, ChatCompletions.ChatCompletionsJsonContext.Default.ChatCompletionRequestMessage)!; + + // Act + ChatMessage chatMessage = message.ToChatMessage(); + + // Assert + Assert.Contains(chatMessage.Contents, c => c is TextContent tc && tc.Text == "You are a helpful assistant."); + } +}