.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:
Mark Wallace
2025-09-09 17:03:10 +00:00
committed by GitHub
parent ec5ea3c8a8
commit 745a05d43c
29 changed files with 1550 additions and 14 deletions
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UserSecretsId>5ee045b0-aea3-4f08-8d31-32d1a6f8fed0</UserSecretsId>
<NoWarn>$(NoWarn);CS1591;VSTHRD111;CA2007;SKEXP0110</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="A2A" />
<PackageReference Include="System.CommandLine" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" VersionOverride="10.0.0-preview.5.25277.114" />
<PackageReference Include="System.Net.ServerSentEvents" VersionOverride="10.0.0-preview.5.25277.114" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Microsoft.Extensions.AI.Agents.A2A\Microsoft.Extensions.AI.Agents.A2A.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Extensions.AI.Agents.Abstractions\Microsoft.Extensions.AI.Agents.Abstractions.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Extensions.AI.Agents.OpenAI\Microsoft.Extensions.AI.Agents.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,64 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ClientModel;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Extensions.AI.Agents.A2A;
using Microsoft.Extensions.Logging;
using OpenAI;
namespace A2A;
internal sealed class HostClientAgent
{
internal HostClientAgent(ILoggerFactory loggerFactory)
{
this._loggerFactory = loggerFactory;
this._logger = loggerFactory.CreateLogger("HostClientAgent");
}
internal async Task InitializeAgentAsync(string modelId, string apiKey, string[] agentUrls)
{
try
{
this._logger.LogInformation("Initializing Agent Framework agent with model: {ModelId}", modelId);
// Connect to the remote agents via A2A
var createAgentTasks = agentUrls.Select(agentUrl => this.CreateAgentAsync(agentUrl));
var agents = await Task.WhenAll(createAgentTasks);
var tools = agents.Select(agent => (AITool)AgentAIFunctionFactory.CreateFromAgent(agent)).ToList();
// Create the agent that uses the remote agents as tools
this.Agent = new OpenAIClient(new ApiKeyCredential(apiKey))
.GetChatClient(modelId)
.CreateAIAgent(instructions: "You specialize in handling queries for users and using your tools to provide answers.", name: "HostClient", tools: tools);
}
catch (Exception ex)
{
this._logger.LogError(ex, "Failed to initialize HostClientAgent");
throw;
}
}
/// <summary>
/// The associated <see cref="Agent"/>
/// </summary>
public AIAgent? Agent { get; private set; }
#region private
private readonly ILoggerFactory _loggerFactory;
private readonly ILogger _logger;
private async Task<AIAgent> CreateAgentAsync(string agentUri)
{
var url = new Uri(agentUri);
var httpClient = new HttpClient
{
Timeout = TimeSpan.FromSeconds(60)
};
var agentCardResolver = new A2ACardResolver(url, httpClient);
return await agentCardResolver.GetAIAgentAsync();
}
#endregion
}
@@ -0,0 +1,80 @@
// Copyright (c) Microsoft. All rights reserved.
using System.CommandLine;
using System.CommandLine.Invocation;
using System.Reflection;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace A2A;
public static class Program
{
public static async Task<int> Main(string[] args)
{
// Create root command with options
var rootCommand = new RootCommand("A2AClient");
rootCommand.SetHandler(HandleCommandsAsync);
// Run the command
return await rootCommand.InvokeAsync(args);
}
public static async Task HandleCommandsAsync(InvocationContext context)
{
// Set up the logging
using var loggerFactory = LoggerFactory.Create(builder =>
{
builder.AddConsole();
builder.SetMinimumLevel(LogLevel.Information);
});
var logger = loggerFactory.CreateLogger("A2AClient");
// Retrieve configuration settings
IConfigurationRoot configRoot = new ConfigurationBuilder()
.AddEnvironmentVariables()
.AddUserSecrets(Assembly.GetExecutingAssembly())
.Build();
var apiKey = configRoot["A2AClient:ApiKey"] ?? throw new ArgumentException("A2AClient:ApiKey must be provided");
var modelId = configRoot["A2AClient:ModelId"] ?? "gpt-4.1";
var agentUrls = configRoot["A2AClient:AgentUrls"] ?? "http://localhost:5000/;http://localhost:5001/;http://localhost:5002/";
// Create the Host agent
var hostAgent = new HostClientAgent(loggerFactory);
await hostAgent.InitializeAgentAsync(modelId, apiKey, agentUrls!.Split(";"));
AgentThread thread = hostAgent.Agent!.GetNewThread();
try
{
while (true)
{
// Get user message
Console.Write("\nUser (:q or quit to exit): ");
string? message = Console.ReadLine();
if (string.IsNullOrWhiteSpace(message))
{
Console.WriteLine("Request cannot be empty.");
continue;
}
if (message == ":q" || message == "quit")
{
break;
}
var agentResponse = await hostAgent.Agent!.RunAsync(message, thread);
foreach (var chatMessage in agentResponse.Messages)
{
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine($"\nAgent: {chatMessage.Text}");
Console.ResetColor();
}
}
}
catch (Exception ex)
{
logger.LogError(ex, "An error occurred while running the A2AClient");
return;
}
}
}
@@ -0,0 +1,26 @@
# A2A Client Sample
Show how to create an A2A Client with a command line interface which invokes agents using the A2A protocol.
## Run the Sample
To run the sample, follow these steps:
1. Run the A2A client:
```bash
cd A2AClient
dotnet run
```
2. Enter your request e.g. "Show me all invoices for Contoso?"
## Set Environment Variables
The agent urls are provided as a ` ` delimited list of strings
```powershell
cd dotnet/samples/A2AClientServer/A2AClient
$env:OPENAI_MODEL_ID="gpt-4o-mini"
$env:OPENAI_API_KEY="<Your OPENAI api key>"
$env:AGENT_URLS="http://localhost:5000/policy;http://localhost:5000/invoice;http://localhost:5000/logistics"
```
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UserSecretsId>5ee045b0-aea3-4f08-8d31-32d1a6f8fed0</UserSecretsId>
<NoWarn>$(NoWarn);CS1591;VSTHRD111;CA2007;SKEXP0110</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="A2A.AspNetCore" />
<PackageReference Include="Azure.AI.Agents.Persistent" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" VersionOverride="10.0.0-preview.5.25277.114" />
<PackageReference Include="System.Net.ServerSentEvents" VersionOverride="10.0.0-preview.5.25277.114" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Microsoft.Extensions.AI.Agents.A2A\Microsoft.Extensions.AI.Agents.A2A.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Extensions.AI.Agents.AzureAI\Microsoft.Extensions.AI.Agents.AzureAI.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.Extensions.AI.Agents.OpenAI\Microsoft.Extensions.AI.Agents.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,82 @@
### Each A2A agent is available at a different host address
@hostInvoice = http://localhost:5000
@hostPolicy = http://localhost:5001
@hostLogistics = http://localhost:5002
### Query agent card for the invoice agent
GET {{hostInvoice}}/.well-known/agent.json
### Send a message to the invoice agent
POST {{hostInvoice}}
Content-Type: application/json
{
"id": "1",
"jsonrpc": "2.0",
"method": "message/send",
"params": {
"id": "12345",
"message": {
"role": "user",
"messageId": "msg_1",
"parts": [
{
"kind": "text",
"text": "Show me all invoices for Contoso?"
}
]
}
}
}
### Query agent card for the policy agent
GET {{hostPolicy}}/.well-known/agent.json
### Send a message to the policy agent
POST {{hostPolicy}}
Content-Type: application/json
{
"id": "1",
"jsonrpc": "2.0",
"method": "message/send",
"params": {
"id": "12345",
"message": {
"role": "user",
"messageId": "msg_1",
"parts": [
{
"kind": "text",
"text": "What is the policy for short shipments?"
}
]
}
}
}
### Query agent card for the logistics agent
GET {{hostLogistics}}/.well-known/agent.json
### Send a message to the logistics agent
POST {{hostLogistics}}
Content-Type: application/json
{
"id": "1",
"jsonrpc": "2.0",
"method": "message/send",
"params": {
"id": "12345",
"message": {
"role": "user",
"messageId": "msg_1",
"parts": [
{
"kind": "text",
"text": "What is the status for SHPMT-SAP-001?"
}
]
}
}
}
@@ -0,0 +1,148 @@
// Copyright (c) Microsoft. All rights reserved.
using A2A;
using Azure.AI.Agents.Persistent;
using Azure.Identity;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Extensions.AI.Agents.A2A;
using OpenAI;
namespace A2AServer;
internal static class HostAgentFactory
{
internal static async Task<A2AHostAgent> CreateFoundryHostAgentAsync(string agentType, string modelId, string endpoint, string assistantId, IList<AITool>? tools = null)
{
var persistentAgentsClient = new PersistentAgentsClient(endpoint, new AzureCliCredential());
PersistentAgent persistentAgent = await persistentAgentsClient.Administration.GetAgentAsync(assistantId);
AIAgent agent = await persistentAgentsClient
.GetAIAgentAsync(persistentAgent.Id, chatOptions: new() { Tools = tools });
AgentCard agentCard = agentType.ToUpperInvariant() switch
{
"INVOICE" => GetInvoiceAgentCard(),
"POLICY" => GetPolicyAgentCard(),
"LOGISTICS" => GetLogisticsAgentCard(),
_ => throw new ArgumentException($"Unsupported agent type: {agentType}"),
};
return new A2AHostAgent(agent, agentCard);
}
internal static async Task<A2AHostAgent> CreateChatCompletionHostAgentAsync(string agentType, string modelId, string apiKey, string name, string instructions, IList<AITool>? tools = null)
{
AIAgent agent = new OpenAIClient(apiKey)
.GetChatClient(modelId)
.CreateAIAgent(instructions, name, tools: tools);
AgentCard agentCard = agentType.ToUpperInvariant() switch
{
"INVOICE" => GetInvoiceAgentCard(),
"POLICY" => GetPolicyAgentCard(),
"LOGISTICS" => GetLogisticsAgentCard(),
_ => throw new ArgumentException($"Unsupported agent type: {agentType}"),
};
return new A2AHostAgent(agent, agentCard);
}
#region private
private static AgentCard GetInvoiceAgentCard()
{
var capabilities = new AgentCapabilities()
{
Streaming = false,
PushNotifications = false,
};
var invoiceQuery = new AgentSkill()
{
Id = "id_invoice_agent",
Name = "InvoiceQuery",
Description = "Handles requests relating to invoices.",
Tags = ["invoice", "semantic-kernel"],
Examples =
[
"List the latest invoices for Contoso.",
],
};
return new()
{
Name = "InvoiceAgent",
Description = "Handles requests relating to invoices.",
Version = "1.0.0",
DefaultInputModes = ["text"],
DefaultOutputModes = ["text"],
Capabilities = capabilities,
Skills = [invoiceQuery],
};
}
private static AgentCard GetPolicyAgentCard()
{
var capabilities = new AgentCapabilities()
{
Streaming = false,
PushNotifications = false,
};
var invoiceQuery = new AgentSkill()
{
Id = "id_policy_agent",
Name = "PolicyAgent",
Description = "Handles requests relating to policies and customer communications.",
Tags = ["policy", "semantic-kernel"],
Examples =
[
"What is the policy for short shipments?",
],
};
return new AgentCard()
{
Name = "PolicyAgent",
Description = "Handles requests relating to policies and customer communications.",
Version = "1.0.0",
DefaultInputModes = ["text"],
DefaultOutputModes = ["text"],
Capabilities = capabilities,
Skills = [invoiceQuery],
};
}
private static AgentCard GetLogisticsAgentCard()
{
var capabilities = new AgentCapabilities()
{
Streaming = false,
PushNotifications = false,
};
var logisticsQuery = new AgentSkill()
{
Id = "id_logistics_agent",
Name = "LogisticsQuery",
Description = "Handles requests relating to logistics.",
Tags = ["logistics", "semantic-kernel"],
Examples =
[
"What is the status for SHPMT-SAP-001",
],
};
return new AgentCard()
{
Name = "LogisticsAgent",
Description = "Handles requests relating to logistics.",
Version = "1.0.0",
DefaultInputModes = ["text"],
DefaultOutputModes = ["text"],
Capabilities = capabilities,
Skills = [logisticsQuery],
};
}
#endregion
}
@@ -0,0 +1,176 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
namespace A2A;
/// <summary>
/// A simple invoice plugin that returns mock data.
/// </summary>
public class Product
{
public string Name { get; set; }
public int Quantity { get; set; }
public decimal Price { get; set; } // Price per unit
public Product(string name, int quantity, decimal price)
{
this.Name = name;
this.Quantity = quantity;
this.Price = price;
}
public decimal TotalPrice()
{
return this.Quantity * this.Price; // Total price for this product
}
}
public class Invoice
{
public string TransactionId { get; set; }
public string InvoiceId { get; set; }
public string CompanyName { get; set; }
public DateTime InvoiceDate { get; set; }
public List<Product> Products { get; set; } // List of products
public Invoice(string transactionId, string invoiceId, string companyName, DateTime invoiceDate, List<Product> products)
{
this.TransactionId = transactionId;
this.InvoiceId = invoiceId;
this.CompanyName = companyName;
this.InvoiceDate = invoiceDate;
this.Products = products;
}
public decimal TotalInvoicePrice()
{
return this.Products.Sum(product => product.TotalPrice()); // Total price of all products in the invoice
}
}
public class InvoiceQuery
{
private readonly List<Invoice> _invoices;
private static readonly Random s_random = new();
public InvoiceQuery()
{
// Extended mock data with quantities and prices
this._invoices =
[
new("TICKET-XYZ987", "INV789", "Contoso", GetRandomDateWithinLastTwoMonths(), new List<Product>
{
new("T-Shirts", 150, 10.00m),
new("Hats", 200, 15.00m),
new("Glasses", 300, 5.00m)
}),
new("TICKET-XYZ111", "INV111", "XStore", GetRandomDateWithinLastTwoMonths(), new List<Product>
{
new("T-Shirts", 2500, 12.00m),
new("Hats", 1500, 8.00m),
new("Glasses", 200, 20.00m)
}),
new("TICKET-XYZ222", "INV222", "Cymbal Direct", GetRandomDateWithinLastTwoMonths(), new List<Product>
{
new("T-Shirts", 1200, 14.00m),
new("Hats", 800, 7.00m),
new("Glasses", 500, 25.00m)
}),
new("TICKET-XYZ333", "INV333", "Contoso", GetRandomDateWithinLastTwoMonths(), new List<Product>
{
new("T-Shirts", 400, 11.00m),
new("Hats", 600, 15.00m),
new("Glasses", 700, 5.00m)
}),
new("TICKET-XYZ444", "INV444", "XStore", GetRandomDateWithinLastTwoMonths(), new List<Product>
{
new("T-Shirts", 800, 10.00m),
new("Hats", 500, 18.00m),
new("Glasses", 300, 22.00m)
}),
new("TICKET-XYZ555", "INV555", "Cymbal Direct", GetRandomDateWithinLastTwoMonths(), new List<Product>
{
new("T-Shirts", 1100, 9.00m),
new("Hats", 900, 12.00m),
new("Glasses", 1200, 15.00m)
}),
new("TICKET-XYZ666", "INV666", "Contoso", GetRandomDateWithinLastTwoMonths(), new List<Product>
{
new("T-Shirts", 2500, 8.00m),
new("Hats", 1200, 10.00m),
new("Glasses", 1000, 6.00m)
}),
new("TICKET-XYZ777", "INV777", "XStore", GetRandomDateWithinLastTwoMonths(), new List<Product>
{
new("T-Shirts", 1900, 13.00m),
new("Hats", 1300, 16.00m),
new("Glasses", 800, 19.00m)
}),
new("TICKET-XYZ888", "INV888", "Cymbal Direct", GetRandomDateWithinLastTwoMonths(), new List<Product>
{
new("T-Shirts", 2200, 11.00m),
new("Hats", 1700, 8.50m),
new("Glasses", 600, 21.00m)
}),
new("TICKET-XYZ999", "INV999", "Contoso", GetRandomDateWithinLastTwoMonths(), new List<Product>
{
new("T-Shirts", 1400, 10.50m),
new("Hats", 1100, 9.00m),
new("Glasses", 950, 12.00m)
})
];
}
public static DateTime GetRandomDateWithinLastTwoMonths()
{
// Get the current date and time
DateTime endDate = DateTime.Now;
// Calculate the start date, which is two months before the current date
DateTime startDate = endDate.AddMonths(-2);
// Generate a random number of days between 0 and the total number of days in the range
int totalDays = (endDate - startDate).Days;
#pragma warning disable CA5394 // Do not use insecure randomness
int randomDays = s_random.Next(0, totalDays + 1); // +1 to include the end date
#pragma warning restore CA5394 // Do not use insecure randomness
// Return the random date
return startDate.AddDays(randomDays);
}
[Description("Retrieves invoices for the specified company and optionally within the specified time range")]
public IEnumerable<Invoice> QueryInvoices(string companyName, DateTime? startDate = null, DateTime? endDate = null)
{
var query = this._invoices.Where(i => i.CompanyName.Equals(companyName, StringComparison.OrdinalIgnoreCase));
if (startDate.HasValue)
{
query = query.Where(i => i.InvoiceDate >= startDate.Value);
}
if (endDate.HasValue)
{
query = query.Where(i => i.InvoiceDate <= endDate.Value);
}
return query.ToList();
}
[Description("Retrieves invoice using the transaction id")]
public IEnumerable<Invoice> QueryByTransactionId(string transactionId)
{
var query = this._invoices.Where(i => i.TransactionId.Equals(transactionId, StringComparison.OrdinalIgnoreCase));
return query.ToList();
}
[Description("Retrieves invoice using the invoice id")]
public IEnumerable<Invoice> QueryByInvoiceId(string invoiceId)
{
var query = this._invoices.Where(i => i.InvoiceId.Equals(invoiceId, StringComparison.OrdinalIgnoreCase));
return query.ToList();
}
}
@@ -0,0 +1,107 @@
// Copyright (c) Microsoft. All rights reserved.
using A2A;
using A2A.AspNetCore;
using A2AServer;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents.A2A;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
string agentId = string.Empty;
string agentType = string.Empty;
for (var i = 0; i < args.Length; i++)
{
if (args[i].StartsWith("--agentId", StringComparison.InvariantCultureIgnoreCase) && i + 1 < args.Length)
{
agentId = args[++i];
}
else if (args[i].StartsWith("--agentType", StringComparison.InvariantCultureIgnoreCase) && i + 1 < args.Length)
{
agentType = args[++i];
}
}
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient().AddLogging();
var app = builder.Build();
var httpClient = app.Services.GetRequiredService<IHttpClientFactory>().CreateClient();
var logger = app.Logger;
IConfigurationRoot configuration = new ConfigurationBuilder()
.AddEnvironmentVariables()
.AddUserSecrets<Program>()
.Build();
string? apiKey = configuration["OPENAI_API_KEY"];
string modelId = configuration["OPENAI_MODEL_ID"] ?? "gpt-4o-mini";
string? endpoint = configuration["FOUNDRY_PROJECT_ENDPOINT"];
var invoiceQueryPlugin = new InvoiceQuery();
IList<AITool> tools =
[
AIFunctionFactory.Create(invoiceQueryPlugin.QueryInvoices),
AIFunctionFactory.Create(invoiceQueryPlugin.QueryByTransactionId),
AIFunctionFactory.Create(invoiceQueryPlugin.QueryByInvoiceId)
];
A2AHostAgent? hostAgent = null;
if (!string.IsNullOrEmpty(endpoint) && !string.IsNullOrEmpty(agentId))
{
hostAgent = agentType.ToUpperInvariant() switch
{
"INVOICE" => await HostAgentFactory.CreateFoundryHostAgentAsync(agentType, modelId, endpoint, agentId, tools),
"POLICY" => await HostAgentFactory.CreateFoundryHostAgentAsync(agentType, modelId, endpoint, agentId),
"LOGISTICS" => await HostAgentFactory.CreateFoundryHostAgentAsync(agentType, modelId, endpoint, agentId),
_ => throw new ArgumentException($"Unsupported agent type: {agentType}"),
};
}
else if (!string.IsNullOrEmpty(apiKey))
{
hostAgent = agentType.ToUpperInvariant() switch
{
"INVOICE" => await HostAgentFactory.CreateChatCompletionHostAgentAsync(
agentType, modelId, apiKey, "InvoiceAgent",
"""
You specialize in handling queries related to invoices.
""", tools),
"POLICY" => await HostAgentFactory.CreateChatCompletionHostAgentAsync(
agentType, modelId, apiKey, "PolicyAgent",
"""
You specialize in handling queries related to policies and customer communications.
Always reply with exactly this text:
Policy: Short Shipment Dispute Handling Policy V2.1
Summary: "For short shipments reported by customers, first verify internal shipment records
(SAP) and physical logistics scan data (BigQuery). If discrepancy is confirmed and logistics data
shows fewer items packed than invoiced, issue a credit for the missing items. Document the
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."
"""),
"LOGISTICS" => await HostAgentFactory.CreateChatCompletionHostAgentAsync(
agentType, modelId, apiKey, "LogisticsAgent",
"""
You specialize in handling queries related to logistics.
Always reply with exactly:
Shipment number: SHPMT-SAP-001
Item: TSHIRT-RED-L
Quantity: 900
"""),
_ => throw new ArgumentException($"Unsupported agent type: {agentType}"),
};
}
else
{
throw new ArgumentException("Either A2AServer:ApiKey or A2AServer:ConnectionString & agentId must be provided");
}
app.MapA2A(hostAgent!.TaskManager!, "/");
await app.RunAsync();
+234
View File
@@ -0,0 +1,234 @@
# A2A Client and Server samples
> **Warning**
> The [A2A protocol](https://google.github.io/A2A/) is still under development and changing fast.
> We will try to keep these samples updated as the protocol evolves.
These samples are built with [official A2A C# SDK](https://www.nuget.org/packages/A2A) and demonstrates:
1. Creating an A2A Server which makes an agent available via the A2A protocol.
2. Creating an A2A Client with a command line interface which invokes agents using the A2A protocol.
The demonstration has two components:
1. `A2AServer` - You will run three instances of the server to correspond to three A2A servers each providing a single Agent i.e., the Invoice, Policy and Logistics agents.
2. `A2AClient` - This represents a client application which will connect to the remote A2A servers using the A2A protocol so that it can use those agents when answering questions you will ask.
<img src="./demo-architecture.png" alt="Demo Architecture"/>
## Configuring Environment Variables
The samples can be configured to use chat completion agents or Azure AI agents.
### Configuring for use with Chat Completion Agents
Provide your OpenAI API key via an environment variable
```powershell
$env:OPENAI_API_KEY="<Your OpenAI API Key>"
```
Use the following commands to run each A2A server:
Execute the following command to build the sample:
```powershell
cd A2AServer
dotnet build
```
```bash
dotnet run --urls "http://localhost:5000;https://localhost:5010" --agentType "invoice" --no-build
```
```bash
dotnet run --urls "http://localhost:5001;https://localhost:5011" --agentType "policy" --no-build
```
```bash
dotnet run --urls "http://localhost:5002;https://localhost:5012" --agentType "logistics" --no-build
```
### Configuring for use with Azure AI Agents
You must create the agents in an Azure AI Foundry project and then provide the project endpoint and agents ids. The instructions for each agent are as follows:
- Invoice Agent
```
You specialize in handling queries related to invoices.
```
- Policy Agent
```
You specialize in handling queries related to policies and customer communications.
Always reply with exactly this text:
Policy: Short Shipment Dispute Handling Policy V2.1
Summary: "For short shipments reported by customers, first verify internal shipment records
(SAP) and physical logistics scan data (BigQuery). If discrepancy is confirmed and logistics data
shows fewer items packed than invoiced, issue a credit for the missing items. Document the
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."
```
- Logistics Agent
```
You specialize in handling queries related to logistics.
Always reply with exactly:
Shipment number: SHPMT-SAP-001
Item: TSHIRT-RED-L
Quantity: 900"
```
```powershell
$env:FOUNDRY_PROJECT_ENDPOINT="https://ai-foundry-your-project.services.ai.azure.com/api/projects/ai-proj-ga-your-project" # Replace with your Foundry Project endpoint
```
Use the following commands to run each A2A server
```bash
dotnet run --urls "http://localhost:5000;https://localhost:5010" --agentId "<Invoice Agent Id>" --agentType "invoice" --no-build
```
```bash
dotnet run --urls "http://localhost:5001;https://localhost:5011" --agentId "<Policy Agent Id>" --agentType "policy" --no-build
```
```bash
dotnet run --urls "http://localhost:5002;https://localhost:5012" --agentId "<Logistics Agent Id>" --agentType "logistics" --no-build
```
### Testing the Agents using the Rest Client
This sample contains a [.http file](https://learn.microsoft.com/aspnet/core/test/http-files?view=aspnetcore-9.0) which can be used to test the agent.
1. In Visual Studio open [./A2AServer/A2AServer.http](./A2AServer/A2AServer.http)
1. There are two sent requests for each agent, e.g., for the invoice agent:
1. Query agent card for the invoice agent
`GET {{hostInvoice}}/.well-known/agent.json`
1. Send a message to the invoice agent
```
POST {{hostInvoice}}
Content-Type: application/json
{
"id": "1",
"jsonrpc": "2.0",
"method": "message/send",
"params": {
"id": "12345",
"message": {
"role": "user",
"messageId": "msg_1",
"parts": [
{
"kind": "text",
"text": "Show me all invoices for Contoso?"
}
]
}
}
}
```
Sample output from the request to display the agent card:
<img src="./rest-client-agent-card.png" alt="Agent Card"/>
Sample output from the request to send a message to the agent via A2A protocol:
<img src="./rest-client-send-message.png" alt="Send Message"/>
### Testing the Agents using the A2A Inspector
The A2A Inspector is a web-based tool designed to help developers inspect, debug, and validate servers that implement the Google A2A (Agent-to-Agent) protocol. It provides a user-friendly interface to interact with an A2A agent, view communication, and ensure specification compliance.
For more information go [here](https://github.com/a2aproject/a2a-inspector).
Running the [inspector with Docker](https://github.com/a2aproject/a2a-inspector?tab=readme-ov-file#option-two-run-with-docker) is the easiest way to get started.
1. Navigate to the A2A Inspector in your browser: [http://127.0.0.1:8080/](http://127.0.0.1:8080/)
1. Enter the URL of the Agent you are running e.g., [http://host.docker.internal:5000](http://host.docker.internal:5000)
1. Connect to the agent and the agent card will be displayed and validated.
1. Type a message and send it to the agent using A2A protocol.
1. The response will be validated automatically and then displayed in the UI.
1. You can select the response to view the raw json.
Agent card after connecting to an agent using the A2A protocol:
<img src="./a2a-inspector-agent-card.png" alt="Agent Card"/>
Sample response after sending a message to the agent via A2A protocol:
<img src="./a2a-inspector-send-message.png" alt="Send Message"/>
Raw JSON response from an A2A agent:
<img src="./a2a-inspector-raw-json-response.png" alt="Response Raw JSON"/>
### Configuring Agents for the A2A Client
The A2A client will connect to remote agents using the A2A protocol.
By default the client will connect to the invoice, policy and logistics agents provided by the sample A2A Server.
These are available at the following URL's:
- Invoice Agent: http://localhost:5000/
- Policy Agent: http://localhost:5001/
- Logistics Agent: http://localhost:5002/
If you want to change which agents are using then set the agents url as a space delimited string as follows:
```powershell
$env:A2A_AGENT_URLS="http://localhost:5000/;http://localhost:5001/;http://localhost:5002/"
```
## Run the Sample
To run the sample, follow these steps:
1. Run the A2A server's using the commands shown earlier
2. Run the A2A client:
```bash
cd A2AClient
dotnet run
```
3. Enter your request e.g. "Customer is disputing transaction TICKET-XYZ987 as they claim the received fewer t-shirts than ordered."
4. The host client agent will call the remote agents, these calls will be displayed as console output. The final answer will use information from the remote agents. The sample below includes all three agents but in your case you may only see the policy and invoice agent.
Sample output from the A2A client:
```
A2AClient> dotnet run
info: HostClientAgent[0]
Initializing Agent Framework agent with model: gpt-4o-mini
User (:q or quit to exit): Customer is disputing transaction TICKET-XYZ987 as they claim the received fewer t-shirts than ordered.
Agent:
Agent:
Agent: The transaction details for **TICKET-XYZ987** are as follows:
- **Invoice ID:** INV789
- **Company Name:** Contoso
- **Invoice Date:** September 4, 2025
- **Products:**
- **T-Shirts:** 150 units at $10.00 each
- **Hats:** 200 units at $15.00 each
- **Glasses:** 300 units at $5.00 each
To proceed with the dispute regarding the quantity of t-shirts delivered, please specify the exact quantity issue how many t-shirts were actually received compared to the ordered amount.
### Customer Service Policy for Handling Disputes
**Short Shipment Dispute Handling Policy V2.1**
- **Summary:** For short shipments reported by customers, first verify internal shipment records and physical logistics scan data. If a discrepancy is confirmed and the logistics data shows fewer items were packed than invoiced, a credit for the missing items will be issued.
- **Follow-up Actions:** Document the resolution in the SAP CRM and notify the customer via email within 2 business days, referencing the original invoice and the credit memo number, using the 'Formal Credit Notification' email template.
Please provide me with the information regarding the specific quantity issue so I can assist you further.
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 KiB