-
Notifications
You must be signed in to change notification settings - Fork 4k
.Net: Initial check-in for the A2A Agent implementation #12050
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
73d2a06
1f90951
21c1858
b8b3bf6
0196641
aba8ada
d1297dd
384de1d
8469ddc
192ebee
908af44
f2516dd
d2ba0d8
4edfb35
7d6ce75
84acb02
bec3c2d
41a1d31
7940e93
2244207
b2439f5
2ba30e3
3898ebe
9458511
47d7e69
6844e8b
bf32517
bbf19ca
a05b44d
ea68957
ea42d2e
8a4d4ff
513d831
059e6eb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<OutputType>Exe</OutputType> | ||
<TargetFramework>net8.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="SharpA2A.Core" /> | ||
<PackageReference Include="SharpA2A.AspNetCore" /> | ||
<PackageReference Include="System.CommandLine" /> | ||
<PackageReference Include="Microsoft.Extensions.Hosting" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\..\..\..\src\Agents\A2A\Agents.A2A.csproj" /> | ||
<ProjectReference Include="..\..\..\..\src\Agents\Core\Agents.Core.csproj" /> | ||
<ProjectReference Include="..\..\..\..\src\Connectors\Connectors.OpenAI\Connectors.OpenAI.csproj" /> | ||
</ItemGroup> | ||
|
||
</Project> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
// Copyright (c) Microsoft. All rights reserved. | ||
|
||
using Microsoft.Extensions.Logging; | ||
using Microsoft.SemanticKernel; | ||
using Microsoft.SemanticKernel.Agents; | ||
using Microsoft.SemanticKernel.Agents.A2A; | ||
using SharpA2A.Core; | ||
|
||
namespace A2A; | ||
|
||
internal sealed class HostClientAgent | ||
{ | ||
internal HostClientAgent(ILogger logger) | ||
{ | ||
this._logger = logger; | ||
} | ||
internal async Task InitializeAgentAsync(string modelId, string apiKey, string[] agentUrls) | ||
{ | ||
try | ||
{ | ||
this._logger.LogInformation("Initializing Semantic Kernel 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might be nice to also have a basic example where we are interacting directly with a single A2AAgent instance. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see you have a single agent demonstration under samples, so maybe this isn't necessary here then after all. |
||
var agentFunctions = agents.Select(agent => AgentKernelFunctionFactory.CreateFromAgent(agent)).ToList(); | ||
var agentPlugin = KernelPluginFactory.CreateFromFunctions("AgentPlugin", agentFunctions); | ||
|
||
// Define the Host agent | ||
var builder = Kernel.CreateBuilder(); | ||
builder.AddOpenAIChatCompletion(modelId, apiKey); | ||
builder.Plugins.Add(agentPlugin); | ||
var kernel = builder.Build(); | ||
kernel.FunctionInvocationFilters.Add(new ConsoleOutputFunctionInvocationFilter()); | ||
|
||
this.Agent = new ChatCompletionAgent() | ||
{ | ||
Kernel = kernel, | ||
Name = "HostClient", | ||
Instructions = | ||
""" | ||
You specialize in handling queries for users and using your tools to provide answers. | ||
""", | ||
Arguments = new KernelArguments(new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }), | ||
}; | ||
} | ||
catch (Exception ex) | ||
{ | ||
this._logger.LogError(ex, "Failed to initialize HostClientAgent"); | ||
throw; | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// The associated <see cref="Agent"/> | ||
/// </summary> | ||
public Agent? Agent { get; private set; } | ||
|
||
#region private | ||
private readonly ILogger _logger; | ||
|
||
private async Task<A2AAgent> CreateAgentAsync(string agentUri) | ||
{ | ||
var httpClient = new HttpClient | ||
{ | ||
BaseAddress = new Uri(agentUri), | ||
Timeout = TimeSpan.FromSeconds(60) | ||
}; | ||
|
||
var client = new A2AClient(httpClient); | ||
var cardResolver = new A2ACardResolver(httpClient); | ||
var agentCard = await cardResolver.GetAgentCardAsync(); | ||
|
||
return new A2AAgent(client, agentCard!); | ||
} | ||
#endregion | ||
} | ||
|
||
internal sealed class ConsoleOutputFunctionInvocationFilter() : IFunctionInvocationFilter | ||
{ | ||
private static string IndentMultilineString(string multilineText, int indentLevel = 1, int spacesPerIndent = 4) | ||
{ | ||
// Create the indentation string | ||
var indentation = new string(' ', indentLevel * spacesPerIndent); | ||
|
||
// Split the text into lines, add indentation, and rejoin | ||
char[] NewLineChars = { '\r', '\n' }; | ||
string[] lines = multilineText.Split(NewLineChars, StringSplitOptions.None); | ||
|
||
return string.Join(Environment.NewLine, lines.Select(line => indentation + line)); | ||
} | ||
public async Task OnFunctionInvocationAsync(FunctionInvocationContext context, Func<FunctionInvocationContext, Task> next) | ||
{ | ||
Console.ForegroundColor = ConsoleColor.DarkGray; | ||
|
||
Console.WriteLine($"\nCalling Agent {context.Function.Name} with arguments:"); | ||
Console.ForegroundColor = ConsoleColor.Gray; | ||
|
||
foreach (var kvp in context.Arguments) | ||
{ | ||
Console.WriteLine(IndentMultilineString($" {kvp.Key}: {kvp.Value}")); | ||
} | ||
|
||
await next(context); | ||
|
||
if (context.Result.GetValue<object>() is ChatMessageContent[] chatMessages) | ||
{ | ||
Console.ForegroundColor = ConsoleColor.DarkGray; | ||
|
||
Console.WriteLine($"Response from Agent {context.Function.Name}:"); | ||
foreach (var message in chatMessages) | ||
{ | ||
Console.ForegroundColor = ConsoleColor.Gray; | ||
|
||
Console.WriteLine(IndentMultilineString($"{message}")); | ||
} | ||
} | ||
Console.ResetColor(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
// Copyright (c) Microsoft. All rights reserved. | ||
|
||
using System.CommandLine; | ||
using System.CommandLine.Invocation; | ||
using System.Reflection; | ||
using Microsoft.Extensions.Configuration; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.SemanticKernel; | ||
using Microsoft.SemanticKernel.Agents; | ||
|
||
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 System.Threading.Tasks.Task HandleCommandsAsync(InvocationContext context) | ||
{ | ||
await RunCliAsync(); | ||
} | ||
|
||
#region private | ||
private static async System.Threading.Tasks.Task RunCliAsync() | ||
{ | ||
// 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/"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Wondering if comma separator or |
||
|
||
// Create the Host agent | ||
var hostAgent = new HostClientAgent(logger); | ||
await hostAgent.InitializeAgentAsync(modelId, apiKey, agentUrls!.Split(" ")); | ||
AgentThread thread = new ChatHistoryAgentThread(); | ||
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; | ||
} | ||
|
||
await foreach (AgentResponseItem<ChatMessageContent> response in hostAgent.Agent!.InvokeAsync(message, thread)) | ||
{ | ||
Console.ForegroundColor = ConsoleColor.Cyan; | ||
Console.WriteLine($"\nAgent: {response.Message.Content}"); | ||
Console.ResetColor(); | ||
|
||
thread = response.Thread; | ||
} | ||
} | ||
} | ||
catch (Exception ex) | ||
{ | ||
logger.LogError(ex, "An error occurred while running the A2AClient"); | ||
return; | ||
} | ||
} | ||
#endregion | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here we are already in the A2AClient directory |
||
dotnet run | ||
``` | ||
2. Enter your request e.g. "Show me all invoices for Contoso?" | ||
|
||
## Set Secrets with Secret Manager | ||
|
||
The agent urls are provided as a ` ` delimited list of strings | ||
|
||
```text | ||
cd dotnet/samples/Demos/A2AClientServer/A2AClient | ||
dotnet user-secrets set "A2AClient:ModelId" "..." | ||
dotnet user-secrets set "A2AClient":ApiKey" "..." | ||
dotnet user-secrets set "A2AClient:AgentUrls" "http://localhost:5000/policy http://localhost:5000/invoice http://localhost:5000/logistics" | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<OutputType>Exe</OutputType> | ||
<TargetFramework>net8.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="SharpA2A.Core" /> | ||
<PackageReference Include="SharpA2A.AspNetCore" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\..\..\..\src\Agents\A2A\Agents.A2A.csproj" /> | ||
<ProjectReference Include="..\..\..\..\src\Agents\AzureAI\Agents.AzureAI.csproj" /> | ||
<ProjectReference Include="..\..\..\..\src\Agents\Core\Agents.Core.csproj" /> | ||
<ProjectReference Include="..\..\..\..\src\Connectors\Connectors.OpenAI\Connectors.OpenAI.csproj" /> | ||
</ItemGroup> | ||
|
||
</Project> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need the asp.net package in the client? I would have expected it's only required for the server?