// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; using System.Linq; using System.Text.Json.Serialization; using Microsoft.Agents.AI.AGUI.Shared; using Microsoft.Extensions.AI; namespace Microsoft.Agents.AI.AGUI.UnitTests; // Custom complex type for testing tool call parameters public sealed class WeatherRequest { public string Location { get; set; } = string.Empty; public string Units { get; set; } = "celsius"; public bool IncludeForecast { get; set; } } // Custom complex type for testing tool call results public sealed class WeatherResponse { public double Temperature { get; set; } public string Conditions { get; set; } = string.Empty; public DateTime Timestamp { get; set; } } // Custom JsonSerializerContext for the custom types [JsonSerializable(typeof(WeatherRequest))] [JsonSerializable(typeof(WeatherResponse))] [JsonSerializable(typeof(Dictionary))] internal sealed partial class CustomTypesContext : JsonSerializerContext; /// /// Unit tests for the class. /// public sealed class AGUIChatMessageExtensionsTests { [Fact] public void AsChatMessages_WithEmptyCollection_ReturnsEmptyList() { // Arrange List aguiMessages = []; // Act IEnumerable chatMessages = aguiMessages.AsChatMessages(AGUIJsonSerializerContext.Default.Options); // Assert Assert.NotNull(chatMessages); Assert.Empty(chatMessages); } [Fact] public void AsChatMessages_WithSingleMessage_ConvertsToChatMessageCorrectly() { // Arrange List aguiMessages = [ new AGUIUserMessage { Id = "msg1", Content = "Hello" } ]; // Act IEnumerable chatMessages = aguiMessages.AsChatMessages(AGUIJsonSerializerContext.Default.Options); // Assert ChatMessage message = Assert.Single(chatMessages); Assert.Equal(ChatRole.User, message.Role); Assert.Equal("Hello", message.Text); } [Fact] public void AsChatMessages_WithMultipleMessages_PreservesOrder() { // Arrange List aguiMessages = [ new AGUIUserMessage { Id = "msg1", Content = "First" }, new AGUIAssistantMessage { Id = "msg2", Content = "Second" }, new AGUIUserMessage { Id = "msg3", Content = "Third" } ]; // Act List chatMessages = aguiMessages.AsChatMessages(AGUIJsonSerializerContext.Default.Options).ToList(); // Assert Assert.Equal(3, chatMessages.Count); Assert.Equal("First", chatMessages[0].Text); Assert.Equal("Second", chatMessages[1].Text); Assert.Equal("Third", chatMessages[2].Text); } [Fact] public void AsChatMessages_MapsAllSupportedRoleTypes_Correctly() { // Arrange List aguiMessages = [ new AGUISystemMessage { Id = "msg1", Content = "System message" }, new AGUIUserMessage { Id = "msg2", Content = "User message" }, new AGUIAssistantMessage { Id = "msg3", Content = "Assistant message" }, new AGUIDeveloperMessage { Id = "msg4", Content = "Developer message" }, new AGUIReasoningMessage { Id = "msg5", Content = "Reasoning message" } ]; // Act List chatMessages = aguiMessages.AsChatMessages(AGUIJsonSerializerContext.Default.Options).ToList(); // Assert Assert.Equal(5, chatMessages.Count); Assert.Equal(ChatRole.System, chatMessages[0].Role); Assert.Equal(ChatRole.User, chatMessages[1].Role); Assert.Equal(ChatRole.Assistant, chatMessages[2].Role); Assert.Equal("developer", chatMessages[3].Role.Value); Assert.Equal(ChatRole.Assistant, chatMessages[4].Role); } [Fact] public void AsAGUIMessages_WithEmptyCollection_ReturnsEmptyList() { // Arrange List chatMessages = []; // Act IEnumerable aguiMessages = chatMessages.AsAGUIMessages(AGUIJsonSerializerContext.Default.Options); // Assert Assert.NotNull(aguiMessages); Assert.Empty(aguiMessages); } [Fact] public void AsAGUIMessages_WithSingleMessage_ConvertsToAGUIMessageCorrectly() { // Arrange List chatMessages = [ new ChatMessage(ChatRole.User, "Hello") { MessageId = "msg1" } ]; // Act IEnumerable aguiMessages = chatMessages.AsAGUIMessages(AGUIJsonSerializerContext.Default.Options); // Assert AGUIMessage message = Assert.Single(aguiMessages); Assert.Equal("msg1", message.Id); Assert.Equal(AGUIRoles.User, message.Role); Assert.Equal("Hello", ((AGUIUserMessage)message).Content); } [Fact] public void AsAGUIMessages_WithMultipleMessages_PreservesOrder() { // Arrange List chatMessages = [ new ChatMessage(ChatRole.User, "First"), new ChatMessage(ChatRole.Assistant, "Second"), new ChatMessage(ChatRole.User, "Third") ]; // Act List aguiMessages = chatMessages.AsAGUIMessages(AGUIJsonSerializerContext.Default.Options).ToList(); // Assert Assert.Equal(3, aguiMessages.Count); Assert.Equal("First", ((AGUIUserMessage)aguiMessages[0]).Content); Assert.Equal("Second", ((AGUIAssistantMessage)aguiMessages[1]).Content); Assert.Equal("Third", ((AGUIUserMessage)aguiMessages[2]).Content); } [Fact] public void AsAGUIMessages_PreservesMessageId_WhenPresent() { // Arrange List chatMessages = [ new ChatMessage(ChatRole.User, "Hello") { MessageId = "msg123" } ]; // Act IEnumerable aguiMessages = chatMessages.AsAGUIMessages(AGUIJsonSerializerContext.Default.Options); // Assert AGUIMessage message = Assert.Single(aguiMessages); Assert.Equal("msg123", message.Id); } [Theory] [InlineData(AGUIRoles.System, "system")] [InlineData(AGUIRoles.User, "user")] [InlineData(AGUIRoles.Assistant, "assistant")] [InlineData(AGUIRoles.Developer, "developer")] public void MapChatRole_WithValidRole_ReturnsCorrectChatRole(string aguiRole, string expectedRoleValue) { // Arrange & Act ChatRole role = AGUIChatMessageExtensions.MapChatRole(aguiRole); // Assert Assert.Equal(expectedRoleValue, role.Value); } [Fact] public void MapChatRole_WithUnknownRole_ThrowsInvalidOperationException() { // Arrange & Act & Assert Assert.Throws(() => AGUIChatMessageExtensions.MapChatRole("unknown")); } [Fact] public void AsAGUIMessages_WithToolResultMessage_SerializesResultCorrectly() { // Arrange var result = new Dictionary { ["temperature"] = 72, ["condition"] = "Sunny" }; FunctionResultContent toolResult = new("call_123", result); ChatMessage toolMessage = new(ChatRole.Tool, [toolResult]); List messages = [toolMessage]; // Act List aguiMessages = messages.AsAGUIMessages(AGUIJsonSerializerContext.Default.Options).ToList(); // Assert AGUIMessage aguiMessage = Assert.Single(aguiMessages); Assert.Equal(AGUIRoles.Tool, aguiMessage.Role); Assert.Equal("call_123", ((AGUIToolMessage)aguiMessage).ToolCallId); Assert.NotEmpty(((AGUIToolMessage)aguiMessage).Content); // Content should be serialized JSON Assert.Contains("temperature", ((AGUIToolMessage)aguiMessage).Content); Assert.Contains("72", ((AGUIToolMessage)aguiMessage).Content); } [Fact] public void AsAGUIMessages_WithNullToolResult_HandlesGracefully() { // Arrange FunctionResultContent toolResult = new("call_456", null); ChatMessage toolMessage = new(ChatRole.Tool, [toolResult]); List messages = [toolMessage]; // Act List aguiMessages = messages.AsAGUIMessages(AGUIJsonSerializerContext.Default.Options).ToList(); // Assert AGUIMessage aguiMessage = Assert.Single(aguiMessages); Assert.Equal(AGUIRoles.Tool, aguiMessage.Role); Assert.Equal("call_456", ((AGUIToolMessage)aguiMessage).ToolCallId); Assert.Equal(string.Empty, ((AGUIToolMessage)aguiMessage).Content); } [Fact] public void AsAGUIMessages_WithoutTypeInfoResolver_ThrowsInvalidOperationException() { // Arrange FunctionResultContent toolResult = new("call_789", "Result"); ChatMessage toolMessage = new(ChatRole.Tool, [toolResult]); List messages = [toolMessage]; System.Text.Json.JsonSerializerOptions optionsWithoutResolver = new(); // Act & Assert NotSupportedException ex = Assert.Throws(() => messages.AsAGUIMessages(optionsWithoutResolver).ToList()); Assert.Contains("JsonTypeInfo", ex.Message); } [Fact] public void AsChatMessages_WithToolMessage_DeserializesResultCorrectly() { // Arrange const string JsonContent = "{\"status\":\"success\",\"value\":42}"; List aguiMessages = [ new AGUIToolMessage { Id = "msg1", Content = JsonContent, ToolCallId = "call_abc" } ]; // Act List chatMessages = aguiMessages.AsChatMessages(AGUIJsonSerializerContext.Default.Options).ToList(); // Assert ChatMessage message = Assert.Single(chatMessages); Assert.Equal(ChatRole.Tool, message.Role); FunctionResultContent result = Assert.IsType(message.Contents[0]); Assert.Equal("call_abc", result.CallId); Assert.NotNull(result.Result); } [Fact] public void AsChatMessages_WithEmptyToolContent_CreatesNullResult() { // Arrange List aguiMessages = [ new AGUIToolMessage { Id = "msg1", Content = string.Empty, ToolCallId = "call_def" } ]; // Act List chatMessages = aguiMessages.AsChatMessages(AGUIJsonSerializerContext.Default.Options).ToList(); // Assert ChatMessage message = Assert.Single(chatMessages); FunctionResultContent result = Assert.IsType(message.Contents[0]); Assert.Equal("call_def", result.CallId); Assert.Equal(string.Empty, result.Result); } [Fact] public void AsChatMessages_WithToolMessageWithoutCallId_TreatsAsRegularMessage() { // Arrange - use valid JSON for Content List aguiMessages = [ new AGUIToolMessage { Id = "msg1", Content = "{\"result\":\"Some content\"}", ToolCallId = string.Empty } ]; // Act List chatMessages = aguiMessages.AsChatMessages(AGUIJsonSerializerContext.Default.Options).ToList(); // Assert ChatMessage message = Assert.Single(chatMessages); Assert.Equal(ChatRole.Tool, message.Role); var resultContent = Assert.IsType(message.Contents.First()); Assert.Equal(string.Empty, resultContent.CallId); } [Fact] public void RoundTrip_ToolResultMessage_PreservesData() { // Arrange var resultData = new Dictionary { ["location"] = "Seattle", ["temperature"] = 68, ["forecast"] = "Partly cloudy" }; FunctionResultContent originalResult = new("call_roundtrip", resultData); ChatMessage originalMessage = new(ChatRole.Tool, [originalResult]); // Act - Convert to AGUI and back List originalList = [originalMessage]; AGUIMessage aguiMessage = originalList.AsAGUIMessages(AGUIJsonSerializerContext.Default.Options).Single(); List aguiList = [aguiMessage]; ChatMessage reconstructedMessage = aguiList.AsChatMessages(AGUIJsonSerializerContext.Default.Options).Single(); // Assert Assert.Equal(ChatRole.Tool, reconstructedMessage.Role); FunctionResultContent reconstructedResult = Assert.IsType(reconstructedMessage.Contents[0]); Assert.Equal("call_roundtrip", reconstructedResult.CallId); Assert.NotNull(reconstructedResult.Result); } [Fact] public void MapChatRole_WithToolRole_ReturnsToolChatRole() { // Arrange & Act ChatRole role = AGUIChatMessageExtensions.MapChatRole(AGUIRoles.Tool); // Assert Assert.Equal(ChatRole.Tool, role); } [Fact] public void AsChatMessages_WithReasoningMessage_ConvertsToTextReasoningContent() { // Arrange List aguiMessages = [ new AGUIReasoningMessage { Id = "reason1", Content = "I need to consider the user's request.", EncryptedValue = "ErgDCkgIDB..." } ]; // Act List chatMessages = aguiMessages.AsChatMessages(AGUIJsonSerializerContext.Default.Options).ToList(); // Assert ChatMessage message = Assert.Single(chatMessages); Assert.Equal(ChatRole.Assistant, message.Role); Assert.Equal("reason1", message.MessageId); var reasoningContent = Assert.IsType(message.Contents[0]); Assert.Equal("I need to consider the user's request.", reasoningContent.Text); Assert.Equal("ErgDCkgIDB...", reasoningContent.ProtectedData); } [Fact] public void AsChatMessages_WithReasoningMessageWithoutEncryptedValue_ConvertsToTextReasoningContent() { // Arrange List aguiMessages = [ new AGUIReasoningMessage { Id = "reason1", Content = "Thinking about this problem." } ]; // Act List chatMessages = aguiMessages.AsChatMessages(AGUIJsonSerializerContext.Default.Options).ToList(); // Assert ChatMessage message = Assert.Single(chatMessages); Assert.Equal(ChatRole.Assistant, message.Role); var reasoningContent = Assert.IsType(message.Contents[0]); Assert.Equal("Thinking about this problem.", reasoningContent.Text); Assert.Null(reasoningContent.ProtectedData); } [Fact] public void AsChatMessages_WithReasoningMessageWithOnlyEncryptedValue_ConvertsToTextReasoningContent() { // Arrange List aguiMessages = [ new AGUIReasoningMessage { Id = "reason1", Content = string.Empty, EncryptedValue = "ErgDCkgIDB..." } ]; // Act List chatMessages = aguiMessages.AsChatMessages(AGUIJsonSerializerContext.Default.Options).ToList(); // Assert ChatMessage message = Assert.Single(chatMessages); var reasoningContent = Assert.IsType(message.Contents[0]); Assert.Equal("", reasoningContent.Text); Assert.Equal("ErgDCkgIDB...", reasoningContent.ProtectedData); } [Fact] public void AsChatMessages_WithEmptyReasoningMessage_ProducesEmptyContents() { // Arrange List aguiMessages = [ new AGUIReasoningMessage { Id = "reason1", Content = string.Empty } ]; // Act List chatMessages = aguiMessages.AsChatMessages(AGUIJsonSerializerContext.Default.Options).ToList(); // Assert ChatMessage message = Assert.Single(chatMessages); Assert.Equal(ChatRole.Assistant, message.Role); Assert.Empty(message.Contents); } [Fact] public void MapChatRole_WithReasoningRole_ReturnsAssistantChatRole() { // Arrange & Act ChatRole role = AGUIChatMessageExtensions.MapChatRole(AGUIRoles.Reasoning); // Assert Assert.Equal(ChatRole.Assistant, role); } [Fact] public void AsChatMessages_WithMixedMessagesIncludingReasoning_PreservesOrder() { // Arrange List aguiMessages = [ new AGUIUserMessage { Id = "msg1", Content = "What is 2+2?" }, new AGUIReasoningMessage { Id = "msg2", Content = "I need to add 2 and 2.", EncryptedValue = "tok-123" }, new AGUIAssistantMessage { Id = "msg3", Content = "The answer is 4." } ]; // Act List chatMessages = aguiMessages.AsChatMessages(AGUIJsonSerializerContext.Default.Options).ToList(); // Assert Assert.Equal(3, chatMessages.Count); Assert.Equal(ChatRole.User, chatMessages[0].Role); Assert.Equal(ChatRole.Assistant, chatMessages[1].Role); Assert.IsType(chatMessages[1].Contents[0]); Assert.Equal(ChatRole.Assistant, chatMessages[2].Role); Assert.Equal("The answer is 4.", chatMessages[2].Text); } [Fact] public void AsAGUIMessages_WithReasoningContent_ProducesReasoningMessage() { // Arrange List chatMessages = [ new ChatMessage(ChatRole.Assistant, [ new TextReasoningContent("I need to think about this.") { ProtectedData = "encrypted-tok-1" } ]) { MessageId = "reason-1" } ]; // Act List aguiMessages = chatMessages.AsAGUIMessages(AGUIJsonSerializerContext.Default.Options).ToList(); // Assert AGUIMessage message = Assert.Single(aguiMessages); var reasoningMessage = Assert.IsType(message); Assert.Equal("reason-1", reasoningMessage.Id); Assert.Equal(AGUIRoles.Reasoning, reasoningMessage.Role); Assert.Equal("I need to think about this.", reasoningMessage.Content); Assert.Equal("encrypted-tok-1", reasoningMessage.EncryptedValue); } [Fact] public void AsAGUIMessages_WithReasoningContentWithoutProtectedData_ProducesReasoningMessage() { // Arrange List chatMessages = [ new ChatMessage(ChatRole.Assistant, [ new TextReasoningContent("Just thinking.") ]) { MessageId = "reason-2" } ]; // Act List aguiMessages = chatMessages.AsAGUIMessages(AGUIJsonSerializerContext.Default.Options).ToList(); // Assert AGUIMessage message = Assert.Single(aguiMessages); var reasoningMessage = Assert.IsType(message); Assert.Equal("Just thinking.", reasoningMessage.Content); Assert.Null(reasoningMessage.EncryptedValue); } [Fact] public void AsAGUIMessages_WithMultipleReasoningChunksInOneMessage_ConcatenatesText() { // Arrange List chatMessages = [ new ChatMessage(ChatRole.Assistant, [ new TextReasoningContent("First part. "), new TextReasoningContent("Second part.") { ProtectedData = "final-token" } ]) { MessageId = "reason-3" } ]; // Act List aguiMessages = chatMessages.AsAGUIMessages(AGUIJsonSerializerContext.Default.Options).ToList(); // Assert AGUIMessage message = Assert.Single(aguiMessages); var reasoningMessage = Assert.IsType(message); Assert.Equal("First part. Second part.", reasoningMessage.Content); Assert.Equal("final-token", reasoningMessage.EncryptedValue); } [Fact] public void AsAGUIMessages_WithMixedReasoningAndTextContent_EmitsBothMessages() { // Arrange List chatMessages = [ new ChatMessage(ChatRole.Assistant, [ new TextReasoningContent("Thinking about the answer.") { ProtectedData = "enc-tok" }, new TextContent("The answer is 42.") ]) { MessageId = "msg-mixed" } ]; // Act List aguiMessages = chatMessages.AsAGUIMessages(AGUIJsonSerializerContext.Default.Options).ToList(); // Assert Assert.Equal(2, aguiMessages.Count); var reasoningMessage = Assert.IsType(aguiMessages[0]); Assert.Equal("msg-mixed", reasoningMessage.Id); Assert.Equal("Thinking about the answer.", reasoningMessage.Content); Assert.Equal("enc-tok", reasoningMessage.EncryptedValue); var assistantMessage = Assert.IsType(aguiMessages[1]); Assert.Equal("msg-mixed", assistantMessage.Id); Assert.Equal("The answer is 42.", assistantMessage.Content); } [Fact] public void AsAGUIMessages_WithReasoningAndToolCallInSameMessage_EmitsBothMessages() { // Arrange var arguments = new Dictionary { ["location"] = "Seattle" }; List chatMessages = [ new ChatMessage(ChatRole.Assistant, [ new TextReasoningContent("I should look up the weather."), new FunctionCallContent("call-1", "GetWeather", arguments) ]) { MessageId = "msg-toolcall" } ]; // Act List aguiMessages = chatMessages.AsAGUIMessages(AGUIJsonSerializerContext.Default.Options).ToList(); // Assert Assert.Equal(2, aguiMessages.Count); var reasoningMessage = Assert.IsType(aguiMessages[0]); Assert.Equal("I should look up the weather.", reasoningMessage.Content); var assistantMessage = Assert.IsType(aguiMessages[1]); Assert.NotNull(assistantMessage.ToolCalls); var toolCall = Assert.Single(assistantMessage.ToolCalls); Assert.Equal("call-1", toolCall.Id); Assert.Equal("GetWeather", toolCall.Function.Name); } [Fact] public void RoundTrip_ReasoningMessage_PreservesData() { // Arrange List originalMessages = [ new ChatMessage(ChatRole.Assistant, [ new TextReasoningContent("Thinking about the problem.") { ProtectedData = "ErgDCkgIDB..." } ]) { MessageId = "reason-rt" } ]; // Act - Convert to AGUI and back AGUIMessage aguiMessage = originalMessages.AsAGUIMessages(AGUIJsonSerializerContext.Default.Options).Single(); List aguiList = [aguiMessage]; ChatMessage reconstructed = aguiList.AsChatMessages(AGUIJsonSerializerContext.Default.Options).Single(); // Assert Assert.Equal(ChatRole.Assistant, reconstructed.Role); var reasoningContent = Assert.IsType(reconstructed.Contents[0]); Assert.Equal("Thinking about the problem.", reasoningContent.Text); Assert.Equal("ErgDCkgIDB...", reasoningContent.ProtectedData); } #region Custom Type Serialization Tests [Fact] public void AsChatMessages_WithFunctionCallContainingCustomType_SerializesCorrectly() { // Arrange var customRequest = new WeatherRequest { Location = "Seattle", Units = "fahrenheit", IncludeForecast = true }; var parameters = new Dictionary { ["location"] = customRequest.Location, ["units"] = customRequest.Units, ["includeForecast"] = customRequest.IncludeForecast }; List aguiMessages = [ new AGUIAssistantMessage { Id = "msg1", ToolCalls = [ new AGUIToolCall { Id = "call_1", Function = new AGUIFunctionCall { Name = "GetWeather", Arguments = System.Text.Json.JsonSerializer.Serialize(parameters, AGUIJsonSerializerContext.Default.Options) } } ] } ]; // Combine contexts for serialization var combinedOptions = new System.Text.Json.JsonSerializerOptions { TypeInfoResolver = System.Text.Json.Serialization.Metadata.JsonTypeInfoResolver.Combine( AGUIJsonSerializerContext.Default, CustomTypesContext.Default) }; // Act IEnumerable chatMessages = aguiMessages.AsChatMessages(combinedOptions); // Assert ChatMessage message = Assert.Single(chatMessages); Assert.Equal(ChatRole.Assistant, message.Role); var toolCallContent = Assert.IsType(message.Contents.First()); Assert.Equal("call_1", toolCallContent.CallId); Assert.Equal("GetWeather", toolCallContent.Name); Assert.NotNull(toolCallContent.Arguments); // Compare as strings since deserialization produces JsonElement objects Assert.Equal("Seattle", ((System.Text.Json.JsonElement)toolCallContent.Arguments["location"]!).GetString()); Assert.Equal("fahrenheit", ((System.Text.Json.JsonElement)toolCallContent.Arguments["units"]!).GetString()); Assert.True(toolCallContent.Arguments["includeForecast"] is System.Text.Json.JsonElement j && j.GetBoolean()); } [Fact] public void AsAGUIMessages_WithFunctionResultContainingCustomType_SerializesCorrectly() { // Arrange var customResponse = new WeatherResponse { Temperature = 72.5, Conditions = "Sunny", Timestamp = DateTime.UtcNow }; var resultObject = new Dictionary { ["temperature"] = customResponse.Temperature, ["conditions"] = customResponse.Conditions, ["timestamp"] = customResponse.Timestamp.ToString("O") }; var resultJson = System.Text.Json.JsonSerializer.Serialize(resultObject, AGUIJsonSerializerContext.Default.Options); var functionResult = new FunctionResultContent("call_1", System.Text.Json.JsonSerializer.Deserialize(resultJson, AGUIJsonSerializerContext.Default.Options)); List chatMessages = [ new ChatMessage(ChatRole.Tool, [functionResult]) ]; // Combine contexts for serialization var combinedOptions = new System.Text.Json.JsonSerializerOptions { TypeInfoResolver = System.Text.Json.Serialization.Metadata.JsonTypeInfoResolver.Combine( AGUIJsonSerializerContext.Default, CustomTypesContext.Default) }; // Act IEnumerable aguiMessages = chatMessages.AsAGUIMessages(combinedOptions); // Assert AGUIMessage message = Assert.Single(aguiMessages); var toolMessage = Assert.IsType(message); Assert.Equal("call_1", toolMessage.ToolCallId); Assert.NotNull(toolMessage.Content); // Verify the content can be deserialized back var deserializedResult = System.Text.Json.JsonSerializer.Deserialize>( toolMessage.Content, combinedOptions); Assert.NotNull(deserializedResult); Assert.Equal(72.5, deserializedResult["temperature"].GetDouble()); Assert.Equal("Sunny", deserializedResult["conditions"].GetString()); } [Fact] public void RoundTrip_WithCustomTypesInFunctionCallAndResult_PreservesData() { // Arrange var customRequest = new WeatherRequest { Location = "New York", Units = "celsius", IncludeForecast = false }; var parameters = new Dictionary { ["location"] = customRequest.Location, ["units"] = customRequest.Units, ["includeForecast"] = customRequest.IncludeForecast }; var customResponse = new WeatherResponse { Temperature = 22.3, Conditions = "Cloudy", Timestamp = DateTime.UtcNow }; var resultObject = new Dictionary { ["temperature"] = customResponse.Temperature, ["conditions"] = customResponse.Conditions, ["timestamp"] = customResponse.Timestamp.ToString("O") }; var resultJson = System.Text.Json.JsonSerializer.Serialize(resultObject, AGUIJsonSerializerContext.Default.Options); var resultElement = System.Text.Json.JsonSerializer.Deserialize(resultJson, AGUIJsonSerializerContext.Default.Options); List originalChatMessages = [ new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call_1", "GetWeather", parameters)]), new ChatMessage(ChatRole.Tool, [new FunctionResultContent("call_1", resultElement)]) ]; // Combine contexts for serialization var combinedOptions = new System.Text.Json.JsonSerializerOptions { TypeInfoResolver = System.Text.Json.Serialization.Metadata.JsonTypeInfoResolver.Combine( AGUIJsonSerializerContext.Default, CustomTypesContext.Default) }; // Act - Convert to AGUI messages and back IEnumerable aguiMessages = originalChatMessages.AsAGUIMessages(combinedOptions); List roundTrippedChatMessages = aguiMessages.AsChatMessages(combinedOptions).ToList(); // Assert Assert.Equal(2, roundTrippedChatMessages.Count); // Verify function call ChatMessage callMessage = roundTrippedChatMessages[0]; Assert.Equal(ChatRole.Assistant, callMessage.Role); var functionCall = Assert.IsType(callMessage.Contents.First()); Assert.Equal("call_1", functionCall.CallId); Assert.Equal("GetWeather", functionCall.Name); Assert.NotNull(functionCall.Arguments); // Compare string values from JsonElement Assert.Equal(customRequest.Location, functionCall.Arguments["location"]?.ToString()); Assert.Equal(customRequest.Units, functionCall.Arguments["units"]?.ToString()); // Verify function result ChatMessage resultMessage = roundTrippedChatMessages[1]; Assert.Equal(ChatRole.Tool, resultMessage.Role); var functionResultContent = Assert.IsType(resultMessage.Contents.First()); Assert.Equal("call_1", functionResultContent.CallId); Assert.NotNull(functionResultContent.Result); } [Fact] public void AsAGUIMessages_WithNestedCustomObjects_HandlesComplexSerialization() { // Arrange - nested custom types var nestedParameters = new Dictionary { ["request"] = new Dictionary { ["location"] = "Boston", ["options"] = new Dictionary { ["units"] = "fahrenheit", ["includeHumidity"] = true, ["daysAhead"] = 5 } } }; var functionCall = new FunctionCallContent("call_nested", "GetDetailedWeather", nestedParameters); List chatMessages = [ new ChatMessage(ChatRole.Assistant, [functionCall]) ]; // Combine contexts for serialization var combinedOptions = new System.Text.Json.JsonSerializerOptions { TypeInfoResolver = System.Text.Json.Serialization.Metadata.JsonTypeInfoResolver.Combine( AGUIJsonSerializerContext.Default, CustomTypesContext.Default) }; // Act IEnumerable aguiMessages = chatMessages.AsAGUIMessages(combinedOptions); // Assert AGUIMessage message = Assert.Single(aguiMessages); var assistantMessage = Assert.IsType(message); Assert.NotNull(assistantMessage.ToolCalls); var toolCall = Assert.Single(assistantMessage.ToolCalls); Assert.Equal("call_nested", toolCall.Id); Assert.Equal("GetDetailedWeather", toolCall.Function?.Name); // Verify nested structure is preserved var deserializedArgs = System.Text.Json.JsonSerializer.Deserialize>( toolCall.Function?.Arguments ?? "{}", combinedOptions); Assert.NotNull(deserializedArgs); Assert.True(deserializedArgs.ContainsKey("request")); } [Fact] public void AsAGUIMessages_WithDictionaryContainingCustomTypes_SerializesDirectly() { // Arrange - Create a dictionary with custom type values (not flattened) var customRequest = new WeatherRequest { Location = "Tokyo", Units = "celsius", IncludeForecast = true }; var parameters = new Dictionary { ["customRequest"] = customRequest, // Custom type as value ["simpleString"] = "test", ["simpleNumber"] = 42 }; List chatMessages = [ new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call_custom", "ProcessWeather", parameters)]) ]; // Combine contexts for serialization var combinedOptions = new System.Text.Json.JsonSerializerOptions { TypeInfoResolver = System.Text.Json.Serialization.Metadata.JsonTypeInfoResolver.Combine( AGUIJsonSerializerContext.Default, CustomTypesContext.Default) }; // Act IEnumerable aguiMessages = chatMessages.AsAGUIMessages(combinedOptions); // Assert AGUIMessage message = Assert.Single(aguiMessages); var assistantMessage = Assert.IsType(message); Assert.NotNull(assistantMessage.ToolCalls); var toolCall = Assert.Single(assistantMessage.ToolCalls); Assert.Equal("call_custom", toolCall.Id); Assert.Equal("ProcessWeather", toolCall.Function?.Name); // Verify custom type was serialized correctly without flattening var deserializedArgs = System.Text.Json.JsonSerializer.Deserialize>( toolCall.Function?.Arguments ?? "{}", combinedOptions); Assert.NotNull(deserializedArgs); Assert.True(deserializedArgs.ContainsKey("customRequest")); Assert.True(deserializedArgs.ContainsKey("simpleString")); Assert.True(deserializedArgs.ContainsKey("simpleNumber")); // Verify the custom type properties are accessible var customRequestElement = deserializedArgs["customRequest"]; Assert.Equal("Tokyo", customRequestElement.GetProperty("Location").GetString()); Assert.Equal("celsius", customRequestElement.GetProperty("Units").GetString()); Assert.True(customRequestElement.GetProperty("IncludeForecast").GetBoolean()); // Verify simple types Assert.Equal("test", deserializedArgs["simpleString"].GetString()); Assert.Equal(42, deserializedArgs["simpleNumber"].GetInt32()); } #endregion }