mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
a3a9147e61
* Rename AgentThread to AgentSession * Add more renames * Update readme files * Revert nullable variable change and further fixes. * Revert change in header name * Fix some comments and tests * Update changelog. * Address PR feedback. * Fixing code review comments. * Fix new errors after merging latest code.
169 lines
6.2 KiB
C#
169 lines
6.2 KiB
C#
// Copyright (c) Microsoft. All rights reserved.
|
|
|
|
using System.Collections.Concurrent;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Text.Json;
|
|
using A2A;
|
|
using Microsoft.Agents.AI;
|
|
using Microsoft.Agents.AI.Hosting.A2A.Converters;
|
|
using Microsoft.Extensions.AI;
|
|
|
|
namespace AgentWebChat.Web;
|
|
|
|
internal sealed class A2AAgentClient : AgentClientBase
|
|
{
|
|
private readonly ILogger _logger;
|
|
private readonly Uri _uri;
|
|
|
|
// because A2A sdk does not provide a client which can handle multiple agents, we need a client per agent
|
|
// for this app the convention is "baseUri/<agentname>"
|
|
private readonly ConcurrentDictionary<string, (A2AClient, A2ACardResolver)> _clients = [];
|
|
|
|
public A2AAgentClient(ILogger logger, Uri baseUri)
|
|
{
|
|
this._logger = logger;
|
|
this._uri = baseUri;
|
|
}
|
|
|
|
public override async IAsyncEnumerable<AgentResponseUpdate> RunStreamingAsync(
|
|
string agentName,
|
|
IList<ChatMessage> messages,
|
|
string? sessionId = null,
|
|
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
|
{
|
|
this._logger.LogInformation("Running agent {AgentName} with {MessageCount} messages via A2A", agentName, messages.Count);
|
|
|
|
var (a2aClient, _) = this.ResolveClient(agentName);
|
|
var contextId = sessionId ?? Guid.NewGuid().ToString("N");
|
|
|
|
// Convert and send messages via A2A without try-catch in yield method
|
|
var results = new List<AgentResponseUpdate>();
|
|
|
|
try
|
|
{
|
|
// Convert all messages to A2A parts and create a single message
|
|
var parts = messages.ToParts();
|
|
var a2aMessage = new AgentMessage
|
|
{
|
|
MessageId = Guid.NewGuid().ToString("N"),
|
|
ContextId = contextId,
|
|
Role = MessageRole.User,
|
|
Parts = parts
|
|
};
|
|
|
|
var messageSendParams = new MessageSendParams { Message = a2aMessage };
|
|
var a2aResponse = await a2aClient.SendMessageAsync(messageSendParams, cancellationToken);
|
|
|
|
// Handle different response types
|
|
if (a2aResponse is AgentMessage message)
|
|
{
|
|
var responseMessage = message.ToChatMessage();
|
|
if (responseMessage is { Contents.Count: > 0 })
|
|
{
|
|
results.Add(new AgentResponseUpdate(responseMessage.Role, responseMessage.Contents)
|
|
{
|
|
MessageId = message.MessageId,
|
|
CreatedAt = DateTimeOffset.UtcNow
|
|
});
|
|
}
|
|
}
|
|
else if (a2aResponse is AgentTask agentTask)
|
|
{
|
|
// Manually convert AgentTask artifacts to ChatMessages since the extension method is internal
|
|
if (agentTask.Artifacts is not null)
|
|
{
|
|
foreach (var artifact in agentTask.Artifacts)
|
|
{
|
|
List<AIContent>? aiContents = null;
|
|
|
|
foreach (var part in artifact.Parts)
|
|
{
|
|
(aiContents ??= []).Add(part.ToAIContent());
|
|
}
|
|
|
|
if (aiContents is not null)
|
|
{
|
|
var additionalProperties = ConvertMetadataToAdditionalProperties(artifact.Metadata);
|
|
var chatMessage = new ChatMessage(ChatRole.Assistant, aiContents)
|
|
{
|
|
AdditionalProperties = additionalProperties,
|
|
RawRepresentation = artifact,
|
|
};
|
|
|
|
results.Add(new AgentResponseUpdate(chatMessage.Role, chatMessage.Contents)
|
|
{
|
|
MessageId = agentTask.Id,
|
|
CreatedAt = DateTimeOffset.UtcNow
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
this._logger.LogWarning("Unsupported A2A response type: {ResponseType}", a2aResponse?.GetType().FullName ?? "null");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
this._logger.LogError(ex, "Error running agent {AgentName} via A2A", agentName);
|
|
|
|
results.Add(new AgentResponseUpdate(ChatRole.Assistant, $"Error: {ex.Message}")
|
|
{
|
|
MessageId = Guid.NewGuid().ToString("N"),
|
|
CreatedAt = DateTimeOffset.UtcNow
|
|
});
|
|
}
|
|
|
|
// Yield the results
|
|
foreach (var result in results)
|
|
{
|
|
yield return result;
|
|
}
|
|
}
|
|
|
|
public override async Task<AgentCard?> GetAgentCardAsync(string agentName, CancellationToken cancellationToken = default)
|
|
{
|
|
this._logger.LogInformation("Retrieving agent card for {Agent}", agentName);
|
|
|
|
var (_, a2aCardResolver) = this.ResolveClient(agentName);
|
|
try
|
|
{
|
|
return await a2aCardResolver.GetAgentCardAsync(cancellationToken);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
this._logger.LogError(ex, "Failed to get agent card for {AgentName}", agentName);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private (A2AClient, A2ACardResolver) ResolveClient(string agentName) =>
|
|
this._clients.GetOrAdd(agentName, name =>
|
|
{
|
|
var uri = new Uri($"{this._uri}/{name}/");
|
|
var a2aClient = new A2AClient(uri);
|
|
|
|
// /v1/card is a default path for A2A agent card discovery
|
|
var a2aCardResolver = new A2ACardResolver(uri, agentCardPath: "/v1/card/");
|
|
|
|
this._logger.LogInformation("Built clients for agent {Agent} with baseUri {Uri}", name, uri);
|
|
return (a2aClient, a2aCardResolver);
|
|
});
|
|
|
|
private static AdditionalPropertiesDictionary? ConvertMetadataToAdditionalProperties(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;
|
|
}
|
|
}
|