update a2a agent to the latest a2a sdk (#5257)

This commit is contained in:
SergeyMenshykh
2026-04-15 11:08:05 +01:00
committed by GitHub
Unverified
parent 485af07b8c
commit 6173e63f0b
18 changed files with 728 additions and 390 deletions
+11 -11
View File
@@ -32,18 +32,18 @@
<!-- Newtonsoft.Json -->
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
<!-- System.* -->
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.4" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.5" />
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
<PackageVersion Include="Microsoft.Bcl.Memory" Version="10.0.4" />
<PackageVersion Include="Microsoft.Bcl.Memory" Version="10.0.5" />
<PackageVersion Include="System.ClientModel" Version="1.10.0" />
<PackageVersion Include="System.CodeDom" Version="10.0.0" />
<PackageVersion Include="System.Collections.Immutable" Version="10.0.1" />
<PackageVersion Include="System.CommandLine" Version="2.0.0-rc.2.25502.107" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="10.0.4" />
<PackageVersion Include="System.Linq.AsyncEnumerable" Version="10.0.4" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="10.0.5" />
<PackageVersion Include="System.Linq.AsyncEnumerable" Version="10.0.5" />
<PackageVersion Include="System.Net.Http.Json" Version="10.0.0" />
<PackageVersion Include="System.Net.ServerSentEvents" Version="10.0.4" />
<PackageVersion Include="System.Text.Json" Version="10.0.4" />
<PackageVersion Include="System.Net.ServerSentEvents" Version="10.0.5" />
<PackageVersion Include="System.Text.Json" Version="10.0.5" />
<PackageVersion Include="System.Threading.Channels" Version="10.0.4" />
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
<PackageVersion Include="System.Net.Security" Version="4.3.2" />
@@ -64,7 +64,7 @@
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.0.0" />
<!-- Microsoft.Extensions.* -->
<PackageVersion Include="Microsoft.Extensions.AI" Version="10.4.0" />
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.4.0" />
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.4.1" />
<PackageVersion Include="Microsoft.Extensions.AI.Evaluation" Version="10.4.0" />
<PackageVersion Include="Microsoft.Extensions.AI.Evaluation.Quality" Version="10.4.0" />
<PackageVersion Include="Microsoft.Extensions.AI.Evaluation.Safety" Version="10.3.0-preview.1.26109.11" />
@@ -77,11 +77,11 @@
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.5" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.4" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.5" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.VectorData.Abstractions" Version="9.7.0" />
@@ -102,8 +102,8 @@
<PackageVersion Include="Microsoft.Agents.Authentication.Msal" Version="1.3.171-beta" />
<PackageVersion Include="Microsoft.Agents.Hosting.AspNetCore" Version="1.3.171-beta" />
<!-- A2A -->
<PackageVersion Include="A2A" Version="0.3.4-preview" />
<PackageVersion Include="A2A.AspNetCore" Version="0.3.4-preview" />
<PackageVersion Include="A2A" Version="1.0.0-preview2" />
<PackageVersion Include="A2A.AspNetCore" Version="1.0.0-preview2" />
<!-- MCP -->
<PackageVersion Include="ModelContextProtocol" Version="1.1.0" />
<!-- Inference SDKs -->
@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
@@ -13,7 +13,6 @@
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="System.Net.ServerSentEvents" />
</ItemGroup>
<ItemGroup>
@@ -62,12 +62,10 @@ public static class Program
}
var agentResponse = await hostAgent.Agent!.RunAsync(message, session, cancellationToken: cancellationToken);
foreach (var chatMessage in agentResponse.Messages)
{
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine($"\nAgent: {chatMessage.Text}");
Console.ResetColor();
}
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine($"\nAgent: {agentResponse.Text}");
Console.ResetColor();
}
}
catch (Exception ex)
@@ -13,7 +13,7 @@ namespace A2AServer;
internal static class HostAgentFactory
{
internal static async Task<(AIAgent, AgentCard)> CreateFoundryHostAgentAsync(string agentType, string model, string endpoint, string agentName, IList<AITool>? tools = null)
internal static async Task<(AIAgent, AgentCard)> CreateFoundryHostAgentAsync(string agentType, string model, string endpoint, string agentName, string[] agentUrls, IList<AITool>? tools = null)
{
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
@@ -25,16 +25,16 @@ internal static class HostAgentFactory
AgentCard agentCard = agentType.ToUpperInvariant() switch
{
"INVOICE" => GetInvoiceAgentCard(),
"POLICY" => GetPolicyAgentCard(),
"LOGISTICS" => GetLogisticsAgentCard(),
"INVOICE" => GetInvoiceAgentCard(agentUrls),
"POLICY" => GetPolicyAgentCard(agentUrls),
"LOGISTICS" => GetLogisticsAgentCard(agentUrls),
_ => throw new ArgumentException($"Unsupported agent type: {agentType}"),
};
return new(agent, agentCard);
}
internal static async Task<(AIAgent, AgentCard)> CreateChatCompletionHostAgentAsync(string agentType, string model, string apiKey, string name, string instructions, IList<AITool>? tools = null)
internal static async Task<(AIAgent, AgentCard)> CreateChatCompletionHostAgentAsync(string agentType, string model, string apiKey, string name, string instructions, string[] agentUrls, IList<AITool>? tools = null)
{
AIAgent agent = new OpenAIClient(apiKey)
.GetChatClient(model)
@@ -42,9 +42,9 @@ internal static class HostAgentFactory
AgentCard agentCard = agentType.ToUpperInvariant() switch
{
"INVOICE" => GetInvoiceAgentCard(),
"POLICY" => GetPolicyAgentCard(),
"LOGISTICS" => GetLogisticsAgentCard(),
"INVOICE" => GetInvoiceAgentCard(agentUrls),
"POLICY" => GetPolicyAgentCard(agentUrls),
"LOGISTICS" => GetLogisticsAgentCard(agentUrls),
_ => throw new ArgumentException($"Unsupported agent type: {agentType}"),
};
@@ -52,7 +52,7 @@ internal static class HostAgentFactory
}
#region private
private static AgentCard GetInvoiceAgentCard()
private static AgentCard GetInvoiceAgentCard(string[] agentUrls)
{
var capabilities = new AgentCapabilities()
{
@@ -81,10 +81,11 @@ internal static class HostAgentFactory
DefaultOutputModes = ["text"],
Capabilities = capabilities,
Skills = [invoiceQuery],
SupportedInterfaces = CreateAgentInterfaces(agentUrls)
};
}
private static AgentCard GetPolicyAgentCard()
private static AgentCard GetPolicyAgentCard(string[] agentUrls)
{
var capabilities = new AgentCapabilities()
{
@@ -113,10 +114,11 @@ internal static class HostAgentFactory
DefaultOutputModes = ["text"],
Capabilities = capabilities,
Skills = [policyQuery],
SupportedInterfaces = CreateAgentInterfaces(agentUrls)
};
}
private static AgentCard GetLogisticsAgentCard()
private static AgentCard GetLogisticsAgentCard(string[] agentUrls)
{
var capabilities = new AgentCapabilities()
{
@@ -145,7 +147,18 @@ internal static class HostAgentFactory
DefaultOutputModes = ["text"],
Capabilities = capabilities,
Skills = [logisticsQuery],
SupportedInterfaces = CreateAgentInterfaces(agentUrls)
};
}
private static List<AgentInterface> CreateAgentInterfaces(string[] agentUrls)
{
return agentUrls.Select(url => new AgentInterface
{
Url = url,
ProtocolBinding = "JSONRPC",
ProtocolVersion = "1.0",
}).ToList();
}
#endregion
}
@@ -38,14 +38,15 @@ IConfigurationRoot configuration = new ConfigurationBuilder()
string? apiKey = configuration["OPENAI_API_KEY"];
string model = configuration["OPENAI_CHAT_MODEL_NAME"] ?? "gpt-5.4-mini";
string? endpoint = configuration["AZURE_AI_PROJECT_ENDPOINT"];
string[] agentUrls = (app.Configuration["urls"] ?? "http://localhost:5000").Split(';');
var invoiceQueryPlugin = new InvoiceQuery();
IList<AITool> tools =
[
[
AIFunctionFactory.Create(invoiceQueryPlugin.QueryInvoices),
AIFunctionFactory.Create(invoiceQueryPlugin.QueryByTransactionId),
AIFunctionFactory.Create(invoiceQueryPlugin.QueryByInvoiceId)
];
];
AIAgent hostA2AAgent;
AgentCard hostA2AAgentCard;
@@ -54,9 +55,9 @@ if (!string.IsNullOrEmpty(endpoint) && !string.IsNullOrEmpty(agentName))
{
(hostA2AAgent, hostA2AAgentCard) = agentType.ToUpperInvariant() switch
{
"INVOICE" => await HostAgentFactory.CreateFoundryHostAgentAsync(agentType, model, endpoint, agentName, tools),
"POLICY" => await HostAgentFactory.CreateFoundryHostAgentAsync(agentType, model, endpoint, agentName),
"LOGISTICS" => await HostAgentFactory.CreateFoundryHostAgentAsync(agentType, model, endpoint, agentName),
"INVOICE" => await HostAgentFactory.CreateFoundryHostAgentAsync(agentType, model, endpoint, agentName, agentUrls, tools),
"POLICY" => await HostAgentFactory.CreateFoundryHostAgentAsync(agentType, model, endpoint, agentName, agentUrls),
"LOGISTICS" => await HostAgentFactory.CreateFoundryHostAgentAsync(agentType, model, endpoint, agentName, agentUrls),
_ => throw new ArgumentException($"Unsupported agent type: {agentType}"),
};
}
@@ -68,7 +69,7 @@ else if (!string.IsNullOrEmpty(apiKey))
agentType, model, apiKey, "InvoiceAgent",
"""
You specialize in handling queries related to invoices.
""", tools),
""", agentUrls, tools),
"POLICY" => await HostAgentFactory.CreateChatCompletionHostAgentAsync(
agentType, model, apiKey, "PolicyAgent",
"""
@@ -84,7 +85,7 @@ else if (!string.IsNullOrEmpty(apiKey))
resolution in SAP CRM and notify the customer via email within 2 business days, referencing the
original invoice and the credit memo number. Use the 'Formal Credit Notification' email
template."
"""),
""", agentUrls),
"LOGISTICS" => await HostAgentFactory.CreateChatCompletionHostAgentAsync(
agentType, model, apiKey, "LogisticsAgent",
"""
@@ -95,7 +96,7 @@ else if (!string.IsNullOrEmpty(apiKey))
Shipment number: SHPMT-SAP-001
Item: TSHIRT-RED-L
Quantity: 900
"""),
""", agentUrls),
_ => throw new ArgumentException($"Unsupported agent type: {agentType}"),
};
}
@@ -104,10 +105,9 @@ else
throw new ArgumentException("Either A2AServer:ApiKey or A2AServer:ConnectionString & agentName must be provided");
}
var a2aTaskManager = app.MapA2A(
app.MapA2A(
hostA2AAgent,
path: "/",
agentCard: hostA2AAgentCard,
taskManager => app.MapWellKnownAgentCard(taskManager, "/"));
agentCard: hostA2AAgentCard);
await app.RunAsync();
@@ -43,20 +43,21 @@ internal sealed class A2AAgentClient : AgentClientBase
{
// Convert all messages to A2A parts and create a single message
var parts = messages.ToParts();
var a2aMessage = new AgentMessage
var a2aMessage = new Message
{
MessageId = Guid.NewGuid().ToString("N"),
ContextId = contextId,
Role = MessageRole.User,
Role = Role.User,
Parts = parts
};
var messageSendParams = new MessageSendParams { Message = a2aMessage };
var messageSendParams = new SendMessageRequest { Message = a2aMessage };
var a2aResponse = await a2aClient.SendMessageAsync(messageSendParams, cancellationToken);
// Handle different response types
if (a2aResponse is AgentMessage message)
if (a2aResponse.PayloadCase == SendMessageResponseCase.Message)
{
var message = a2aResponse.Message!;
var responseMessage = message.ToChatMessage();
if (responseMessage is { Contents.Count: > 0 })
{
@@ -67,9 +68,10 @@ internal sealed class A2AAgentClient : AgentClientBase
});
}
}
else if (a2aResponse is AgentTask agentTask)
else if (a2aResponse.PayloadCase == SendMessageResponseCase.Task)
{
// Manually convert AgentTask artifacts to ChatMessages since the extension method is internal
var agentTask = a2aResponse.Task!;
if (agentTask.Artifacts is not null)
{
foreach (var artifact in agentTask.Artifacts)
+109 -96
View File
@@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.ServerSentEvents;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Threading;
@@ -100,64 +99,47 @@ public sealed class A2AAgent : AIAgent
this._logger.LogA2AAgentInvokingAgent(nameof(RunAsync), this.Id, this.Name);
A2AResponse? a2aResponse = null;
if (GetContinuationToken(messages, options) is { } token)
{
a2aResponse = await this._a2aClient.GetTaskAsync(token.TaskId, cancellationToken).ConfigureAwait(false);
}
else
{
MessageSendParams sendParams = new()
{
Message = CreateA2AMessage(typedSession, messages),
Metadata = options?.AdditionalProperties?.ToA2AMetadata()
};
AgentTask agentTask = await this._a2aClient.GetTaskAsync(new GetTaskRequest { Id = token.TaskId }, cancellationToken).ConfigureAwait(false);
a2aResponse = await this._a2aClient.SendMessageAsync(sendParams, cancellationToken).ConfigureAwait(false);
this._logger.LogAgentChatClientInvokedAgent(nameof(RunAsync), this.Id, this.Name);
UpdateSession(typedSession, agentTask.ContextId, agentTask.Id);
return this.ConvertToAgentResponse(agentTask);
}
SendMessageRequest sendParams = new()
{
Message = CreateA2AMessage(typedSession, messages),
Metadata = options?.AdditionalProperties?.ToA2AMetadata(),
Configuration = new SendMessageConfiguration { ReturnImmediately = options?.AllowBackgroundResponses is true }
};
SendMessageResponse a2aResponse = await this._a2aClient.SendMessageAsync(sendParams, cancellationToken).ConfigureAwait(false);
this._logger.LogAgentChatClientInvokedAgent(nameof(RunAsync), this.Id, this.Name);
if (a2aResponse is AgentMessage message)
if (a2aResponse.PayloadCase == SendMessageResponseCase.Message)
{
var message = a2aResponse.Message!;
UpdateSession(typedSession, message.ContextId);
return new AgentResponse
{
AgentId = this.Id,
ResponseId = message.MessageId,
FinishReason = ChatFinishReason.Stop,
RawRepresentation = message,
Messages = [message.ToChatMessage()],
AdditionalProperties = message.Metadata?.ToAdditionalProperties(),
};
return this.ConvertToAgentResponse(message);
}
if (a2aResponse is AgentTask agentTask)
if (a2aResponse.PayloadCase == SendMessageResponseCase.Task)
{
var agentTask = a2aResponse.Task!;
UpdateSession(typedSession, agentTask.ContextId, agentTask.Id);
var response = new AgentResponse
{
AgentId = this.Id,
ResponseId = agentTask.Id,
FinishReason = MapTaskStateToFinishReason(agentTask.Status.State),
RawRepresentation = agentTask,
Messages = agentTask.ToChatMessages() ?? [],
ContinuationToken = CreateContinuationToken(agentTask.Id, agentTask.Status.State),
AdditionalProperties = agentTask.Metadata?.ToAdditionalProperties(),
};
if (agentTask.ToChatMessages() is { Count: > 0 } taskMessages)
{
response.Messages = taskMessages;
}
return response;
return this.ConvertToAgentResponse(agentTask);
}
throw new NotSupportedException($"Only Message and AgentTask responses are supported from A2A agents. Received: {a2aResponse.GetType().FullName ?? "null"}");
throw new NotSupportedException($"Only Message and AgentTask responses are supported from A2A agents. Received: {a2aResponse.PayloadCase}");
}
/// <inheritdoc/>
@@ -169,59 +151,61 @@ public sealed class A2AAgent : AIAgent
this._logger.LogA2AAgentInvokingAgent(nameof(RunStreamingAsync), this.Id, this.Name);
ConfiguredCancelableAsyncEnumerable<SseItem<A2AEvent>> a2aSseEvents;
ConfiguredCancelableAsyncEnumerable<StreamResponse> streamEvents;
if (options?.ContinuationToken is not null)
if (GetContinuationToken(messages, options) is { } token)
{
// Task stream resumption is not well defined in the A2A v2.* specification, leaving it to the agent implementations.
// The v3.0 specification improves this by defining task stream reconnection that allows obtaining the same stream
// from the beginning, but it does not define stream resumption from a specific point in the stream.
// Therefore, the code should be updated once the A2A .NET library supports the A2A v3.0 specification,
// and AF has the necessary model to allow consumers to know whether they need to resume the stream and add new updates to
// the existing ones or reconnect the stream and obtain all updates again.
// For more details, see the following issue: https://github.com/microsoft/agent-framework/issues/1764
throw new InvalidOperationException("Reconnecting to task streams using continuation tokens is not supported yet.");
// a2aSseEvents = this._a2aClient.SubscribeToTaskAsync(token.TaskId, cancellationToken).ConfigureAwait(false);
streamEvents = this._a2aClient.SubscribeToTaskAsync(new SubscribeToTaskRequest { Id = token.TaskId }, cancellationToken).ConfigureAwait(false);
}
MessageSendParams sendParams = new()
else
{
Message = CreateA2AMessage(typedSession, messages),
Metadata = options?.AdditionalProperties?.ToA2AMetadata()
};
SendMessageRequest sendParams = new()
{
Message = CreateA2AMessage(typedSession, messages),
Metadata = options?.AdditionalProperties?.ToA2AMetadata()
};
a2aSseEvents = this._a2aClient.SendMessageStreamingAsync(sendParams, cancellationToken).ConfigureAwait(false);
streamEvents = this._a2aClient.SendStreamingMessageAsync(sendParams, cancellationToken).ConfigureAwait(false);
}
this._logger.LogAgentChatClientInvokedAgent(nameof(RunStreamingAsync), this.Id, this.Name);
string? contextId = null;
string? taskId = null;
await foreach (var sseEvent in a2aSseEvents)
await foreach (var streamResponse in streamEvents)
{
if (sseEvent.Data is AgentMessage message)
switch (streamResponse.PayloadCase)
{
contextId = message.ContextId;
case StreamResponseCase.Message:
var message = streamResponse.Message!;
contextId = message.ContextId;
yield return this.ConvertToAgentResponseUpdate(message);
break;
yield return this.ConvertToAgentResponseUpdate(message);
}
else if (sseEvent.Data is AgentTask task)
{
contextId = task.ContextId;
taskId = task.Id;
case StreamResponseCase.Task:
var task = streamResponse.Task!;
contextId = task.ContextId;
taskId = task.Id;
yield return this.ConvertToAgentResponseUpdate(task);
break;
yield return this.ConvertToAgentResponseUpdate(task);
}
else if (sseEvent.Data is TaskUpdateEvent taskUpdateEvent)
{
contextId = taskUpdateEvent.ContextId;
taskId = taskUpdateEvent.TaskId;
case StreamResponseCase.StatusUpdate:
var statusUpdate = streamResponse.StatusUpdate!;
contextId = statusUpdate.ContextId;
taskId = statusUpdate.TaskId;
yield return this.ConvertToAgentResponseUpdate(statusUpdate);
break;
yield return this.ConvertToAgentResponseUpdate(taskUpdateEvent);
}
else
{
throw new NotSupportedException($"Only message, task, task update events are supported from A2A agents. Received: {sseEvent.Data.GetType().FullName ?? "null"}");
case StreamResponseCase.ArtifactUpdate:
var artifactUpdate = streamResponse.ArtifactUpdate!;
contextId = artifactUpdate.ContextId;
taskId = artifactUpdate.TaskId;
yield return this.ConvertToAgentResponseUpdate(artifactUpdate);
break;
default:
throw new NotSupportedException($"Only message, task, task update events are supported from A2A agents. Received: {streamResponse.PayloadCase}");
}
}
@@ -284,7 +268,7 @@ public sealed class A2AAgent : AIAgent
session.TaskId = taskId;
}
private static AgentMessage CreateA2AMessage(A2AAgentSession typedSession, IEnumerable<ChatMessage> messages)
private static Message CreateA2AMessage(A2AAgentSession typedSession, IEnumerable<ChatMessage> messages)
{
var a2aMessage = messages.ToA2AMessage();
@@ -324,7 +308,34 @@ public sealed class A2AAgent : AIAgent
return null;
}
private AgentResponseUpdate ConvertToAgentResponseUpdate(AgentMessage message)
private AgentResponse ConvertToAgentResponse(Message message)
{
return new AgentResponse
{
AgentId = this.Id,
ResponseId = message.MessageId,
FinishReason = ChatFinishReason.Stop,
RawRepresentation = message,
Messages = [message.ToChatMessage()],
AdditionalProperties = message.Metadata?.ToAdditionalProperties(),
};
}
private AgentResponse ConvertToAgentResponse(AgentTask agentTask)
{
return new AgentResponse
{
AgentId = this.Id,
ResponseId = agentTask.Id,
FinishReason = MapTaskStateToFinishReason(agentTask.Status.State),
RawRepresentation = agentTask,
Messages = agentTask.ToChatMessages() ?? [],
ContinuationToken = CreateContinuationToken(agentTask.Id, agentTask.Status.State),
AdditionalProperties = agentTask.Metadata?.ToAdditionalProperties(),
};
}
private AgentResponseUpdate ConvertToAgentResponseUpdate(Message message)
{
return new AgentResponseUpdate
{
@@ -353,28 +364,30 @@ public sealed class A2AAgent : AIAgent
};
}
private AgentResponseUpdate ConvertToAgentResponseUpdate(TaskUpdateEvent taskUpdateEvent)
private AgentResponseUpdate ConvertToAgentResponseUpdate(TaskStatusUpdateEvent statusUpdateEvent)
{
AgentResponseUpdate responseUpdate = new()
return new AgentResponseUpdate
{
AgentId = this.Id,
ResponseId = taskUpdateEvent.TaskId,
RawRepresentation = taskUpdateEvent,
ResponseId = statusUpdateEvent.TaskId,
RawRepresentation = statusUpdateEvent,
Role = ChatRole.Assistant,
AdditionalProperties = taskUpdateEvent.Metadata?.ToAdditionalProperties() ?? [],
FinishReason = MapTaskStateToFinishReason(statusUpdateEvent.Status.State),
AdditionalProperties = statusUpdateEvent.Metadata?.ToAdditionalProperties() ?? [],
};
}
if (taskUpdateEvent is TaskArtifactUpdateEvent artifactUpdateEvent)
private AgentResponseUpdate ConvertToAgentResponseUpdate(TaskArtifactUpdateEvent artifactUpdateEvent)
{
return new AgentResponseUpdate
{
responseUpdate.Contents = artifactUpdateEvent.Artifact.ToAIContents();
responseUpdate.RawRepresentation = artifactUpdateEvent;
}
else if (taskUpdateEvent is TaskStatusUpdateEvent statusUpdateEvent)
{
responseUpdate.FinishReason = MapTaskStateToFinishReason(statusUpdateEvent.Status.State);
}
return responseUpdate;
AgentId = this.Id,
ResponseId = artifactUpdateEvent.TaskId,
RawRepresentation = artifactUpdateEvent,
Role = ChatRole.Assistant,
Contents = artifactUpdateEvent.Artifact.ToAIContents(),
AdditionalProperties = artifactUpdateEvent.Metadata?.ToAdditionalProperties() ?? [],
};
}
private static ChatFinishReason? MapTaskStateToFinishReason(TaskState state)
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Linq;
using System.Net.Http;
using Microsoft.Agents.AI;
using Microsoft.Extensions.Logging;
@@ -29,8 +30,12 @@ public static class A2AAgentCardExtensions
/// <returns>An <see cref="AIAgent"/> instance backed by the A2A agent.</returns>
public static AIAgent AsAIAgent(this AgentCard card, HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null)
{
// TODO: Refactor to support interface selection from card.SupportedInterfaces.
var url = card.SupportedInterfaces?.FirstOrDefault()?.Url
?? throw new InvalidOperationException("The AgentCard does not have any SupportedInterfaces with a URL.");
// Create the A2A client using the agent URL from the card.
var a2aClient = new A2AClient(new Uri(card.Url), httpClient);
var a2aClient = new A2AClient(new Uri(url), httpClient);
return a2aClient.AsAIAgent(name: card.Name, description: card.Description, loggerFactory: loggerFactory);
}
@@ -11,7 +11,7 @@ namespace Microsoft.Extensions.AI;
/// </summary>
internal static class ChatMessageExtensions
{
internal static AgentMessage ToA2AMessage(this IEnumerable<ChatMessage> messages)
internal static Message ToA2AMessage(this IEnumerable<ChatMessage> messages)
{
List<Part> allParts = [];
@@ -23,10 +23,10 @@ internal static class ChatMessageExtensions
}
}
return new AgentMessage
return new Message
{
MessageId = Guid.NewGuid().ToString("N"),
Role = MessageRole.User,
Role = Role.User,
Parts = allParts,
};
}
@@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(TargetFrameworksCore)</TargetFrameworks>
<VersionSuffix>preview</VersionSuffix>
<NoWarn>$(NoWarn);MEAI001</NoWarn>
</PropertyGroup>
@@ -6,9 +6,7 @@ using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.ServerSentEvents;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
@@ -89,14 +87,17 @@ public sealed class A2AAgentTests : IDisposable
public async Task RunAsync_WithValidUserMessage_RunsSuccessfullyAsync()
{
// Arrange
this._handler.ResponseToReturn = new AgentMessage
this._handler.ResponseToReturn = new SendMessageResponse
{
MessageId = "response-123",
Role = MessageRole.Agent,
Parts =
[
new TextPart { Text = "Hello! How can I help you today?" }
]
Message = new Message
{
MessageId = "response-123",
Role = Role.Agent,
Parts =
[
new Part { Text = "Hello! How can I help you today?" }
]
}
};
var inputMessages = new List<ChatMessage>
@@ -108,11 +109,11 @@ public sealed class A2AAgentTests : IDisposable
var result = await this._agent.RunAsync(inputMessages);
// Assert input message sent to A2AClient
var inputMessage = this._handler.CapturedMessageSendParams?.Message;
var inputMessage = this._handler.CapturedSendMessageRequest?.Message;
Assert.NotNull(inputMessage);
Assert.Single(inputMessage.Parts);
Assert.Equal(MessageRole.User, inputMessage.Role);
Assert.Equal("Hello, world!", ((TextPart)inputMessage.Parts[0]).Text);
Assert.Equal(Role.User, inputMessage.Role);
Assert.Equal("Hello, world!", inputMessage.Parts[0].Text);
// Assert response from A2AClient is converted correctly
Assert.NotNull(result);
@@ -120,8 +121,8 @@ public sealed class A2AAgentTests : IDisposable
Assert.Equal("response-123", result.ResponseId);
Assert.NotNull(result.RawRepresentation);
Assert.IsType<AgentMessage>(result.RawRepresentation);
Assert.Equal("response-123", ((AgentMessage)result.RawRepresentation).MessageId);
Assert.IsType<Message>(result.RawRepresentation);
Assert.Equal("response-123", ((Message)result.RawRepresentation).MessageId);
Assert.Single(result.Messages);
Assert.Equal(ChatRole.Assistant, result.Messages[0].Role);
@@ -133,15 +134,18 @@ public sealed class A2AAgentTests : IDisposable
public async Task RunAsync_WithNewSession_UpdatesSessionConversationIdAsync()
{
// Arrange
this._handler.ResponseToReturn = new AgentMessage
this._handler.ResponseToReturn = new SendMessageResponse
{
MessageId = "response-123",
Role = MessageRole.Agent,
Parts =
[
new TextPart { Text = "Response" }
],
ContextId = "new-context-id"
Message = new Message
{
MessageId = "response-123",
Role = Role.Agent,
Parts =
[
new Part { Text = "Response" }
],
ContextId = "new-context-id"
}
};
var inputMessages = new List<ChatMessage>
@@ -177,7 +181,7 @@ public sealed class A2AAgentTests : IDisposable
await this._agent.RunAsync(inputMessages, session);
// Assert
var message = this._handler.CapturedMessageSendParams?.Message;
var message = this._handler.CapturedSendMessageRequest?.Message;
Assert.NotNull(message);
Assert.Equal("existing-context-id", message.ContextId);
}
@@ -191,15 +195,18 @@ public sealed class A2AAgentTests : IDisposable
new(ChatRole.User, "Test message")
};
this._handler.ResponseToReturn = new AgentMessage
this._handler.ResponseToReturn = new SendMessageResponse
{
MessageId = "response-123",
Role = MessageRole.Agent,
Parts =
[
new TextPart { Text = "Response" }
],
ContextId = "different-context"
Message = new Message
{
MessageId = "response-123",
Role = Role.Agent,
Parts =
[
new Part { Text = "Response" }
],
ContextId = "different-context"
}
};
var session = await this._agent.CreateSessionAsync();
@@ -219,12 +226,15 @@ public sealed class A2AAgentTests : IDisposable
new(ChatRole.User, "Hello, streaming!")
};
this._handler.StreamingResponseToReturn = new AgentMessage()
this._handler.StreamingResponseToReturn = new StreamResponse
{
MessageId = "stream-1",
Role = MessageRole.Agent,
Parts = [new TextPart { Text = "Hello" }],
ContextId = "stream-context"
Message = new Message
{
MessageId = "stream-1",
Role = Role.Agent,
Parts = [new Part { Text = "Hello" }],
ContextId = "stream-context"
}
};
// Act
@@ -238,11 +248,11 @@ public sealed class A2AAgentTests : IDisposable
Assert.Single(updates);
// Assert input message sent to A2AClient
var inputMessage = this._handler.CapturedMessageSendParams?.Message;
var inputMessage = this._handler.CapturedSendMessageRequest?.Message;
Assert.NotNull(inputMessage);
Assert.Single(inputMessage.Parts);
Assert.Equal(MessageRole.User, inputMessage.Role);
Assert.Equal("Hello, streaming!", ((TextPart)inputMessage.Parts[0]).Text);
Assert.Equal(Role.User, inputMessage.Role);
Assert.Equal("Hello, streaming!", inputMessage.Parts[0].Text);
// Assert response from A2AClient is converted correctly
Assert.Equal(ChatRole.Assistant, updates[0].Role);
@@ -251,8 +261,8 @@ public sealed class A2AAgentTests : IDisposable
Assert.Equal(this._agent.Id, updates[0].AgentId);
Assert.Equal("stream-1", updates[0].ResponseId);
Assert.Equal(ChatFinishReason.Stop, updates[0].FinishReason);
Assert.IsType<AgentMessage>(updates[0].RawRepresentation);
Assert.Equal("stream-1", ((AgentMessage)updates[0].RawRepresentation!).MessageId);
Assert.IsType<Message>(updates[0].RawRepresentation);
Assert.Equal("stream-1", ((Message)updates[0].RawRepresentation!).MessageId);
}
[Fact]
@@ -264,12 +274,15 @@ public sealed class A2AAgentTests : IDisposable
new(ChatRole.User, "Test streaming")
};
this._handler.StreamingResponseToReturn = new AgentMessage()
this._handler.StreamingResponseToReturn = new StreamResponse
{
MessageId = "stream-1",
Role = MessageRole.Agent,
Parts = [new TextPart { Text = "Response" }],
ContextId = "new-stream-context"
Message = new Message
{
MessageId = "stream-1",
Role = Role.Agent,
Parts = [new Part { Text = "Response" }],
ContextId = "new-stream-context"
}
};
var session = await this._agent.CreateSessionAsync();
@@ -294,7 +307,7 @@ public sealed class A2AAgentTests : IDisposable
new(ChatRole.User, "Test streaming")
};
this._handler.StreamingResponseToReturn = new AgentMessage();
this._handler.StreamingResponseToReturn = new StreamResponse { Message = new Message() };
var session = await this._agent.CreateSessionAsync();
var a2aSession = (A2AAgentSession)session;
@@ -307,7 +320,7 @@ public sealed class A2AAgentTests : IDisposable
}
// Assert
var message = this._handler.CapturedMessageSendParams?.Message;
var message = this._handler.CapturedSendMessageRequest?.Message;
Assert.NotNull(message);
Assert.Equal("existing-context-id", message.ContextId);
}
@@ -325,12 +338,15 @@ public sealed class A2AAgentTests : IDisposable
new(ChatRole.User, "Test streaming")
};
this._handler.StreamingResponseToReturn = new AgentMessage()
this._handler.StreamingResponseToReturn = new StreamResponse
{
MessageId = "stream-1",
Role = MessageRole.Agent,
Parts = [new TextPart { Text = "Response" }],
ContextId = "different-context"
Message = new Message
{
MessageId = "stream-1",
Role = Role.Agent,
Parts = [new Part { Text = "Response" }],
ContextId = "different-context"
}
};
// Act
@@ -346,12 +362,15 @@ public sealed class A2AAgentTests : IDisposable
public async Task RunStreamingAsync_AllowsNonUserRoleMessagesAsync()
{
// Arrange
this._handler.StreamingResponseToReturn = new AgentMessage()
this._handler.StreamingResponseToReturn = new StreamResponse
{
MessageId = "stream-1",
Role = MessageRole.Agent,
Parts = [new TextPart { Text = "Response" }],
ContextId = "new-stream-context"
Message = new Message
{
MessageId = "stream-1",
Role = Role.Agent,
Parts = [new Part { Text = "Response" }],
ContextId = "new-stream-context"
}
};
var inputMessages = new List<ChatMessage>
@@ -385,13 +404,13 @@ public sealed class A2AAgentTests : IDisposable
await this._agent.RunAsync(inputMessages);
// Assert
var message = this._handler.CapturedMessageSendParams?.Message;
var message = this._handler.CapturedSendMessageRequest?.Message;
Assert.NotNull(message);
Assert.Equal(2, message.Parts.Count);
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", ((FilePart)message.Parts[1]).File.Uri?.ToString());
Assert.Equal(PartContentCase.Text, message.Parts[0].ContentCase);
Assert.Equal("Check this file:", message.Parts[0].Text);
Assert.Equal(PartContentCase.Url, message.Parts[1].ContentCase);
Assert.Equal("https://example.com/file.pdf", message.Parts[1].Url);
}
[Fact]
@@ -413,10 +432,11 @@ public sealed class A2AAgentTests : IDisposable
public async Task RunAsync_WithContinuationToken_CallsGetTaskAsyncAsync()
{
// Arrange
this._handler.ResponseToReturn = new AgentTask
this._handler.AgentTaskToReturn = new AgentTask
{
Id = "task-123",
ContextId = "context-123"
ContextId = "context-123",
Status = new() { State = TaskState.Submitted }
};
var options = new AgentRunOptions { ContinuationToken = new A2AContinuationToken("task-123") };
@@ -425,19 +445,22 @@ public sealed class A2AAgentTests : IDisposable
await this._agent.RunAsync([], options: options);
// Assert
Assert.Equal("tasks/get", this._handler.CapturedJsonRpcRequest?.Method);
Assert.Equal("task-123", this._handler.CapturedTaskIdParams?.Id);
Assert.Equal("GetTask", this._handler.CapturedJsonRpcRequest?.Method);
Assert.Equal("task-123", this._handler.CapturedGetTaskRequest?.Id);
}
[Fact]
public async Task RunAsync_WithTaskInSessionAndMessage_AddTaskAsReferencesToMessageAsync()
{
// Arrange
this._handler.ResponseToReturn = new AgentMessage
this._handler.ResponseToReturn = new SendMessageResponse
{
MessageId = "response-123",
Role = MessageRole.Agent,
Parts = [new TextPart { Text = "Response to task" }]
Message = new Message
{
MessageId = "response-123",
Role = Role.Agent,
Parts = [new Part { Text = "Response to task" }]
}
};
var session = (A2AAgentSession)await this._agent.CreateSessionAsync();
@@ -449,7 +472,7 @@ public sealed class A2AAgentTests : IDisposable
await this._agent.RunAsync(inputMessage, session);
// Assert
var message = this._handler.CapturedMessageSendParams?.Message;
var message = this._handler.CapturedSendMessageRequest?.Message;
Assert.Null(message?.TaskId);
Assert.NotNull(message?.ReferenceTaskIds);
Assert.Contains("task-123", message.ReferenceTaskIds);
@@ -459,11 +482,14 @@ public sealed class A2AAgentTests : IDisposable
public async Task RunAsync_WithAgentTask_UpdatesSessionTaskIdAsync()
{
// Arrange
this._handler.ResponseToReturn = new AgentTask
this._handler.ResponseToReturn = new SendMessageResponse
{
Id = "task-456",
ContextId = "context-789",
Status = new() { State = TaskState.Submitted }
Task = new AgentTask
{
Id = "task-456",
ContextId = "context-789",
Status = new() { State = TaskState.Submitted }
}
};
var session = await this._agent.CreateSessionAsync();
@@ -480,16 +506,19 @@ public sealed class A2AAgentTests : IDisposable
public async Task RunAsync_WithAgentTaskResponse_ReturnsTaskResponseCorrectlyAsync()
{
// Arrange
this._handler.ResponseToReturn = new AgentTask
this._handler.ResponseToReturn = new SendMessageResponse
{
Id = "task-789",
ContextId = "context-456",
Status = new() { State = TaskState.Submitted },
Metadata = new Dictionary<string, JsonElement>
Task = new AgentTask
{
Id = "task-789",
ContextId = "context-456",
Status = new() { State = TaskState.Submitted },
Metadata = new Dictionary<string, JsonElement>
{
{ "key1", JsonSerializer.SerializeToElement("value1") },
{ "count", JsonSerializer.SerializeToElement(42) }
}
}
};
var session = await this._agent.CreateSessionAsync();
@@ -532,11 +561,14 @@ public sealed class A2AAgentTests : IDisposable
public async Task RunAsync_WithVariousTaskStates_ReturnsCorrectTokenAsync(TaskState taskState)
{
// Arrange
this._handler.ResponseToReturn = new AgentTask
this._handler.ResponseToReturn = new SendMessageResponse
{
Id = "task-123",
ContextId = "context-123",
Status = new() { State = taskState }
Task = new AgentTask
{
Id = "task-123",
ContextId = "context-123",
Status = new() { State = taskState }
}
};
// Act
@@ -583,15 +615,76 @@ public sealed class A2AAgentTests : IDisposable
});
}
[Fact]
public async Task RunStreamingAsync_WithContinuationToken_UsesSubscribeToTaskMethodAsync()
{
// Arrange
this._handler.StreamingResponseToReturn = new StreamResponse
{
Message = new Message
{
MessageId = "response-123",
Role = Role.Agent,
Parts = [new Part { Text = "Continuation response" }]
}
};
var options = new AgentRunOptions { ContinuationToken = new A2AContinuationToken("task-456") };
// Act
await foreach (var _ in this._agent.RunStreamingAsync([], null, options))
{
// Just iterate through to trigger the logic
}
// Assert - verify SubscribeToTask was called (not SendStreamingMessage)
Assert.Single(this._handler.CapturedJsonRpcRequests);
Assert.Equal("SubscribeToTask", this._handler.CapturedJsonRpcRequests[0].Method);
}
[Fact]
public async Task RunStreamingAsync_WithContinuationToken_PassesCorrectTaskIdAsync()
{
// Arrange
this._handler.StreamingResponseToReturn = new StreamResponse
{
Message = new Message
{
MessageId = "response-123",
Role = Role.Agent,
Parts = [new Part { Text = "Continuation response" }]
}
};
const string ExpectedTaskId = "my-task-789";
var options = new AgentRunOptions { ContinuationToken = new A2AContinuationToken(ExpectedTaskId) };
// Act
await foreach (var _ in this._agent.RunStreamingAsync([], null, options))
{
// Just iterate through to trigger the logic
}
// Assert - verify the task ID was passed correctly
Assert.NotEmpty(this._handler.CapturedJsonRpcRequests);
var subscribeRequest = this._handler.CapturedJsonRpcRequests[0];
var subscribeParams = subscribeRequest.Params?.Deserialize<SubscribeToTaskRequest>(A2AJsonUtilities.DefaultOptions);
Assert.NotNull(subscribeParams);
Assert.Equal(ExpectedTaskId, subscribeParams.Id);
}
[Fact]
public async Task RunStreamingAsync_WithTaskInSessionAndMessage_AddTaskAsReferencesToMessageAsync()
{
// Arrange
this._handler.StreamingResponseToReturn = new AgentMessage
this._handler.StreamingResponseToReturn = new StreamResponse
{
MessageId = "response-123",
Role = MessageRole.Agent,
Parts = [new TextPart { Text = "Response to task" }]
Message = new Message
{
MessageId = "response-123",
Role = Role.Agent,
Parts = [new Part { Text = "Response to task" }]
}
};
var session = (A2AAgentSession)await this._agent.CreateSessionAsync();
@@ -604,7 +697,7 @@ public sealed class A2AAgentTests : IDisposable
}
// Assert
var message = this._handler.CapturedMessageSendParams?.Message;
var message = this._handler.CapturedSendMessageRequest?.Message;
Assert.Null(message?.TaskId);
Assert.NotNull(message?.ReferenceTaskIds);
Assert.Contains("task-123", message.ReferenceTaskIds);
@@ -614,11 +707,14 @@ public sealed class A2AAgentTests : IDisposable
public async Task RunStreamingAsync_WithAgentTask_UpdatesSessionTaskIdAsync()
{
// Arrange
this._handler.StreamingResponseToReturn = new AgentTask
this._handler.StreamingResponseToReturn = new StreamResponse
{
Id = "task-456",
ContextId = "context-789",
Status = new() { State = TaskState.Submitted }
Task = new AgentTask
{
Id = "task-456",
ContextId = "context-789",
Status = new() { State = TaskState.Submitted }
}
};
var session = await this._agent.CreateSessionAsync();
@@ -642,15 +738,18 @@ public sealed class A2AAgentTests : IDisposable
const string ContextId = "ctx-456";
const string MessageText = "Hello from agent!";
this._handler.StreamingResponseToReturn = new AgentMessage
this._handler.StreamingResponseToReturn = new StreamResponse
{
MessageId = MessageId,
Role = MessageRole.Agent,
ContextId = ContextId,
Parts =
[
new TextPart { Text = MessageText }
]
Message = new Message
{
MessageId = MessageId,
Role = Role.Agent,
ContextId = ContextId,
Parts =
[
new Part { Text = MessageText }
]
}
};
// Act
@@ -670,8 +769,8 @@ public sealed class A2AAgentTests : IDisposable
Assert.Equal(this._agent.Id, update0.AgentId);
Assert.Equal(MessageText, update0.Text);
Assert.Equal(ChatFinishReason.Stop, update0.FinishReason);
Assert.IsType<AgentMessage>(update0.RawRepresentation);
Assert.Equal(MessageId, ((AgentMessage)update0.RawRepresentation!).MessageId);
Assert.IsType<Message>(update0.RawRepresentation);
Assert.Equal(MessageId, ((Message)update0.RawRepresentation!).MessageId);
}
[Fact]
@@ -681,18 +780,21 @@ public sealed class A2AAgentTests : IDisposable
const string TaskId = "task-789";
const string ContextId = "ctx-012";
this._handler.StreamingResponseToReturn = new AgentTask
this._handler.StreamingResponseToReturn = new StreamResponse
{
Id = TaskId,
ContextId = ContextId,
Status = new() { State = TaskState.Submitted },
Artifacts = [
Task = new AgentTask
{
Id = TaskId,
ContextId = ContextId,
Status = new() { State = TaskState.Submitted },
Artifacts = [
new()
{
ArtifactId = "art-123",
Parts = [new TextPart { Text = "Task artifact content" }]
Parts = [new Part { Text = "Task artifact content" }]
}
]
}
};
var session = await this._agent.CreateSessionAsync();
@@ -728,11 +830,14 @@ public sealed class A2AAgentTests : IDisposable
const string TaskId = "task-status-123";
const string ContextId = "ctx-status-456";
this._handler.StreamingResponseToReturn = new TaskStatusUpdateEvent
this._handler.StreamingResponseToReturn = new StreamResponse
{
TaskId = TaskId,
ContextId = ContextId,
Status = new() { State = TaskState.Working }
StatusUpdate = new TaskStatusUpdateEvent
{
TaskId = TaskId,
ContextId = ContextId,
Status = new() { State = TaskState.Working }
}
};
var session = await this._agent.CreateSessionAsync();
@@ -768,14 +873,17 @@ public sealed class A2AAgentTests : IDisposable
const string ContextId = "ctx-artifact-456";
const string ArtifactContent = "Task artifact data";
this._handler.StreamingResponseToReturn = new TaskArtifactUpdateEvent
this._handler.StreamingResponseToReturn = new StreamResponse
{
TaskId = TaskId,
ContextId = ContextId,
Artifact = new()
ArtifactUpdate = new TaskArtifactUpdateEvent
{
ArtifactId = "artifact-789",
Parts = [new TextPart { Text = ArtifactContent }]
TaskId = TaskId,
ContextId = ContextId,
Artifact = new()
{
ArtifactId = "artifact-789",
Parts = [new Part { Text = ArtifactContent }]
}
}
};
@@ -848,15 +956,18 @@ public sealed class A2AAgentTests : IDisposable
public async Task RunAsync_WithAgentMessageResponseMetadata_ReturnsMetadataAsAdditionalPropertiesAsync()
{
// Arrange
this._handler.ResponseToReturn = new AgentMessage
this._handler.ResponseToReturn = new SendMessageResponse
{
MessageId = "response-123",
Role = MessageRole.Agent,
Parts = [new TextPart { Text = "Response with metadata" }],
Metadata = new Dictionary<string, JsonElement>
Message = new Message
{
{ "responseKey1", JsonSerializer.SerializeToElement("responseValue1") },
{ "responseCount", JsonSerializer.SerializeToElement(99) }
MessageId = "response-123",
Role = Role.Agent,
Parts = [new Part { Text = "Response with metadata" }],
Metadata = new Dictionary<string, JsonElement>
{
{ "responseKey1", JsonSerializer.SerializeToElement("responseValue1") },
{ "responseCount", JsonSerializer.SerializeToElement(99) }
}
}
};
@@ -877,14 +988,17 @@ public sealed class A2AAgentTests : IDisposable
}
[Fact]
public async Task RunAsync_WithAdditionalProperties_PropagatesThemAsMetadataToMessageSendParamsAsync()
public async Task RunAsync_WithAdditionalProperties_PropagatesThemAsMetadataToSendMessageRequestAsync()
{
// Arrange
this._handler.ResponseToReturn = new AgentMessage
this._handler.ResponseToReturn = new SendMessageResponse
{
MessageId = "response-123",
Role = MessageRole.Agent,
Parts = [new TextPart { Text = "Response" }]
Message = new Message
{
MessageId = "response-123",
Role = Role.Agent,
Parts = [new Part { Text = "Response" }]
}
};
var inputMessages = new List<ChatMessage>
@@ -906,22 +1020,25 @@ public sealed class A2AAgentTests : IDisposable
await this._agent.RunAsync(inputMessages, null, options);
// Assert
Assert.NotNull(this._handler.CapturedMessageSendParams);
Assert.NotNull(this._handler.CapturedMessageSendParams.Metadata);
Assert.Equal("value1", this._handler.CapturedMessageSendParams.Metadata["key1"].GetString());
Assert.Equal(42, this._handler.CapturedMessageSendParams.Metadata["key2"].GetInt32());
Assert.True(this._handler.CapturedMessageSendParams.Metadata["key3"].GetBoolean());
Assert.NotNull(this._handler.CapturedSendMessageRequest);
Assert.NotNull(this._handler.CapturedSendMessageRequest.Metadata);
Assert.Equal("value1", this._handler.CapturedSendMessageRequest.Metadata["key1"].GetString());
Assert.Equal(42, this._handler.CapturedSendMessageRequest.Metadata["key2"].GetInt32());
Assert.True(this._handler.CapturedSendMessageRequest.Metadata["key3"].GetBoolean());
}
[Fact]
public async Task RunAsync_WithNullAdditionalProperties_DoesNotSetMetadataAsync()
{
// Arrange
this._handler.ResponseToReturn = new AgentMessage
this._handler.ResponseToReturn = new SendMessageResponse
{
MessageId = "response-123",
Role = MessageRole.Agent,
Parts = [new TextPart { Text = "Response" }]
Message = new Message
{
MessageId = "response-123",
Role = Role.Agent,
Parts = [new Part { Text = "Response" }]
}
};
var inputMessages = new List<ChatMessage>
@@ -938,19 +1055,22 @@ public sealed class A2AAgentTests : IDisposable
await this._agent.RunAsync(inputMessages, null, options);
// Assert
Assert.NotNull(this._handler.CapturedMessageSendParams);
Assert.Null(this._handler.CapturedMessageSendParams.Metadata);
Assert.NotNull(this._handler.CapturedSendMessageRequest);
Assert.Null(this._handler.CapturedSendMessageRequest.Metadata);
}
[Fact]
public async Task RunStreamingAsync_WithAdditionalProperties_PropagatesThemAsMetadataToMessageSendParamsAsync()
public async Task RunStreamingAsync_WithAdditionalProperties_PropagatesThemAsMetadataToSendMessageRequestAsync()
{
// Arrange
this._handler.StreamingResponseToReturn = new AgentMessage
this._handler.StreamingResponseToReturn = new StreamResponse
{
MessageId = "stream-123",
Role = MessageRole.Agent,
Parts = [new TextPart { Text = "Streaming response" }]
Message = new Message
{
MessageId = "stream-123",
Role = Role.Agent,
Parts = [new Part { Text = "Streaming response" }]
}
};
var inputMessages = new List<ChatMessage>
@@ -974,22 +1094,25 @@ public sealed class A2AAgentTests : IDisposable
}
// Assert
Assert.NotNull(this._handler.CapturedMessageSendParams);
Assert.NotNull(this._handler.CapturedMessageSendParams.Metadata);
Assert.Equal("streamValue1", this._handler.CapturedMessageSendParams.Metadata["streamKey1"].GetString());
Assert.Equal(100, this._handler.CapturedMessageSendParams.Metadata["streamKey2"].GetInt32());
Assert.False(this._handler.CapturedMessageSendParams.Metadata["streamKey3"].GetBoolean());
Assert.NotNull(this._handler.CapturedSendMessageRequest);
Assert.NotNull(this._handler.CapturedSendMessageRequest.Metadata);
Assert.Equal("streamValue1", this._handler.CapturedSendMessageRequest.Metadata["streamKey1"].GetString());
Assert.Equal(100, this._handler.CapturedSendMessageRequest.Metadata["streamKey2"].GetInt32());
Assert.False(this._handler.CapturedSendMessageRequest.Metadata["streamKey3"].GetBoolean());
}
[Fact]
public async Task RunStreamingAsync_WithNullAdditionalProperties_DoesNotSetMetadataAsync()
{
// Arrange
this._handler.StreamingResponseToReturn = new AgentMessage
this._handler.StreamingResponseToReturn = new StreamResponse
{
MessageId = "stream-123",
Role = MessageRole.Agent,
Parts = [new TextPart { Text = "Streaming response" }]
Message = new Message
{
MessageId = "stream-123",
Role = Role.Agent,
Parts = [new Part { Text = "Streaming response" }]
}
};
var inputMessages = new List<ChatMessage>
@@ -1008,8 +1131,115 @@ public sealed class A2AAgentTests : IDisposable
}
// Assert
Assert.NotNull(this._handler.CapturedMessageSendParams);
Assert.Null(this._handler.CapturedMessageSendParams.Metadata);
Assert.NotNull(this._handler.CapturedSendMessageRequest);
Assert.Null(this._handler.CapturedSendMessageRequest.Metadata);
}
[Fact]
public async Task RunAsync_WithDefaultOptions_SetsBlockingToTrueAsync()
{
// Arrange
var inputMessages = new List<ChatMessage>
{
new(ChatRole.User, "Test message")
};
// Act
await this._agent.RunAsync(inputMessages);
// Assert
Assert.NotNull(this._handler.CapturedSendMessageRequest);
Assert.NotNull(this._handler.CapturedSendMessageRequest.Configuration);
Assert.False(this._handler.CapturedSendMessageRequest.Configuration.ReturnImmediately);
}
[Fact]
public async Task RunAsync_WithAllowBackgroundResponsesTrue_SetsReturnImmediatelyToTrueAsync()
{
// Arrange
var inputMessages = new List<ChatMessage>
{
new(ChatRole.User, "Test message")
};
var session = await this._agent.CreateSessionAsync();
var options = new AgentRunOptions { AllowBackgroundResponses = true };
// Act
await this._agent.RunAsync(inputMessages, session, options);
// Assert
Assert.NotNull(this._handler.CapturedSendMessageRequest);
Assert.NotNull(this._handler.CapturedSendMessageRequest.Configuration);
Assert.True(this._handler.CapturedSendMessageRequest.Configuration.ReturnImmediately);
}
[Fact]
public async Task RunAsync_WithAllowBackgroundResponsesFalse_SetsReturnImmediatelyToFalseAsync()
{
// Arrange
var inputMessages = new List<ChatMessage>
{
new(ChatRole.User, "Test message")
};
var options = new AgentRunOptions { AllowBackgroundResponses = false };
// Act
await this._agent.RunAsync(inputMessages, null, options);
// Assert
Assert.NotNull(this._handler.CapturedSendMessageRequest);
Assert.NotNull(this._handler.CapturedSendMessageRequest.Configuration);
Assert.False(this._handler.CapturedSendMessageRequest.Configuration.ReturnImmediately);
}
[Fact]
public async Task RunAsync_WithNullOptions_SetsReturnImmediatelyToFalseAsync()
{
// Arrange
var inputMessages = new List<ChatMessage>
{
new(ChatRole.User, "Test message")
};
// Act
await this._agent.RunAsync(inputMessages, null, null);
// Assert
Assert.NotNull(this._handler.CapturedSendMessageRequest);
Assert.NotNull(this._handler.CapturedSendMessageRequest.Configuration);
Assert.False(this._handler.CapturedSendMessageRequest.Configuration.ReturnImmediately);
}
[Fact]
public async Task RunStreamingAsync_SendMessageRequest_DoesNotSetReturnImmediatelyConfigurationAsync()
{
// Arrange
this._handler.StreamingResponseToReturn = new StreamResponse
{
Message = new Message
{
MessageId = "response-123",
Role = Role.Agent,
Parts = [new Part { Text = "Streaming response" }]
}
};
var inputMessages = new List<ChatMessage>
{
new(ChatRole.User, "Test message")
};
// Act
await foreach (var _ in this._agent.RunStreamingAsync(inputMessages))
{
// Just iterate through to trigger the logic
}
// Assert
Assert.NotNull(this._handler.CapturedSendMessageRequest);
Assert.Null(this._handler.CapturedSendMessageRequest.Configuration);
}
[Fact]
@@ -1256,6 +1486,7 @@ public sealed class A2AAgentTests : IDisposable
public void Dispose()
{
this._a2aClient.Dispose();
this._handler.Dispose();
this._httpClient.Dispose();
}
@@ -1269,13 +1500,17 @@ public sealed class A2AAgentTests : IDisposable
{
public JsonRpcRequest? CapturedJsonRpcRequest { get; set; }
public MessageSendParams? CapturedMessageSendParams { get; set; }
public List<JsonRpcRequest> CapturedJsonRpcRequests { get; } = [];
public TaskIdParams? CapturedTaskIdParams { get; set; }
public SendMessageRequest? CapturedSendMessageRequest { get; set; }
public A2AEvent? ResponseToReturn { get; set; }
public GetTaskRequest? CapturedGetTaskRequest { get; set; }
public A2AEvent? StreamingResponseToReturn { get; set; }
public SendMessageResponse? ResponseToReturn { get; set; }
public AgentTask? AgentTaskToReturn { get; set; }
public StreamResponse? StreamingResponseToReturn { get; set; }
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
@@ -1286,22 +1521,46 @@ public sealed class A2AAgentTests : IDisposable
this.CapturedJsonRpcRequest = JsonSerializer.Deserialize<JsonRpcRequest>(content);
try
if (this.CapturedJsonRpcRequest is not null)
{
this.CapturedMessageSendParams = this.CapturedJsonRpcRequest?.Params?.Deserialize<MessageSendParams>();
this.CapturedJsonRpcRequests.Add(this.CapturedJsonRpcRequest);
}
catch { /* Ignore deserialization errors for non-MessageSendParams requests */ }
try
{
this.CapturedTaskIdParams = this.CapturedJsonRpcRequest?.Params?.Deserialize<TaskIdParams>();
this.CapturedSendMessageRequest = this.CapturedJsonRpcRequest?.Params?.Deserialize<SendMessageRequest>(A2AJsonUtilities.DefaultOptions);
}
catch { /* Ignore deserialization errors for non-SendMessageRequest requests */ }
try
{
this.CapturedGetTaskRequest = this.CapturedJsonRpcRequest?.Params?.Deserialize<GetTaskRequest>(A2AJsonUtilities.DefaultOptions);
}
catch { /* Ignore deserialization errors for non-GetTaskRequest requests */ }
// Return the pre-configured AgentTask response (for tasks/get)
if (this.AgentTaskToReturn is not null && this.CapturedJsonRpcRequest?.Method == "GetTask")
{
var jsonRpcResponse = new JsonRpcResponse
{
Id = "response-id",
Result = JsonSerializer.SerializeToNode(this.AgentTaskToReturn, A2AJsonUtilities.DefaultOptions)
};
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(JsonSerializer.Serialize(jsonRpcResponse), Encoding.UTF8, "application/json")
};
}
catch { /* Ignore deserialization errors for non-TaskIdParams requests */ }
// Return the pre-configured non-streaming response
if (this.ResponseToReturn is not null)
{
var jsonRpcResponse = JsonRpcResponse.CreateJsonRpcResponse("response-id", this.ResponseToReturn);
var jsonRpcResponse = new JsonRpcResponse
{
Id = "response-id",
Result = JsonSerializer.SerializeToNode(this.ResponseToReturn, A2AJsonUtilities.DefaultOptions)
};
return new HttpResponseMessage(HttpStatusCode.OK)
{
@@ -1311,22 +1570,18 @@ public sealed class A2AAgentTests : IDisposable
// Return the pre-configured streaming response
else if (this.StreamingResponseToReturn is not null)
{
var jsonRpcResponse = new JsonRpcResponse
{
Id = "response-id",
Result = JsonSerializer.SerializeToNode(this.StreamingResponseToReturn, A2AJsonUtilities.DefaultOptions)
};
var stream = new MemoryStream();
await SseFormatter.WriteAsync(
new SseItem<JsonRpcResponse>[]
{
new(JsonRpcResponse.CreateJsonRpcResponse("response-id", this.StreamingResponseToReturn!))
}.ToAsyncEnumerable(),
stream,
(item, writer) =>
{
using Utf8JsonWriter json = new(writer, new() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping });
JsonSerializer.Serialize(json, item.Data);
},
cancellationToken
);
var writer = new StreamWriter(stream);
await writer.WriteAsync($"data: {JsonSerializer.Serialize(jsonRpcResponse, A2AJsonUtilities.DefaultOptions)}\n\n");
#pragma warning disable CA2016 // Forward the 'CancellationToken' parameter to methods; overload doesn't exist downlevel
await writer.FlushAsync();
#pragma warning restore CA2016
stream.Position = 0;
return new HttpResponseMessage(HttpStatusCode.OK)
@@ -1339,7 +1594,11 @@ public sealed class A2AAgentTests : IDisposable
}
else
{
var jsonRpcResponse = JsonRpcResponse.CreateJsonRpcResponse<A2AEvent>("response-id", new AgentMessage());
var jsonRpcResponse = new JsonRpcResponse
{
Id = "response-id",
Result = JsonSerializer.SerializeToNode(new SendMessageResponse { Message = new Message() }, A2AJsonUtilities.DefaultOptions)
};
return new HttpResponseMessage(HttpStatusCode.OK)
{
@@ -42,14 +42,14 @@ public sealed class A2AAIContentExtensionsTests
Assert.NotNull(result);
Assert.Equal(3, result.Count);
var firstTextPart = Assert.IsType<TextPart>(result[0]);
Assert.Equal("First text", firstTextPart.Text);
Assert.Equal(PartContentCase.Text, result[0].ContentCase);
Assert.Equal("First text", result[0].Text);
var filePart = Assert.IsType<FilePart>(result[1]);
Assert.Equal("https://example.com/file1.txt", filePart.File.Uri?.ToString());
Assert.Equal(PartContentCase.Url, result[1].ContentCase);
Assert.Equal("https://example.com/file1.txt", result[1].Url);
var secondTextPart = Assert.IsType<TextPart>(result[2]);
Assert.Equal("Second text", secondTextPart.Text);
Assert.Equal(PartContentCase.Text, result[2].ContentCase);
Assert.Equal("Second text", result[2].Text);
}
[Fact]
@@ -72,14 +72,14 @@ public sealed class A2AAIContentExtensionsTests
Assert.NotNull(result);
Assert.Equal(3, result.Count);
var firstTextPart = Assert.IsType<TextPart>(result[0]);
Assert.Equal("First text", firstTextPart.Text);
Assert.Equal(PartContentCase.Text, result[0].ContentCase);
Assert.Equal("First text", result[0].Text);
var filePart = Assert.IsType<FilePart>(result[1]);
Assert.Equal("https://example.com/file.txt", filePart.File.Uri?.ToString());
Assert.Equal(PartContentCase.Url, result[1].ContentCase);
Assert.Equal("https://example.com/file.txt", result[1].Url);
var secondTextPart = Assert.IsType<TextPart>(result[2]);
Assert.Equal("Second text", secondTextPart.Text);
Assert.Equal(PartContentCase.Text, result[2].ContentCase);
Assert.Equal("Second text", result[2].Text);
}
// Mock class for testing unsupported scenarios
@@ -3,6 +3,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
@@ -26,7 +27,7 @@ public sealed class A2AAgentCardExtensionsTests
{
Name = "Test Agent",
Description = "A test agent for unit testing",
Url = "http://test-endpoint/agent"
SupportedInterfaces = [new AgentInterface { Url = "http://test-endpoint/agent" }]
};
}
@@ -50,10 +51,10 @@ public sealed class A2AAgentCardExtensionsTests
using var handler = new HttpMessageHandlerStub();
using var httpClient = new HttpClient(handler, false);
handler.ResponsesToReturn.Enqueue(new AgentMessage
handler.ResponsesToReturn.Enqueue(new Message
{
Role = MessageRole.Agent,
Parts = [new TextPart { Text = "Response" }],
Role = Role.Agent,
Parts = [Part.FromText("Response")],
});
var agent = this._agentCard.AsAIAgent(httpClient);
@@ -66,6 +67,41 @@ public sealed class A2AAgentCardExtensionsTests
Assert.Equal(new Uri("http://test-endpoint/agent"), handler.CapturedUris[0]);
}
[Fact]
public async Task AsAIAgent_WithMultipleInterfaces_UsesFirstInterfaceAsync()
{
// Arrange
var card = new AgentCard
{
Name = "Multi-Interface Agent",
Description = "An agent with multiple interfaces",
SupportedInterfaces =
[
new AgentInterface { Url = "http://first/agent" },
new AgentInterface { Url = "http://second/agent", ProtocolBinding = "grpc" },
new AgentInterface { Url = "http://third/agent", ProtocolBinding = "http" },
]
};
using var handler = new HttpMessageHandlerStub();
using var httpClient = new HttpClient(handler, false);
handler.ResponsesToReturn.Enqueue(new Message
{
Role = Role.Agent,
Parts = [Part.FromText("Response")],
});
var agent = card.AsAIAgent(httpClient);
// Act
await agent.RunAsync("Test input");
// Assert
Assert.Single(handler.CapturedUris);
Assert.Equal(new Uri("http://first/agent"), handler.CapturedUris[0]);
}
internal sealed class HttpMessageHandlerStub : HttpMessageHandler
{
public Queue ResponsesToReturn { get; } = new();
@@ -86,13 +122,18 @@ public sealed class A2AAgentCardExtensionsTests
Content = new StringContent(json, Encoding.UTF8, "application/json")
};
}
else if (response is AgentMessage message)
else if (response is Message message)
{
var jsonRpcResponse = JsonRpcResponse.CreateJsonRpcResponse<A2AEvent>("response-id", message);
var sendMessageResponse = new SendMessageResponse { Message = message };
var jsonRpcResponse = new JsonRpcResponse
{
Id = "response-id",
Result = JsonSerializer.SerializeToNode(sendMessageResponse, A2AJsonUtilities.DefaultOptions)
};
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(JsonSerializer.Serialize(jsonRpcResponse), Encoding.UTF8, "application/json")
Content = new StringContent(JsonSerializer.Serialize(jsonRpcResponse, A2AJsonUtilities.DefaultOptions), Encoding.UTF8, "application/json")
};
}
@@ -40,7 +40,7 @@ public sealed class A2AAgentTaskExtensionsTests
{
Id = "task1",
Artifacts = [],
Status = new AgentTaskStatus { State = TaskState.Completed },
Status = new TaskStatus { State = TaskState.Completed },
};
// Act
@@ -58,7 +58,7 @@ public sealed class A2AAgentTaskExtensionsTests
{
Id = "task1",
Artifacts = null,
Status = new AgentTaskStatus { State = TaskState.Completed },
Status = new TaskStatus { State = TaskState.Completed },
};
// Act
@@ -76,7 +76,7 @@ public sealed class A2AAgentTaskExtensionsTests
{
Id = "task1",
Artifacts = [],
Status = new AgentTaskStatus { State = TaskState.Completed },
Status = new TaskStatus { State = TaskState.Completed },
};
// Act
@@ -94,7 +94,7 @@ public sealed class A2AAgentTaskExtensionsTests
{
Id = "task1",
Artifacts = null,
Status = new AgentTaskStatus { State = TaskState.Completed },
Status = new TaskStatus { State = TaskState.Completed },
};
// Act
@@ -110,14 +110,14 @@ public sealed class A2AAgentTaskExtensionsTests
// Arrange
var artifact = new Artifact
{
Parts = [new TextPart { Text = "response" }],
Parts = [Part.FromText("response")],
};
var agentTask = new AgentTask
{
Id = "task1",
Artifacts = [artifact],
Status = new AgentTaskStatus { State = TaskState.Completed },
Status = new TaskStatus { State = TaskState.Completed },
};
// Act
@@ -136,15 +136,15 @@ public sealed class A2AAgentTaskExtensionsTests
// Arrange
var artifact1 = new Artifact
{
Parts = [new TextPart { Text = "content1" }],
Parts = [Part.FromText("content1")],
};
var artifact2 = new Artifact
{
Parts =
[
new TextPart { Text = "content2" },
new TextPart { Text = "content3" }
Part.FromText("content2"),
Part.FromText("content3")
],
};
@@ -152,7 +152,7 @@ public sealed class A2AAgentTaskExtensionsTests
{
Id = "task1",
Artifacts = [artifact1, artifact2],
Status = new AgentTaskStatus { State = TaskState.Completed },
Status = new TaskStatus { State = TaskState.Completed },
};
// Act
@@ -22,9 +22,9 @@ public sealed class A2AArtifactExtensionsTests
Name = "comprehensive-artifact",
Parts =
[
new TextPart { Text = "First part" },
new TextPart { Text = "Second part" },
new TextPart { Text = "Third part" }
Part.FromText("First part"),
Part.FromText("Second part"),
Part.FromText("Third part")
],
Metadata = new Dictionary<string, JsonElement>
{
@@ -66,9 +66,9 @@ public sealed class A2AArtifactExtensionsTests
Name = "test",
Parts =
[
new TextPart { Text = "Part 1" },
new TextPart { Text = "Part 2" },
new TextPart { Text = "Part 3" }
Part.FromText("Part 1"),
Part.FromText("Part 2"),
Part.FromText("Part 3")
],
Metadata = null
};
@@ -37,7 +37,7 @@ public sealed class A2ACardResolverExtensionsTests : IDisposable
{
Name = "Test Agent",
Description = "A test agent for unit testing",
Url = "http://test-endpoint/agent"
SupportedInterfaces = [new AgentInterface { Url = "http://test-endpoint/agent" }]
});
// Act
@@ -60,12 +60,12 @@ public sealed class A2ACardResolverExtensionsTests : IDisposable
// Arrange
this._handler.ResponsesToReturn.Enqueue(new AgentCard
{
Url = "http://test-endpoint/agent"
SupportedInterfaces = [new AgentInterface { Url = "http://test-endpoint/agent" }]
});
this._handler.ResponsesToReturn.Enqueue(new AgentMessage
this._handler.ResponsesToReturn.Enqueue(new Message
{
Role = MessageRole.Agent,
Parts = [new TextPart { Text = "Response" }],
Role = Role.Agent,
Parts = [Part.FromText("Response")],
});
var agent = await this._resolver.GetAIAgentAsync(this._httpClient);
@@ -104,13 +104,18 @@ public sealed class A2ACardResolverExtensionsTests : IDisposable
Content = new StringContent(json, Encoding.UTF8, "application/json")
};
}
else if (response is AgentMessage message)
else if (response is Message message)
{
var jsonRpcResponse = JsonRpcResponse.CreateJsonRpcResponse<A2AEvent>("response-id", message);
var sendMessageResponse = new SendMessageResponse { Message = message };
var jsonRpcResponse = new JsonRpcResponse
{
Id = "response-id",
Result = JsonSerializer.SerializeToNode(sendMessageResponse, A2AJsonUtilities.DefaultOptions)
};
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(JsonSerializer.Serialize(jsonRpcResponse), Encoding.UTF8, "application/json")
Content = new StringContent(JsonSerializer.Serialize(jsonRpcResponse, A2AJsonUtilities.DefaultOptions), Encoding.UTF8, "application/json")
};
}
@@ -32,20 +32,19 @@ public sealed class ChatMessageExtensionsTests
Assert.NotNull(a2aMessage.MessageId);
Assert.NotEmpty(a2aMessage.MessageId);
Assert.Equal(MessageRole.User, a2aMessage.Role);
Assert.Equal(Role.User, a2aMessage.Role);
Assert.NotNull(a2aMessage.Parts);
Assert.Equal(3, a2aMessage.Parts.Count);
var filePart = Assert.IsType<FilePart>(a2aMessage.Parts[0]);
Assert.NotNull(filePart.File);
Assert.Equal("https://example.com/report.pdf", filePart.File.Uri?.ToString());
Assert.Equal(PartContentCase.Url, a2aMessage.Parts[0].ContentCase);
Assert.Equal("https://example.com/report.pdf", a2aMessage.Parts[0].Url);
var secondTextPart = Assert.IsType<TextPart>(a2aMessage.Parts[1]);
Assert.Equal("please summarize the file content", secondTextPart.Text);
Assert.Equal(PartContentCase.Text, a2aMessage.Parts[1].ContentCase);
Assert.Equal("please summarize the file content", a2aMessage.Parts[1].Text);
var thirdTextPart = Assert.IsType<TextPart>(a2aMessage.Parts[2]);
Assert.Equal("and send it to me over email", thirdTextPart.Text);
Assert.Equal(PartContentCase.Text, a2aMessage.Parts[2].ContentCase);
Assert.Equal("and send it to me over email", a2aMessage.Parts[2].Text);
}
[Fact]
@@ -71,19 +70,18 @@ public sealed class ChatMessageExtensionsTests
Assert.NotNull(a2aMessage.MessageId);
Assert.NotEmpty(a2aMessage.MessageId);
Assert.Equal(MessageRole.User, a2aMessage.Role);
Assert.Equal(Role.User, a2aMessage.Role);
Assert.NotNull(a2aMessage.Parts);
Assert.Equal(3, a2aMessage.Parts.Count);
var filePart = Assert.IsType<FilePart>(a2aMessage.Parts[0]);
Assert.NotNull(filePart.File);
Assert.Equal("https://example.com/report.pdf", filePart.File.Uri?.ToString());
Assert.Equal(PartContentCase.Url, a2aMessage.Parts[0].ContentCase);
Assert.Equal("https://example.com/report.pdf", a2aMessage.Parts[0].Url);
var secondTextPart = Assert.IsType<TextPart>(a2aMessage.Parts[1]);
Assert.Equal("please summarize the file content", secondTextPart.Text);
Assert.Equal(PartContentCase.Text, a2aMessage.Parts[1].ContentCase);
Assert.Equal("please summarize the file content", a2aMessage.Parts[1].Text);
var thirdTextPart = Assert.IsType<TextPart>(a2aMessage.Parts[2]);
Assert.Equal("and send it to me over email", thirdTextPart.Text);
Assert.Equal(PartContentCase.Text, a2aMessage.Parts[2].ContentCase);
Assert.Equal("and send it to me over email", a2aMessage.Parts[2].Text);
}
}
@@ -1,5 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(TargetFrameworksCore)</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.A2A\Microsoft.Agents.AI.A2A.csproj" />
</ItemGroup>