From 73d2a06e8153975889c6e035a3a0ec7ca4e01797 Mon Sep 17 00:00:00 2001
From: markwallace-microsoft
<127216156+markwallace-microsoft@users.noreply.github.com>
Date: Wed, 14 May 2025 08:05:16 +0100
Subject: [PATCH 01/14] Initial check-in for the A2A Agent implementation
---
dotnet/Directory.Packages.props | 1 +
dotnet/SK-dotnet.sln | 9 ++
.../A2A/Step01_A2AAgent.cs | 41 +++++
.../GettingStartedWithAgents.csproj | 1 +
dotnet/src/Agents/A2A/A2AAgent.cs | 141 ++++++++++++++++++
dotnet/src/Agents/A2A/A2AAgentThread.cs | 55 +++++++
dotnet/src/Agents/A2A/Agents.A2A.csproj | 44 ++++++
.../InternalUtilities/TestConfiguration.cs | 6 +
8 files changed, 298 insertions(+)
create mode 100644 dotnet/samples/GettingStartedWithAgents/A2A/Step01_A2AAgent.cs
create mode 100644 dotnet/src/Agents/A2A/A2AAgent.cs
create mode 100644 dotnet/src/Agents/A2A/A2AAgentThread.cs
create mode 100644 dotnet/src/Agents/A2A/Agents.A2A.csproj
diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index b3d2c48aa69e..5491ac0a6c5b 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -89,6 +89,7 @@
+
diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln
index eefa32d32d28..9d1c85a7a87b 100644
--- a/dotnet/SK-dotnet.sln
+++ b/dotnet/SK-dotnet.sln
@@ -550,6 +550,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Runtime.InProcess.UnitTests
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VectorData.UnitTests", "src\Connectors\VectorData.UnitTests\VectorData.UnitTests.csproj", "{AAC7B5E8-CC4E-49D0-AF6A-2B4F7B43BD84}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Agents.A2A", "src\Agents\A2A\Agents.A2A.csproj", "{38F1D24F-C7B4-58CC-D104-311D786A73CF}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -1511,6 +1513,12 @@ Global
{AAC7B5E8-CC4E-49D0-AF6A-2B4F7B43BD84}.Publish|Any CPU.Build.0 = Debug|Any CPU
{AAC7B5E8-CC4E-49D0-AF6A-2B4F7B43BD84}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AAC7B5E8-CC4E-49D0-AF6A-2B4F7B43BD84}.Release|Any CPU.Build.0 = Release|Any CPU
+ {38F1D24F-C7B4-58CC-D104-311D786A73CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {38F1D24F-C7B4-58CC-D104-311D786A73CF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {38F1D24F-C7B4-58CC-D104-311D786A73CF}.Publish|Any CPU.ActiveCfg = Publish|Any CPU
+ {38F1D24F-C7B4-58CC-D104-311D786A73CF}.Publish|Any CPU.Build.0 = Publish|Any CPU
+ {38F1D24F-C7B4-58CC-D104-311D786A73CF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {38F1D24F-C7B4-58CC-D104-311D786A73CF}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1716,6 +1724,7 @@ Global
{CCC909E4-5269-A31E-0BFD-4863B4B29BBB} = {A70ED5A7-F8E1-4A57-9455-3C05989542DA}
{DA6B4ED4-ED0B-D25C-889C-9F940E714891} = {A70ED5A7-F8E1-4A57-9455-3C05989542DA}
{AAC7B5E8-CC4E-49D0-AF6A-2B4F7B43BD84} = {5A7028A7-4DDF-4E4F-84A9-37CE8F8D7E89}
+ {38F1D24F-C7B4-58CC-D104-311D786A73CF} = {6823CD5E-2ABE-41EB-B865-F86EC13F0CF9}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83}
diff --git a/dotnet/samples/GettingStartedWithAgents/A2A/Step01_A2AAgent.cs b/dotnet/samples/GettingStartedWithAgents/A2A/Step01_A2AAgent.cs
new file mode 100644
index 000000000000..fc58ddab0274
--- /dev/null
+++ b/dotnet/samples/GettingStartedWithAgents/A2A/Step01_A2AAgent.cs
@@ -0,0 +1,41 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents;
+using Microsoft.SemanticKernel.Agents.A2A;
+using SharpA2A.Core;
+
+namespace GettingStarted.A2A;
+
+///
+/// This example demonstrates similarity between using
+/// and other agent types.
+///
+public class Step01_A2AAgent(ITestOutputHelper output) : BaseAzureAgentTest(output)
+{
+ [Fact]
+ public async Task UseA2AAgent()
+ {
+ // Create an A2A agent instance
+ using var httpClient = new HttpClient
+ {
+ BaseAddress = new Uri(TestConfiguration.A2A.Agent)
+ };
+ var client = new A2AClient(httpClient);
+ var cardResolver = new A2ACardResolver(httpClient);
+ var agentCard = await cardResolver.GetAgentCardAsync();
+ Console.WriteLine(JsonSerializer.Serialize(agentCard, s_jsonSerializerOptions));
+ var agent = new A2AAgent(client, agentCard);
+
+ // Invoke the A2A agent
+ await foreach (AgentResponseItem response in agent.InvokeAsync("Hello"))
+ {
+ this.WriteAgentChatMessage(response);
+ }
+ }
+
+ #region private
+ private static JsonSerializerOptions s_jsonSerializerOptions = new() { WriteIndented = true };
+ #endregion
+}
diff --git a/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj b/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj
index 90818906f219..d17737fb3d69 100644
--- a/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj
+++ b/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj
@@ -42,6 +42,7 @@
+
diff --git a/dotnet/src/Agents/A2A/A2AAgent.cs b/dotnet/src/Agents/A2A/A2AAgent.cs
new file mode 100644
index 000000000000..912ac4ea1caf
--- /dev/null
+++ b/dotnet/src/Agents/A2A/A2AAgent.cs
@@ -0,0 +1,141 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.SemanticKernel.ChatCompletion;
+using SharpA2A.Core;
+
+namespace Microsoft.SemanticKernel.Agents.A2A;
+
+///
+/// Provides a specialized based on the A2A Protocol.
+///
+public sealed class A2AAgent : Agent
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// A2AClient instance to associate with the agent.
+ /// AgentCard instance associated ith the agent.
+ public A2AAgent(A2AClient client, AgentCard agentCard)
+ {
+ this.Client = client;
+ this.AgentCard = agentCard;
+ }
+
+ ///
+ /// The associated client.
+ ///
+ public A2AClient Client { get; }
+
+ ///
+ /// The associated agent card.
+ ///
+ public AgentCard AgentCard { get; }
+
+ ///
+ public override async IAsyncEnumerable> InvokeAsync(ICollection messages, AgentThread? thread = null, AgentInvokeOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ Verify.NotNull(messages);
+
+ var agentThread = await this.EnsureThreadExistsWithMessagesAsync(
+ messages,
+ thread,
+ () => new A2AAgentThread(this.Client),
+ cancellationToken).ConfigureAwait(false);
+
+ // Invoke the agent.
+ var invokeResults = this.InternalInvokeAsync(
+ this.AgentCard.Name,
+ messages,
+ agentThread,
+ options ?? new AgentInvokeOptions(),
+ cancellationToken);
+
+ // Notify the thread of new messages and return them to the caller.
+ await foreach (var result in invokeResults.ConfigureAwait(false))
+ {
+ await this.NotifyThreadOfNewMessage(agentThread, result, cancellationToken).ConfigureAwait(false);
+ yield return new(result, agentThread);
+ }
+ }
+
+ ///
+ public override IAsyncEnumerable> InvokeStreamingAsync(ICollection messages, AgentThread? thread = null, AgentInvokeOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ throw new NotImplementedException();
+ }
+
+ ///
+ protected override Task CreateChannelAsync(CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+ }
+
+ ///
+ protected override IEnumerable GetChannelKeys()
+ {
+ throw new NotImplementedException();
+ }
+
+ ///
+ protected override Task RestoreChannelAsync(string channelState, CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+ }
+
+ #region private
+ private async IAsyncEnumerable> InternalInvokeAsync(string name, ICollection messages, A2AAgentThread thread, AgentInvokeOptions options, [EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ Verify.NotNull(messages);
+
+ foreach (var message in messages)
+ {
+ await foreach (var result in this.InvokeAgentAsync(name, message, thread, options, cancellationToken).ConfigureAwait(false))
+ {
+ await this.NotifyThreadOfNewMessage(thread, result, cancellationToken).ConfigureAwait(false);
+ yield return new(result, thread);
+ }
+ }
+ }
+
+ private async IAsyncEnumerable> InvokeAgentAsync(string name, ChatMessageContent message, A2AAgentThread thread, AgentInvokeOptions options, [EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ var taskSendParams = new TaskSendParams
+ {
+ Id = thread.Id!,
+ SessionId = thread.SessionId,
+ Message = new Message
+ {
+ Role = message.Role.ToString(),
+ Parts =
+ [
+ new TextPart
+ {
+ Text = message.Content! // TODO handle multiple items
+ }
+ ]
+ }
+ };
+
+ var agentTask = await this.Client.Send(taskSendParams).ConfigureAwait(false);
+ if (agentTask.Artifacts != null && agentTask.Artifacts.Count > 0)
+ {
+ foreach (var artifact in agentTask.Artifacts)
+ {
+ foreach (var part in artifact.Parts)
+ {
+ if (part is TextPart textPart)
+ {
+ yield return new AgentResponseItem(new ChatMessageContent(AuthorRole.Assistant, textPart.Text), thread);
+ }
+ }
+ }
+ Console.WriteLine();
+ }
+ }
+ #endregion
+}
diff --git a/dotnet/src/Agents/A2A/A2AAgentThread.cs b/dotnet/src/Agents/A2A/A2AAgentThread.cs
new file mode 100644
index 000000000000..26847054a31a
--- /dev/null
+++ b/dotnet/src/Agents/A2A/A2AAgentThread.cs
@@ -0,0 +1,55 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using SharpA2A.Core;
+
+namespace Microsoft.SemanticKernel.Agents.A2A;
+
+///
+/// Represents a conversation thread for an A2A agent.
+///
+public sealed class A2AAgentThread : AgentThread
+{
+ ///
+ /// Initializes a new instance of the class that resumes an existing thread.
+ ///
+ /// The agents client to use for interacting with threads.
+ /// The ID of an existing thread to resume.
+ public A2AAgentThread(A2AClient client, string? id = null)
+ {
+ Verify.NotNull(client);
+
+ this._client = client;
+ this.Id = id ?? Guid.NewGuid().ToString("N");
+ this.SessionId = this.Id;
+ }
+
+ ///
+ /// Gets the session id of the current thread.
+ ///
+ public string SessionId { get; init; }
+
+ ///
+ protected override Task CreateInternalAsync(CancellationToken cancellationToken)
+ {
+ return Task.FromResult(Guid.NewGuid().ToString("N"));
+ }
+
+ ///
+ protected override Task DeleteInternalAsync(CancellationToken cancellationToken)
+ {
+ return Task.CompletedTask;
+ }
+
+ ///
+ protected override Task OnNewMessageInternalAsync(ChatMessageContent newMessage, CancellationToken cancellationToken = default)
+ {
+ return Task.CompletedTask;
+ }
+
+ #region private
+ private readonly A2AClient _client;
+ #endregion
+}
diff --git a/dotnet/src/Agents/A2A/Agents.A2A.csproj b/dotnet/src/Agents/A2A/Agents.A2A.csproj
new file mode 100644
index 000000000000..9defa8d88691
--- /dev/null
+++ b/dotnet/src/Agents/A2A/Agents.A2A.csproj
@@ -0,0 +1,44 @@
+
+
+
+
+ Microsoft.SemanticKernel.Agents.A2A
+ Microsoft.SemanticKernel.Agents.A2A
+ net8.0;netstandard2.0
+ $(NoWarn);SKEXP0110
+ false
+ alpha
+
+
+
+
+
+
+ Semantic Kernel Agents - A2A
+ Defines a concrete Agent based on the A2A Protocol.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dotnet/src/InternalUtilities/samples/InternalUtilities/TestConfiguration.cs b/dotnet/src/InternalUtilities/samples/InternalUtilities/TestConfiguration.cs
index e140e9b17a10..ae9dd3835696 100644
--- a/dotnet/src/InternalUtilities/samples/InternalUtilities/TestConfiguration.cs
+++ b/dotnet/src/InternalUtilities/samples/InternalUtilities/TestConfiguration.cs
@@ -53,6 +53,7 @@ public static void Initialize(IConfigurationRoot configRoot)
public static ApplicationInsightsConfig ApplicationInsights => LoadSection();
public static CrewAIConfig CrewAI => LoadSection();
public static BedrockAgentConfig BedrockAgent => LoadSection();
+ public static A2AConfig A2A => LoadSection();
public static IConfiguration GetSection(string caller)
{
@@ -350,4 +351,9 @@ public class BedrockAgentConfig
public string FoundationModel { get; set; }
public string? KnowledgeBaseId { get; set; }
}
+
+ public class A2AConfig
+ {
+ public string Agent { get; set; } = "http://localhost:5000";
+ }
}
From 21c185835be6fc213cffe70980f031debfd921d3 Mon Sep 17 00:00:00 2001
From: markwallace-microsoft
<127216156+markwallace-microsoft@users.noreply.github.com>
Date: Wed, 14 May 2025 08:56:16 +0100
Subject: [PATCH 02/14] Fix warning
---
dotnet/samples/GettingStartedWithAgents/A2A/Step01_A2AAgent.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/dotnet/samples/GettingStartedWithAgents/A2A/Step01_A2AAgent.cs b/dotnet/samples/GettingStartedWithAgents/A2A/Step01_A2AAgent.cs
index fc58ddab0274..1b66f6cddd7b 100644
--- a/dotnet/samples/GettingStartedWithAgents/A2A/Step01_A2AAgent.cs
+++ b/dotnet/samples/GettingStartedWithAgents/A2A/Step01_A2AAgent.cs
@@ -36,6 +36,6 @@ public async Task UseA2AAgent()
}
#region private
- private static JsonSerializerOptions s_jsonSerializerOptions = new() { WriteIndented = true };
+ private static readonly JsonSerializerOptions s_jsonSerializerOptions = new() { WriteIndented = true };
#endregion
}
From b8b3bf6a12bee9653c0657512046a66a05fe1bbe Mon Sep 17 00:00:00 2001
From: markwallace-microsoft
<127216156+markwallace-microsoft@users.noreply.github.com>
Date: Wed, 14 May 2025 17:05:44 +0100
Subject: [PATCH 03/14] Add A2A Client and Server samples
---
dotnet/Directory.Packages.props | 2 +
dotnet/SK-dotnet.sln | 21 ++
.../A2AClient/A2AClient.csproj | 25 ++
.../A2AClient/HostClientAgent.cs | 76 ++++++
.../A2AClientServer/A2AClient/Program.cs | 106 ++++++++
.../A2AServer/A2AServer.csproj | 23 ++
.../A2AClientServer/A2AServer/A2AServer.http | 51 ++++
.../A2AServer/CurrencyAgent.cs | 179 +++++++++++++
.../A2AClientServer/A2AServer/InvoiceAgent.cs | 238 ++++++++++++++++++
.../A2AServer/LogisticsAgent.cs | 7 +
.../A2AClientServer/A2AServer/PolicyAgent.cs | 142 +++++++++++
.../A2AClientServer/A2AServer/Program.cs | 30 +++
.../samples/Demos/A2AClientServer/README.md | 0
dotnet/src/Agents/A2A/A2AAgent.cs | 2 +
dotnet/src/Agents/A2A/A2AHostAgent.cs | 89 +++++++
15 files changed, 991 insertions(+)
create mode 100644 dotnet/samples/Demos/A2AClientServer/A2AClient/A2AClient.csproj
create mode 100644 dotnet/samples/Demos/A2AClientServer/A2AClient/HostClientAgent.cs
create mode 100644 dotnet/samples/Demos/A2AClientServer/A2AClient/Program.cs
create mode 100644 dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.csproj
create mode 100644 dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.http
create mode 100644 dotnet/samples/Demos/A2AClientServer/A2AServer/CurrencyAgent.cs
create mode 100644 dotnet/samples/Demos/A2AClientServer/A2AServer/InvoiceAgent.cs
create mode 100644 dotnet/samples/Demos/A2AClientServer/A2AServer/LogisticsAgent.cs
create mode 100644 dotnet/samples/Demos/A2AClientServer/A2AServer/PolicyAgent.cs
create mode 100644 dotnet/samples/Demos/A2AClientServer/A2AServer/Program.cs
create mode 100644 dotnet/samples/Demos/A2AClientServer/README.md
create mode 100644 dotnet/src/Agents/A2A/A2AHostAgent.cs
diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index 5491ac0a6c5b..fd5499829f8f 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -90,6 +90,8 @@
+
+
diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln
index 9d1c85a7a87b..bcae0eddf1d5 100644
--- a/dotnet/SK-dotnet.sln
+++ b/dotnet/SK-dotnet.sln
@@ -552,6 +552,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VectorData.UnitTests", "src
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Agents.A2A", "src\Agents\A2A\Agents.A2A.csproj", "{38F1D24F-C7B4-58CC-D104-311D786A73CF}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "A2AClientServer", "A2AClientServer", "{5B1ECD1B-3C38-4458-A227-89846AF13760}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "A2AClient", "samples\Demos\A2AClientServer\A2AClient\A2AClient.csproj", "{F293D014-97E2-18CB-FA0F-0A0FBE149286}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "A2AServer", "samples\Demos\A2AClientServer\A2AServer\A2AServer.csproj", "{D5324629-DFED-4095-EA74-A0234AC9EB4E}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -1519,6 +1525,18 @@ Global
{38F1D24F-C7B4-58CC-D104-311D786A73CF}.Publish|Any CPU.Build.0 = Publish|Any CPU
{38F1D24F-C7B4-58CC-D104-311D786A73CF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{38F1D24F-C7B4-58CC-D104-311D786A73CF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F293D014-97E2-18CB-FA0F-0A0FBE149286}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F293D014-97E2-18CB-FA0F-0A0FBE149286}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F293D014-97E2-18CB-FA0F-0A0FBE149286}.Publish|Any CPU.ActiveCfg = Release|Any CPU
+ {F293D014-97E2-18CB-FA0F-0A0FBE149286}.Publish|Any CPU.Build.0 = Release|Any CPU
+ {F293D014-97E2-18CB-FA0F-0A0FBE149286}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F293D014-97E2-18CB-FA0F-0A0FBE149286}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D5324629-DFED-4095-EA74-A0234AC9EB4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D5324629-DFED-4095-EA74-A0234AC9EB4E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D5324629-DFED-4095-EA74-A0234AC9EB4E}.Publish|Any CPU.ActiveCfg = Release|Any CPU
+ {D5324629-DFED-4095-EA74-A0234AC9EB4E}.Publish|Any CPU.Build.0 = Release|Any CPU
+ {D5324629-DFED-4095-EA74-A0234AC9EB4E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D5324629-DFED-4095-EA74-A0234AC9EB4E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1725,6 +1743,9 @@ Global
{DA6B4ED4-ED0B-D25C-889C-9F940E714891} = {A70ED5A7-F8E1-4A57-9455-3C05989542DA}
{AAC7B5E8-CC4E-49D0-AF6A-2B4F7B43BD84} = {5A7028A7-4DDF-4E4F-84A9-37CE8F8D7E89}
{38F1D24F-C7B4-58CC-D104-311D786A73CF} = {6823CD5E-2ABE-41EB-B865-F86EC13F0CF9}
+ {5B1ECD1B-3C38-4458-A227-89846AF13760} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263}
+ {F293D014-97E2-18CB-FA0F-0A0FBE149286} = {5B1ECD1B-3C38-4458-A227-89846AF13760}
+ {D5324629-DFED-4095-EA74-A0234AC9EB4E} = {5B1ECD1B-3C38-4458-A227-89846AF13760}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83}
diff --git a/dotnet/samples/Demos/A2AClientServer/A2AClient/A2AClient.csproj b/dotnet/samples/Demos/A2AClientServer/A2AClient/A2AClient.csproj
new file mode 100644
index 000000000000..57b7db353a53
--- /dev/null
+++ b/dotnet/samples/Demos/A2AClientServer/A2AClient/A2AClient.csproj
@@ -0,0 +1,25 @@
+
+
+
+ Exe
+ net9.0
+ enable
+ enable
+ 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0
+ $(NoWarn);CS1591;VSTHRD111;CA2007;SKEXP0110
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/Demos/A2AClientServer/A2AClient/HostClientAgent.cs b/dotnet/samples/Demos/A2AClientServer/A2AClient/HostClientAgent.cs
new file mode 100644
index 000000000000..fd9fde019606
--- /dev/null
+++ b/dotnet/samples/Demos/A2AClientServer/A2AClient/HostClientAgent.cs
@@ -0,0 +1,76 @@
+// 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 class HostClientAgent
+{
+ internal HostClientAgent(ILogger logger)
+ {
+ this._logger = logger;
+ }
+ internal async Task InitializeAgentAsync(string modelId, string apiKey, string baseAddress)
+ {
+ try
+ {
+ this._logger.LogInformation("Initializing Semantic Kernel agent with model: {ModelId}", modelId);
+
+ // Connect to the remote agents via A2A
+ var agentPlugin = KernelPluginFactory.CreateFromFunctions("AgentPlugin",
+ [
+ AgentKernelFunctionFactory.CreateFromAgent(await this.CreateAgentAsync($"{baseAddress}/currency/")),
+ AgentKernelFunctionFactory.CreateFromAgent(await this.CreateAgentAsync($"{baseAddress}/invoice/"))
+ ]);
+
+ // Define the TravelPlannerAgent
+ var builder = Kernel.CreateBuilder();
+ builder.AddOpenAIChatCompletion(modelId, apiKey);
+ builder.Plugins.Add(agentPlugin);
+ var kernel = builder.Build();
+
+ 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;
+ }
+ }
+
+ ///
+ /// The associated
+ ///
+ public Agent? Agent { get; private set; }
+
+ #region private
+ private readonly ILogger _logger;
+
+ private async Task CreateAgentAsync(string agentUri)
+ {
+ var httpClient = new HttpClient
+ {
+ BaseAddress = new Uri(agentUri)
+ };
+
+ var client = new A2AClient(httpClient);
+ var cardResolver = new A2ACardResolver(httpClient);
+ var agentCard = await cardResolver.GetAgentCardAsync();
+
+ return new A2AAgent(client, agentCard);
+ }
+ #endregion
+}
diff --git a/dotnet/samples/Demos/A2AClientServer/A2AClient/Program.cs b/dotnet/samples/Demos/A2AClientServer/A2AClient/Program.cs
new file mode 100644
index 000000000000..a76a90663acc
--- /dev/null
+++ b/dotnet/samples/Demos/A2AClientServer/A2AClient/Program.cs
@@ -0,0 +1,106 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.CommandLine;
+using System.CommandLine.Invocation;
+using System.Reflection;
+using System.Text.Encodings.Web;
+using System.Text.Json;
+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 Main(string[] args)
+ {
+ // Create root command with options
+ var rootCommand = new RootCommand("A2AClient")
+ {
+ s_agentOption,
+ };
+
+ // Replace the problematic line with the following:
+ rootCommand.SetHandler(RunCliAsync);
+
+ // Run the command
+ return await rootCommand.InvokeAsync(args);
+ }
+
+ public static async System.Threading.Tasks.Task RunCliAsync(InvocationContext context)
+ {
+ string agent = context.ParseResult.GetValueForOption(s_agentOption)!;
+
+ await RunCliAsync(agent);
+ }
+
+ #region private
+ private static readonly Option s_agentOption = new(
+ "--agent",
+ getDefaultValue: () => "http://localhost:10000",
+ description: "Agent URL");
+
+ private static readonly JsonSerializerOptions s_jsonOptions = new JsonSerializerOptions
+ {
+ Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
+ };
+
+ private static async System.Threading.Tasks.Task RunCliAsync(string agentUrl)
+ {
+ // 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();
+ string apiKey = configRoot["OPENAI_API_KEY"] ?? throw new ArgumentException("OPENAI_API_KEY must be provided");
+ string modelId = configRoot["OPENAI_CHAT_MODEL_ID"] ?? "gpt-4.1";
+ string baseAddress = configRoot["AGENT_URL"] ?? "http://localhost:5000";
+
+ // Create the Host agent
+ var hostAgent = new HostClientAgent(logger);
+ await hostAgent.InitializeAgentAsync(modelId, apiKey, baseAddress);
+
+ 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;
+ }
+
+ Console.ForegroundColor = ConsoleColor.Cyan;
+ await foreach (AgentResponseItem response in hostAgent.Agent!.InvokeAsync(message))
+ {
+ Console.WriteLine($"Agent: {response.Message.Content}");
+ }
+ Console.ResetColor();
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "An error occurred while running the A2AClient");
+ return;
+ }
+ }
+ #endregion
+}
diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.csproj b/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.csproj
new file mode 100644
index 000000000000..26742cee16bc
--- /dev/null
+++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.csproj
@@ -0,0 +1,23 @@
+
+
+
+ Exe
+ net9.0
+ enable
+ enable
+ 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0
+ $(NoWarn);CS1591;VSTHRD111;CA2007
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.http b/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.http
new file mode 100644
index 000000000000..ddc720633dbf
--- /dev/null
+++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.http
@@ -0,0 +1,51 @@
+@host = http://localhost:5000
+
+### Query agent card for the invoice agent
+GET {{host}}/invoice/.well-known/agent.json
+
+### Send a task to the invoice agent
+POST {{host}}/invoice
+Content-Type: application/json
+
+{
+ "id": "1",
+ "jsonrpc": "2.0",
+ "method": "task/send",
+ "params": {
+ "id": "12345",
+ "message": {
+ "role": "user",
+ "parts": [
+ {
+ "type": "text",
+ "text": "Show me all invoices for Contoso?"
+ }
+ ]
+ }
+ }
+}
+
+### Query agent card for the currency agent
+GET {{host}}/currency/.well-known/agent.json
+
+### Send a task to the currency agent
+POST {{host}}/currency
+Content-Type: application/json
+
+{
+ "id": "1",
+ "jsonrpc": "2.0",
+ "method": "task/send",
+ "params": {
+ "id": "12345",
+ "message": {
+ "role": "user",
+ "parts": [
+ {
+ "type": "text",
+ "text": "What is the current exchange rather for Dollars to Euro?"
+ }
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/CurrencyAgent.cs b/dotnet/samples/Demos/A2AClientServer/A2AServer/CurrencyAgent.cs
new file mode 100644
index 000000000000..c55fcdc340cf
--- /dev/null
+++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/CurrencyAgent.cs
@@ -0,0 +1,179 @@
+// Copyright (c) Microsoft. All rights reserved.
+using System.ComponentModel;
+using System.Text.Json;
+using Microsoft.Extensions.Logging;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents;
+using Microsoft.SemanticKernel.Agents.A2A;
+using Polly;
+using SharpA2A.Core;
+
+namespace A2A;
+
+internal class CurrencyAgent : A2AHostAgent
+{
+ internal CurrencyAgent(string modelId, string apiKey, ILogger logger) : base(logger)
+ {
+ this._logger = logger;
+ this._httpClient = new HttpClient();
+
+ this._currencyPlugin = new CurrencyPlugin(
+ logger: new Logger(new LoggerFactory()),
+ httpClient: this._httpClient);
+
+ this.Agent = this.InitializeAgent(modelId, apiKey);
+ }
+
+ public override AgentCard GetAgentCard(string agentUrl)
+ {
+ var capabilities = new AgentCapabilities()
+ {
+ Streaming = false,
+ PushNotifications = false,
+ };
+
+ var invoiceQuery = new AgentSkill()
+ {
+ Id = "id_currency_agent",
+ Name = "CurrencyAgent",
+ Description = "Handles requests relating to currency exchange.",
+ Tags = ["currency", "semantic-kernel"],
+ Examples =
+ [
+ "What is the current exchange rather for Dollars to Euro?",
+ ],
+ };
+
+ return new AgentCard()
+ {
+ Name = "CurrencyAgent",
+ Description = "Handles requests relating to currency exchange.",
+ Url = agentUrl,
+ Version = "1.0.0",
+ DefaultInputModes = ["text"],
+ DefaultOutputModes = ["text"],
+ Capabilities = capabilities,
+ Skills = [invoiceQuery],
+ };
+ }
+
+ #region private
+ private readonly CurrencyPlugin _currencyPlugin;
+ private readonly HttpClient _httpClient;
+ private readonly ILogger _logger;
+
+ private ChatCompletionAgent InitializeAgent(string modelId, string apiKey)
+ {
+ try
+ {
+ this._logger.LogInformation("Initializing Semantic Kernel agent with model {ModelId}", modelId);
+
+ // Define the TravelPlannerAgent
+ var builder = Kernel.CreateBuilder();
+ builder.AddOpenAIChatCompletion(modelId, apiKey);
+ builder.Plugins.AddFromObject(this._currencyPlugin);
+ var kernel = builder.Build();
+ return new ChatCompletionAgent()
+ {
+ Kernel = kernel,
+ Name = "CurrencyAgent",
+ Instructions =
+ """
+ You specialize in handling queries related to currency exchange rates.
+ """,
+ Arguments = new KernelArguments(new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }),
+ };
+ }
+ catch (Exception ex)
+ {
+ this._logger.LogError(ex, "Failed to initialize InvoiceAgent");
+ throw;
+ }
+ }
+ #endregion
+}
+
+///
+/// A simple currency plugin that leverages Frankfurter for exchange rates.
+/// The Plugin is used by the currency_exchange_agent.
+///
+public class CurrencyPlugin
+{
+ private readonly ILogger _logger;
+ private readonly HttpClient _httpClient;
+ private readonly IAsyncPolicy _retryPolicy;
+
+ ///
+ /// Initialize a new instance of the CurrencyPlugin
+ ///
+ /// Logger for the plugin
+ /// HTTP client factory for making API requests
+ public CurrencyPlugin(ILogger logger, HttpClient httpClient)
+ {
+ this._logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ this._httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
+
+ // Create a retry policy for transient HTTP errors
+ this._retryPolicy = Policy
+ .HandleResult(r => !r.IsSuccessStatusCode && this.IsTransientError(r))
+ .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
+ }
+
+ ///
+ /// Retrieves exchange rate between currency_from and currency_to using Frankfurter API
+ ///
+ /// Currency code to convert from, e.g. USD
+ /// Currency code to convert to, e.g. EUR or INR
+ /// Date or 'latest'
+ /// String representation of exchange rate
+ [KernelFunction]
+ [Description("Retrieves exchange rate between currency_from and currency_to using Frankfurter API")]
+ public async Task GetExchangeRateAsync(
+ [Description("Currency code to convert from, e.g. USD")] string currencyFrom,
+ [Description("Currency code to convert to, e.g. EUR or INR")] string currencyTo,
+ [Description("Date or 'latest'")] string date = "latest")
+ {
+ try
+ {
+ this._logger.LogInformation("Getting exchange rate from {CurrencyFrom} to {CurrencyTo} for date {Date}",
+ currencyFrom, currencyTo, date);
+
+ // Build request URL with query parameters
+ var requestUri = $"https://api.frankfurter.app/{date}?from={Uri.EscapeDataString(currencyFrom)}&to={Uri.EscapeDataString(currencyTo)}";
+
+ // Use retry policy for resilience
+ var response = await this._retryPolicy.ExecuteAsync(() => _httpClient.GetAsync(requestUri)).ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
+
+ var jsonContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
+ var data = JsonSerializer.Deserialize(jsonContent);
+
+ if (!data.TryGetProperty("rates", out var rates) ||
+ !rates.TryGetProperty(currencyTo, out var rate))
+ {
+ this._logger.LogWarning("Could not retrieve rate for {CurrencyFrom} to {CurrencyTo}", currencyFrom, currencyTo);
+ return $"Could not retrieve rate for {currencyFrom} to {currencyTo}";
+ }
+
+ return $"1 {currencyFrom} = {rate.GetDecimal()} {currencyTo}";
+ }
+ catch (Exception ex)
+ {
+ this._logger.LogError(ex, "Error getting exchange rate from {CurrencyFrom} to {CurrencyTo}", currencyFrom, currencyTo);
+ return $"Currency API call failed: {ex.Message}";
+ }
+ }
+
+ ///
+ /// Checks if the HTTP response indicates a transient error
+ ///
+ /// HTTP response message
+ /// True if the status code indicates a transient error
+ private bool IsTransientError(HttpResponseMessage response)
+ {
+ int statusCode = (int)response.StatusCode;
+ return statusCode == 408 // Request Timeout
+ || statusCode == 429 // Too Many Requests
+ || statusCode >= 500 && statusCode < 600; // Server errors
+ }
+}
diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/InvoiceAgent.cs b/dotnet/samples/Demos/A2AClientServer/A2AServer/InvoiceAgent.cs
new file mode 100644
index 000000000000..7789339e523d
--- /dev/null
+++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/InvoiceAgent.cs
@@ -0,0 +1,238 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// Copyright (c) Microsoft. All rights reserved.
+using System.ComponentModel;
+using Microsoft.Extensions.Logging;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents;
+using Microsoft.SemanticKernel.Agents.A2A;
+using SharpA2A.Core;
+
+namespace A2A;
+
+internal class InvoiceAgent : A2AHostAgent
+{
+ internal InvoiceAgent(string modelId, string apiKey, ILogger logger) : base(logger)
+ {
+ this._logger = logger;
+ this.Agent = this.InitializeAgent(modelId, apiKey);
+ }
+
+ public override AgentCard GetAgentCard(string agentUrl)
+ {
+ var capabilities = new AgentCapabilities()
+ {
+ Streaming = false,
+ PushNotifications = false,
+ };
+
+ var invoiceQuery = new AgentSkill()
+ {
+ Id = "id_invoice_agent",
+ Name = "InvoiceAgent",
+ Description = "Handles requests relating to invoices.",
+ Tags = ["invoice", "semantic-kernel"],
+ Examples =
+ [
+ "List the latest invoices for Contoso.",
+ ],
+ };
+
+ return new AgentCard()
+ {
+ Name = "InvoiceAgent",
+ Description = "Handles requests relating to invoices.",
+ Url = agentUrl,
+ Version = "1.0.0",
+ DefaultInputModes = ["text"],
+ DefaultOutputModes = ["text"],
+ Capabilities = capabilities,
+ Skills = [invoiceQuery],
+ };
+ }
+
+ #region private
+ private readonly ILogger _logger;
+
+ private ChatCompletionAgent InitializeAgent(string modelId, string apiKey)
+ {
+ try
+ {
+ this._logger.LogInformation("Initializing Semantic Kernel agent with model {ModelId}", modelId);
+
+ // Define the TravelPlannerAgent
+ var builder = Kernel.CreateBuilder();
+ builder.AddOpenAIChatCompletion(modelId, apiKey);
+ builder.Plugins.AddFromType();
+ var kernel = builder.Build();
+ return new ChatCompletionAgent()
+ {
+ Kernel = kernel,
+ Name = "InvoiceAgent",
+ Instructions =
+ """
+ You specialize in handling queries related to invoices.
+ """,
+ Arguments = new KernelArguments(new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }),
+ };
+ }
+ catch (Exception ex)
+ {
+ this._logger.LogError(ex, "Failed to initialize InvoiceAgent");
+ throw;
+ }
+ }
+ #endregion
+}
+
+///
+/// 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()
+ {
+ return this.Quantity * this.Price; // Total price for this product
+ }
+}
+
+public class Invoice
+{
+ public int InvoiceId { get; set; }
+ public string CompanyName { get; set; }
+ public DateTime InvoiceDate { get; set; }
+ public List Products { get; set; } // List of products
+
+ public Invoice(int invoiceId, string companyName, DateTime invoiceDate, List products)
+ {
+ 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 InvoiceQueryPlugin
+{
+ private readonly List _invoices;
+ private static readonly Random s_random = new();
+
+ public InvoiceQueryPlugin()
+ {
+ // Extended mock data with quantities and prices
+ this._invoices =
+ [
+ new(1, "Contoso", GetRandomDateWithinLastTwoMonths(), new List
+ {
+ new("T-Shirts", 150, 10.00m),
+ new("Hats", 200, 15.00m),
+ new("Glasses", 300, 5.00m)
+ }),
+ new(2, "XStore", GetRandomDateWithinLastTwoMonths(), new List
+ {
+ new("T-Shirts", 2500, 12.00m),
+ new("Hats", 1500, 8.00m),
+ new("Glasses", 200, 20.00m)
+ }),
+ new(3, "Cymbal Direct", GetRandomDateWithinLastTwoMonths(), new List
+ {
+ new("T-Shirts", 1200, 14.00m),
+ new("Hats", 800, 7.00m),
+ new("Glasses", 500, 25.00m)
+ }),
+ new(4, "Contoso", GetRandomDateWithinLastTwoMonths(), new List
+ {
+ new("T-Shirts", 400, 11.00m),
+ new("Hats", 600, 15.00m),
+ new("Glasses", 700, 5.00m)
+ }),
+ new(5, "XStore", GetRandomDateWithinLastTwoMonths(), new List
+ {
+ new("T-Shirts", 800, 10.00m),
+ new("Hats", 500, 18.00m),
+ new("Glasses", 300, 22.00m)
+ }),
+ new(6, "Cymbal Direct", GetRandomDateWithinLastTwoMonths(), new List
+ {
+ new("T-Shirts", 1100, 9.00m),
+ new("Hats", 900, 12.00m),
+ new("Glasses", 1200, 15.00m)
+ }),
+ new(7, "Contoso", GetRandomDateWithinLastTwoMonths(), new List
+ {
+ new("T-Shirts", 2500, 8.00m),
+ new("Hats", 1200, 10.00m),
+ new("Glasses", 1000, 6.00m)
+ }),
+ new(8, "XStore", GetRandomDateWithinLastTwoMonths(), new List
+ {
+ new("T-Shirts", 1900, 13.00m),
+ new("Hats", 1300, 16.00m),
+ new("Glasses", 800, 19.00m)
+ }),
+ new(9, "Cymbal Direct", GetRandomDateWithinLastTwoMonths(), new List
+ {
+ new("T-Shirts", 2200, 11.00m),
+ new("Hats", 1700, 8.50m),
+ new("Glasses", 600, 21.00m)
+ }),
+ new(10, "Contoso", GetRandomDateWithinLastTwoMonths(), new List
+ {
+ 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;
+ int randomDays = s_random.Next(0, totalDays + 1); // +1 to include the end date
+
+ // Return the random date
+ return startDate.AddDays(randomDays);
+ }
+
+ [KernelFunction]
+ [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();
+ }
+}
diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/LogisticsAgent.cs b/dotnet/samples/Demos/A2AClientServer/A2AServer/LogisticsAgent.cs
new file mode 100644
index 000000000000..4236f17bc977
--- /dev/null
+++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/LogisticsAgent.cs
@@ -0,0 +1,7 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace A2A;
+
+internal class LogisticsAgent
+{
+}
diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/PolicyAgent.cs b/dotnet/samples/Demos/A2AClientServer/A2AServer/PolicyAgent.cs
new file mode 100644
index 000000000000..81bf4f49f583
--- /dev/null
+++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/PolicyAgent.cs
@@ -0,0 +1,142 @@
+// 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 class PolicyAgent : A2AHostAgent
+{
+ internal PolicyAgent(string modelId, string apiKey, ILogger logger) : base(logger)
+ {
+ this._logger = logger;
+ this._httpClient = new HttpClient();
+
+ // Add TextSearch over the shipping policies
+
+ this.Agent = this.InitializeAgent(modelId, apiKey);
+ }
+
+ public override AgentCard GetAgentCard(string agentUrl)
+ {
+ 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.",
+ Url = agentUrl,
+ Version = "1.0.0",
+ DefaultInputModes = ["text"],
+ DefaultOutputModes = ["text"],
+ Capabilities = capabilities,
+ Skills = [invoiceQuery],
+ };
+ }
+
+ #region private
+ private readonly HttpClient _httpClient;
+ private readonly ILogger _logger;
+
+ private ChatCompletionAgent InitializeAgent(string modelId, string apiKey)
+ {
+ try
+ {
+ this._logger.LogInformation("Initializing Semantic Kernel agent with model {ModelId}", modelId);
+
+ // Define the TravelPlannerAgent
+ var builder = Kernel.CreateBuilder();
+ builder.AddOpenAIChatCompletion(modelId, apiKey);
+ //builder.Plugins.AddFromObject(this._policyPlugin);
+ var kernel = builder.Build();
+ return new ChatCompletionAgent()
+ {
+ Kernel = kernel,
+ Name = "PolicyAgent",
+ Instructions =
+ """
+ You specialize in handling queries related to policies and customer communications.
+ """,
+ Arguments = new KernelArguments(new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }),
+ };
+ }
+ catch (Exception ex)
+ {
+ this._logger.LogError(ex, "Failed to initialize InvoiceAgent");
+ throw;
+ }
+ }
+ #endregion
+}
+
+public class ShippingPolicy
+{
+ public string PolicyName { get; set; }
+ public string Description { get; set; }
+
+ public ShippingPolicy(string policyName, string description)
+ {
+ this.PolicyName = policyName;
+ this.Description = description;
+ }
+
+ public override string ToString()
+ {
+ return $"{this.PolicyName}: {this.Description}";
+ }
+}
+
+public class ShippingPolicies
+{
+ private readonly List _policies;
+
+ public ShippingPolicies()
+ {
+ this._policies = new List
+ {
+ new ("Late Shipments", "If a shipment is not delivered by the expected delivery date, customers will be notified and offered a discount on their next order."),
+ new ("Missing Shipments", "In cases where a shipment is reported missing, an investigation will be initiated within 48 hours, and a replacement will be sent if necessary."),
+ new ("Short Shipments", "If a shipment arrives with missing items, customers should report it within 7 days for a full refund or replacement."),
+ new ("Damaged Goods", "If goods are received damaged, customers must report the issue within 48 hours. A replacement or refund will be offered after inspection."),
+ new ("Return Policy", "Customers can return items within 30 days of receipt for a full refund, provided they are in original condition."),
+ new ("Delivery Area Limitations", "We currently only ship to specific regions. Please check our website for a list of eligible shipping areas."),
+ new ("International Shipping", "International shipments may be subject to customs duties and taxes, which are the responsibility of the customer.")
+ };
+ }
+
+ public void AddPolicy(ShippingPolicy policy)
+ {
+ this._policies.Add(policy);
+ }
+
+ public List GetPolicies()
+ {
+ return this._policies;
+ }
+
+ public void DisplayPolicies()
+ {
+ foreach (var policy in this._policies)
+ {
+ Console.WriteLine(policy);
+ }
+ }
+}
diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/Program.cs b/dotnet/samples/Demos/A2AClientServer/A2AServer/Program.cs
new file mode 100644
index 000000000000..bb44f83e2612
--- /dev/null
+++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/Program.cs
@@ -0,0 +1,30 @@
+// Copyright (c) Microsoft. All rights reserved.
+using A2A;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.DependencyInjection;
+using SharpA2A.AspNetCore;
+using SharpA2A.Core;
+
+var builder = WebApplication.CreateBuilder(args);
+builder.Services.AddHttpClient().AddLogging();
+var app = builder.Build();
+
+var configuration = app.Configuration;
+var httpClient = app.Services.GetRequiredService().CreateClient();
+var logger = app.Logger;
+
+string apiKey = configuration["OPENAI_API_KEY"] ?? throw new ArgumentException("OPENAI_API_KEY must be provided");
+string modelId = configuration["OPENAI_CHAT_MODEL_ID"] ?? "gpt-4.1";
+string baseAddress = configuration["AGENT_URL"] ?? "http://localhost:5000";
+
+var invoiceAgent = new InvoiceAgent(modelId, apiKey, logger);
+var invoiceTaskManager = new TaskManager();
+invoiceAgent.Attach(invoiceTaskManager);
+app.MapA2A(invoiceTaskManager, "/invoice");
+
+var currencyAgent = new CurrencyAgent(modelId, apiKey, logger);
+var currencyTaskManager = new TaskManager();
+currencyAgent.Attach(currencyTaskManager);
+app.MapA2A(currencyTaskManager, "/currency");
+
+await app.RunAsync();
diff --git a/dotnet/samples/Demos/A2AClientServer/README.md b/dotnet/samples/Demos/A2AClientServer/README.md
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/dotnet/src/Agents/A2A/A2AAgent.cs b/dotnet/src/Agents/A2A/A2AAgent.cs
index 912ac4ea1caf..468031d58837 100644
--- a/dotnet/src/Agents/A2A/A2AAgent.cs
+++ b/dotnet/src/Agents/A2A/A2AAgent.cs
@@ -24,6 +24,8 @@ public A2AAgent(A2AClient client, AgentCard agentCard)
{
this.Client = client;
this.AgentCard = agentCard;
+ this.Name = agentCard.Name;
+ this.Description = agentCard.Description;
}
///
diff --git a/dotnet/src/Agents/A2A/A2AHostAgent.cs b/dotnet/src/Agents/A2A/A2AHostAgent.cs
new file mode 100644
index 000000000000..3ffbe9669ba7
--- /dev/null
+++ b/dotnet/src/Agents/A2A/A2AHostAgent.cs
@@ -0,0 +1,89 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using SharpA2A.Core;
+
+namespace Microsoft.SemanticKernel.Agents.A2A;
+
+///
+/// Host which will attach a to a
+///
+public abstract class A2AHostAgent
+{
+ ///
+ /// Initializes a new instance of the SemanticKernelTravelAgent
+ ///
+ ///
+ protected A2AHostAgent(ILogger logger)
+ {
+ this._logger = logger;
+ }
+
+ ///
+ /// The associated
+ ///
+ public Agent? Agent { get; protected set; }
+
+ ///
+ /// Attach the to the provided
+ ///
+ ///
+ public void Attach(ITaskManager taskManager)
+ {
+ Verify.NotNull(taskManager);
+
+ this._taskManager = taskManager;
+ taskManager.OnTaskCreated = this.ExecuteAgentTaskAsync;
+ taskManager.OnTaskUpdated = this.ExecuteAgentTaskAsync;
+ taskManager.OnAgentCardQuery = this.GetAgentCard;
+ }
+ ///
+ /// Execute the specific
+ ///
+ ///
+ ///
+ ///
+ public async Task ExecuteAgentTaskAsync(AgentTask task)
+ {
+ Verify.NotNull(task);
+ Verify.NotNull(this.Agent);
+
+ if (this._taskManager is null)
+ {
+ throw new InvalidOperationException("TaskManager must be attached before executing an agent task.");
+ }
+
+ await this._taskManager.UpdateStatusAsync(task.Id, TaskState.Working).ConfigureAwait(false);
+
+ // Get message from the user
+ var userMessage = task.History!.Last().Parts.First().AsTextPart().Text;
+
+ // Get the response from the agent
+ var artifact = new Artifact();
+ await foreach (AgentResponseItem response in this.Agent.InvokeAsync(userMessage).ConfigureAwait(false))
+ {
+ var content = response.Message.Content;
+ artifact.Parts.Add(new TextPart() { Text = content! });
+ }
+
+ // Return as artifacts
+ await this._taskManager.ReturnArtifactAsync(task.Id, artifact).ConfigureAwait(false);
+ await this._taskManager.UpdateStatusAsync(task.Id, TaskState.Completed).ConfigureAwait(false);
+ }
+
+ ///
+ /// Return the associated with this hosted agent.
+ ///
+ /// Current URL for the agent
+#pragma warning disable CA1054 // URI-like parameters should not be strings
+ public abstract AgentCard GetAgentCard(string agentUrl);
+#pragma warning restore CA1054 // URI-like parameters should not be strings
+
+ #region private
+ private readonly ILogger _logger;
+ private ITaskManager? _taskManager;
+ #endregion
+}
From 0196641e05a6eeaad41b39f26f18dcb5383db504 Mon Sep 17 00:00:00 2001
From: markwallace-microsoft
<127216156+markwallace-microsoft@users.noreply.github.com>
Date: Wed, 14 May 2025 20:06:29 +0100
Subject: [PATCH 04/14] Mark assembly as experimental
---
dotnet/src/Agents/A2A/Properties/AssemblyInfo.cs | 6 ++++++
1 file changed, 6 insertions(+)
create mode 100644 dotnet/src/Agents/A2A/Properties/AssemblyInfo.cs
diff --git a/dotnet/src/Agents/A2A/Properties/AssemblyInfo.cs b/dotnet/src/Agents/A2A/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000000..bd1c0f58314e
--- /dev/null
+++ b/dotnet/src/Agents/A2A/Properties/AssemblyInfo.cs
@@ -0,0 +1,6 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics.CodeAnalysis;
+
+// This assembly is currently experimental.
+[assembly: Experimental("SKEXP0110")]
From aba8ada343ac4cebb0d186f26c6ad91ce5ea38b7 Mon Sep 17 00:00:00 2001
From: markwallace-microsoft
<127216156+markwallace-microsoft@users.noreply.github.com>
Date: Wed, 14 May 2025 20:20:31 +0100
Subject: [PATCH 05/14] Fix warnings
---
.../A2AClientServer/A2AClient/A2AClient.csproj | 4 ++++
.../A2AClient/HostClientAgent.cs | 2 +-
.../Demos/A2AClientServer/A2AClient/Program.cs | 7 -------
.../A2AClientServer/A2AServer/A2AServer.csproj | 6 +++++-
.../A2AClientServer/A2AServer/CurrencyAgent.cs | 14 ++++++++++----
.../A2AClientServer/A2AServer/InvoiceAgent.cs | 2 --
.../A2AClientServer/A2AServer/LogisticsAgent.cs | 7 -------
.../A2AClientServer/A2AServer/PolicyAgent.cs | 17 ++---------------
8 files changed, 22 insertions(+), 37 deletions(-)
delete mode 100644 dotnet/samples/Demos/A2AClientServer/A2AServer/LogisticsAgent.cs
diff --git a/dotnet/samples/Demos/A2AClientServer/A2AClient/A2AClient.csproj b/dotnet/samples/Demos/A2AClientServer/A2AClient/A2AClient.csproj
index 57b7db353a53..d126dcde1b32 100644
--- a/dotnet/samples/Demos/A2AClientServer/A2AClient/A2AClient.csproj
+++ b/dotnet/samples/Demos/A2AClientServer/A2AClient/A2AClient.csproj
@@ -9,6 +9,10 @@
$(NoWarn);CS1591;VSTHRD111;CA2007;SKEXP0110
+
+ true
+
+
diff --git a/dotnet/samples/Demos/A2AClientServer/A2AClient/HostClientAgent.cs b/dotnet/samples/Demos/A2AClientServer/A2AClient/HostClientAgent.cs
index fd9fde019606..3f9533112464 100644
--- a/dotnet/samples/Demos/A2AClientServer/A2AClient/HostClientAgent.cs
+++ b/dotnet/samples/Demos/A2AClientServer/A2AClient/HostClientAgent.cs
@@ -8,7 +8,7 @@
namespace A2A;
-internal class HostClientAgent
+internal sealed class HostClientAgent
{
internal HostClientAgent(ILogger logger)
{
diff --git a/dotnet/samples/Demos/A2AClientServer/A2AClient/Program.cs b/dotnet/samples/Demos/A2AClientServer/A2AClient/Program.cs
index a76a90663acc..d252e122b556 100644
--- a/dotnet/samples/Demos/A2AClientServer/A2AClient/Program.cs
+++ b/dotnet/samples/Demos/A2AClientServer/A2AClient/Program.cs
@@ -3,8 +3,6 @@
using System.CommandLine;
using System.CommandLine.Invocation;
using System.Reflection;
-using System.Text.Encodings.Web;
-using System.Text.Json;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
@@ -42,11 +40,6 @@ public static async System.Threading.Tasks.Task RunCliAsync(InvocationContext co
getDefaultValue: () => "http://localhost:10000",
description: "Agent URL");
- private static readonly JsonSerializerOptions s_jsonOptions = new JsonSerializerOptions
- {
- Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
- };
-
private static async System.Threading.Tasks.Task RunCliAsync(string agentUrl)
{
// Set up the logging
diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.csproj b/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.csproj
index 26742cee16bc..ed6a2d30776b 100644
--- a/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.csproj
+++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.csproj
@@ -6,9 +6,13 @@
enable
enable
5ee045b0-aea3-4f08-8d31-32d1a6f8fed0
- $(NoWarn);CS1591;VSTHRD111;CA2007
+ $(NoWarn);CS1591;VSTHRD111;CA2007;SKEXP0110
+
+ true
+
+
diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/CurrencyAgent.cs b/dotnet/samples/Demos/A2AClientServer/A2AServer/CurrencyAgent.cs
index c55fcdc340cf..e5cf9c5e65ed 100644
--- a/dotnet/samples/Demos/A2AClientServer/A2AServer/CurrencyAgent.cs
+++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/CurrencyAgent.cs
@@ -6,11 +6,12 @@
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Agents.A2A;
using Polly;
+using Polly.Retry;
using SharpA2A.Core;
namespace A2A;
-internal class CurrencyAgent : A2AHostAgent
+internal class CurrencyAgent : A2AHostAgent, IDisposable
{
internal CurrencyAgent(string modelId, string apiKey, ILogger logger) : base(logger)
{
@@ -24,6 +25,11 @@ internal CurrencyAgent(string modelId, string apiKey, ILogger logger) : base(log
this.Agent = this.InitializeAgent(modelId, apiKey);
}
+ public void Dispose()
+ {
+ this._httpClient.Dispose();
+ }
+
public override AgentCard GetAgentCard(string agentUrl)
{
var capabilities = new AgentCapabilities()
@@ -101,7 +107,7 @@ public class CurrencyPlugin
{
private readonly ILogger _logger;
private readonly HttpClient _httpClient;
- private readonly IAsyncPolicy _retryPolicy;
+ private readonly AsyncRetryPolicy _retryPolicy;
///
/// Initialize a new instance of the CurrencyPlugin
@@ -139,10 +145,10 @@ public async Task GetExchangeRateAsync(
currencyFrom, currencyTo, date);
// Build request URL with query parameters
- var requestUri = $"https://api.frankfurter.app/{date}?from={Uri.EscapeDataString(currencyFrom)}&to={Uri.EscapeDataString(currencyTo)}";
+ var requestUri = new Uri($"https://api.frankfurter.app/{date}?from={Uri.EscapeDataString(currencyFrom)}&to={Uri.EscapeDataString(currencyTo)}");
// Use retry policy for resilience
- var response = await this._retryPolicy.ExecuteAsync(() => _httpClient.GetAsync(requestUri)).ConfigureAwait(false);
+ var response = await this._retryPolicy.ExecuteAsync(() => this._httpClient.GetAsync(requestUri)).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var jsonContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/InvoiceAgent.cs b/dotnet/samples/Demos/A2AClientServer/A2AServer/InvoiceAgent.cs
index 7789339e523d..58cf879ac048 100644
--- a/dotnet/samples/Demos/A2AClientServer/A2AServer/InvoiceAgent.cs
+++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/InvoiceAgent.cs
@@ -1,6 +1,4 @@
// Copyright (c) Microsoft. All rights reserved.
-
-// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/LogisticsAgent.cs b/dotnet/samples/Demos/A2AClientServer/A2AServer/LogisticsAgent.cs
deleted file mode 100644
index 4236f17bc977..000000000000
--- a/dotnet/samples/Demos/A2AClientServer/A2AServer/LogisticsAgent.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-namespace A2A;
-
-internal class LogisticsAgent
-{
-}
diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/PolicyAgent.cs b/dotnet/samples/Demos/A2AClientServer/A2AServer/PolicyAgent.cs
index 81bf4f49f583..1a2a5235007c 100644
--- a/dotnet/samples/Demos/A2AClientServer/A2AServer/PolicyAgent.cs
+++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/PolicyAgent.cs
@@ -12,7 +12,6 @@ internal class PolicyAgent : A2AHostAgent
internal PolicyAgent(string modelId, string apiKey, ILogger logger) : base(logger)
{
this._logger = logger;
- this._httpClient = new HttpClient();
// Add TextSearch over the shipping policies
@@ -53,7 +52,6 @@ public override AgentCard GetAgentCard(string agentUrl)
}
#region private
- private readonly HttpClient _httpClient;
private readonly ILogger _logger;
private ChatCompletionAgent InitializeAgent(string modelId, string apiKey)
@@ -122,21 +120,10 @@ public ShippingPolicies()
};
}
+ public List GetPolicies => this._policies;
+
public void AddPolicy(ShippingPolicy policy)
{
this._policies.Add(policy);
}
-
- public List GetPolicies()
- {
- return this._policies;
- }
-
- public void DisplayPolicies()
- {
- foreach (var policy in this._policies)
- {
- Console.WriteLine(policy);
- }
- }
}
From d1297dd99ad636d7cbcf65eccfcf7bb6144e3ea2 Mon Sep 17 00:00:00 2001
From: markwallace-microsoft
<127216156+markwallace-microsoft@users.noreply.github.com>
Date: Wed, 14 May 2025 20:24:09 +0100
Subject: [PATCH 06/14] Fix warnings
---
dotnet/samples/Demos/A2AClientServer/A2AServer/CurrencyAgent.cs | 2 +-
dotnet/samples/Demos/A2AClientServer/A2AServer/InvoiceAgent.cs | 2 +-
dotnet/samples/Demos/A2AClientServer/A2AServer/PolicyAgent.cs | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/CurrencyAgent.cs b/dotnet/samples/Demos/A2AClientServer/A2AServer/CurrencyAgent.cs
index e5cf9c5e65ed..32fc26e8f86c 100644
--- a/dotnet/samples/Demos/A2AClientServer/A2AServer/CurrencyAgent.cs
+++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/CurrencyAgent.cs
@@ -11,7 +11,7 @@
namespace A2A;
-internal class CurrencyAgent : A2AHostAgent, IDisposable
+internal sealed class CurrencyAgent : A2AHostAgent, IDisposable
{
internal CurrencyAgent(string modelId, string apiKey, ILogger logger) : base(logger)
{
diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/InvoiceAgent.cs b/dotnet/samples/Demos/A2AClientServer/A2AServer/InvoiceAgent.cs
index 58cf879ac048..e508ed6f0661 100644
--- a/dotnet/samples/Demos/A2AClientServer/A2AServer/InvoiceAgent.cs
+++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/InvoiceAgent.cs
@@ -8,7 +8,7 @@
namespace A2A;
-internal class InvoiceAgent : A2AHostAgent
+internal sealed class InvoiceAgent : A2AHostAgent
{
internal InvoiceAgent(string modelId, string apiKey, ILogger logger) : base(logger)
{
diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/PolicyAgent.cs b/dotnet/samples/Demos/A2AClientServer/A2AServer/PolicyAgent.cs
index 1a2a5235007c..e36e42aa22ca 100644
--- a/dotnet/samples/Demos/A2AClientServer/A2AServer/PolicyAgent.cs
+++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/PolicyAgent.cs
@@ -7,7 +7,7 @@
namespace A2A;
-internal class PolicyAgent : A2AHostAgent
+internal sealed class PolicyAgent : A2AHostAgent
{
internal PolicyAgent(string modelId, string apiKey, ILogger logger) : base(logger)
{
From 384de1dc0241aad274825698baf006546d3c0e35 Mon Sep 17 00:00:00 2001
From: markwallace-microsoft
<127216156+markwallace-microsoft@users.noreply.github.com>
Date: Wed, 14 May 2025 20:39:54 +0100
Subject: [PATCH 07/14] Skip .net8.0 build for A2AClientServer
---
dotnet/samples/Demos/A2AClientServer/A2AClient/A2AClient.csproj | 2 +-
dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.csproj | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/dotnet/samples/Demos/A2AClientServer/A2AClient/A2AClient.csproj b/dotnet/samples/Demos/A2AClientServer/A2AClient/A2AClient.csproj
index d126dcde1b32..9f4387215ae3 100644
--- a/dotnet/samples/Demos/A2AClientServer/A2AClient/A2AClient.csproj
+++ b/dotnet/samples/Demos/A2AClientServer/A2AClient/A2AClient.csproj
@@ -10,7 +10,7 @@
- true
+ true
diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.csproj b/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.csproj
index ed6a2d30776b..6b0e3a385efe 100644
--- a/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.csproj
+++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.csproj
@@ -10,7 +10,7 @@
- true
+ true
From 8469ddceaaa905a657a1ea911666a46750915872 Mon Sep 17 00:00:00 2001
From: markwallace-microsoft
<127216156+markwallace-microsoft@users.noreply.github.com>
Date: Wed, 14 May 2025 20:51:12 +0100
Subject: [PATCH 08/14] Skip .net8.0 build for A2AClientServer
---
.../samples/Demos/A2AClientServer/A2AClient/A2AClient.csproj | 2 +-
.../samples/Demos/A2AClientServer/A2AServer/A2AServer.csproj | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/dotnet/samples/Demos/A2AClientServer/A2AClient/A2AClient.csproj b/dotnet/samples/Demos/A2AClientServer/A2AClient/A2AClient.csproj
index 9f4387215ae3..83de0aea9245 100644
--- a/dotnet/samples/Demos/A2AClientServer/A2AClient/A2AClient.csproj
+++ b/dotnet/samples/Demos/A2AClientServer/A2AClient/A2AClient.csproj
@@ -10,7 +10,7 @@
- true
+ true
diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.csproj b/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.csproj
index 6b0e3a385efe..f389ac068e0d 100644
--- a/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.csproj
+++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.csproj
@@ -10,9 +10,9 @@
- true
+ true
-
+
From 192ebeebb861b028aa3c02e0079bb099f20803d3 Mon Sep 17 00:00:00 2001
From: markwallace-microsoft
<127216156+markwallace-microsoft@users.noreply.github.com>
Date: Wed, 14 May 2025 20:51:12 +0100
Subject: [PATCH 09/14] Skip .net8.0 build for A2AClientServer
---
.../Demos/A2AClientServer/A2AClient/A2AClient.csproj | 6 +-----
.../Demos/A2AClientServer/A2AServer/A2AServer.csproj | 4 ----
2 files changed, 1 insertion(+), 9 deletions(-)
diff --git a/dotnet/samples/Demos/A2AClientServer/A2AClient/A2AClient.csproj b/dotnet/samples/Demos/A2AClientServer/A2AClient/A2AClient.csproj
index 9f4387215ae3..ca9f0e3841e7 100644
--- a/dotnet/samples/Demos/A2AClientServer/A2AClient/A2AClient.csproj
+++ b/dotnet/samples/Demos/A2AClientServer/A2AClient/A2AClient.csproj
@@ -8,11 +8,7 @@
5ee045b0-aea3-4f08-8d31-32d1a6f8fed0
$(NoWarn);CS1591;VSTHRD111;CA2007;SKEXP0110
-
-
- true
-
-
+
diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.csproj b/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.csproj
index 6b0e3a385efe..daede2e3254e 100644
--- a/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.csproj
+++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.csproj
@@ -9,10 +9,6 @@
$(NoWarn);CS1591;VSTHRD111;CA2007;SKEXP0110
-
- true
-
-
From f2516ddc3cbe78d0cad7eaad688410fd7b816cab Mon Sep 17 00:00:00 2001
From: markwallace-microsoft
<127216156+markwallace-microsoft@users.noreply.github.com>
Date: Wed, 14 May 2025 21:41:39 +0100
Subject: [PATCH 10/14] Bump package versions and add readme
---
dotnet/Directory.Packages.props | 4 +--
.../A2AClient/A2AClient.csproj | 2 +-
.../A2AServer/A2AServer.csproj | 2 +-
.../samples/Demos/A2AClientServer/README.md | 33 +++++++++++++++++++
4 files changed, 37 insertions(+), 4 deletions(-)
diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index fd5499829f8f..28e4dbf92fbc 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -89,8 +89,8 @@
-
-
+
+
diff --git a/dotnet/samples/Demos/A2AClientServer/A2AClient/A2AClient.csproj b/dotnet/samples/Demos/A2AClientServer/A2AClient/A2AClient.csproj
index 57b7db353a53..1bd899ff15c4 100644
--- a/dotnet/samples/Demos/A2AClientServer/A2AClient/A2AClient.csproj
+++ b/dotnet/samples/Demos/A2AClientServer/A2AClient/A2AClient.csproj
@@ -2,7 +2,7 @@
Exe
- net9.0
+ net9.0;net8.0
enable
enable
5ee045b0-aea3-4f08-8d31-32d1a6f8fed0
diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.csproj b/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.csproj
index daede2e3254e..20d7a01d7ea7 100644
--- a/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.csproj
+++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.csproj
@@ -2,7 +2,7 @@
Exe
- net9.0
+ net9.0;net8.0
enable
enable
5ee045b0-aea3-4f08-8d31-32d1a6f8fed0
diff --git a/dotnet/samples/Demos/A2AClientServer/README.md b/dotnet/samples/Demos/A2AClientServer/README.md
index e69de29bb2d1..281a694cc4a7 100644
--- a/dotnet/samples/Demos/A2AClientServer/README.md
+++ b/dotnet/samples/Demos/A2AClientServer/README.md
@@ -0,0 +1,33 @@
+# 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 [SharpA2A.Core](https://www.nuget.org/packages/SharpA2A.Core) and demonstrate:
+
+1. Creating an A2A Server which exposes multiple agents using the A2A protocol.
+2. Creating an A2A Client with a command line interface which invokes agents using the A2A protocol.
+
+## Configuring Secrets or Environment Variables
+
+The samples require an OpenAI API key.
+
+Create an environment variable need `OPENAI_API_KEY` with your OpenAI API key.
+
+
+## Run the Sample
+
+To run the sample, follow these steps:
+
+1. Run the A2A server:
+ ```bash
+ cd A2AServer
+ dotnet run
+ ```
+2. Run the A2A client:
+ ```bash
+ cd A2AClient
+ dotnet run
+ ```
+3. Enter your request e.g. "Show me all invoices for Contoso?"
\ No newline at end of file
From d2ba0d8d09eb40f30ebc0f3e3c8c508e55dda553 Mon Sep 17 00:00:00 2001
From: markwallace-microsoft
<127216156+markwallace-microsoft@users.noreply.github.com>
Date: Wed, 14 May 2025 21:56:46 +0100
Subject: [PATCH 11/14] Revert to just net8.0
---
dotnet/samples/Demos/A2AClientServer/A2AClient/A2AClient.csproj | 2 +-
dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.csproj | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/dotnet/samples/Demos/A2AClientServer/A2AClient/A2AClient.csproj b/dotnet/samples/Demos/A2AClientServer/A2AClient/A2AClient.csproj
index 1bd899ff15c4..47b2de0f5411 100644
--- a/dotnet/samples/Demos/A2AClientServer/A2AClient/A2AClient.csproj
+++ b/dotnet/samples/Demos/A2AClientServer/A2AClient/A2AClient.csproj
@@ -2,7 +2,7 @@
Exe
- net9.0;net8.0
+ net8.0
enable
enable
5ee045b0-aea3-4f08-8d31-32d1a6f8fed0
diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.csproj b/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.csproj
index 20d7a01d7ea7..c009fd4f4794 100644
--- a/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.csproj
+++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.csproj
@@ -2,7 +2,7 @@
Exe
- net9.0;net8.0
+ net8.0
enable
enable
5ee045b0-aea3-4f08-8d31-32d1a6f8fed0
From 4edfb35f04025078fc28d87f242ae70643da35b6 Mon Sep 17 00:00:00 2001
From: markwallace-microsoft
<127216156+markwallace-microsoft@users.noreply.github.com>
Date: Sat, 17 May 2025 13:48:11 +0100
Subject: [PATCH 12/14] Merge Shawn's changes and support ChatCompletion or
AzureAI agents
---
dotnet/SK-dotnet.sln | 3 +
.../A2AClient/HostClientAgent.cs | 63 +++++++--
.../A2AClientServer/A2AClient/Program.cs | 40 +++---
.../Demos/A2AClientServer/A2AClient/README.md | 26 ++++
.../A2AServer/A2AServer.csproj | 1 +
.../A2AClientServer/A2AServer/A2AServer.http | 35 ++++-
.../A2AServer/AzureAI/AzureAICurrencyAgent.cs | 103 +++++++++++++++
.../A2AServer/AzureAI/AzureAIInvoiceAgent.cs | 80 +++++++++++
.../AzureAI/AzureAILogisticsAgent.cs | 78 +++++++++++
.../A2AServer/AzureAI/AzureAIPolicyAgent.cs | 78 +++++++++++
.../A2AServer/ChatCompletion/CurrencyAgent.cs | 94 +++++++++++++
.../A2AServer/ChatCompletion/InvoiceAgent.cs | 83 ++++++++++++
.../ChatCompletion/LogisticsAgent.cs | 90 +++++++++++++
.../{ => ChatCompletion}/PolicyAgent.cs | 108 +++++++--------
.../CurrencyPlugin.cs} | 92 +------------
.../InvoiceQueryPlugin.cs} | 125 +++++-------------
.../A2AClientServer/A2AServer/Program.cs | 68 ++++++++--
.../samples/Demos/A2AClientServer/README.md | 46 ++++++-
18 files changed, 919 insertions(+), 294 deletions(-)
create mode 100644 dotnet/samples/Demos/A2AClientServer/A2AClient/README.md
create mode 100644 dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAICurrencyAgent.cs
create mode 100644 dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAIInvoiceAgent.cs
create mode 100644 dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAILogisticsAgent.cs
create mode 100644 dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAIPolicyAgent.cs
create mode 100644 dotnet/samples/Demos/A2AClientServer/A2AServer/ChatCompletion/CurrencyAgent.cs
create mode 100644 dotnet/samples/Demos/A2AClientServer/A2AServer/ChatCompletion/InvoiceAgent.cs
create mode 100644 dotnet/samples/Demos/A2AClientServer/A2AServer/ChatCompletion/LogisticsAgent.cs
rename dotnet/samples/Demos/A2AClientServer/A2AServer/{ => ChatCompletion}/PolicyAgent.cs (57%)
rename dotnet/samples/Demos/A2AClientServer/A2AServer/{CurrencyAgent.cs => Plugins/CurrencyPlugin.cs} (59%)
rename dotnet/samples/Demos/A2AClientServer/A2AServer/{InvoiceAgent.cs => Plugins/InvoiceQueryPlugin.cs} (55%)
diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln
index bcae0eddf1d5..e850edd09aae 100644
--- a/dotnet/SK-dotnet.sln
+++ b/dotnet/SK-dotnet.sln
@@ -553,6 +553,9 @@ EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Agents.A2A", "src\Agents\A2A\Agents.A2A.csproj", "{38F1D24F-C7B4-58CC-D104-311D786A73CF}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "A2AClientServer", "A2AClientServer", "{5B1ECD1B-3C38-4458-A227-89846AF13760}"
+ ProjectSection(SolutionItems) = preProject
+ samples\Demos\A2AClientServer\README.md = samples\Demos\A2AClientServer\README.md
+ EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "A2AClient", "samples\Demos\A2AClientServer\A2AClient\A2AClient.csproj", "{F293D014-97E2-18CB-FA0F-0A0FBE149286}"
EndProject
diff --git a/dotnet/samples/Demos/A2AClientServer/A2AClient/HostClientAgent.cs b/dotnet/samples/Demos/A2AClientServer/A2AClient/HostClientAgent.cs
index 3f9533112464..ed8723e84072 100644
--- a/dotnet/samples/Demos/A2AClientServer/A2AClient/HostClientAgent.cs
+++ b/dotnet/samples/Demos/A2AClientServer/A2AClient/HostClientAgent.cs
@@ -14,24 +14,24 @@ internal HostClientAgent(ILogger logger)
{
this._logger = logger;
}
- internal async Task InitializeAgentAsync(string modelId, string apiKey, string baseAddress)
+ 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 agentPlugin = KernelPluginFactory.CreateFromFunctions("AgentPlugin",
- [
- AgentKernelFunctionFactory.CreateFromAgent(await this.CreateAgentAsync($"{baseAddress}/currency/")),
- AgentKernelFunctionFactory.CreateFromAgent(await this.CreateAgentAsync($"{baseAddress}/invoice/"))
- ]);
+ var createAgentTasks = agentUrls.Select(agentUrl => this.CreateAgentAsync(agentUrl));
+ var agents = await Task.WhenAll(createAgentTasks);
+ var agentFunctions = agents.Select(agent => AgentKernelFunctionFactory.CreateFromAgent(agent)).ToList();
+ var agentPlugin = KernelPluginFactory.CreateFromFunctions("AgentPlugin", agentFunctions);
- // Define the TravelPlannerAgent
+ // 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()
{
@@ -63,14 +63,59 @@ private async Task CreateAgentAsync(string agentUri)
{
var httpClient = new HttpClient
{
- BaseAddress = new Uri(agentUri)
+ 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);
+ 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
+ string 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 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