.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 17:59:27 +00:00
committed by GitHub
co-authored by Stephen Toub
parent 22d76e5780
commit d7f422b92c
14 changed files with 25 additions and 1073 deletions
@@ -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;
}
}