mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.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:
co-authored by
Stephen Toub
parent
22d76e5780
commit
d7f422b92c
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user