diff --git a/dotnet/samples/05-end-to-end/A2AClientServer/A2AClient/A2AClient.csproj b/dotnet/samples/05-end-to-end/A2AClientServer/A2AClient/A2AClient.csproj
index 01a5a069bc6..b33a4a28abe 100644
--- a/dotnet/samples/05-end-to-end/A2AClientServer/A2AClient/A2AClient.csproj
+++ b/dotnet/samples/05-end-to-end/A2AClientServer/A2AClient/A2AClient.csproj
@@ -10,14 +10,11 @@
-
-
-
diff --git a/dotnet/samples/05-end-to-end/A2AClientServer/A2AClient/HostClientAgent.cs b/dotnet/samples/05-end-to-end/A2AClientServer/A2AClient/HostClientAgent.cs
deleted file mode 100644
index 4daf2c542b1..00000000000
--- a/dotnet/samples/05-end-to-end/A2AClientServer/A2AClient/HostClientAgent.cs
+++ /dev/null
@@ -1,62 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-using System.ClientModel;
-using Microsoft.Agents.AI;
-using Microsoft.Extensions.AI;
-using Microsoft.Extensions.Logging;
-using OpenAI;
-using OpenAI.Chat;
-
-namespace A2A;
-
-internal sealed class HostClientAgent
-{
- internal HostClientAgent(ILoggerFactory 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(CreateAgentAsync);
- var agents = await Task.WhenAll(createAgentTasks);
- var tools = agents.Select(agent => (AITool)agent.AsAIFunction()).ToList();
-
- // Create the agent that uses the remote agents as tools
- this.Agent = new OpenAIClient(new ApiKeyCredential(apiKey))
- .GetChatClient(modelId)
- .AsAIAgent(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;
- }
- }
-
- ///
- /// The associated
- ///
- public AIAgent? Agent { get; private set; }
-
- #region private
- private readonly ILogger _logger;
-
- private static async Task 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
-}
diff --git a/dotnet/samples/05-end-to-end/A2AClientServer/A2AClient/Program.cs b/dotnet/samples/05-end-to-end/A2AClientServer/A2AClient/Program.cs
index 2175e13e717..72f8eca8f7a 100644
--- a/dotnet/samples/05-end-to-end/A2AClientServer/A2AClient/Program.cs
+++ b/dotnet/samples/05-end-to-end/A2AClientServer/A2AClient/Program.cs
@@ -1,77 +1,41 @@
// Copyright (c) Microsoft. All rights reserved.
-using System.CommandLine;
-using System.Reflection;
+// This sample shows how to discover and invoke an agent hosted by an A2A server.
+
+using A2A;
using Microsoft.Agents.AI;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.Logging;
-namespace A2A;
+// Initialize an A2ACardResolver to discover the policy agent.
+var agentUrl = Environment.GetEnvironmentVariable("A2A_AGENT_URL") ?? "http://localhost:5000/";
+var agentCardResolver = new A2ACardResolver(new Uri(agentUrl));
+
+// Create an AIAgent from the A2A agent card.
+AIAgent policyAgent = await agentCardResolver.GetAIAgentAsync();
+
+// Create a session so requests share the same A2A protocol context.
+AgentSession session = await policyAgent.CreateSessionAsync();
-public static class Program
+while (true)
{
- public static async Task Main(string[] args)
- {
- // Create root command with options
- var rootCommand = new RootCommand("A2AClient");
- rootCommand.SetAction((_, ct) => HandleCommandsAsync(ct));
+ // Read the next request from the console.
+ Console.Write("\nUser (:q or quit to exit): ");
+ string? message = Console.ReadLine();
- // Run the command
- return await rootCommand.Parse(args).InvokeAsync();
+ if (string.IsNullOrWhiteSpace(message))
+ {
+ Console.WriteLine("Request cannot be empty.");
+ continue;
}
- private static async Task HandleCommandsAsync(CancellationToken cancellationToken)
+ if (message is ":q" or "quit")
{
- // 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-5.4-mini";
- 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(";"));
- AgentSession session = await hostAgent.Agent!.CreateSessionAsync(cancellationToken);
- 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 is ":q" or "quit")
- {
- break;
- }
+ break;
+ }
- var agentResponse = await hostAgent.Agent!.RunAsync(message, session, cancellationToken: cancellationToken);
+ // Invoke the remote policy agent over A2A and display its response.
+ AgentResponse response = await policyAgent.RunAsync(message, session);
- Console.ForegroundColor = ConsoleColor.Cyan;
- Console.WriteLine($"\nAgent: {agentResponse.Text}");
- Console.ResetColor();
- }
- }
- catch (Exception ex)
- {
- logger.LogError(ex, "An error occurred while running the A2AClient");
- return;
- }
- }
+ Console.ForegroundColor = ConsoleColor.Cyan;
+ Console.WriteLine($"\nPolicy agent: {response.Text}");
+ Console.ResetColor();
}
diff --git a/dotnet/samples/05-end-to-end/A2AClientServer/A2AClient/README.md b/dotnet/samples/05-end-to-end/A2AClientServer/A2AClient/README.md
index 8e0418c2294..36895765f8c 100644
--- a/dotnet/samples/05-end-to-end/A2AClientServer/A2AClient/README.md
+++ b/dotnet/samples/05-end-to-end/A2AClientServer/A2AClient/README.md
@@ -1,26 +1,58 @@
-
-# A2A Client Sample
-Show how to create an A2A Client with a command line interface which invokes agents using the A2A protocol.
+# A2A client
-## Run the Sample
+This console client discovers and invokes the policy agent using the A2A protocol.
-To run the sample, follow these steps:
+## Run the sample
-1. Run the A2A client:
- ```bash
- cd A2AClient
- dotnet run
- ```
-2. Enter your request e.g. "Show me all invoices for Contoso?"
+Start the server in a separate terminal:
-## Set Environment Variables
+```powershell
+az login
+$env:FOUNDRY_PROJECT_ENDPOINT=""
+$env:FOUNDRY_MODEL="gpt-5.4-mini"
+cd dotnet\samples\05-end-to-end\A2AClientServer\A2AServer
+dotnet run
+```
+
+Keep the server running, then start the client:
+
+```powershell
+cd dotnet\samples\05-end-to-end\A2AClientServer\A2AClient
+dotnet run
+```
-The agent urls are provided as a ` ` delimited list of strings
+Ask a question such as `What is the policy for short shipments?`.
+
+The client connects to `http://localhost:5000/` by default. To use another
+endpoint, restart the server with its listening and advertised URLs set:
```powershell
-cd dotnet/samples/05-end-to-end/A2AClientServer/A2AClient
+$env:ASPNETCORE_URLS="http://localhost:6000"
+$env:A2A_AGENT_URL="http://localhost:6000/"
+dotnet run
+```
-$env:OPENAI_CHAT_MODEL_NAME="gpt-5.4-mini"
-$env:OPENAI_API_KEY=""
-$env:AGENT_URLS="http://localhost:5000/policy;http://localhost:5000/invoice;http://localhost:5000/logistics"
+Then set the discovery URL before starting the client:
+
+```powershell
+$env:A2A_AGENT_URL="http://localhost:6000/"
+dotnet run
```
+
+## Test the server with the HTTP file
+
+With the server running, open `..\A2AServer\A2AServer.http` in an editor that
+supports HTTP files, such as Visual Studio or Visual Studio Code with an HTTP
+client extension. Run the first request to retrieve the agent card, or the
+second request to invoke the policy agent directly.
+
+The file targets `http://localhost:5000` by default. Update its `@host` variable
+if the server listens at another address.
+
+## Inspect the server with A2A Inspector
+
+Follow the [A2A Inspector setup instructions](https://github.com/a2aproject/a2a-inspector)
+and connect it to the running server at `http://localhost:5000`.
+
+The Inspector provides another A2A client for viewing the agent card and sending
+messages without running this console client.
diff --git a/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/A2AServer.csproj b/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/A2AServer.csproj
index 98b7b293c81..1a3e72335d5 100644
--- a/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/A2AServer.csproj
+++ b/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/A2AServer.csproj
@@ -9,7 +9,6 @@
-
@@ -24,7 +23,6 @@
-
diff --git a/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/A2AServer.http b/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/A2AServer.http
index 9e50c67fec2..26d481c4004 100644
--- a/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/A2AServer.http
+++ b/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/A2AServer.http
@@ -1,85 +1,25 @@
-### Each A2A agent is available at a different host address
-@hostInvoice = http://localhost:5000
-@hostPolicy = http://localhost:5001
-@hostLogistics = http://localhost:5002
+@host = http://localhost:5000
-### Query agent card for the invoice agent
-GET {{hostInvoice}}/.well-known/agent-card.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": {
- "kind": "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-card.json
+### Query the policy agent card
+GET {{host}}/.well-known/agent-card.json
### Send a message to the policy agent
-POST {{hostPolicy}}
+POST {{host}}
Content-Type: application/json
{
"id": "1",
"jsonrpc": "2.0",
- "method": "message/send",
+ "method": "SendMessage",
"params": {
- "id": "12345",
"message": {
- "kind": "message",
- "role": "user",
+ "role": "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-card.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": {
- "kind": "message",
- "role": "user",
- "messageId": "msg_1",
- "parts": [
- {
- "kind": "text",
- "text": "What is the status for SHPMT-SAP-001?"
- }
- ]
- }
- }
-}
\ No newline at end of file
diff --git a/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/HostAgentFactory.cs b/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/HostAgentFactory.cs
deleted file mode 100644
index db2412b6487..00000000000
--- a/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/HostAgentFactory.cs
+++ /dev/null
@@ -1,176 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using A2A;
-using Azure.AI.Projects;
-using Azure.AI.Projects.Agents;
-using Azure.Identity;
-using Microsoft.Agents.AI;
-using Microsoft.Extensions.AI;
-using OpenAI;
-using OpenAI.Chat;
-using AgentCard = A2A.AgentCard;
-
-namespace A2AServer;
-
-internal static class HostAgentFactory
-{
- internal static async Task<(AIAgent, AgentCard)> CreateFoundryHostAgentAsync(string agentType, string model, string endpoint, string agentName, string[] agentUrls, IList? 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
- // latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
- var aiProjectClient = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential());
-
- ProjectsAgentRecord agentRecord = await aiProjectClient.AgentAdministrationClient.GetAgentAsync(agentName);
- AIAgent agent = aiProjectClient.AsAIAgent(agentRecord, tools: tools);
-
- AgentCard agentCard = agentType.ToUpperInvariant() switch
- {
- "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, string[] agentUrls, IList? tools = null)
- {
- AIAgent agent = new OpenAIClient(apiKey)
- .GetChatClient(model)
- .AsAIAgent(instructions, name, tools: tools);
-
- AgentCard agentCard = agentType.ToUpperInvariant() switch
- {
- "INVOICE" => GetInvoiceAgentCard(agentUrls),
- "POLICY" => GetPolicyAgentCard(agentUrls),
- "LOGISTICS" => GetLogisticsAgentCard(agentUrls),
- _ => throw new ArgumentException($"Unsupported agent type: {agentType}"),
- };
-
- return new(agent, agentCard);
- }
-
- #region private
- private static AgentCard GetInvoiceAgentCard(string[] agentUrls)
- {
- var capabilities = new AgentCapabilities()
- {
- Streaming = false,
- PushNotifications = false,
- };
-
- var invoiceQuery = new A2A.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],
- SupportedInterfaces = CreateAgentInterfaces(agentUrls)
- };
- }
-
- private static AgentCard GetPolicyAgentCard(string[] agentUrls)
- {
- var capabilities = new AgentCapabilities()
- {
- Streaming = false,
- PushNotifications = false,
- };
-
- var policyQuery = new A2A.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 = [policyQuery],
- SupportedInterfaces = CreateAgentInterfaces(agentUrls)
- };
- }
-
- private static AgentCard GetLogisticsAgentCard(string[] agentUrls)
- {
- var capabilities = new AgentCapabilities()
- {
- Streaming = false,
- PushNotifications = false,
- };
-
- var logisticsQuery = new A2A.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],
- SupportedInterfaces = CreateAgentInterfaces(agentUrls)
- };
- }
-
- private static List CreateAgentInterfaces(string[] agentUrls)
- {
- List agentInterfaces = [];
-
- agentInterfaces.AddRange(agentUrls.Select(url => new AgentInterface
- {
- Url = url,
- ProtocolBinding = ProtocolBindingNames.JsonRpc,
- ProtocolVersion = "1.0",
- }));
-
- agentInterfaces.AddRange(agentUrls.Select(url => new AgentInterface
- {
- Url = url,
- ProtocolBinding = ProtocolBindingNames.HttpJson,
- ProtocolVersion = "1.0",
- }));
-
- return agentInterfaces;
- }
- #endregion
-}
diff --git a/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/Models/InvoiceQuery.cs b/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/Models/InvoiceQuery.cs
deleted file mode 100644
index 2b2d142c461..00000000000
--- a/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/Models/InvoiceQuery.cs
+++ /dev/null
@@ -1,167 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.ComponentModel;
-
-namespace A2A;
-
-///
-/// A simple invoice plugin that returns mock data.
-///
-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() => 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 Products { get; set; } // List of products
-
- public Invoice(string transactionId, string invoiceId, string companyName, DateTime invoiceDate, List products)
- {
- this.TransactionId = transactionId;
- this.InvoiceId = invoiceId;
- this.CompanyName = companyName;
- this.InvoiceDate = invoiceDate;
- this.Products = products;
- }
-
- public decimal TotalInvoicePrice() => this.Products.Sum(product => product.TotalPrice()); // Total price of all products in the invoice
-}
-
-public class InvoiceQuery
-{
- private readonly List _invoices;
-
- public InvoiceQuery()
- {
- // Extended mock data with quantities and prices
- this._invoices =
- [
- new("TICKET-XYZ987", "INV789", "Contoso", GetRandomDateWithinLastTwoMonths(),
- [
- new("T-Shirts", 150, 10.00m),
- new("Hats", 200, 15.00m),
- new("Glasses", 300, 5.00m)
- ]),
- new("TICKET-XYZ111", "INV111", "XStore", GetRandomDateWithinLastTwoMonths(),
- [
- new("T-Shirts", 2500, 12.00m),
- new("Hats", 1500, 8.00m),
- new("Glasses", 200, 20.00m)
- ]),
- new("TICKET-XYZ222", "INV222", "Cymbal Direct", GetRandomDateWithinLastTwoMonths(),
- [
- new("T-Shirts", 1200, 14.00m),
- new("Hats", 800, 7.00m),
- new("Glasses", 500, 25.00m)
- ]),
- new("TICKET-XYZ333", "INV333", "Contoso", GetRandomDateWithinLastTwoMonths(),
- [
- new("T-Shirts", 400, 11.00m),
- new("Hats", 600, 15.00m),
- new("Glasses", 700, 5.00m)
- ]),
- new("TICKET-XYZ444", "INV444", "XStore", GetRandomDateWithinLastTwoMonths(),
- [
- new("T-Shirts", 800, 10.00m),
- new("Hats", 500, 18.00m),
- new("Glasses", 300, 22.00m)
- ]),
- new("TICKET-XYZ555", "INV555", "Cymbal Direct", GetRandomDateWithinLastTwoMonths(),
- [
- new("T-Shirts", 1100, 9.00m),
- new("Hats", 900, 12.00m),
- new("Glasses", 1200, 15.00m)
- ]),
- new("TICKET-XYZ666", "INV666", "Contoso", GetRandomDateWithinLastTwoMonths(),
- [
- new("T-Shirts", 2500, 8.00m),
- new("Hats", 1200, 10.00m),
- new("Glasses", 1000, 6.00m)
- ]),
- new("TICKET-XYZ777", "INV777", "XStore", GetRandomDateWithinLastTwoMonths(),
- [
- new("T-Shirts", 1900, 13.00m),
- new("Hats", 1300, 16.00m),
- new("Glasses", 800, 19.00m)
- ]),
- new("TICKET-XYZ888", "INV888", "Cymbal Direct", GetRandomDateWithinLastTwoMonths(),
- [
- new("T-Shirts", 2200, 11.00m),
- new("Hats", 1700, 8.50m),
- new("Glasses", 600, 21.00m)
- ]),
- new("TICKET-XYZ999", "INV999", "Contoso", GetRandomDateWithinLastTwoMonths(),
- [
- 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.UtcNow;
-
- // 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;
- int randomDays = Random.Shared.Next(0, totalDays + 1); // +1 to include the end date
-
- // Return the random date
- return startDate.AddDays(randomDays);
- }
-
- [Description("Retrieves invoices for the specified company and optionally within the specified time range")]
- public IEnumerable 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 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 QueryByInvoiceId(string invoiceId)
- {
- var query = this._invoices.Where(i => i.InvoiceId.Equals(invoiceId, StringComparison.OrdinalIgnoreCase));
-
- return query.ToList();
- }
-}
diff --git a/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/PolicyAgentCard.cs b/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/PolicyAgentCard.cs
new file mode 100644
index 00000000000..e9d827aaaa6
--- /dev/null
+++ b/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/PolicyAgentCard.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using A2A;
+
+namespace A2AServer;
+
+internal static class PolicyAgentCard
+{
+ internal static AgentCard Create(string agentUrl)
+ {
+ return new()
+ {
+ Name = "PolicyAgent",
+ Description = "Handles requests relating to policies and customer communications.",
+ Version = "1.0.0",
+ DefaultInputModes = ["text/plain"],
+ DefaultOutputModes = ["text/plain"],
+ Capabilities = new()
+ {
+ Streaming = false,
+ PushNotifications = false,
+ },
+ Skills =
+ [
+ new()
+ {
+ Id = "id_policy_agent",
+ Name = "PolicyAgent",
+ Description = "Handles requests relating to policies and customer communications.",
+ Tags = ["policy"],
+ Examples = ["What is the policy for short shipments?"],
+ },
+ ],
+ SupportedInterfaces =
+ [
+ new()
+ {
+ Url = agentUrl,
+ ProtocolBinding = ProtocolBindingNames.JsonRpc,
+ ProtocolVersion = "1.0",
+ },
+ new()
+ {
+ Url = agentUrl,
+ ProtocolBinding = ProtocolBindingNames.HttpJson,
+ ProtocolVersion = "1.0",
+ },
+ ],
+ };
+ }
+}
diff --git a/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/Program.cs b/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/Program.cs
index a2834fb69d6..ec64e950f4a 100644
--- a/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/Program.cs
+++ b/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/Program.cs
@@ -1,105 +1,53 @@
// Copyright (c) Microsoft. All rights reserved.
+
+// This sample shows how to host a policy agent and expose it through the A2A protocol.
+
using A2A;
using A2A.AspNetCore;
using A2AServer;
+using Azure.AI.Projects;
+using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.AspNetCore.Builder;
-using Microsoft.Extensions.AI;
-using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
-string agentName = string.Empty;
-string agentType = string.Empty;
-
-for (var i = 0; i < args.Length; i++)
-{
- if (args[i].Equals("--agentName", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
- {
- agentName = args[++i];
- }
- else if (args[i].Equals("--agentType", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
- {
- agentType = args[++i];
- }
-}
-
+// Create the ASP.NET Core host and register the services required by A2A.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient().AddLogging();
-IConfigurationRoot configuration = new ConfigurationBuilder()
- .AddEnvironmentVariables()
- .AddUserSecrets()
- .Build();
-
-string? apiKey = configuration["OPENAI_API_KEY"];
-string model = configuration["OPENAI_CHAT_MODEL_NAME"] ?? "gpt-5.4-mini";
-string? endpoint = configuration["FOUNDRY_PROJECT_ENDPOINT"];
-string[] agentUrls = (builder.Configuration["urls"] ?? "http://localhost:5000").Split(';');
-
-var invoiceQueryPlugin = new InvoiceQuery();
-IList tools =
-[
- AIFunctionFactory.Create(invoiceQueryPlugin.QueryInvoices),
- AIFunctionFactory.Create(invoiceQueryPlugin.QueryByTransactionId),
- AIFunctionFactory.Create(invoiceQueryPlugin.QueryByInvoiceId)
-];
-
-AIAgent hostA2AAgent;
-AgentCard hostA2AAgentCard;
-
-if (!string.IsNullOrEmpty(endpoint) && !string.IsNullOrEmpty(agentName))
-{
- (hostA2AAgent, hostA2AAgentCard) = agentType.ToUpperInvariant() switch
- {
- "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}"),
- };
-}
-else if (!string.IsNullOrEmpty(apiKey))
-{
- (hostA2AAgent, hostA2AAgentCard) = agentType.ToUpperInvariant() switch
- {
- "INVOICE" => await HostAgentFactory.CreateChatCompletionHostAgentAsync(
- agentType, model, apiKey, "InvoiceAgent",
- """
- You specialize in handling queries related to invoices.
- """, agentUrls, tools),
- "POLICY" => await HostAgentFactory.CreateChatCompletionHostAgentAsync(
- agentType, model, 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."
- """, agentUrls),
- "LOGISTICS" => await HostAgentFactory.CreateChatCompletionHostAgentAsync(
- agentType, model, 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
- """, agentUrls),
- _ => throw new ArgumentException($"Unsupported agent type: {agentType}"),
- };
-}
-else
-{
- throw new ArgumentException("Either A2AServer:ApiKey or A2AServer:ConnectionString & agentName must be provided");
-}
+// Read the Microsoft Foundry project and model configuration.
+var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
+var model = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
+var agentUrl = Environment.GetEnvironmentVariable("A2A_AGENT_URL") ?? "http://localhost:5000";
+
+const string PolicyInstructions =
+ """
+ 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."
+ """;
+
+// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
+// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
+// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
+AIAgent policyAgent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
+ .AsAIAgent(
+ model: model,
+ instructions: PolicyInstructions,
+ name: "PolicyAgent"
+ );
+
+// Create the agent card published at the well-known discovery endpoint.
+AgentCard policyAgentCard = PolicyAgentCard.Create(agentUrl);
// IMPORTANT: In production, register an AgentIsolationKeyProvider to isolate sessions and tasks by authenticated caller.
// Without this, contextId/taskId alone are the lookup keys — any caller who knows them can access another caller's data.
@@ -108,14 +56,18 @@ You specialize in handling queries related to logistics.
// By default, NoopAgentSessionStore is used — sessions are not persisted across requests.
// To enable multi-turn conversations, register a session store explicitly, e.g.:
-// builder.Services.AddKeyedSingleton(hostA2AAgent.Name, new InMemoryAgentSessionStore());
+// builder.Services.AddKeyedSingleton(policyAgent.Name, new InMemoryAgentSessionStore());
-builder.AddA2AServer(hostA2AAgent);
+// Register the policy agent with the A2A hosting services.
+builder.AddA2AServer(policyAgent);
var app = builder.Build();
-app.MapA2AHttpJson(hostA2AAgent, "/");
-app.MapA2AJsonRpc(hostA2AAgent, "/");
-app.MapWellKnownAgentCard(hostA2AAgentCard);
+// Expose the agent through both supported A2A protocol bindings.
+app.MapA2AHttpJson(policyAgent, "/");
+app.MapA2AJsonRpc(policyAgent, "/");
+
+// Publish the agent card at the well-known discovery endpoint.
+app.MapWellKnownAgentCard(policyAgentCard);
await app.RunAsync();
diff --git a/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/README.md b/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/README.md
new file mode 100644
index 00000000000..7f273667248
--- /dev/null
+++ b/dotnet/samples/05-end-to-end/A2AClientServer/A2AServer/README.md
@@ -0,0 +1,48 @@
+# A2A server
+
+This server hosts a policy agent and exposes it through the A2A JSON-RPC and
+HTTP+JSON protocol bindings.
+
+## Run the server
+
+Authenticate with Azure CLI and configure your Microsoft Foundry project:
+
+```powershell
+az login
+$env:FOUNDRY_PROJECT_ENDPOINT=""
+$env:FOUNDRY_MODEL="gpt-5.4-mini"
+```
+
+Start the server:
+
+```powershell
+cd dotnet\samples\05-end-to-end\A2AClientServer\A2AServer
+dotnet run
+```
+
+The server must remain running while you use the client, the HTTP requests, or
+the A2A Inspector. By default, it listens at `http://localhost:5000`.
+
+`ASPNETCORE_URLS` controls the address the server listens on, and
+`A2A_AGENT_URL` controls the public URL advertised in the agent card. Both
+default to `http://localhost:5000`. Set them together when the server should
+run at a different address.
+
+## Test with the HTTP file
+
+Open `A2AServer.http` in an editor that supports HTTP files, such as Visual
+Studio or Visual Studio Code with an HTTP client extension, and run either request:
+
+1. `Query the policy agent card` retrieves the discovery document.
+2. `Send a message to the policy agent` invokes the agent through JSON-RPC.
+
+Start the server before running these requests. If the server uses a different
+address, update the `@host` variable at the top of `A2AServer.http`.
+
+## Inspect with A2A Inspector
+
+Follow the [A2A Inspector setup instructions](https://github.com/a2aproject/a2a-inspector),
+start the Inspector, and connect it to `http://localhost:5000`.
+
+The Inspector discovers `/.well-known/agent-card.json`, displays the policy
+agent card, and lets you send messages to the running server.
diff --git a/dotnet/samples/05-end-to-end/A2AClientServer/README.md b/dotnet/samples/05-end-to-end/A2AClientServer/README.md
index d0771ff4e10..5700d023a17 100644
--- a/dotnet/samples/05-end-to-end/A2AClientServer/README.md
+++ b/dotnet/samples/05-end-to-end/A2AClientServer/README.md
@@ -1,235 +1,52 @@
-# A2A Client and Server samples
+# A2A policy agent client and server
-> **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.
+This sample demonstrates a minimal end-to-end A2A flow:
-These samples are built with [official A2A C# SDK](https://www.nuget.org/packages/A2A) and demonstrates:
+1. `A2AServer` hosts a policy agent and publishes its agent card.
+2. `A2AClient` discovers the policy agent and sends it messages over A2A.
-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.
+## Prerequisites
-The demonstration has two components:
+- .NET 10 SDK
+- A Microsoft Foundry project with a deployed model
+- Azure CLI authentication (`az login`)
-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.
+## Run the sample
-
+Open two terminals from this directory.
-## 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=""
-```
-
-Use the following commands to run each A2A server:
-
-Execute the following command to build the sample:
+In the first terminal, start the policy agent:
```powershell
+$env:FOUNDRY_PROJECT_ENDPOINT=""
+$env:FOUNDRY_MODEL="gpt-5.4-mini"
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 a Microsoft Foundry project and then provide the project endpoint and agent 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" --agentName "" --agentType "invoice" --no-build
-```
-
-```bash
-dotnet run --urls "http://localhost:5001;https://localhost:5011" --agentName "" --agentType "policy" --no-build
-```
-
-```bash
-dotnet run --urls "http://localhost:5002;https://localhost:5012" --agentName "" --agentType "logistics" --no-build
+dotnet run
```
-### Testing the Agents using the Rest Client
-
-This sample contains a [.http file](https://learn.microsoft.com/aspnet/core/test/http-files?view=aspnetcore-10.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-card.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": {
- "kind": "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:
-
-
-
-Sample output from the request to send a message to the agent via A2A protocol:
-
-
-
-### 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 (Agent2Agent) 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:
-
-
-
-Sample response after sending a message to the agent via A2A protocol:
-
-
-
-Raw JSON response from an A2A agent:
-
-
-
-### 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:
+In the second terminal, start the client:
```powershell
-$env:A2A_AGENT_URLS="http://localhost:5000/;http://localhost:5001/;http://localhost:5002/"
+cd A2AClient
+dotnet run
```
-## 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:
+Then ask:
+```text
+What is the policy for short shipments?
```
-A2AClient> dotnet run
-info: HostClientAgent[0]
- Initializing Agent Framework agent with model: gpt-5.4-mini
-
-User (:q or quit to exit): Customer is disputing transaction TICKET-XYZ987 as they claim the received fewer t-shirts than ordered.
-Agent:
+The server listens on `http://localhost:5000` by default. `A2AServer/A2AServer.http`
+contains requests for inspecting the agent card and calling the agent directly.
+The server must be running before using either the HTTP file or the
+[A2A Inspector](https://github.com/a2aproject/a2a-inspector). See the server and
+client READMEs for detailed instructions.
-Agent:
+## Optional configuration
-Agent: The transaction details for **TICKET-XYZ987** are as follows:
+`FOUNDRY_MODEL` is optional and defaults to `gpt-5.4-mini`. The server creates the
+policy agent with the Microsoft Foundry Responses API.
-- **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.
-```
+`A2A_AGENT_URL` optionally sets the public server URL advertised in the agent
+card and defaults to `http://localhost:5000`.
diff --git a/dotnet/samples/05-end-to-end/A2AClientServer/a2a-inspector-agent-card.png b/dotnet/samples/05-end-to-end/A2AClientServer/a2a-inspector-agent-card.png
deleted file mode 100644
index 8385a2b68a4..00000000000
Binary files a/dotnet/samples/05-end-to-end/A2AClientServer/a2a-inspector-agent-card.png and /dev/null differ
diff --git a/dotnet/samples/05-end-to-end/A2AClientServer/a2a-inspector-raw-json-response.png b/dotnet/samples/05-end-to-end/A2AClientServer/a2a-inspector-raw-json-response.png
deleted file mode 100644
index 038ef344edf..00000000000
Binary files a/dotnet/samples/05-end-to-end/A2AClientServer/a2a-inspector-raw-json-response.png and /dev/null differ
diff --git a/dotnet/samples/05-end-to-end/A2AClientServer/a2a-inspector-send-message.png b/dotnet/samples/05-end-to-end/A2AClientServer/a2a-inspector-send-message.png
deleted file mode 100644
index 49fa857955c..00000000000
Binary files a/dotnet/samples/05-end-to-end/A2AClientServer/a2a-inspector-send-message.png and /dev/null differ
diff --git a/dotnet/samples/05-end-to-end/A2AClientServer/demo-architecture.png b/dotnet/samples/05-end-to-end/A2AClientServer/demo-architecture.png
deleted file mode 100644
index 6ae351907a6..00000000000
Binary files a/dotnet/samples/05-end-to-end/A2AClientServer/demo-architecture.png and /dev/null differ
diff --git a/dotnet/samples/05-end-to-end/A2AClientServer/rest-client-agent-card.png b/dotnet/samples/05-end-to-end/A2AClientServer/rest-client-agent-card.png
deleted file mode 100644
index 44651487a8e..00000000000
Binary files a/dotnet/samples/05-end-to-end/A2AClientServer/rest-client-agent-card.png and /dev/null differ
diff --git a/dotnet/samples/05-end-to-end/A2AClientServer/rest-client-send-message.png b/dotnet/samples/05-end-to-end/A2AClientServer/rest-client-send-message.png
deleted file mode 100644
index fe65f5c92dd..00000000000
Binary files a/dotnet/samples/05-end-to-end/A2AClientServer/rest-client-send-message.png and /dev/null differ