mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET: Add an A2A client server sample (#633)
* Add an A2A client server sample * Address code review feedback * Start to fix code review feedback * Start to fix code review feedback * Start to fix code review feedback
This commit is contained in:
committed by
GitHub
Unverified
parent
ec5ea3c8a8
commit
745a05d43c
@@ -40,6 +40,7 @@
|
||||
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="9.0.8" />
|
||||
<PackageVersion Include="System.Threading.Channels" Version="9.0.8" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
|
||||
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||
<!-- OpenTelemetry -->
|
||||
<PackageVersion Include="OpenTelemetry" Version="1.12.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.12.0" />
|
||||
|
||||
@@ -7,6 +7,11 @@
|
||||
<Folder Name="/Samples/">
|
||||
<File Path="samples/README.md" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/A2AClientServer/">
|
||||
<File Path="samples/A2AClientServer/README.md" />
|
||||
<Project Path="samples/A2AClientServer/A2AClient/A2AClient.csproj" />
|
||||
<Project Path="samples/A2AClientServer/A2AServer/A2AServer.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/AgentWebChat/">
|
||||
<Project Path="samples/AgentWebChat/AgentWebChat.AgentHost/AgentWebChat.AgentHost.csproj" />
|
||||
<Project Path="samples/AgentWebChat/AgentWebChat.AppHost/AgentWebChat.AppHost.csproj" />
|
||||
|
||||
@@ -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();
|
||||
@@ -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 |
@@ -69,7 +69,7 @@ internal sealed class A2AAgent : AIAgent
|
||||
|
||||
if (a2aResponse is Message message)
|
||||
{
|
||||
UpdateThreadConversationId(thread, message);
|
||||
UpdateThreadConversationId(thread, message.ContextId);
|
||||
|
||||
return new AgentRunResponse
|
||||
{
|
||||
@@ -80,8 +80,21 @@ internal sealed class A2AAgent : AIAgent
|
||||
AdditionalProperties = message.Metadata.ToAdditionalProperties(),
|
||||
};
|
||||
}
|
||||
if (a2aResponse is AgentTask agentTask)
|
||||
{
|
||||
UpdateThreadConversationId(thread, agentTask.ContextId);
|
||||
|
||||
throw new NotSupportedException($"Only message responses are supported from A2A agents. Received: {a2aResponse.GetType().FullName ?? "null"}");
|
||||
return new AgentRunResponse
|
||||
{
|
||||
AgentId = this.Id,
|
||||
ResponseId = agentTask.Id,
|
||||
RawRepresentation = agentTask,
|
||||
Messages = agentTask.ToChatMessages(),
|
||||
AdditionalProperties = agentTask.Metadata.ToAdditionalProperties(),
|
||||
};
|
||||
}
|
||||
|
||||
throw new NotSupportedException($"Only Message and AgentTask responses are supported from A2A agents. Received: {a2aResponse.GetType().FullName ?? "null"}");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -107,7 +120,7 @@ internal sealed class A2AAgent : AIAgent
|
||||
throw new NotSupportedException($"Only message responses are supported from A2A agents. Received: {sseEvent.Data?.GetType().FullName ?? "null"}");
|
||||
}
|
||||
|
||||
UpdateThreadConversationId(thread, message);
|
||||
UpdateThreadConversationId(thread, message.ContextId);
|
||||
|
||||
yield return new AgentRunResponseUpdate
|
||||
{
|
||||
@@ -147,22 +160,22 @@ internal sealed class A2AAgent : AIAgent
|
||||
}
|
||||
}
|
||||
|
||||
private static void UpdateThreadConversationId(AgentThread? thread, Message message)
|
||||
private static void UpdateThreadConversationId(AgentThread? thread, string? contextId)
|
||||
{
|
||||
if (thread is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Surface cases where the A2A agent responds with a message that
|
||||
// Surface cases where the A2A agent responds with a response that
|
||||
// has a different context Id than the thread's conversation Id.
|
||||
if (thread.ConversationId is not null && message.ContextId is not null && thread.ConversationId != message.ContextId)
|
||||
if (thread.ConversationId is not null && contextId is not null && thread.ConversationId != contextId)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"The {nameof(message.ContextId)} returned from the A2A agent is different from the conversation Id of the provided {nameof(AgentThread)}.");
|
||||
$"The {nameof(contextId)} returned from the A2A agent is different from the conversation Id of the provided {nameof(AgentThread)}.");
|
||||
}
|
||||
|
||||
// Assign a server-generated context Id to the thread if it's not already set.
|
||||
thread.ConversationId ??= message.ContextId;
|
||||
thread.ConversationId ??= contextId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using A2A;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.A2A;
|
||||
|
||||
/// <summary>
|
||||
/// Host which will attach an <see cref="AIAgent"/> to a <see cref="ITaskManager"/>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This implementation only handles:
|
||||
/// <list type="bullet">
|
||||
/// <item><code>TaskManager.OnMessageReceived</code></item>
|
||||
/// <item><code>TaskManager.OnAgentCardQuery</code></item>
|
||||
/// </list>
|
||||
/// Support for task management will be added later as part of the long-running task execution work.
|
||||
/// </remarks>
|
||||
public sealed class A2AHostAgent
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the SemanticKernelTravelAgent
|
||||
/// </summary>
|
||||
public A2AHostAgent(AIAgent agent, AgentCard agentCard, TaskManager? taskManager = null)
|
||||
{
|
||||
Throw.IfNull(agent);
|
||||
Throw.IfNull(agentCard);
|
||||
|
||||
this.Agent = agent;
|
||||
this._agentCard = agentCard;
|
||||
|
||||
this.Attach(taskManager ?? new TaskManager());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The associated <see cref="AIAgent"/>
|
||||
/// </summary>
|
||||
public AIAgent? Agent { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The associated <see cref="ITaskManager"/>
|
||||
/// </summary>
|
||||
public TaskManager? TaskManager => this._taskManager;
|
||||
|
||||
/// <summary>
|
||||
/// Attach the <see cref="A2AAgent"/> to the provided <see cref="ITaskManager"/>
|
||||
/// </summary>
|
||||
/// <param name="taskManager"></param>
|
||||
public void Attach(TaskManager taskManager)
|
||||
{
|
||||
Throw.IfNull(taskManager);
|
||||
|
||||
this._taskManager = taskManager;
|
||||
taskManager.OnMessageReceived = this.OnMessageReceivedAsync;
|
||||
taskManager.OnAgentCardQuery = this.GetAgentCardAsync;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle a received message
|
||||
/// </summary>
|
||||
/// <param name="messageSend">The <see cref="MessageSendParams"/> to handle</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to cancel the operation</param>
|
||||
public async Task<Message> OnMessageReceivedAsync(MessageSendParams messageSend, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(messageSend);
|
||||
Throw.IfNull(this.Agent);
|
||||
|
||||
if (this._taskManager is null)
|
||||
{
|
||||
throw new InvalidOperationException("TaskManager must be attached before handling an agent message.");
|
||||
}
|
||||
|
||||
// Get message from the user
|
||||
var userMessage = messageSend.Message.ToChatMessage();
|
||||
|
||||
// Get the response from the agent
|
||||
var message = new Message();
|
||||
var agentResponse = await this.Agent.RunAsync(userMessage, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
foreach (var chatMessage in agentResponse.Messages)
|
||||
{
|
||||
var content = chatMessage.Text;
|
||||
message.Parts.Add(new TextPart() { Text = content! });
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return the <see cref="AgentCard"/> associated with this hosted agent.
|
||||
/// </summary>
|
||||
/// <param name="agentUrl">Current URL for the agent</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to cancel the operation</param>
|
||||
#pragma warning disable CA1054 // URI-like parameters should not be strings
|
||||
public Task<AgentCard> GetAgentCardAsync(string agentUrl, CancellationToken cancellationToken)
|
||||
{
|
||||
// Ensure the URL is in the correct format
|
||||
Uri uri = new(agentUrl);
|
||||
agentUrl = $"{uri.Scheme}://{uri.Host}:{uri.Port}/";
|
||||
|
||||
this._agentCard.Url = agentUrl;
|
||||
return Task.FromResult(this._agentCard);
|
||||
}
|
||||
#pragma warning restore CA1054 // URI-like parameters should not be strings
|
||||
|
||||
#region private
|
||||
private readonly AgentCard _agentCard;
|
||||
private TaskManager? _taskManager;
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using A2A;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.A2A;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for the <see cref="AgentTask"/> class.
|
||||
/// </summary>
|
||||
internal static class A2AAgentTaskExtensions
|
||||
{
|
||||
internal static IList<ChatMessage> ToChatMessages(this AgentTask agentTask)
|
||||
{
|
||||
_ = Throw.IfNull(agentTask);
|
||||
|
||||
List<ChatMessage> messages = [];
|
||||
|
||||
if (agentTask.Artifacts is not null)
|
||||
{
|
||||
foreach (var artifact in agentTask.Artifacts)
|
||||
{
|
||||
messages.Add(artifact.ToChatMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using A2A;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.A2A;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for the <see cref="Artifact"/> class.
|
||||
/// </summary>
|
||||
internal static class A2AArtifactExtensions
|
||||
{
|
||||
internal static ChatMessage ToChatMessage(this Artifact artifact)
|
||||
{
|
||||
List<AIContent>? aiContents = null;
|
||||
|
||||
foreach (var part in artifact.Parts)
|
||||
{
|
||||
(aiContents ??= []).Add(part.ToAIContent());
|
||||
}
|
||||
|
||||
return new ChatMessage(ChatRole.Assistant, aiContents)
|
||||
{
|
||||
AdditionalProperties = artifact.Metadata.ToAdditionalProperties(),
|
||||
RawRepresentation = artifact,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ namespace Microsoft.Extensions.AI.Agents.A2A;
|
||||
/// </summary>
|
||||
internal static class A2AMessageExtensions
|
||||
{
|
||||
public static ChatMessage ToChatMessage(this Message message)
|
||||
internal static ChatMessage ToChatMessage(this Message message)
|
||||
{
|
||||
List<AIContent>? aiContents = null;
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ internal static class A2AMetadataExtensions
|
||||
/// </summary>
|
||||
/// <param name="metadata">The metadata dictionary to convert.</param>
|
||||
/// <returns>The converted <see cref="AdditionalPropertiesDictionary"/>, or null if the input is null or empty.</returns>
|
||||
public static AdditionalPropertiesDictionary? ToAdditionalProperties(this Dictionary<string, JsonElement>? metadata)
|
||||
internal static AdditionalPropertiesDictionary? ToAdditionalProperties(this Dictionary<string, JsonElement>? metadata)
|
||||
{
|
||||
if (metadata is not { Count: > 0 })
|
||||
{
|
||||
|
||||
@@ -15,7 +15,7 @@ internal static class A2APartExtensions
|
||||
/// </summary>
|
||||
/// <param name="part">The A2A part to convert.</param>
|
||||
/// <returns>The corresponding <see cref="AIContent"/>.</returns>
|
||||
public static AIContent ToAIContent(this Part part)
|
||||
internal static AIContent ToAIContent(this Part part)
|
||||
{
|
||||
return part switch
|
||||
{
|
||||
|
||||
@@ -16,7 +16,7 @@ internal static class AIContentExtensions
|
||||
/// </summary>
|
||||
/// <param name="contents">The collection of AI contents to convert.</param>"
|
||||
/// <returns>The list of A2A <see cref="Part"/> objects.</returns>
|
||||
public static List<Part>? ToA2AParts(this IEnumerable<AIContent> contents)
|
||||
internal static List<Part>? ToA2AParts(this IEnumerable<AIContent> contents)
|
||||
{
|
||||
List<Part>? parts = null;
|
||||
|
||||
@@ -33,7 +33,7 @@ internal static class AIContentExtensions
|
||||
/// </summary>
|
||||
/// <param name="content">AI content to convert.</param>
|
||||
/// <returns>The corresponding A2A <see cref="Part"/> object.</returns>
|
||||
public static Part ToA2APart(this AIContent content)
|
||||
internal static Part ToA2APart(this AIContent content)
|
||||
{
|
||||
return content switch
|
||||
{
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Microsoft.Extensions.AI.Agents.A2A;
|
||||
/// </summary>
|
||||
internal static class ChatMessageExtensions
|
||||
{
|
||||
public static Message ToA2AMessage(this IReadOnlyCollection<ChatMessage> messages)
|
||||
internal static Message ToA2AMessage(this IReadOnlyCollection<ChatMessage> messages)
|
||||
{
|
||||
List<Part> allParts = [];
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
using static Microsoft.Extensions.AI.Agents.OpenTelemetryConsts.GenAI;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents;
|
||||
|
||||
/// <summary>
|
||||
/// Provides factory methods for creating implementations of <see cref="AIFunction"/> backed by an <see cref="AIAgent" />.
|
||||
/// </summary>
|
||||
public static class AgentAIFunctionFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a <see cref="AIFunction"/> that will invoke the provided Agent.
|
||||
/// </summary>
|
||||
/// <param name="agent">The <see cref="Agent" /> to be represented via the created <see cref="AIFunction"/>.</param>
|
||||
/// <param name="options">Metadata to use to override defaults inferred from <paramref name="agent"/>.</param>
|
||||
/// <returns>The created <see cref="AIFunction"/> for invoking the <see cref="AIAgent"/>.</returns>
|
||||
public static AIFunction CreateFromAgent(
|
||||
AIAgent agent,
|
||||
AIFunctionFactoryOptions? options = null)
|
||||
{
|
||||
Throw.IfNull(agent);
|
||||
|
||||
async Task<string> RunAgentAsync(string query, CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await agent.RunAsync(query, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
return response.Text;
|
||||
}
|
||||
|
||||
return AIFunctionFactory.Create(RunAgentAsync, options ?? new()
|
||||
{
|
||||
Name = agent.Name,
|
||||
Description = agent.Description,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Moq;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the <see cref="AgentAIFunctionFactory"/> class.
|
||||
/// </summary>
|
||||
public class AgentAIFunctionFactoryTests
|
||||
{
|
||||
[Fact]
|
||||
public void CreateFromAgent_WithNullAgent_ThrowsArgumentNullException()
|
||||
{
|
||||
// Act & Assert
|
||||
var exception = Assert.Throws<ArgumentNullException>(() =>
|
||||
AgentAIFunctionFactory.CreateFromAgent(null!));
|
||||
|
||||
Assert.Equal("agent", exception.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateFromAgent_WithValidAgent_ReturnsAIFunction()
|
||||
{
|
||||
// Arrange
|
||||
var mockAgent = new Mock<AIAgent>();
|
||||
mockAgent.Setup(a => a.Name).Returns("TestAgent");
|
||||
mockAgent.Setup(a => a.Description).Returns("Test agent description");
|
||||
|
||||
// Act
|
||||
var result = AgentAIFunctionFactory.CreateFromAgent(mockAgent.Object);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.True(result is AIFunction);
|
||||
Assert.Equal("TestAgent", result.Name);
|
||||
Assert.Equal("Test agent description", result.Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateFromAgent_WithAgentHavingNullName_UsesDefaultName()
|
||||
{
|
||||
// Arrange
|
||||
var mockAgent = new Mock<AIAgent>();
|
||||
mockAgent.Setup(a => a.Name).Returns((string?)null);
|
||||
mockAgent.Setup(a => a.Description).Returns("Test description");
|
||||
|
||||
// Act
|
||||
var result = AgentAIFunctionFactory.CreateFromAgent(mockAgent.Object);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(result.Name);
|
||||
Assert.Equal("Test description", result.Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateFromAgent_WithAgentHavingNullDescription_UsesEmptyDescription()
|
||||
{
|
||||
// Arrange
|
||||
var mockAgent = new Mock<AIAgent>();
|
||||
mockAgent.Setup(a => a.Name).Returns("TestAgent");
|
||||
mockAgent.Setup(a => a.Description).Returns((string?)null);
|
||||
|
||||
// Act
|
||||
var result = AgentAIFunctionFactory.CreateFromAgent(mockAgent.Object);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("TestAgent", result.Name);
|
||||
Assert.Equal(string.Empty, result.Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateFromAgent_WithCustomOptions_UsesCustomOptions()
|
||||
{
|
||||
// Arrange
|
||||
var mockAgent = new Mock<AIAgent>();
|
||||
mockAgent.Setup(a => a.Name).Returns("TestAgent");
|
||||
mockAgent.Setup(a => a.Description).Returns("Test agent description");
|
||||
|
||||
var customOptions = new AIFunctionFactoryOptions
|
||||
{
|
||||
Name = "CustomName",
|
||||
Description = "Custom description"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = AgentAIFunctionFactory.CreateFromAgent(mockAgent.Object, customOptions);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("CustomName", result.Name);
|
||||
Assert.Equal("Custom description", result.Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateFromAgent_WithNullOptions_UsesAgentProperties()
|
||||
{
|
||||
// Arrange
|
||||
var mockAgent = new Mock<AIAgent>();
|
||||
mockAgent.Setup(a => a.Name).Returns("TestAgent");
|
||||
mockAgent.Setup(a => a.Description).Returns("Test agent description");
|
||||
|
||||
// Act
|
||||
var result = AgentAIFunctionFactory.CreateFromAgent(mockAgent.Object, null);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("TestAgent", result.Name);
|
||||
Assert.Equal("Test agent description", result.Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateFromAgent_WhenFunctionInvokedAsync_CallsAgentRunAsync()
|
||||
{
|
||||
// Arrange
|
||||
var expectedResponse = new AgentRunResponse(new ChatMessage(ChatRole.Assistant, "Test response"));
|
||||
var testAgent = new TestAgent("TestAgent", "Test description", expectedResponse);
|
||||
|
||||
var aiFunction = AgentAIFunctionFactory.CreateFromAgent(testAgent);
|
||||
|
||||
// Act
|
||||
var arguments = new AIFunctionArguments() { ["query"] = "Test query" };
|
||||
var result = await aiFunction.InvokeAsync(arguments);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("Test response", result.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateFromAgent_WhenFunctionInvokedWithCancellationTokenAsync_PassesCancellationTokenAsync()
|
||||
{
|
||||
// Arrange
|
||||
var expectedResponse = new AgentRunResponse(new ChatMessage(ChatRole.Assistant, "Test response"));
|
||||
var testAgent = new TestAgent("TestAgent", "Test description", expectedResponse);
|
||||
using var cancellationTokenSource = new CancellationTokenSource();
|
||||
var cancellationToken = cancellationTokenSource.Token;
|
||||
|
||||
var aiFunction = AgentAIFunctionFactory.CreateFromAgent(testAgent);
|
||||
|
||||
// Act
|
||||
var arguments = new AIFunctionArguments() { ["query"] = "Test query" };
|
||||
var result = await aiFunction.InvokeAsync(arguments, cancellationToken);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("Test response", result.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateFromAgent_WhenAgentThrowsExceptionAsync_PropagatesExceptionAsync()
|
||||
{
|
||||
// Arrange
|
||||
var expectedException = new InvalidOperationException("Test exception");
|
||||
var testAgent = new TestAgent("TestAgent", "Test description", expectedException);
|
||||
|
||||
var aiFunction = AgentAIFunctionFactory.CreateFromAgent(testAgent);
|
||||
|
||||
// Act & Assert
|
||||
var arguments = new AIFunctionArguments() { ["query"] = "Test query" };
|
||||
var actualException = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||
await aiFunction.InvokeAsync(arguments));
|
||||
|
||||
Assert.Same(expectedException, actualException);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateFromAgent_ReturnsInvokableFunction()
|
||||
{
|
||||
// Arrange
|
||||
var mockAgent = new Mock<AIAgent>();
|
||||
mockAgent.Setup(a => a.Name).Returns("TestAgent");
|
||||
mockAgent.Setup(a => a.Description).Returns("Test description");
|
||||
|
||||
// Act
|
||||
var result = AgentAIFunctionFactory.CreateFromAgent(mockAgent.Object);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
|
||||
// Verify the function has the expected parameter schema
|
||||
var parameters = result.JsonSchema;
|
||||
|
||||
// Verify it has a query parameter
|
||||
Assert.True(parameters.TryGetProperty("properties", out var properties));
|
||||
Assert.True(properties.TryGetProperty("query", out var queryProperty));
|
||||
Assert.True(queryProperty.TryGetProperty("type", out var typeProperty));
|
||||
Assert.Equal("string", typeProperty.GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateFromAgent_WithEmptyAgentName_CreatesValidFunction()
|
||||
{
|
||||
// Arrange
|
||||
var mockAgent = new Mock<AIAgent>();
|
||||
mockAgent.Setup(a => a.Name).Returns(string.Empty);
|
||||
mockAgent.Setup(a => a.Description).Returns("Test description");
|
||||
|
||||
// Act
|
||||
var result = AgentAIFunctionFactory.CreateFromAgent(mockAgent.Object);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(string.Empty, result.Name);
|
||||
Assert.Equal("Test description", result.Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateFromAgent_WithEmptyAgentDescription_CreatesValidFunction()
|
||||
{
|
||||
// Arrange
|
||||
var mockAgent = new Mock<AIAgent>();
|
||||
mockAgent.Setup(a => a.Name).Returns("TestAgent");
|
||||
mockAgent.Setup(a => a.Description).Returns(string.Empty);
|
||||
|
||||
// Act
|
||||
var result = AgentAIFunctionFactory.CreateFromAgent(mockAgent.Object);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("TestAgent", result.Name);
|
||||
Assert.Equal(string.Empty, result.Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateFromAgent_WithCustomOptionsOverridingNullAgentProperties_UsesCustomOptions()
|
||||
{
|
||||
// Arrange
|
||||
var mockAgent = new Mock<AIAgent>();
|
||||
mockAgent.Setup(a => a.Name).Returns((string?)null);
|
||||
mockAgent.Setup(a => a.Description).Returns((string?)null);
|
||||
|
||||
var customOptions = new AIFunctionFactoryOptions
|
||||
{
|
||||
Name = "OverrideName",
|
||||
Description = "Override description"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = AgentAIFunctionFactory.CreateFromAgent(mockAgent.Object, customOptions);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("OverrideName", result.Name);
|
||||
Assert.Equal("Override description", result.Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateFromAgent_InvokeWithComplexResponseFromAgentAsync_ReturnsCorrectResponseAsync()
|
||||
{
|
||||
// Arrange
|
||||
var expectedResponse = new AgentRunResponse
|
||||
{
|
||||
AgentId = "agent-123",
|
||||
ResponseId = "response-456",
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
Messages = { new ChatMessage(ChatRole.Assistant, "Complex response") }
|
||||
};
|
||||
|
||||
var testAgent = new TestAgent("TestAgent", "Test description", expectedResponse);
|
||||
var aiFunction = AgentAIFunctionFactory.CreateFromAgent(testAgent);
|
||||
|
||||
// Act
|
||||
var arguments = new AIFunctionArguments() { ["query"] = "Test query" };
|
||||
var result = await aiFunction.InvokeAsync(arguments);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("Complex response", result.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test implementation of AIAgent for testing purposes.
|
||||
/// </summary>
|
||||
private sealed class TestAgent : AIAgent
|
||||
{
|
||||
private readonly AgentRunResponse? _responseToReturn;
|
||||
private readonly Exception? _exceptionToThrow;
|
||||
|
||||
public TestAgent(string? name, string? description, AgentRunResponse responseToReturn)
|
||||
{
|
||||
this.Name = name;
|
||||
this.Description = description;
|
||||
this._responseToReturn = responseToReturn;
|
||||
}
|
||||
|
||||
public TestAgent(string? name, string? description, Exception exceptionToThrow)
|
||||
{
|
||||
this.Name = name;
|
||||
this.Description = description;
|
||||
this._exceptionToThrow = exceptionToThrow;
|
||||
}
|
||||
|
||||
public override string? Name { get; }
|
||||
public override string? Description { get; }
|
||||
|
||||
public List<ChatMessage> ReceivedMessages { get; } = new();
|
||||
public CancellationToken LastCancellationToken { get; private set; }
|
||||
public int RunAsyncCallCount { get; private set; }
|
||||
|
||||
public override Task<AgentRunResponse> RunAsync(
|
||||
IReadOnlyCollection<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
this.RunAsyncCallCount++;
|
||||
this.LastCancellationToken = cancellationToken;
|
||||
this.ReceivedMessages.AddRange(messages);
|
||||
|
||||
if (this._exceptionToThrow != null)
|
||||
{
|
||||
throw this._exceptionToThrow;
|
||||
}
|
||||
|
||||
return Task.FromResult(this._responseToReturn!);
|
||||
}
|
||||
|
||||
public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
|
||||
IReadOnlyCollection<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
var response = await this.RunAsync(messages, thread, options, cancellationToken);
|
||||
foreach (var update in response.ToAgentRunResponseUpdates())
|
||||
{
|
||||
yield return update;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user