.NET: Use extension methods from A2A package for converting between MEAI & A2A model classes (#1600)

* use extension methods from A2A package for converting between MEAI and A2A model classes.

* Update dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/A2AAgentTests.cs

Co-authored-by: Stephen Toub <stoub@microsoft.com>

* remove unused using

---------

Co-authored-by: Stephen Toub <stoub@microsoft.com>
This commit is contained in:
SergeyMenshykh
2025-10-21 18:59:27 +01:00
committed by GitHub
Unverified
parent 22d76e5780
commit d7f422b92c
14 changed files with 25 additions and 1073 deletions
+2 -2
View File
@@ -72,8 +72,8 @@
<!-- Agent SDKs -->
<PackageVersion Include="Microsoft.Agents.CopilotStudio.Client" Version="1.2.41" />
<!-- A2A -->
<PackageVersion Include="A2A" Version="0.3.1-preview" />
<PackageVersion Include="A2A.AspNetCore" Version="0.3.1-preview" />
<PackageVersion Include="A2A" Version="0.3.3-preview" />
<PackageVersion Include="A2A.AspNetCore" Version="0.3.3-preview" />
<!-- MCP -->
<PackageVersion Include="ModelContextProtocol" Version="0.4.0-preview.2" />
<!-- Inference SDKs -->
@@ -58,7 +58,7 @@ internal sealed class A2AAgentClient : AgentClientBase
if (a2aResponse is AgentMessage message)
{
var responseMessage = message.ToChatMessage();
if (responseMessage is not null)
if (responseMessage is { Contents.Count: > 0 })
{
results.Add(new AgentRunResponseUpdate(responseMessage.Role, responseMessage.Contents)
{
@@ -78,11 +78,7 @@ internal sealed class A2AAgentClient : AgentClientBase
foreach (var part in artifact.Parts)
{
var aiContent = ConvertPartToAIContent(part);
if (aiContent != null)
{
(aiContents ??= []).Add(aiContent);
}
(aiContents ??= []).Add(part.ToAIContent());
}
if (aiContents is not null)
@@ -155,20 +151,6 @@ internal sealed class A2AAgentClient : AgentClientBase
return (a2aClient, a2aCardResolver);
});
private static AIContent? ConvertPartToAIContent(Part part) =>
part switch
{
TextPart textPart => new TextContent(textPart.Text)
{
RawRepresentation = textPart
},
FilePart filePart when filePart.File is FileWithUri fileWithUrl => new HostedFileContent(fileWithUrl.Uri)
{
RawRepresentation = filePart
},
_ => null
};
private static AdditionalPropertiesDictionary? ConvertMetadataToAdditionalProperties(Dictionary<string, JsonElement>? metadata)
{
if (metadata is not { Count: > 0 })
@@ -184,22 +166,3 @@ internal sealed class A2AAgentClient : AgentClientBase
return additionalProperties;
}
}
// Extension method to convert multiple chat messages to A2A messages
internal static class ChatMessageExtensions
{
public static List<AgentMessage> ToA2AMessages(this IList<ChatMessage> chatMessages)
{
if (chatMessages is null || chatMessages.Count == 0)
{
return [];
}
var result = new List<AgentMessage>();
foreach (var chatMessage in chatMessages)
{
result.Add(chatMessage.ToA2AMessage());
}
return result;
}
}
@@ -4,6 +4,7 @@ using System;
using System.Threading;
using System.Threading.Tasks;
using A2A;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI.A2A;
@@ -15,13 +15,13 @@ internal static class A2AAIContentExtensions
/// </summary>
/// <param name="contents">The collection of AI contents to convert.</param>"
/// <returns>The list of A2A <see cref="Part"/> objects.</returns>
internal static List<Part>? ToA2AParts(this IEnumerable<AIContent> contents)
internal static List<Part>? ToParts(this IEnumerable<AIContent> contents)
{
List<Part>? parts = null;
foreach (var content in contents)
{
var part = content.ToA2APart();
var part = content.ToPart();
if (part is not null)
{
(parts ??= []).Add(part);
@@ -30,18 +30,4 @@ internal static class A2AAIContentExtensions
return parts;
}
/// <summary>
/// Converts a <see cref="AIContent"/> to a <see cref="Part"/> object."/>
/// </summary>
/// <param name="content">AI content to convert.</param>
/// <returns>The corresponding A2A <see cref="Part"/> object, or null if the content type is not supported.</returns>
internal static Part? ToA2APart(this AIContent content) =>
content switch
{
TextContent textContent => new TextPart { Text = textContent.Text },
HostedFileContent hostedFileContent => new FilePart { File = new FileWithUri { Uri = hostedFileContent.FileId } },
// Ignore unknown content types (FunctionCallContent, FunctionResultContent, etc.)
_ => null,
};
}
@@ -1,32 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using Microsoft.Extensions.AI;
namespace A2A;
/// <summary>
/// Extension methods for the <see cref="AgentMessage"/> class.
/// </summary>
internal static class A2AMessageExtensions
{
internal static ChatMessage ToChatMessage(this AgentMessage message)
{
List<AIContent>? aiContents = null;
foreach (var part in message.Parts)
{
var content = part.ToAIContent();
if (content is not null)
{
(aiContents ??= []).Add(content);
}
}
return new ChatMessage(ChatRole.Assistant, aiContents)
{
AdditionalProperties = message.Metadata.ToAdditionalProperties(),
RawRepresentation = message,
};
}
}
@@ -1,35 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.AI;
namespace A2A;
/// <summary>
/// Extension methods for the <see cref="Part"/> class.
/// </summary>
internal static class A2APartExtensions
{
/// <summary>
/// Converts an A2A <see cref="Part"/> to an <see cref="AIContent"/>.
/// </summary>
/// <param name="part">The A2A part to convert.</param>
/// <returns>The corresponding <see cref="AIContent"/>, or null if the part type is not supported.</returns>
internal static AIContent? ToAIContent(this Part part) =>
part switch
{
TextPart textPart => new TextContent(textPart.Text)
{
RawRepresentation = textPart,
AdditionalProperties = textPart.Metadata.ToAdditionalProperties()
},
FilePart filePart when filePart.File is FileWithUri fileWithUrl => new HostedFileContent(fileWithUrl.Uri)
{
RawRepresentation = filePart,
AdditionalProperties = filePart.Metadata.ToAdditionalProperties()
},
// Ignore unknown part types (DataPart, etc.)
_ => null
};
}
@@ -17,7 +17,7 @@ internal static class ChatMessageExtensions
foreach (var message in messages)
{
if (message.Contents.ToA2AParts() is { Count: > 0 } ps)
if (message.Contents.ToParts() is { Count: > 0 } ps)
{
allParts.AddRange(ps);
}
@@ -1,8 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Text.Json;
using A2A;
using Microsoft.Extensions.AI;
@@ -22,23 +20,16 @@ internal static class MessageConverter
{
foreach (var content in chatMessage.Contents)
{
var part = ConvertAIContentToPart(content);
var part = content.ToPart();
if (part is not null)
{
parts.Add(part);
}
}
// If no parts were created from content, create a text part from the message text
if (chatMessage.Contents.Count == 0 && !string.IsNullOrEmpty(chatMessage.Text))
{
parts.Add(new TextPart { Text = chatMessage.Text });
}
}
return parts;
}
/// <summary>
/// Converts A2A MessageSendParams to a collection of Microsoft.Extensions.AI ChatMessage objects.
/// </summary>
@@ -54,203 +45,9 @@ internal static class MessageConverter
var result = new List<ChatMessage>();
if (messageSendParams.Message?.Parts is not null)
{
var chatMessage = ToChatMessage(messageSendParams.Message);
if (chatMessage is not null)
{
result.Add(chatMessage);
}
result.Add(messageSendParams.Message.ToChatMessage());
}
return result;
}
/// <summary>
/// Converts collection of A2A <see cref="AgentMessage"/> to a collection of <see cref="ChatMessage"/> objects.
/// </summary>
/// <returns>A read-only collection of ChatMessage objects.</returns>
public static IReadOnlyCollection<ChatMessage> ToChatMessages(this ICollection<AgentMessage> messages)
{
if (messages is null || messages.Count == 0)
{
return [];
}
var result = new List<ChatMessage>();
foreach (var message in messages)
{
var chatMessage = ToChatMessage(message);
if (chatMessage is not null)
{
result.Add(chatMessage);
}
}
return result;
}
/// <summary>
/// Converts a single <see cref="AgentMessage"/> to a <see cref="ChatMessage"/>.
/// </summary>
/// <param name="message">The A2A message to convert.</param>
/// <returns>A ChatMessage object, or null if conversion is not possible.</returns>
public static ChatMessage? ToChatMessage(this AgentMessage message)
{
if (message?.Parts is not { Count: > 0 })
{
return null;
}
var chatRole = ConvertMessageRoleToChatRole(message.Role);
var content = new List<AIContent>();
foreach (var part in message.Parts)
{
var aiContent = ConvertPartToAIContent(part);
if (aiContent is not null)
{
content.Add(aiContent);
}
}
// If no valid content was extracted, return null
if (content.Count == 0)
{
return null;
}
// Create the ChatMessage with appropriate metadata
var chatMessage = new ChatMessage(chatRole, content)
{
MessageId = message.MessageId,
RawRepresentation = message
};
// Add any additional properties if needed
if (message.Metadata is not null)
{
chatMessage.AdditionalProperties = message.Metadata.ToAdditionalPropertiesDictionary();
}
return chatMessage;
}
/// <summary>
/// Converts A2A MessageRole to Microsoft.Extensions.AI ChatRole.
/// </summary>
/// <param name="messageRole">The A2A message role.</param>
/// <returns>The corresponding ChatRole.</returns>
private static ChatRole ConvertMessageRoleToChatRole(MessageRole messageRole) => messageRole switch
{
MessageRole.User => ChatRole.User,
MessageRole.Agent => ChatRole.Assistant,
_ => ChatRole.User
};
/// <summary>
/// Converts an A2A Part to Microsoft.Extensions.AI AIContent.
/// </summary>
/// <param name="part">The A2A part to convert.</param>
/// <returns>An AIContent object, or null if conversion is not possible.</returns>
#pragma warning disable CA1859 // Use concrete types when possible for improved performance
private static AIContent? ConvertPartToAIContent(Part part) =>
part switch
{
TextPart textPart => new TextContent(textPart.Text)
{
RawRepresentation = textPart,
AdditionalProperties = textPart.Metadata?.ToAdditionalPropertiesDictionary()
},
// Ignore unknown content types (FilePart, DataPart, etc.)
_ => null
};
/// <summary>
/// Converts Microsoft.Extensions.AI ChatMessage back to A2A Message format.
/// This is useful for the reverse operation.
/// </summary>
/// <param name="chatMessage">The ChatMessage to convert.</param>
/// <returns>An A2A Message object.</returns>
public static AgentMessage ToA2AMessage(this ChatMessage chatMessage)
{
if (chatMessage is null)
{
throw new ArgumentNullException(nameof(chatMessage));
}
var message = new AgentMessage
{
MessageId = chatMessage.MessageId ?? Guid.NewGuid().ToString("N"),
Role = ConvertChatRoleToMessageRole(chatMessage.Role),
Parts = []
};
// Convert content to parts
foreach (var content in chatMessage.Contents)
{
var part = ConvertAIContentToPart(content);
if (part is not null)
{
message.Parts.Add(part);
}
}
// If no parts were created from content, create a text part from the message text
if (message.Parts.Count == 0 && !string.IsNullOrEmpty(chatMessage.Text))
{
message.Parts.Add(new TextPart { Text = chatMessage.Text });
}
return message;
}
/// <summary>
/// Converts Microsoft.Extensions.AI ChatRole to A2A MessageRole.
/// </summary>
/// <param name="chatRole">The ChatRole to convert.</param>
/// <returns>The corresponding MessageRole.</returns>
private static MessageRole ConvertChatRoleToMessageRole(ChatRole chatRole)
{
if (chatRole == ChatRole.User)
{
return MessageRole.User;
}
if (chatRole == ChatRole.Assistant)
{
return MessageRole.Agent;
}
return MessageRole.User; // Default fallback
}
/// <summary>
/// Converts Microsoft.Extensions.AI AIContent to A2A Part.
/// </summary>
/// <param name="content">The AIContent to convert.</param>
/// <returns>A Part object, or null if conversion is not possible.</returns>
#pragma warning disable CA1859 // Use concrete types when possible for improved performance
private static Part? ConvertAIContentToPart(AIContent content) =>
content switch
{
TextContent textContent => new TextPart
{
Text = textContent.Text
},
// Ignore unknown content types (FunctionCallContent, FunctionResultContent, etc.)
_ => null
};
private static AdditionalPropertiesDictionary? ToAdditionalPropertiesDictionary(this Dictionary<string, JsonElement> 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;
}
}
@@ -371,7 +371,7 @@ public sealed class A2AAgentTests : IDisposable
new(ChatRole.User,
[
new TextContent("Check this file:"),
new HostedFileContent("https://example.com/file.pdf")
new UriContent("https://example.com/file.pdf", "application/pdf")
])
};
@@ -385,7 +385,7 @@ public sealed class A2AAgentTests : IDisposable
Assert.IsType<TextPart>(message.Parts[0]);
Assert.Equal("Check this file:", ((TextPart)message.Parts[0]).Text);
Assert.IsType<FilePart>(message.Parts[1]);
Assert.Equal("https://example.com/file.pdf", ((FileWithUri)((FilePart)message.Parts[1]).File).Uri);
Assert.Equal("https://example.com/file.pdf", ((FilePart)message.Parts[1]).File.Uri?.ToString());
}
public void Dispose()
@@ -11,55 +11,6 @@ namespace Microsoft.Agents.AI.A2A.UnitTests;
/// </summary>
public sealed class A2AAIContentExtensionsTests
{
[Fact]
public void ToA2APart_WithTextContent_ReturnsTextPart()
{
// Arrange
var textContent = new TextContent("Hello, world!");
// Act
var result = textContent.ToA2APart();
// Assert
Assert.NotNull(result);
var textPart = Assert.IsType<TextPart>(result);
Assert.Equal("Hello, world!", textPart.Text);
}
[Fact]
public void ToA2APart_WithHostedFileContent_ReturnsFilePart()
{
// Arrange
const string Uri = "https://example.com/file.txt";
var hostedFileContent = new HostedFileContent(Uri);
// Act
var result = hostedFileContent.ToA2APart();
// Assert
Assert.NotNull(result);
var filePart = Assert.IsType<FilePart>(result);
Assert.NotNull(filePart.File);
var fileWithUri = Assert.IsType<FileWithUri>(filePart.File);
Assert.Equal(Uri, fileWithUri.Uri);
}
[Fact]
public void ToA2APart_WithUnsupportedContentType_ReturnsNull()
{
// Arrange
var unsupportedContent = new MockAIContent();
// Act
var result = unsupportedContent.ToA2APart();
// Assert
Assert.Null(result);
}
[Fact]
public void ToA2AParts_WithEmptyCollection_ReturnsNull()
{
@@ -67,7 +18,7 @@ public sealed class A2AAIContentExtensionsTests
var emptyContents = new List<AIContent>();
// Act
var result = emptyContents.ToA2AParts();
var result = emptyContents.ToParts();
// Assert
Assert.Null(result);
@@ -80,31 +31,25 @@ public sealed class A2AAIContentExtensionsTests
var contents = new List<AIContent>
{
new TextContent("First text"),
new HostedFileContent("https://example.com/file1.txt"),
new UriContent("https://example.com/file1.txt", "file/txt"),
new TextContent("Second text"),
new HostedFileContent("https://example.com/file2.txt")
};
// Act
var result = contents.ToA2AParts();
var result = contents.ToParts();
// Assert
Assert.NotNull(result);
Assert.Equal(4, result.Count);
Assert.Equal(3, result.Count);
var firstTextPart = Assert.IsType<TextPart>(result[0]);
Assert.Equal("First text", firstTextPart.Text);
var firstFilePart = Assert.IsType<FilePart>(result[1]);
var firstFileWithUri = Assert.IsType<FileWithUri>(firstFilePart.File);
Assert.Equal("https://example.com/file1.txt", firstFileWithUri.Uri);
var filePart = Assert.IsType<FilePart>(result[1]);
Assert.Equal("https://example.com/file1.txt", filePart.File.Uri?.ToString());
var secondTextPart = Assert.IsType<TextPart>(result[2]);
Assert.Equal("Second text", secondTextPart.Text);
var secondFilePart = Assert.IsType<FilePart>(result[3]);
var secondFileWithUri = Assert.IsType<FileWithUri>(secondFilePart.File);
Assert.Equal("https://example.com/file2.txt", secondFileWithUri.Uri);
}
[Fact]
@@ -115,13 +60,13 @@ public sealed class A2AAIContentExtensionsTests
{
new TextContent("First text"),
new MockAIContent(), // Unsupported - should be ignored
new HostedFileContent("https://example.com/file.txt"),
new UriContent("https://example.com/file.txt", "file/txt"),
new MockAIContent(), // Unsupported - should be ignored
new TextContent("Second text")
};
// Act
var result = contents.ToA2AParts();
var result = contents.ToParts();
// Assert
Assert.NotNull(result);
@@ -131,8 +76,7 @@ public sealed class A2AAIContentExtensionsTests
Assert.Equal("First text", firstTextPart.Text);
var filePart = Assert.IsType<FilePart>(result[1]);
var fileWithUri = Assert.IsType<FileWithUri>(filePart.File);
Assert.Equal("https://example.com/file.txt", fileWithUri.Uri);
Assert.Equal("https://example.com/file.txt", filePart.File.Uri?.ToString());
var secondTextPart = Assert.IsType<TextPart>(result[2]);
Assert.Equal("Second text", secondTextPart.Text);
@@ -1,65 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Text.Json;
using A2A;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.A2A.UnitTests;
/// <summary>
/// Unit tests for the <see cref="A2AMessageExtensions"/> class.
/// </summary>
public sealed class A2AMessageExtensionsTests
{
[Fact]
public void ToChatMessage_WithMixedParts_ReturnsChatMessageWithMixedContents()
{
// Arrange
const string Uri = "https://example.com/image.jpg";
var metadata = new Dictionary<string, JsonElement>
{
["isUrgent"] = JsonDocument.Parse("true").RootElement
};
var message = new AgentMessage
{
MessageId = "mixed-parts-id",
Role = MessageRole.Agent,
Parts =
[
new TextPart { Text = "Here's an image:" },
new FilePart { File = new FileWithUri { Uri = Uri } },
new TextPart { Text = "What do you think?" }
],
Metadata = metadata
};
// Act
var result = message.ToChatMessage();
// Assert
Assert.NotNull(result);
Assert.Equal(ChatRole.Assistant, result.Role);
Assert.Equal(message, result.RawRepresentation);
Assert.NotNull(result.Contents);
Assert.Equal(3, result.Contents.Count);
var firstContent = Assert.IsType<TextContent>(result.Contents[0]);
Assert.Equal("Here's an image:", firstContent.Text);
var fileContent = Assert.IsType<HostedFileContent>(result.Contents[1]);
Assert.Equal(Uri, fileContent.FileId);
var lastContent = Assert.IsType<TextContent>(result.Contents[2]);
Assert.Equal("What do you think?", lastContent.Text);
Assert.NotNull(result.AdditionalProperties);
Assert.Single(result.AdditionalProperties);
Assert.True(result.AdditionalProperties.ContainsKey("isUrgent"));
Assert.True(((JsonElement)result.AdditionalProperties["isUrgent"]!).GetBoolean());
}
}
@@ -1,96 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Text.Json;
using A2A;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.A2A.UnitTests;
/// <summary>
/// Unit tests for the <see cref="A2APartExtensions"/> class.
/// </summary>
public sealed class A2APartExtensionsTests
{
[Fact]
public void ToAIContent_WithTextPart_ReturnsTextContent()
{
// Arrange
var textPart = new TextPart { Text = "Hello, world!" };
// Act
var result = textPart.ToAIContent();
// Assert
Assert.NotNull(result);
Assert.Equal(textPart, result.RawRepresentation);
var textContent = Assert.IsType<TextContent>(result);
Assert.Equal("Hello, world!", textContent.Text);
}
[Fact]
public void ToAIContent_WithTextPartWithMetadata_ReturnsTextContentWithAdditionalProperties()
{
// Arrange
var metadata = new Dictionary<string, JsonElement>
{
["key1"] = JsonDocument.Parse("\"value1\"").RootElement,
["key2"] = JsonDocument.Parse("42").RootElement,
["key3"] = JsonDocument.Parse("true").RootElement
};
var textPart = new TextPart
{
Text = "Hello with metadata!",
Metadata = metadata
};
// Act
var result = textPart.ToAIContent();
// Assert
Assert.NotNull(result);
var textContent = Assert.IsType<TextContent>(result);
Assert.Equal("Hello with metadata!", textContent.Text);
Assert.NotNull(textContent.AdditionalProperties);
Assert.Equal(3, textContent.AdditionalProperties.Count);
Assert.True(textContent.AdditionalProperties.ContainsKey("key1"));
Assert.True(textContent.AdditionalProperties.ContainsKey("key2"));
Assert.True(textContent.AdditionalProperties.ContainsKey("key3"));
}
[Fact]
public void ToAIContent_WithFilePartWithFileWithUri_ReturnsHostedFileContent()
{
// Arrange
const string Uri = "https://example.com/file.txt";
var filePart = new FilePart { File = new FileWithUri { Uri = Uri } };
// Act
var result = filePart.ToAIContent();
// Assert
Assert.NotNull(result);
Assert.Equal(filePart, result.RawRepresentation);
var hostedFileContent = Assert.IsType<HostedFileContent>(result);
Assert.Equal(Uri, hostedFileContent.FileId);
Assert.Null(hostedFileContent.AdditionalProperties);
}
[Fact]
public void ToAIContent_WithCustomPartType_ReturnsNull()
{
// Arrange
var customPart = new MockPart();
// Act
var result = customPart.ToAIContent();
// Assert
Assert.Null(result);
}
// Mock class for testing unsupported scenarios
private sealed class MockPart : Part;
}
@@ -17,7 +17,7 @@ public sealed class ChatMessageExtensionsTests
// Arrange
var contents = new List<AIContent>
{
new HostedFileContent("https://example.com/report.pdf"),
new UriContent("https://example.com/report.pdf", "file/pdf"),
new TextContent("please summarize the file content"),
new TextContent("and send it to me over email")
};
@@ -39,8 +39,7 @@ public sealed class ChatMessageExtensionsTests
var filePart = Assert.IsType<FilePart>(a2aMessage.Parts[0]);
Assert.NotNull(filePart.File);
var fileWithUri = Assert.IsType<FileWithUri>(filePart.File);
Assert.Equal("https://example.com/report.pdf", fileWithUri.Uri);
Assert.Equal("https://example.com/report.pdf", filePart.File.Uri?.ToString());
var secondTextPart = Assert.IsType<TextPart>(a2aMessage.Parts[1]);
Assert.Equal("please summarize the file content", secondTextPart.Text);
@@ -54,7 +53,7 @@ public sealed class ChatMessageExtensionsTests
{
// Arrange
var firstMessage = new ChatMessage(ChatRole.User, [
new HostedFileContent("https://example.com/report.pdf")
new UriContent("https://example.com/report.pdf", "file/pdf"),
]);
var secondMessage = new ChatMessage(ChatRole.User, [
new TextContent("please summarize the file content")
@@ -79,8 +78,7 @@ public sealed class ChatMessageExtensionsTests
var filePart = Assert.IsType<FilePart>(a2aMessage.Parts[0]);
Assert.NotNull(filePart.File);
var fileWithUri = Assert.IsType<FileWithUri>(filePart.File);
Assert.Equal("https://example.com/report.pdf", fileWithUri.Uri);
Assert.Equal("https://example.com/report.pdf", filePart.File.Uri?.ToString());
var secondTextPart = Assert.IsType<TextPart>(a2aMessage.Parts[1]);
Assert.Equal("please summarize the file content", secondTextPart.Text);
@@ -1,9 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using A2A;
using Microsoft.Agents.AI.Hosting.A2A.Converters;
using Microsoft.Extensions.AI;
@@ -85,510 +82,4 @@ public class MessageConverterTests
var textContent = Assert.IsType<TextContent>(chatMessage.Contents.First());
Assert.Equal("Hello, world!", textContent.Text);
}
[Fact]
public void ToChatMessages_MessageCollection_Null_ReturnsEmptyCollection()
{
ICollection<AgentMessage>? messages = null;
var result = messages!.ToChatMessages();
Assert.NotNull(result);
Assert.Empty(result);
}
[Fact]
public void ToChatMessages_MessageCollection_Empty_ReturnsEmptyCollection()
{
var messages = new List<AgentMessage>();
var result = messages.ToChatMessages();
Assert.NotNull(result);
Assert.Empty(result);
}
[Fact]
public void ToChatMessages_MessageCollection_WithValidMessages_ReturnsCorrectChatMessages()
{
var messages = new List<AgentMessage>
{
new()
{
MessageId = "user-msg",
Role = MessageRole.User,
Parts = [new TextPart { Text = "User message" }]
},
new()
{
MessageId = "agent-msg",
Role = MessageRole.Agent,
Parts = [new TextPart { Text = "Agent response" }]
}
};
var result = messages.ToChatMessages();
Assert.NotNull(result);
Assert.Equal(2, result.Count);
var userMessage = result.First();
Assert.Equal("user-msg", userMessage.MessageId);
Assert.Equal(ChatRole.User, userMessage.Role);
Assert.Equal("User message", ((TextContent)userMessage.Contents.First()).Text);
var agentMessage = result.Skip(1).First();
Assert.Equal("agent-msg", agentMessage.MessageId);
Assert.Equal(ChatRole.Assistant, agentMessage.Role);
Assert.Equal("Agent response", ((TextContent)agentMessage.Contents.First()).Text);
}
[Fact]
public void ToChatMessages_MessageCollection_SkipsInvalidMessages_ReturnsValidChatMessages()
{
var messages = new List<AgentMessage>
{
new()
{
MessageId = "valid-msg",
Role = MessageRole.User,
Parts = [new TextPart { Text = "Valid message" }]
},
new()
{
MessageId = "invalid-msg",
Role = MessageRole.User,
Parts = null! // Invalid - no parts
}
};
var result = messages.ToChatMessages();
Assert.NotNull(result);
Assert.Single(result);
var validMessage = result.First();
Assert.Equal("valid-msg", validMessage.MessageId);
Assert.Equal("Valid message", ((TextContent)validMessage.Contents.First()).Text);
}
[Fact]
public void ToA2AMessage_NullChatMessage_ThrowsArgumentNullException()
{
ChatMessage? chatMessage = null;
Assert.Throws<ArgumentNullException>(() => chatMessage!.ToA2AMessage());
}
[Fact]
public void ToA2AMessage_ValidChatMessage_ReturnsCorrectA2AMessage()
{
var chatMessage = new ChatMessage(ChatRole.User, "Hello, world!")
{
MessageId = "test-id"
};
var result = chatMessage.ToA2AMessage();
Assert.NotNull(result);
Assert.Equal("test-id", result.MessageId);
Assert.Equal(MessageRole.User, result.Role);
Assert.Single(result.Parts);
var textPart = Assert.IsType<TextPart>(result.Parts.First());
Assert.Equal("Hello, world!", textPart.Text);
}
[Fact]
public void ToA2AMessage_ChatMessageWithoutMessageId_GeneratesNewMessageId()
{
var chatMessage = new ChatMessage(ChatRole.Assistant, "Response message");
var result = chatMessage.ToA2AMessage();
Assert.NotNull(result);
Assert.NotNull(result.MessageId);
Assert.NotEmpty(result.MessageId);
Assert.Equal(MessageRole.Agent, result.Role);
}
[Fact]
public void ToA2AMessage_ChatMessageWithTextContent_ReturnsCorrectTextPart()
{
var textContent = new TextContent("Test content");
var chatMessage = new ChatMessage(ChatRole.User, [textContent]);
var result = chatMessage.ToA2AMessage();
Assert.NotNull(result);
Assert.Single(result.Parts);
var textPart = Assert.IsType<TextPart>(result.Parts.First());
Assert.Equal("Test content", textPart.Text);
}
[Fact]
public void ToA2AMessage_ChatMessageWithUnsupportedContent_IgnoresUnsupportedContent()
{
var unsupportedContent = new DataContent(new byte[] { 1, 2, 3 }, "image/png");
var chatMessage = new ChatMessage(ChatRole.User, [unsupportedContent]);
var result = chatMessage.ToA2AMessage();
// Should create a message but ignore the unsupported content
Assert.NotNull(result);
Assert.Empty(result.Parts);
}
[Fact]
public void ToA2AMessage_ChatMessageWithEmptyContent_CreatesTextPartFromMessageText()
{
var chatMessage = new ChatMessage(ChatRole.User, "Fallback text");
var result = chatMessage.ToA2AMessage();
Assert.NotNull(result);
Assert.Single(result.Parts);
var textPart = Assert.IsType<TextPart>(result.Parts.First());
Assert.Equal("Fallback text", textPart.Text);
}
[Fact]
public void ConvertMessageRoleToChatRole_UserRole_ReturnsUserChatRole()
{
var message = new AgentMessage
{
MessageId = "test",
Role = MessageRole.User,
Parts = [new TextPart { Text = "Test" }]
};
var result = new List<AgentMessage> { message }.ToChatMessages();
var chatMessage = result.First();
Assert.Equal(ChatRole.User, chatMessage.Role);
}
[Fact]
public void ConvertMessageRoleToChatRole_AgentRole_ReturnsAssistantChatRole()
{
var message = new AgentMessage
{
MessageId = "test",
Role = MessageRole.Agent,
Parts = [new TextPart { Text = "Test" }]
};
var result = new List<AgentMessage> { message }.ToChatMessages();
var chatMessage = result.First();
Assert.Equal(ChatRole.Assistant, chatMessage.Role);
}
[Fact]
public void ConvertMessageRoleToChatRole_UnknownRole_ReturnsUserChatRole()
{
var message = new AgentMessage
{
MessageId = "test",
Role = (MessageRole)999, // Unknown role
Parts = [new TextPart { Text = "Test" }]
};
var result = new List<AgentMessage> { message }.ToChatMessages();
var chatMessage = result.First();
Assert.Equal(ChatRole.User, chatMessage.Role);
}
[Fact]
public void ConvertChatRoleToMessageRole_UserRole_ReturnsUserMessageRole()
{
var chatMessage = new ChatMessage(ChatRole.User, "Test message");
var result = chatMessage.ToA2AMessage();
Assert.Equal(MessageRole.User, result.Role);
}
[Fact]
public void ConvertChatRoleToMessageRole_AssistantRole_ReturnsAgentMessageRole()
{
var chatMessage = new ChatMessage(ChatRole.Assistant, "Test message");
var result = chatMessage.ToA2AMessage();
Assert.Equal(MessageRole.Agent, result.Role);
}
[Fact]
public void ConvertChatRoleToMessageRole_SystemRole_ReturnsUserMessageRole()
{
var chatMessage = new ChatMessage(ChatRole.System, "Test message");
var result = chatMessage.ToA2AMessage();
Assert.Equal(MessageRole.User, result.Role);
}
[Fact]
public void ConvertChatRoleToMessageRole_ToolRole_ReturnsUserMessageRole()
{
var chatMessage = new ChatMessage(ChatRole.Tool, "Test message");
var result = chatMessage.ToA2AMessage();
Assert.Equal(MessageRole.User, result.Role);
}
[Fact]
public void ConvertPartToAIContent_TextPart_ReturnsTextContent()
{
var textPart = new TextPart { Text = "Sample text" };
var message = new AgentMessage
{
MessageId = "test",
Role = MessageRole.User,
Parts = [textPart]
};
var result = new List<AgentMessage> { message }.ToChatMessages();
var chatMessage = result.First();
var textContent = Assert.IsType<TextContent>(chatMessage.Contents.First());
Assert.Equal("Sample text", textContent.Text);
Assert.Equal(textPart, textContent.RawRepresentation);
}
[Fact]
public void ConvertPartToAIContent_TextPartWithMetadata_PreservesMetadata()
{
var metadata = new Dictionary<string, JsonElement>
{
["key1"] = JsonDocument.Parse("\"value1\"").RootElement,
["key2"] = JsonDocument.Parse("42").RootElement
};
var textPart = new TextPart
{
Text = "Text with metadata",
Metadata = metadata
};
var message = new AgentMessage
{
MessageId = "test",
Role = MessageRole.User,
Parts = [textPart]
};
var result = new List<AgentMessage> { message }.ToChatMessages();
var chatMessage = result.First();
var textContent = Assert.IsType<TextContent>(chatMessage.Contents.First());
Assert.NotNull(textContent.AdditionalProperties);
Assert.Equal(2, textContent.AdditionalProperties.Count);
Assert.True(textContent.AdditionalProperties.ContainsKey("key1"));
Assert.True(textContent.AdditionalProperties.ContainsKey("key2"));
}
[Fact]
public void ConvertPartToAIContent_FilePart_IgnoresUnsupportedPart()
{
var filePart = new FilePart();
var message = new AgentMessage
{
MessageId = "test",
Role = MessageRole.User,
Parts = [filePart]
};
var result = new List<AgentMessage> { message }.ToChatMessages();
// Should return empty collection since FilePart is ignored
Assert.NotNull(result);
Assert.Empty(result);
}
[Fact]
public void ConvertPartToAIContent_DataPart_IgnoresUnsupportedPart()
{
var dataPart = new DataPart();
var message = new AgentMessage
{
MessageId = "test",
Role = MessageRole.User,
Parts = [dataPart]
};
var result = new List<AgentMessage> { message }.ToChatMessages();
// Should return empty collection since DataPart is ignored
Assert.NotNull(result);
Assert.Empty(result);
}
[Fact]
public void ConvertMessageToChatMessage_WithMetadata_PreservesMetadataInAdditionalProperties()
{
var metadata = new Dictionary<string, JsonElement>
{
["timestamp"] = JsonDocument.Parse("\"2024-01-01T00:00:00Z\"").RootElement,
["priority"] = JsonDocument.Parse("1").RootElement
};
var message = new AgentMessage
{
MessageId = "test-id",
Role = MessageRole.User,
Parts = [new TextPart { Text = "Test" }],
Metadata = metadata
};
var result = new List<AgentMessage> { message }.ToChatMessages();
var chatMessage = result.First();
Assert.NotNull(chatMessage.AdditionalProperties);
Assert.Equal(2, chatMessage.AdditionalProperties.Count);
Assert.True(chatMessage.AdditionalProperties.ContainsKey("timestamp"));
Assert.True(chatMessage.AdditionalProperties.ContainsKey("priority"));
}
[Fact]
public void ConvertMessageToChatMessage_WithRawRepresentation_PreservesOriginalMessage()
{
var message = new AgentMessage
{
MessageId = "test-id",
Role = MessageRole.Agent,
Parts = [new TextPart { Text = "Test response" }]
};
var result = new List<AgentMessage> { message }.ToChatMessages();
var chatMessage = result.First();
Assert.Equal(message, chatMessage.RawRepresentation);
}
[Fact]
public void ToChatMessages_MessageWithEmptyParts_ReturnsEmptyCollection()
{
var message = new AgentMessage
{
MessageId = "test-id",
Role = MessageRole.User,
Parts = [] // Empty list
};
var result = new List<AgentMessage> { message }.ToChatMessages();
Assert.NotNull(result);
Assert.Empty(result);
}
[Fact]
public void ToA2AMessage_ChatMessageWithMultipleTextContents_CreatesMultipleParts()
{
var contents = new List<AIContent>
{
new TextContent("First part"),
new TextContent("Second part")
};
var chatMessage = new ChatMessage(ChatRole.User, contents);
var result = chatMessage.ToA2AMessage();
Assert.NotNull(result);
Assert.Equal(2, result.Parts.Count);
var firstPart = Assert.IsType<TextPart>(result.Parts[0]);
var secondPart = Assert.IsType<TextPart>(result.Parts[1]);
Assert.Equal("First part", firstPart.Text);
Assert.Equal("Second part", secondPart.Text);
}
[Fact]
public void ToAdditionalPropertiesDictionary_NullMetadata_ReturnsNull()
{
var message = new AgentMessage
{
MessageId = "test-id",
Role = MessageRole.User,
Parts = [new TextPart { Text = "Test" }],
Metadata = null
};
var result = new List<AgentMessage> { message }.ToChatMessages();
var chatMessage = result.First();
Assert.Null(chatMessage.AdditionalProperties);
}
[Fact]
public void ToAdditionalPropertiesDictionary_EmptyMetadata_ReturnsNull()
{
var message = new AgentMessage
{
MessageId = "test-id",
Role = MessageRole.User,
Parts = [new TextPart { Text = "Test" }],
Metadata = []
};
var result = new List<AgentMessage> { message }.ToChatMessages();
var chatMessage = result.First();
Assert.Null(chatMessage.AdditionalProperties);
}
[Fact]
public void ConvertPartToAIContent_MixedPartsWithUnsupported_IgnoresUnsupportedParts()
{
var message = new AgentMessage
{
MessageId = "test",
Role = MessageRole.User,
Parts = [
new TextPart { Text = "First part" },
new DataPart(), // Unsupported - should be ignored
new TextPart { Text = "Second part" },
new FilePart() // Unsupported - should be ignored
]
};
var result = new List<AgentMessage> { message }.ToChatMessages();
Assert.NotNull(result);
Assert.Single(result);
var chatMessage = result.First();
Assert.Equal(2, chatMessage.Contents.Count);
var firstContent = Assert.IsType<TextContent>(chatMessage.Contents[0]);
Assert.Equal("First part", firstContent.Text);
var secondContent = Assert.IsType<TextContent>(chatMessage.Contents[1]);
Assert.Equal("Second part", secondContent.Text);
}
[Fact]
public void ToA2AMessage_MixedContentWithUnsupported_IgnoresUnsupportedContent()
{
var contents = new List<AIContent>
{
new TextContent("First text"),
new DataContent(new byte[] { 1, 2, 3 }, "image/png"), // Unsupported - should be ignored
new TextContent("Second text")
};
var chatMessage = new ChatMessage(ChatRole.User, contents);
var result = chatMessage.ToA2AMessage();
Assert.NotNull(result);
Assert.Equal(2, result.Parts.Count);
var firstPart = Assert.IsType<TextPart>(result.Parts[0]);
Assert.Equal("First text", firstPart.Text);
var secondPart = Assert.IsType<TextPart>(result.Parts[1]);
Assert.Equal("Second text", secondPart.Text);
}
}