diff --git a/dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs b/dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs index 42f92be1b1..96a8856dea 100644 --- a/dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs @@ -84,9 +84,13 @@ protected override async Task RunCoreAsync(IEnumerable RunCoreStreami // a2aSseEvents = this._a2aClient.SubscribeToTaskAsync(token.TaskId, cancellationToken).ConfigureAwait(false); } - var a2aMessage = CreateA2AMessage(typedThread, messages); + MessageSendParams sendParams = new() + { + Message = CreateA2AMessage(typedThread, messages), + Metadata = options?.AdditionalProperties?.ToA2AMetadata() + }; - a2aSseEvents = this._a2aClient.SendMessageStreamingAsync(new MessageSendParams { Message = a2aMessage }, cancellationToken).ConfigureAwait(false); + a2aSseEvents = this._a2aClient.SendMessageStreamingAsync(sendParams, cancellationToken).ConfigureAwait(false); this._logger.LogAgentChatClientInvokedAgent(nameof(RunStreamingAsync), this.Id, this.Name); @@ -198,10 +206,10 @@ protected override async IAsyncEnumerable RunCoreStreami protected override string? IdCore => this._id; /// - public override string? Name => this._name ?? base.Name; + public override string? Name => this._name; /// - public override string? Description => this._description ?? base.Description; + public override string? Description => this._description; private A2AAgentThread GetA2AThread(AgentThread? thread, AgentRunOptions? options) { diff --git a/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AMetadataExtensions.cs b/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AMetadataExtensions.cs index c0dedbd541..3c81c6abe8 100644 --- a/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AMetadataExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AMetadataExtensions.cs @@ -14,6 +14,9 @@ internal static class A2AMetadataExtensions /// /// Converts a dictionary of metadata to an . /// + /// + /// This method can be replaced by the one from A2A SDK once it is public. + /// /// The metadata dictionary to convert. /// The converted , or null if the input is null or empty. internal static AdditionalPropertiesDictionary? ToAdditionalProperties(this Dictionary? metadata) diff --git a/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/AdditionalPropertiesDictionaryExtensions.cs b/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/AdditionalPropertiesDictionaryExtensions.cs new file mode 100644 index 0000000000..a3340d2ca8 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/AdditionalPropertiesDictionaryExtensions.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text.Json; +using Microsoft.Agents.AI; + +namespace Microsoft.Extensions.AI; + +/// +/// Extension methods for AdditionalPropertiesDictionary. +/// +internal static class AdditionalPropertiesDictionaryExtensions +{ + /// + /// Converts an to a dictionary of values suitable for A2A metadata. + /// + /// + /// This method can be replaced by the one from A2A SDK once it is available. + /// + /// The additional properties dictionary to convert, or null. + /// A dictionary of JSON elements representing the metadata, or null if the input is null or empty. + internal static Dictionary? ToA2AMetadata(this AdditionalPropertiesDictionary? additionalProperties) + { + if (additionalProperties is not { Count: > 0 }) + { + return null; + } + + var metadata = new Dictionary(); + + foreach (var kvp in additionalProperties) + { + if (kvp.Value is JsonElement) + { + metadata[kvp.Key] = (JsonElement)kvp.Value!; + continue; + } + + metadata[kvp.Key] = JsonSerializer.SerializeToElement(kvp.Value, A2AJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object))); + } + + return metadata; + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AIAgentExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AIAgentExtensions.cs index c54af66bb8..499d724b1a 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AIAgentExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AIAgentExtensions.cs @@ -43,10 +43,14 @@ async Task OnMessageReceivedAsync(MessageSendParams messageSendPara { var contextId = messageSendParams.Message.ContextId ?? Guid.NewGuid().ToString("N"); var thread = await hostAgent.GetOrCreateThreadAsync(contextId, cancellationToken).ConfigureAwait(false); + var options = messageSendParams.Metadata is not { Count: > 0 } + ? null + : new AgentRunOptions { AdditionalProperties = messageSendParams.Metadata.ToAdditionalProperties() }; var response = await hostAgent.RunAsync( messageSendParams.ToChatMessages(), thread: thread, + options: options, cancellationToken: cancellationToken).ConfigureAwait(false); await hostAgent.SaveThreadAsync(contextId, thread, cancellationToken).ConfigureAwait(false); @@ -56,7 +60,8 @@ async Task OnMessageReceivedAsync(MessageSendParams messageSendPara MessageId = response.ResponseId ?? Guid.NewGuid().ToString("N"), ContextId = contextId, Role = MessageRole.Agent, - Parts = parts + Parts = parts, + Metadata = response.AdditionalProperties?.ToA2AMetadata() }; } } diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/Converters/A2AMetadataExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/Converters/A2AMetadataExtensions.cs new file mode 100644 index 0000000000..010264bb65 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/Converters/A2AMetadataExtensions.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text.Json; +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.AI.Hosting.A2A.Converters; + +/// +/// Extension methods for A2A metadata dictionary. +/// +internal static class A2AMetadataExtensions +{ + /// + /// Converts a dictionary of metadata to an . + /// + /// + /// This method can be replaced by the one from A2A SDK once it is public. + /// + /// The metadata dictionary to convert. + /// The converted , or null if the input is null or empty. + internal static AdditionalPropertiesDictionary? ToAdditionalProperties(this Dictionary? metadata) + { + if (metadata is not { Count: > 0 }) + { + return null; + } + + var additionalProperties = new AdditionalPropertiesDictionary(); + foreach (var kvp in metadata) + { + additionalProperties[kvp.Key] = kvp.Value; + } + return additionalProperties; + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/Converters/AdditionalPropertiesDictionaryExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/Converters/AdditionalPropertiesDictionaryExtensions.cs new file mode 100644 index 0000000000..d46ef72d1f --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/Converters/AdditionalPropertiesDictionaryExtensions.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text.Json; +using A2A; +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.AI.Hosting.A2A.Converters; + +/// +/// Extension methods for AdditionalPropertiesDictionary. +/// +internal static class AdditionalPropertiesDictionaryExtensions +{ + /// + /// Converts an to a dictionary of values suitable for A2A metadata. + /// + /// + /// This method can be replaced by the one from A2A SDK once it is available. + /// + /// The additional properties dictionary to convert, or null. + /// A dictionary of JSON elements representing the metadata, or null if the input is null or empty. + internal static Dictionary? ToA2AMetadata(this AdditionalPropertiesDictionary? additionalProperties) + { + if (additionalProperties is not { Count: > 0 }) + { + return null; + } + + var metadata = new Dictionary(); + + foreach (var kvp in additionalProperties) + { + if (kvp.Value is JsonElement) + { + metadata[kvp.Key] = (JsonElement)kvp.Value!; + continue; + } + + metadata[kvp.Key] = JsonSerializer.SerializeToElement(kvp.Value, A2AJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object))); + } + + return metadata; + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/A2AAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/A2AAgentTests.cs index 0b491fb303..236ae7b332 100644 --- a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/A2AAgentTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/A2AAgentTests.cs @@ -832,6 +832,174 @@ await Assert.ThrowsAsync(async () => }); } + [Fact] + public async Task RunAsync_WithAgentMessageResponseMetadata_ReturnsMetadataAsAdditionalPropertiesAsync() + { + // Arrange + this._handler.ResponseToReturn = new AgentMessage + { + MessageId = "response-123", + Role = MessageRole.Agent, + Parts = [new TextPart { Text = "Response with metadata" }], + Metadata = new Dictionary + { + { "responseKey1", JsonSerializer.SerializeToElement("responseValue1") }, + { "responseCount", JsonSerializer.SerializeToElement(99) } + } + }; + + var inputMessages = new List + { + new(ChatRole.User, "Test message") + }; + + // Act + var result = await this._agent.RunAsync(inputMessages); + + // Assert + Assert.NotNull(result.AdditionalProperties); + Assert.NotNull(result.AdditionalProperties["responseKey1"]); + Assert.Equal("responseValue1", ((JsonElement)result.AdditionalProperties["responseKey1"]!).GetString()); + Assert.NotNull(result.AdditionalProperties["responseCount"]); + Assert.Equal(99, ((JsonElement)result.AdditionalProperties["responseCount"]!).GetInt32()); + } + + [Fact] + public async Task RunAsync_WithAdditionalProperties_PropagatesThemAsMetadataToMessageSendParamsAsync() + { + // Arrange + this._handler.ResponseToReturn = new AgentMessage + { + MessageId = "response-123", + Role = MessageRole.Agent, + Parts = [new TextPart { Text = "Response" }] + }; + + var inputMessages = new List + { + new(ChatRole.User, "Test message") + }; + + var options = new AgentRunOptions + { + AdditionalProperties = new() + { + { "key1", "value1" }, + { "key2", 42 }, + { "key3", true } + } + }; + + // Act + await this._agent.RunAsync(inputMessages, null, options); + + // Assert + Assert.NotNull(this._handler.CapturedMessageSendParams); + Assert.NotNull(this._handler.CapturedMessageSendParams.Metadata); + Assert.Equal("value1", this._handler.CapturedMessageSendParams.Metadata["key1"].GetString()); + Assert.Equal(42, this._handler.CapturedMessageSendParams.Metadata["key2"].GetInt32()); + Assert.True(this._handler.CapturedMessageSendParams.Metadata["key3"].GetBoolean()); + } + + [Fact] + public async Task RunAsync_WithNullAdditionalProperties_DoesNotSetMetadataAsync() + { + // Arrange + this._handler.ResponseToReturn = new AgentMessage + { + MessageId = "response-123", + Role = MessageRole.Agent, + Parts = [new TextPart { Text = "Response" }] + }; + + var inputMessages = new List + { + new(ChatRole.User, "Test message") + }; + + var options = new AgentRunOptions + { + AdditionalProperties = null + }; + + // Act + await this._agent.RunAsync(inputMessages, null, options); + + // Assert + Assert.NotNull(this._handler.CapturedMessageSendParams); + Assert.Null(this._handler.CapturedMessageSendParams.Metadata); + } + + [Fact] + public async Task RunStreamingAsync_WithAdditionalProperties_PropagatesThemAsMetadataToMessageSendParamsAsync() + { + // Arrange + this._handler.StreamingResponseToReturn = new AgentMessage + { + MessageId = "stream-123", + Role = MessageRole.Agent, + Parts = [new TextPart { Text = "Streaming response" }] + }; + + var inputMessages = new List + { + new(ChatRole.User, "Test streaming message") + }; + + var options = new AgentRunOptions + { + AdditionalProperties = new() + { + { "streamKey1", "streamValue1" }, + { "streamKey2", 100 }, + { "streamKey3", false } + } + }; + + // Act + await foreach (var _ in this._agent.RunStreamingAsync(inputMessages, null, options)) + { + } + + // Assert + Assert.NotNull(this._handler.CapturedMessageSendParams); + Assert.NotNull(this._handler.CapturedMessageSendParams.Metadata); + Assert.Equal("streamValue1", this._handler.CapturedMessageSendParams.Metadata["streamKey1"].GetString()); + Assert.Equal(100, this._handler.CapturedMessageSendParams.Metadata["streamKey2"].GetInt32()); + Assert.False(this._handler.CapturedMessageSendParams.Metadata["streamKey3"].GetBoolean()); + } + + [Fact] + public async Task RunStreamingAsync_WithNullAdditionalProperties_DoesNotSetMetadataAsync() + { + // Arrange + this._handler.StreamingResponseToReturn = new AgentMessage + { + MessageId = "stream-123", + Role = MessageRole.Agent, + Parts = [new TextPart { Text = "Streaming response" }] + }; + + var inputMessages = new List + { + new(ChatRole.User, "Test streaming message") + }; + + var options = new AgentRunOptions + { + AdditionalProperties = null + }; + + // Act + await foreach (var _ in this._agent.RunStreamingAsync(inputMessages, null, options)) + { + } + + // Assert + Assert.NotNull(this._handler.CapturedMessageSendParams); + Assert.Null(this._handler.CapturedMessageSendParams.Metadata); + } + [Fact] public async Task RunAsync_WithInvalidThreadType_ThrowsInvalidOperationExceptionAsync() { diff --git a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/AdditionalPropertiesDictionaryExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/AdditionalPropertiesDictionaryExtensionsTests.cs new file mode 100644 index 0000000000..4972b8857f --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/AdditionalPropertiesDictionaryExtensionsTests.cs @@ -0,0 +1,186 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text.Json; +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.AI.A2A.UnitTests; + +/// +/// Unit tests for the class. +/// +public sealed class AdditionalPropertiesDictionaryExtensionsTests +{ + [Fact] + public void ToA2AMetadata_WithNullAdditionalProperties_ReturnsNull() + { + // Arrange + AdditionalPropertiesDictionary? additionalProperties = null; + + // Act + Dictionary? result = additionalProperties.ToA2AMetadata(); + + // Assert + Assert.Null(result); + } + + [Fact] + public void ToA2AMetadata_WithEmptyAdditionalProperties_ReturnsNull() + { + // Arrange + AdditionalPropertiesDictionary additionalProperties = []; + + // Act + Dictionary? result = additionalProperties.ToA2AMetadata(); + + // Assert + Assert.Null(result); + } + + [Fact] + public void ToA2AMetadata_WithStringValue_ReturnsMetadataWithJsonElement() + { + // Arrange + AdditionalPropertiesDictionary additionalProperties = new() + { + { "stringKey", "stringValue" } + }; + + // Act + Dictionary? result = additionalProperties.ToA2AMetadata(); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.True(result.ContainsKey("stringKey")); + Assert.Equal("stringValue", result["stringKey"].GetString()); + } + + [Fact] + public void ToA2AMetadata_WithNumericValue_ReturnsMetadataWithJsonElement() + { + // Arrange + AdditionalPropertiesDictionary additionalProperties = new() + { + { "numberKey", 42 } + }; + + // Act + Dictionary? result = additionalProperties.ToA2AMetadata(); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.True(result.ContainsKey("numberKey")); + Assert.Equal(42, result["numberKey"].GetInt32()); + } + + [Fact] + public void ToA2AMetadata_WithBooleanValue_ReturnsMetadataWithJsonElement() + { + // Arrange + AdditionalPropertiesDictionary additionalProperties = new() + { + { "booleanKey", true } + }; + + // Act + Dictionary? result = additionalProperties.ToA2AMetadata(); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.True(result.ContainsKey("booleanKey")); + Assert.True(result["booleanKey"].GetBoolean()); + } + + [Fact] + public void ToA2AMetadata_WithMultipleProperties_ReturnsMetadataWithAllProperties() + { + // Arrange + AdditionalPropertiesDictionary additionalProperties = new() + { + { "stringKey", "stringValue" }, + { "numberKey", 42 }, + { "booleanKey", true } + }; + + // Act + Dictionary? result = additionalProperties.ToA2AMetadata(); + + // Assert + Assert.NotNull(result); + Assert.Equal(3, result.Count); + + Assert.True(result.ContainsKey("stringKey")); + Assert.Equal("stringValue", result["stringKey"].GetString()); + + Assert.True(result.ContainsKey("numberKey")); + Assert.Equal(42, result["numberKey"].GetInt32()); + + Assert.True(result.ContainsKey("booleanKey")); + Assert.True(result["booleanKey"].GetBoolean()); + } + + [Fact] + public void ToA2AMetadata_WithArrayValue_ReturnsMetadataWithJsonElement() + { + // Arrange + int[] arrayValue = [1, 2, 3]; + AdditionalPropertiesDictionary additionalProperties = new() + { + { "arrayKey", arrayValue } + }; + + // Act + Dictionary? result = additionalProperties.ToA2AMetadata(); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.True(result.ContainsKey("arrayKey")); + Assert.Equal(JsonValueKind.Array, result["arrayKey"].ValueKind); + Assert.Equal(3, result["arrayKey"].GetArrayLength()); + } + + [Fact] + public void ToA2AMetadata_WithNullValue_ReturnsMetadataWithNullJsonElement() + { + // Arrange + AdditionalPropertiesDictionary additionalProperties = new() + { + { "nullKey", null! } + }; + + // Act + Dictionary? result = additionalProperties.ToA2AMetadata(); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.True(result.ContainsKey("nullKey")); + Assert.Equal(JsonValueKind.Null, result["nullKey"].ValueKind); + } + + [Fact] + public void ToA2AMetadata_WithJsonElementValue_ReturnsMetadataWithJsonElement() + { + // Arrange + JsonElement jsonElement = JsonSerializer.SerializeToElement(new { name = "test", value = 123 }); + AdditionalPropertiesDictionary additionalProperties = new() + { + { "jsonElementKey", jsonElement } + }; + + // Act + Dictionary? result = additionalProperties.ToA2AMetadata(); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.True(result.ContainsKey("jsonElementKey")); + Assert.Equal(JsonValueKind.Object, result["jsonElementKey"].ValueKind); + Assert.Equal("test", result["jsonElementKey"].GetProperty("name").GetString()); + Assert.Equal(123, result["jsonElementKey"].GetProperty("value").GetInt32()); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/AIAgentExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/AIAgentExtensionsTests.cs new file mode 100644 index 0000000000..0d5b895974 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/AIAgentExtensionsTests.cs @@ -0,0 +1,218 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using A2A; +using Microsoft.Extensions.AI; +using Moq; +using Moq.Protected; + +namespace Microsoft.Agents.AI.Hosting.A2A.UnitTests; + +/// +/// Unit tests for the class. +/// +public sealed class AIAgentExtensionsTests +{ + /// + /// Verifies that when messageSendParams.Metadata is null, the options passed to RunAsync are null. + /// + [Fact] + public async Task MapA2A_WhenMetadataIsNull_PassesNullOptionsToRunAsync() + { + // Arrange + AgentRunOptions? capturedOptions = null; + ITaskManager taskManager = CreateAgentMock(options => capturedOptions = options).Object.MapA2A(); + + // Act + await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams + { + Message = new AgentMessage { MessageId = "test-id", Role = MessageRole.User, Parts = [new TextPart { Text = "Hello" }] }, + Metadata = null + }); + + // Assert + Assert.Null(capturedOptions); + } + + /// + /// Verifies that when messageSendParams.Metadata has values, the options.AdditionalProperties contains the converted values. + /// + [Fact] + public async Task MapA2A_WhenMetadataHasValues_PassesOptionsWithAdditionalPropertiesToRunAsync() + { + // Arrange + AgentRunOptions? capturedOptions = null; + ITaskManager taskManager = CreateAgentMock(options => capturedOptions = options).Object.MapA2A(); + + // Act + await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams + { + Message = new AgentMessage { MessageId = "test-id", Role = MessageRole.User, Parts = [new TextPart { Text = "Hello" }] }, + Metadata = new Dictionary + { + ["key1"] = JsonSerializer.SerializeToElement("value1"), + ["key2"] = JsonSerializer.SerializeToElement(42) + } + }); + + // Assert + Assert.NotNull(capturedOptions); + Assert.NotNull(capturedOptions.AdditionalProperties); + Assert.Equal(2, capturedOptions.AdditionalProperties.Count); + Assert.True(capturedOptions.AdditionalProperties.ContainsKey("key1")); + Assert.True(capturedOptions.AdditionalProperties.ContainsKey("key2")); + } + + /// + /// Verifies that when messageSendParams.Metadata is an empty dictionary, the options passed to RunAsync is null + /// because the ToAdditionalProperties extension method returns null for empty dictionaries. + /// + [Fact] + public async Task MapA2A_WhenMetadataIsEmptyDictionary_PassesNullOptionsToRunAsync() + { + // Arrange + AgentRunOptions? capturedOptions = null; + ITaskManager taskManager = CreateAgentMock(options => capturedOptions = options).Object.MapA2A(); + + // Act + await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams + { + Message = new AgentMessage { MessageId = "test-id", Role = MessageRole.User, Parts = [new TextPart { Text = "Hello" }] }, + Metadata = [] + }); + + // Assert + Assert.Null(capturedOptions); + } + + /// + /// Verifies that when the agent response has AdditionalProperties, the returned AgentMessage.Metadata contains the converted values. + /// + [Fact] + public async Task MapA2A_WhenResponseHasAdditionalProperties_ReturnsAgentMessageWithMetadataAsync() + { + // Arrange + AdditionalPropertiesDictionary additionalProps = new() + { + ["responseKey1"] = "responseValue1", + ["responseKey2"] = 123 + }; + AgentRunResponse response = new([new ChatMessage(ChatRole.Assistant, "Test response")]) + { + AdditionalProperties = additionalProps + }; + ITaskManager taskManager = CreateAgentMockWithResponse(response).Object.MapA2A(); + + // Act + A2AResponse a2aResponse = await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams + { + Message = new AgentMessage { MessageId = "test-id", Role = MessageRole.User, Parts = [new TextPart { Text = "Hello" }] } + }); + + // Assert + AgentMessage agentMessage = Assert.IsType(a2aResponse); + Assert.NotNull(agentMessage.Metadata); + Assert.Equal(2, agentMessage.Metadata.Count); + Assert.True(agentMessage.Metadata.ContainsKey("responseKey1")); + Assert.True(agentMessage.Metadata.ContainsKey("responseKey2")); + Assert.Equal("responseValue1", agentMessage.Metadata["responseKey1"].GetString()); + Assert.Equal(123, agentMessage.Metadata["responseKey2"].GetInt32()); + } + + /// + /// Verifies that when the agent response has null AdditionalProperties, the returned AgentMessage.Metadata is null. + /// + [Fact] + public async Task MapA2A_WhenResponseHasNullAdditionalProperties_ReturnsAgentMessageWithNullMetadataAsync() + { + // Arrange + AgentRunResponse response = new([new ChatMessage(ChatRole.Assistant, "Test response")]) + { + AdditionalProperties = null + }; + ITaskManager taskManager = CreateAgentMockWithResponse(response).Object.MapA2A(); + + // Act + A2AResponse a2aResponse = await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams + { + Message = new AgentMessage { MessageId = "test-id", Role = MessageRole.User, Parts = [new TextPart { Text = "Hello" }] } + }); + + // Assert + AgentMessage agentMessage = Assert.IsType(a2aResponse); + Assert.Null(agentMessage.Metadata); + } + + /// + /// Verifies that when the agent response has empty AdditionalProperties, the returned AgentMessage.Metadata is null. + /// + [Fact] + public async Task MapA2A_WhenResponseHasEmptyAdditionalProperties_ReturnsAgentMessageWithNullMetadataAsync() + { + // Arrange + AgentRunResponse response = new([new ChatMessage(ChatRole.Assistant, "Test response")]) + { + AdditionalProperties = [] + }; + ITaskManager taskManager = CreateAgentMockWithResponse(response).Object.MapA2A(); + + // Act + A2AResponse a2aResponse = await InvokeOnMessageReceivedAsync(taskManager, new MessageSendParams + { + Message = new AgentMessage { MessageId = "test-id", Role = MessageRole.User, Parts = [new TextPart { Text = "Hello" }] } + }); + + // Assert + AgentMessage agentMessage = Assert.IsType(a2aResponse); + Assert.Null(agentMessage.Metadata); + } + + private static Mock CreateAgentMock(Action optionsCallback) + { + Mock agentMock = new() { CallBase = true }; + agentMock.SetupGet(x => x.Name).Returns("TestAgent"); + agentMock.Setup(x => x.GetNewThread()).Returns(new TestAgentThread()); + agentMock + .Protected() + .Setup>("RunCoreAsync", + ItExpr.IsAny>(), + ItExpr.IsAny(), + ItExpr.IsAny(), + ItExpr.IsAny()) + .Callback, AgentThread?, AgentRunOptions?, CancellationToken>( + (_, _, options, _) => optionsCallback(options)) + .ReturnsAsync(new AgentRunResponse([new ChatMessage(ChatRole.Assistant, "Test response")])); + + return agentMock; + } + + private static Mock CreateAgentMockWithResponse(AgentRunResponse response) + { + Mock agentMock = new() { CallBase = true }; + agentMock.SetupGet(x => x.Name).Returns("TestAgent"); + agentMock.Setup(x => x.GetNewThread()).Returns(new TestAgentThread()); + agentMock + .Protected() + .Setup>("RunCoreAsync", + ItExpr.IsAny>(), + ItExpr.IsAny(), + ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(response); + + return agentMock; + } + + private static async Task InvokeOnMessageReceivedAsync(ITaskManager taskManager, MessageSendParams messageSendParams) + { + Func>? handler = taskManager.OnMessageReceived; + Assert.NotNull(handler); + return await handler.Invoke(messageSendParams, CancellationToken.None); + } + + private sealed class TestAgentThread : AgentThread; +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Converters/AdditionalPropertiesDictionaryExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Converters/AdditionalPropertiesDictionaryExtensionsTests.cs new file mode 100644 index 0000000000..e0c8c4e96b --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Converters/AdditionalPropertiesDictionaryExtensionsTests.cs @@ -0,0 +1,187 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text.Json; +using Microsoft.Agents.AI.Hosting.A2A.Converters; +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.AI.Hosting.A2A.UnitTests.Converters; + +/// +/// Unit tests for the class. +/// +public sealed class AdditionalPropertiesDictionaryExtensionsTests +{ + [Fact] + public void ToA2AMetadata_WithNullAdditionalProperties_ReturnsNull() + { + // Arrange + AdditionalPropertiesDictionary? additionalProperties = null; + + // Act + Dictionary? result = additionalProperties.ToA2AMetadata(); + + // Assert + Assert.Null(result); + } + + [Fact] + public void ToA2AMetadata_WithEmptyAdditionalProperties_ReturnsNull() + { + // Arrange + AdditionalPropertiesDictionary additionalProperties = []; + + // Act + Dictionary? result = additionalProperties.ToA2AMetadata(); + + // Assert + Assert.Null(result); + } + + [Fact] + public void ToA2AMetadata_WithStringValue_ReturnsMetadataWithJsonElement() + { + // Arrange + AdditionalPropertiesDictionary additionalProperties = new() + { + { "stringKey", "stringValue" } + }; + + // Act + Dictionary? result = additionalProperties.ToA2AMetadata(); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.True(result.ContainsKey("stringKey")); + Assert.Equal("stringValue", result["stringKey"].GetString()); + } + + [Fact] + public void ToA2AMetadata_WithNumericValue_ReturnsMetadataWithJsonElement() + { + // Arrange + AdditionalPropertiesDictionary additionalProperties = new() + { + { "numberKey", 42 } + }; + + // Act + Dictionary? result = additionalProperties.ToA2AMetadata(); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.True(result.ContainsKey("numberKey")); + Assert.Equal(42, result["numberKey"].GetInt32()); + } + + [Fact] + public void ToA2AMetadata_WithBooleanValue_ReturnsMetadataWithJsonElement() + { + // Arrange + AdditionalPropertiesDictionary additionalProperties = new() + { + { "booleanKey", true } + }; + + // Act + Dictionary? result = additionalProperties.ToA2AMetadata(); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.True(result.ContainsKey("booleanKey")); + Assert.True(result["booleanKey"].GetBoolean()); + } + + [Fact] + public void ToA2AMetadata_WithMultipleProperties_ReturnsMetadataWithAllProperties() + { + // Arrange + AdditionalPropertiesDictionary additionalProperties = new() + { + { "stringKey", "stringValue" }, + { "numberKey", 42 }, + { "booleanKey", true } + }; + + // Act + Dictionary? result = additionalProperties.ToA2AMetadata(); + + // Assert + Assert.NotNull(result); + Assert.Equal(3, result.Count); + + Assert.True(result.ContainsKey("stringKey")); + Assert.Equal("stringValue", result["stringKey"].GetString()); + + Assert.True(result.ContainsKey("numberKey")); + Assert.Equal(42, result["numberKey"].GetInt32()); + + Assert.True(result.ContainsKey("booleanKey")); + Assert.True(result["booleanKey"].GetBoolean()); + } + + [Fact] + public void ToA2AMetadata_WithArrayValue_ReturnsMetadataWithJsonElement() + { + // Arrange + int[] arrayValue = [1, 2, 3]; + AdditionalPropertiesDictionary additionalProperties = new() + { + { "arrayKey", arrayValue } + }; + + // Act + Dictionary? result = additionalProperties.ToA2AMetadata(); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.True(result.ContainsKey("arrayKey")); + Assert.Equal(JsonValueKind.Array, result["arrayKey"].ValueKind); + Assert.Equal(3, result["arrayKey"].GetArrayLength()); + } + + [Fact] + public void ToA2AMetadata_WithNullValue_ReturnsMetadataWithNullJsonElement() + { + // Arrange + AdditionalPropertiesDictionary additionalProperties = new() + { + { "nullKey", null! } + }; + + // Act + Dictionary? result = additionalProperties.ToA2AMetadata(); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.True(result.ContainsKey("nullKey")); + Assert.Equal(JsonValueKind.Null, result["nullKey"].ValueKind); + } + + [Fact] + public void ToA2AMetadata_WithJsonElementValue_ReturnsMetadataWithJsonElement() + { + // Arrange + JsonElement jsonElement = JsonSerializer.SerializeToElement(new { name = "test", value = 123 }); + AdditionalPropertiesDictionary additionalProperties = new() + { + { "jsonElementKey", jsonElement } + }; + + // Act + Dictionary? result = additionalProperties.ToA2AMetadata(); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.True(result.ContainsKey("jsonElementKey")); + Assert.Equal(JsonValueKind.Object, result["jsonElementKey"].ValueKind); + Assert.Equal("test", result["jsonElementKey"].GetProperty("name").GetString()); + Assert.Equal(123, result["jsonElementKey"].GetProperty("value").GetInt32()); + } +}