Skip to content

Commit bb8f7d1

Browse files
.Net: Initial check-in for the A2A Agent implementation (#12050)
### Motivation and Context <!-- Thank you for your contribution to the semantic-kernel repo! Please help reviewers and future users, providing the following information: 1. Why is this change required? 2. What problem does it solve? 3. What scenario does it contribute to? 4. If it fixes an open issue, please link to the issue here. --> ### Description <!-- Describe your changes, the overall approach, the underlying design. These notes will help understanding how your code works. Thanks! --> ### Contribution Checklist <!-- Before submitting this PR, please make sure: --> - [ ] The code builds clean without any errors or warnings - [ ] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [ ] All unit tests pass, and I have added new tests where possible - [ ] I didn't break anyone 😄
1 parent cb74902 commit bb8f7d1

30 files changed

+1920
-6
lines changed

dotnet/Directory.Packages.props

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@
102102
<PackageVersion Include="System.Text.Json" Version="8.0.5" />
103103
<PackageVersion Include="System.ValueTuple" Version="4.6.1" />
104104
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
105+
<PackageVersion Include="SharpA2A.Core" Version="0.2.1-preview.1" />
106+
<PackageVersion Include="SharpA2A.AspNetCore" Version="0.2.1-preview.1" />
107+
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
105108
<!-- Tokenizers -->
106109
<PackageVersion Include="Microsoft.ML.Tokenizers" Version="1.0.2" />
107110
<PackageVersion Include="Microsoft.DeepDev.TokenizerLib" Version="1.3.3" />

dotnet/SK-dotnet.slnx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@
4949
<Project Path="samples/Demos/TimePlugin/TimePlugin.csproj" />
5050
<Project Path="samples/Demos/VectorStoreRAG/VectorStoreRAG.csproj" />
5151
</Folder>
52+
<Folder Name="/samples/Demos/A2AClientServer/">
53+
<Project Path="samples/Demos/A2AClientServer/A2AClient/A2AClient.csproj" />
54+
<Project Path="samples/Demos/A2AClientServer/A2AServer/A2AServer.csproj" />
55+
</Folder>
5256
<Folder Name="/samples/Demos/AgentFrameworkWithAspire/">
5357
<File Path="samples/Demos/AgentFrameworkWithAspire/README.md" />
5458
<Project Path="samples/Demos/AgentFrameworkWithAspire/ChatWithAgent.ApiService/ChatWithAgent.ApiService.csproj" />
@@ -87,6 +91,7 @@
8791
<Project Path="src/SemanticKernel.UnitTests/SemanticKernel.UnitTests.csproj" />
8892
</Folder>
8993
<Folder Name="/src/agents/">
94+
<Project Path="src/Agents/A2A/Agents.A2A.csproj" />
9095
<Project Path="src/Agents/Abstractions/Agents.Abstractions.csproj" />
9196
<Project Path="src/Agents/AzureAI/Agents.AzureAI.csproj" />
9297
<Project Path="src/Agents/Bedrock/Agents.Bedrock.csproj" />
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net8.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
<UserSecretsId>5ee045b0-aea3-4f08-8d31-32d1a6f8fed0</UserSecretsId>
9+
<NoWarn>$(NoWarn);CS1591;VSTHRD111;CA2007;SKEXP0110</NoWarn>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<PackageReference Include="SharpA2A.Core" />
14+
<PackageReference Include="System.CommandLine" />
15+
<PackageReference Include="Microsoft.Extensions.Hosting" />
16+
</ItemGroup>
17+
18+
<ItemGroup>
19+
<ProjectReference Include="..\..\..\..\src\Agents\A2A\Agents.A2A.csproj" />
20+
<ProjectReference Include="..\..\..\..\src\Agents\Core\Agents.Core.csproj" />
21+
<ProjectReference Include="..\..\..\..\src\Connectors\Connectors.OpenAI\Connectors.OpenAI.csproj" />
22+
</ItemGroup>
23+
24+
</Project>
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using Microsoft.Extensions.Logging;
4+
using Microsoft.SemanticKernel;
5+
using Microsoft.SemanticKernel.Agents;
6+
using Microsoft.SemanticKernel.Agents.A2A;
7+
using SharpA2A.Core;
8+
9+
namespace A2A;
10+
11+
internal sealed class HostClientAgent
12+
{
13+
internal HostClientAgent(ILogger logger)
14+
{
15+
this._logger = logger;
16+
}
17+
internal async Task InitializeAgentAsync(string modelId, string apiKey, string[] agentUrls)
18+
{
19+
try
20+
{
21+
this._logger.LogInformation("Initializing Semantic Kernel agent with model: {ModelId}", modelId);
22+
23+
// Connect to the remote agents via A2A
24+
var createAgentTasks = agentUrls.Select(agentUrl => this.CreateAgentAsync(agentUrl));
25+
var agents = await Task.WhenAll(createAgentTasks);
26+
var agentFunctions = agents.Select(agent => AgentKernelFunctionFactory.CreateFromAgent(agent)).ToList();
27+
var agentPlugin = KernelPluginFactory.CreateFromFunctions("AgentPlugin", agentFunctions);
28+
29+
// Define the Host agent
30+
var builder = Kernel.CreateBuilder();
31+
builder.AddOpenAIChatCompletion(modelId, apiKey);
32+
builder.Plugins.Add(agentPlugin);
33+
var kernel = builder.Build();
34+
kernel.FunctionInvocationFilters.Add(new ConsoleOutputFunctionInvocationFilter());
35+
36+
this.Agent = new ChatCompletionAgent()
37+
{
38+
Kernel = kernel,
39+
Name = "HostClient",
40+
Instructions =
41+
"""
42+
You specialize in handling queries for users and using your tools to provide answers.
43+
""",
44+
Arguments = new KernelArguments(new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }),
45+
};
46+
}
47+
catch (Exception ex)
48+
{
49+
this._logger.LogError(ex, "Failed to initialize HostClientAgent");
50+
throw;
51+
}
52+
}
53+
54+
/// <summary>
55+
/// The associated <see cref="Agent"/>
56+
/// </summary>
57+
public Agent? Agent { get; private set; }
58+
59+
#region private
60+
private readonly ILogger _logger;
61+
62+
private async Task<A2AAgent> CreateAgentAsync(string agentUri)
63+
{
64+
var httpClient = new HttpClient
65+
{
66+
BaseAddress = new Uri(agentUri),
67+
Timeout = TimeSpan.FromSeconds(60)
68+
};
69+
70+
var client = new A2AClient(httpClient);
71+
var cardResolver = new A2ACardResolver(httpClient);
72+
var agentCard = await cardResolver.GetAgentCardAsync();
73+
74+
return new A2AAgent(client, agentCard!);
75+
}
76+
#endregion
77+
}
78+
79+
internal sealed class ConsoleOutputFunctionInvocationFilter() : IFunctionInvocationFilter
80+
{
81+
private static string IndentMultilineString(string multilineText, int indentLevel = 1, int spacesPerIndent = 4)
82+
{
83+
// Create the indentation string
84+
var indentation = new string(' ', indentLevel * spacesPerIndent);
85+
86+
// Split the text into lines, add indentation, and rejoin
87+
char[] NewLineChars = { '\r', '\n' };
88+
string[] lines = multilineText.Split(NewLineChars, StringSplitOptions.None);
89+
90+
return string.Join(Environment.NewLine, lines.Select(line => indentation + line));
91+
}
92+
public async Task OnFunctionInvocationAsync(FunctionInvocationContext context, Func<FunctionInvocationContext, Task> next)
93+
{
94+
Console.ForegroundColor = ConsoleColor.DarkGray;
95+
96+
Console.WriteLine($"\nCalling Agent {context.Function.Name} with arguments:");
97+
Console.ForegroundColor = ConsoleColor.Gray;
98+
99+
foreach (var kvp in context.Arguments)
100+
{
101+
Console.WriteLine(IndentMultilineString($" {kvp.Key}: {kvp.Value}"));
102+
}
103+
104+
await next(context);
105+
106+
if (context.Result.GetValue<object>() is ChatMessageContent[] chatMessages)
107+
{
108+
Console.ForegroundColor = ConsoleColor.DarkGray;
109+
110+
Console.WriteLine($"Response from Agent {context.Function.Name}:");
111+
foreach (var message in chatMessages)
112+
{
113+
Console.ForegroundColor = ConsoleColor.Gray;
114+
115+
Console.WriteLine(IndentMultilineString($"{message}"));
116+
}
117+
}
118+
Console.ResetColor();
119+
}
120+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System.CommandLine;
4+
using System.CommandLine.Invocation;
5+
using System.Reflection;
6+
using Microsoft.Extensions.Configuration;
7+
using Microsoft.Extensions.Logging;
8+
using Microsoft.SemanticKernel;
9+
using Microsoft.SemanticKernel.Agents;
10+
11+
namespace A2A;
12+
13+
public static class Program
14+
{
15+
public static async Task<int> Main(string[] args)
16+
{
17+
// Create root command with options
18+
var rootCommand = new RootCommand("A2AClient");
19+
rootCommand.SetHandler(HandleCommandsAsync);
20+
21+
// Run the command
22+
return await rootCommand.InvokeAsync(args);
23+
}
24+
25+
public static async System.Threading.Tasks.Task HandleCommandsAsync(InvocationContext context)
26+
{
27+
await RunCliAsync();
28+
}
29+
30+
#region private
31+
private static async System.Threading.Tasks.Task RunCliAsync()
32+
{
33+
// Set up the logging
34+
using var loggerFactory = LoggerFactory.Create(builder =>
35+
{
36+
builder.AddConsole();
37+
builder.SetMinimumLevel(LogLevel.Information);
38+
});
39+
var logger = loggerFactory.CreateLogger("A2AClient");
40+
41+
// Retrieve configuration settings
42+
IConfigurationRoot configRoot = new ConfigurationBuilder()
43+
.AddEnvironmentVariables()
44+
.AddUserSecrets(Assembly.GetExecutingAssembly())
45+
.Build();
46+
var apiKey = configRoot["A2AClient:ApiKey"] ?? throw new ArgumentException("A2AClient:ApiKey must be provided");
47+
var modelId = configRoot["A2AClient:ModelId"] ?? "gpt-4.1";
48+
var agentUrls = configRoot["A2AClient:AgentUrls"] ?? "http://localhost:5000/;http://localhost:5001/;http://localhost:5002/";
49+
50+
// Create the Host agent
51+
var hostAgent = new HostClientAgent(logger);
52+
await hostAgent.InitializeAgentAsync(modelId, apiKey, agentUrls!.Split(";"));
53+
AgentThread thread = new ChatHistoryAgentThread();
54+
try
55+
{
56+
while (true)
57+
{
58+
// Get user message
59+
Console.Write("\nUser (:q or quit to exit): ");
60+
string? message = Console.ReadLine();
61+
if (string.IsNullOrWhiteSpace(message))
62+
{
63+
Console.WriteLine("Request cannot be empty.");
64+
continue;
65+
}
66+
67+
if (message == ":q" || message == "quit")
68+
{
69+
break;
70+
}
71+
72+
await foreach (AgentResponseItem<ChatMessageContent> response in hostAgent.Agent!.InvokeAsync(message, thread))
73+
{
74+
Console.ForegroundColor = ConsoleColor.Cyan;
75+
Console.WriteLine($"\nAgent: {response.Message.Content}");
76+
Console.ResetColor();
77+
78+
thread = response.Thread;
79+
}
80+
}
81+
}
82+
catch (Exception ex)
83+
{
84+
logger.LogError(ex, "An error occurred while running the A2AClient");
85+
return;
86+
}
87+
}
88+
#endregion
89+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
2+
# A2A Client Sample
3+
Show how to create an A2A Client with a command line interface which invokes agents using the A2A protocol.
4+
5+
## Run the Sample
6+
7+
To run the sample, follow these steps:
8+
9+
1. Run the A2A client:
10+
```bash
11+
cd A2AClient
12+
dotnet run
13+
```
14+
2. Enter your request e.g. "Show me all invoices for Contoso?"
15+
16+
## Set Secrets with Secret Manager
17+
18+
The agent urls are provided as a ` ` delimited list of strings
19+
20+
```text
21+
cd dotnet/samples/Demos/A2AClientServer/A2AClient
22+
23+
dotnet user-secrets set "A2AClient:ModelId" "..."
24+
dotnet user-secrets set "A2AClient":ApiKey" "..."
25+
dotnet user-secrets set "A2AClient:AgentUrls" "http://localhost:5000/policy;http://localhost:5000/invoice;http://localhost:5000/logistics"
26+
```
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net8.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
<UserSecretsId>5ee045b0-aea3-4f08-8d31-32d1a6f8fed0</UserSecretsId>
9+
<NoWarn>$(NoWarn);CS1591;VSTHRD111;CA2007;SKEXP0110</NoWarn>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<PackageReference Include="SharpA2A.Core" />
14+
<PackageReference Include="SharpA2A.AspNetCore" />
15+
</ItemGroup>
16+
17+
<ItemGroup>
18+
<ProjectReference Include="..\..\..\..\src\Agents\A2A\Agents.A2A.csproj" />
19+
<ProjectReference Include="..\..\..\..\src\Agents\AzureAI\Agents.AzureAI.csproj" />
20+
<ProjectReference Include="..\..\..\..\src\Agents\Core\Agents.Core.csproj" />
21+
<ProjectReference Include="..\..\..\..\src\Connectors\Connectors.OpenAI\Connectors.OpenAI.csproj" />
22+
</ItemGroup>
23+
24+
</Project>

0 commit comments

Comments
 (0)