From f1ea75a3550028c93c411094cc77681412208646 Mon Sep 17 00:00:00 2001 From: AkiKurisu <2683987717@qq.com> Date: Thu, 26 Feb 2026 11:35:28 +0800 Subject: [PATCH 1/3] 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. --- .../ChatCompletions/Models/ChatCompletionRequestMessage.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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..4fe9c43aa9 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,15 @@ 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"); @@ -167,7 +168,7 @@ public override ChatMessage ToChatMessage() { if (this.Content.IsText) { - return new(ChatRole.User, this.Content.Text); + return new(new ChatRole(this.Role), this.Content.Text); } throw new InvalidOperationException("FunctionMessage Content must be text"); From 1da9cbfc3d3424376e758569e593c6c21c376dde Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Wed, 11 Mar 2026 16:17:54 +0000 Subject: [PATCH 2/3] Update changes --- .../Models/ChatCompletionRequestMessage.cs | 5 +- ...pletionRequestMessageToChatMessageTests.cs | 115 ++++++++++++++++++ 2 files changed, 119 insertions(+), 1 deletion(-) 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 4fe9c43aa9..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 @@ -40,6 +40,7 @@ internal abstract record ChatCompletionRequestMessage public virtual ChatMessage ToChatMessage() { var role = new ChatRole(this.Role); + if (this.Content.IsText) { return new(role, this.Content.Text); @@ -166,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(new ChatRole(this.Role), 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..437235cab3 --- /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."); + } +} From ac03b07ecb598909d89f59bb5742cf02815b9320 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Thu, 12 Mar 2026 14:16:06 +0000 Subject: [PATCH 3/3] Fix formatting in ToChatMessage tests --- ...pletionRequestMessageToChatMessageTests.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ChatCompletionRequestMessageToChatMessageTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ChatCompletionRequestMessageToChatMessageTests.cs index 437235cab3..406f0a32e1 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ChatCompletionRequestMessageToChatMessageTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ChatCompletionRequestMessageToChatMessageTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Linq; using System.Text.Json; @@ -22,7 +22,7 @@ public sealed class ChatCompletionRequestMessageToChatMessageTests public void ToChatMessage_PreservesRole_ForTextContent(string expectedRole, string json) { // Arrange - ChatCompletionRequestMessage message = JsonSerializer.Deserialize( + ChatCompletionRequestMessage message = JsonSerializer.Deserialize( json, ChatCompletions.ChatCompletionsJsonContext.Default.ChatCompletionRequestMessage)!; // Act @@ -37,9 +37,9 @@ public void ToChatMessage_PreservesRole_ForTextContent(string expectedRole, stri 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)!; + const string Json = """{"role":"function","name":"get_weather","content":"sunny"}"""; + ChatCompletionRequestMessage message = JsonSerializer.Deserialize( + Json, ChatCompletions.ChatCompletionsJsonContext.Default.ChatCompletionRequestMessage)!; // Act ChatMessage chatMessage = message.ToChatMessage(); @@ -58,7 +58,7 @@ public void ToChatMessage_PreservesRole_ForMultiPartContent(string expectedRole) { // Arrange string json = $$"""{"role":"{{expectedRole}}","content":[{"type":"text","text":"Hello!"}]}"""; - ChatCompletionRequestMessage message = JsonSerializer.Deserialize( + ChatCompletionRequestMessage message = JsonSerializer.Deserialize( json, ChatCompletions.ChatCompletionsJsonContext.Default.ChatCompletionRequestMessage)!; // Act @@ -85,7 +85,7 @@ public void ToChatMessage_MultiTurnConversation_PreservesAllRoles() // Act ChatMessage[] chatMessages = jsons - .Select(j => JsonSerializer.Deserialize( + .Select(j => JsonSerializer.Deserialize( j, ChatCompletions.ChatCompletionsJsonContext.Default.ChatCompletionRequestMessage)!) .Select(m => m.ToChatMessage()) .ToArray(); @@ -102,9 +102,9 @@ public void ToChatMessage_MultiTurnConversation_PreservesAllRoles() 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)!; + 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();