mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET: Add an A2A client server sample (#633)
* Add an A2A client server sample * Address code review feedback * Start to fix code review feedback * Start to fix code review feedback * Start to fix code review feedback
This commit is contained in:
@@ -69,7 +69,7 @@ internal sealed class A2AAgent : AIAgent
|
||||
|
||||
if (a2aResponse is Message message)
|
||||
{
|
||||
UpdateThreadConversationId(thread, message);
|
||||
UpdateThreadConversationId(thread, message.ContextId);
|
||||
|
||||
return new AgentRunResponse
|
||||
{
|
||||
@@ -80,8 +80,21 @@ internal sealed class A2AAgent : AIAgent
|
||||
AdditionalProperties = message.Metadata.ToAdditionalProperties(),
|
||||
};
|
||||
}
|
||||
if (a2aResponse is AgentTask agentTask)
|
||||
{
|
||||
UpdateThreadConversationId(thread, agentTask.ContextId);
|
||||
|
||||
throw new NotSupportedException($"Only message responses are supported from A2A agents. Received: {a2aResponse.GetType().FullName ?? "null"}");
|
||||
return new AgentRunResponse
|
||||
{
|
||||
AgentId = this.Id,
|
||||
ResponseId = agentTask.Id,
|
||||
RawRepresentation = agentTask,
|
||||
Messages = agentTask.ToChatMessages(),
|
||||
AdditionalProperties = agentTask.Metadata.ToAdditionalProperties(),
|
||||
};
|
||||
}
|
||||
|
||||
throw new NotSupportedException($"Only Message and AgentTask responses are supported from A2A agents. Received: {a2aResponse.GetType().FullName ?? "null"}");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -107,7 +120,7 @@ internal sealed class A2AAgent : AIAgent
|
||||
throw new NotSupportedException($"Only message responses are supported from A2A agents. Received: {sseEvent.Data?.GetType().FullName ?? "null"}");
|
||||
}
|
||||
|
||||
UpdateThreadConversationId(thread, message);
|
||||
UpdateThreadConversationId(thread, message.ContextId);
|
||||
|
||||
yield return new AgentRunResponseUpdate
|
||||
{
|
||||
@@ -147,22 +160,22 @@ internal sealed class A2AAgent : AIAgent
|
||||
}
|
||||
}
|
||||
|
||||
private static void UpdateThreadConversationId(AgentThread? thread, Message message)
|
||||
private static void UpdateThreadConversationId(AgentThread? thread, string? contextId)
|
||||
{
|
||||
if (thread is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Surface cases where the A2A agent responds with a message that
|
||||
// Surface cases where the A2A agent responds with a response that
|
||||
// has a different context Id than the thread's conversation Id.
|
||||
if (thread.ConversationId is not null && message.ContextId is not null && thread.ConversationId != message.ContextId)
|
||||
if (thread.ConversationId is not null && contextId is not null && thread.ConversationId != contextId)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"The {nameof(message.ContextId)} returned from the A2A agent is different from the conversation Id of the provided {nameof(AgentThread)}.");
|
||||
$"The {nameof(contextId)} returned from the A2A agent is different from the conversation Id of the provided {nameof(AgentThread)}.");
|
||||
}
|
||||
|
||||
// Assign a server-generated context Id to the thread if it's not already set.
|
||||
thread.ConversationId ??= message.ContextId;
|
||||
thread.ConversationId ??= contextId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using A2A;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.A2A;
|
||||
|
||||
/// <summary>
|
||||
/// Host which will attach an <see cref="AIAgent"/> to a <see cref="ITaskManager"/>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This implementation only handles:
|
||||
/// <list type="bullet">
|
||||
/// <item><code>TaskManager.OnMessageReceived</code></item>
|
||||
/// <item><code>TaskManager.OnAgentCardQuery</code></item>
|
||||
/// </list>
|
||||
/// Support for task management will be added later as part of the long-running task execution work.
|
||||
/// </remarks>
|
||||
public sealed class A2AHostAgent
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the SemanticKernelTravelAgent
|
||||
/// </summary>
|
||||
public A2AHostAgent(AIAgent agent, AgentCard agentCard, TaskManager? taskManager = null)
|
||||
{
|
||||
Throw.IfNull(agent);
|
||||
Throw.IfNull(agentCard);
|
||||
|
||||
this.Agent = agent;
|
||||
this._agentCard = agentCard;
|
||||
|
||||
this.Attach(taskManager ?? new TaskManager());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The associated <see cref="AIAgent"/>
|
||||
/// </summary>
|
||||
public AIAgent? Agent { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The associated <see cref="ITaskManager"/>
|
||||
/// </summary>
|
||||
public TaskManager? TaskManager => this._taskManager;
|
||||
|
||||
/// <summary>
|
||||
/// Attach the <see cref="A2AAgent"/> to the provided <see cref="ITaskManager"/>
|
||||
/// </summary>
|
||||
/// <param name="taskManager"></param>
|
||||
public void Attach(TaskManager taskManager)
|
||||
{
|
||||
Throw.IfNull(taskManager);
|
||||
|
||||
this._taskManager = taskManager;
|
||||
taskManager.OnMessageReceived = this.OnMessageReceivedAsync;
|
||||
taskManager.OnAgentCardQuery = this.GetAgentCardAsync;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle a received message
|
||||
/// </summary>
|
||||
/// <param name="messageSend">The <see cref="MessageSendParams"/> to handle</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to cancel the operation</param>
|
||||
public async Task<Message> OnMessageReceivedAsync(MessageSendParams messageSend, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(messageSend);
|
||||
Throw.IfNull(this.Agent);
|
||||
|
||||
if (this._taskManager is null)
|
||||
{
|
||||
throw new InvalidOperationException("TaskManager must be attached before handling an agent message.");
|
||||
}
|
||||
|
||||
// Get message from the user
|
||||
var userMessage = messageSend.Message.ToChatMessage();
|
||||
|
||||
// Get the response from the agent
|
||||
var message = new Message();
|
||||
var agentResponse = await this.Agent.RunAsync(userMessage, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
foreach (var chatMessage in agentResponse.Messages)
|
||||
{
|
||||
var content = chatMessage.Text;
|
||||
message.Parts.Add(new TextPart() { Text = content! });
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return the <see cref="AgentCard"/> associated with this hosted agent.
|
||||
/// </summary>
|
||||
/// <param name="agentUrl">Current URL for the agent</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to cancel the operation</param>
|
||||
#pragma warning disable CA1054 // URI-like parameters should not be strings
|
||||
public Task<AgentCard> GetAgentCardAsync(string agentUrl, CancellationToken cancellationToken)
|
||||
{
|
||||
// Ensure the URL is in the correct format
|
||||
Uri uri = new(agentUrl);
|
||||
agentUrl = $"{uri.Scheme}://{uri.Host}:{uri.Port}/";
|
||||
|
||||
this._agentCard.Url = agentUrl;
|
||||
return Task.FromResult(this._agentCard);
|
||||
}
|
||||
#pragma warning restore CA1054 // URI-like parameters should not be strings
|
||||
|
||||
#region private
|
||||
private readonly AgentCard _agentCard;
|
||||
private TaskManager? _taskManager;
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using A2A;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.A2A;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for the <see cref="AgentTask"/> class.
|
||||
/// </summary>
|
||||
internal static class A2AAgentTaskExtensions
|
||||
{
|
||||
internal static IList<ChatMessage> ToChatMessages(this AgentTask agentTask)
|
||||
{
|
||||
_ = Throw.IfNull(agentTask);
|
||||
|
||||
List<ChatMessage> messages = [];
|
||||
|
||||
if (agentTask.Artifacts is not null)
|
||||
{
|
||||
foreach (var artifact in agentTask.Artifacts)
|
||||
{
|
||||
messages.Add(artifact.ToChatMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using A2A;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.A2A;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for the <see cref="Artifact"/> class.
|
||||
/// </summary>
|
||||
internal static class A2AArtifactExtensions
|
||||
{
|
||||
internal static ChatMessage ToChatMessage(this Artifact artifact)
|
||||
{
|
||||
List<AIContent>? aiContents = null;
|
||||
|
||||
foreach (var part in artifact.Parts)
|
||||
{
|
||||
(aiContents ??= []).Add(part.ToAIContent());
|
||||
}
|
||||
|
||||
return new ChatMessage(ChatRole.Assistant, aiContents)
|
||||
{
|
||||
AdditionalProperties = artifact.Metadata.ToAdditionalProperties(),
|
||||
RawRepresentation = artifact,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ namespace Microsoft.Extensions.AI.Agents.A2A;
|
||||
/// </summary>
|
||||
internal static class A2AMessageExtensions
|
||||
{
|
||||
public static ChatMessage ToChatMessage(this Message message)
|
||||
internal static ChatMessage ToChatMessage(this Message message)
|
||||
{
|
||||
List<AIContent>? aiContents = null;
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ internal static class A2AMetadataExtensions
|
||||
/// </summary>
|
||||
/// <param name="metadata">The metadata dictionary to convert.</param>
|
||||
/// <returns>The converted <see cref="AdditionalPropertiesDictionary"/>, or null if the input is null or empty.</returns>
|
||||
public static AdditionalPropertiesDictionary? ToAdditionalProperties(this Dictionary<string, JsonElement>? metadata)
|
||||
internal static AdditionalPropertiesDictionary? ToAdditionalProperties(this Dictionary<string, JsonElement>? metadata)
|
||||
{
|
||||
if (metadata is not { Count: > 0 })
|
||||
{
|
||||
|
||||
@@ -15,7 +15,7 @@ internal static class A2APartExtensions
|
||||
/// </summary>
|
||||
/// <param name="part">The A2A part to convert.</param>
|
||||
/// <returns>The corresponding <see cref="AIContent"/>.</returns>
|
||||
public static AIContent ToAIContent(this Part part)
|
||||
internal static AIContent ToAIContent(this Part part)
|
||||
{
|
||||
return part switch
|
||||
{
|
||||
|
||||
@@ -16,7 +16,7 @@ internal static class AIContentExtensions
|
||||
/// </summary>
|
||||
/// <param name="contents">The collection of AI contents to convert.</param>"
|
||||
/// <returns>The list of A2A <see cref="Part"/> objects.</returns>
|
||||
public static List<Part>? ToA2AParts(this IEnumerable<AIContent> contents)
|
||||
internal static List<Part>? ToA2AParts(this IEnumerable<AIContent> contents)
|
||||
{
|
||||
List<Part>? parts = null;
|
||||
|
||||
@@ -33,7 +33,7 @@ internal static class AIContentExtensions
|
||||
/// </summary>
|
||||
/// <param name="content">AI content to convert.</param>
|
||||
/// <returns>The corresponding A2A <see cref="Part"/> object.</returns>
|
||||
public static Part ToA2APart(this AIContent content)
|
||||
internal static Part ToA2APart(this AIContent content)
|
||||
{
|
||||
return content switch
|
||||
{
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Microsoft.Extensions.AI.Agents.A2A;
|
||||
/// </summary>
|
||||
internal static class ChatMessageExtensions
|
||||
{
|
||||
public static Message ToA2AMessage(this IReadOnlyCollection<ChatMessage> messages)
|
||||
internal static Message ToA2AMessage(this IReadOnlyCollection<ChatMessage> messages)
|
||||
{
|
||||
List<Part> allParts = [];
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
using static Microsoft.Extensions.AI.Agents.OpenTelemetryConsts.GenAI;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents;
|
||||
|
||||
/// <summary>
|
||||
/// Provides factory methods for creating implementations of <see cref="AIFunction"/> backed by an <see cref="AIAgent" />.
|
||||
/// </summary>
|
||||
public static class AgentAIFunctionFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a <see cref="AIFunction"/> that will invoke the provided Agent.
|
||||
/// </summary>
|
||||
/// <param name="agent">The <see cref="Agent" /> to be represented via the created <see cref="AIFunction"/>.</param>
|
||||
/// <param name="options">Metadata to use to override defaults inferred from <paramref name="agent"/>.</param>
|
||||
/// <returns>The created <see cref="AIFunction"/> for invoking the <see cref="AIAgent"/>.</returns>
|
||||
public static AIFunction CreateFromAgent(
|
||||
AIAgent agent,
|
||||
AIFunctionFactoryOptions? options = null)
|
||||
{
|
||||
Throw.IfNull(agent);
|
||||
|
||||
async Task<string> RunAgentAsync(string query, CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await agent.RunAsync(query, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
return response.Text;
|
||||
}
|
||||
|
||||
return AIFunctionFactory.Create(RunAgentAsync, options ?? new()
|
||||
{
|
||||
Name = agent.Name,
|
||||
Description = agent.Description,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user