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() is ChatMessageContent[] chatMessages) + { + Console.ForegroundColor = ConsoleColor.DarkGray; + + Console.WriteLine($"Got Response from Agent {context.Function.Name}:"); + foreach (var message in chatMessages) + { + Console.ForegroundColor = ConsoleColor.Gray; + + Console.WriteLine(IndentMultilineString($"{message}")); + } + } + Console.ResetColor(); + } +} + diff --git a/dotnet/samples/Demos/A2AClientServer/A2AClient/Program.cs b/dotnet/samples/Demos/A2AClientServer/A2AClient/Program.cs index d252e122b556..9213884d71a3 100644 --- a/dotnet/samples/Demos/A2AClientServer/A2AClient/Program.cs +++ b/dotnet/samples/Demos/A2AClientServer/A2AClient/Program.cs @@ -15,32 +15,20 @@ 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); + var rootCommand = new RootCommand("A2AClient"); + rootCommand.SetHandler(HandleCommandsAsync); // Run the command return await rootCommand.InvokeAsync(args); } - public static async System.Threading.Tasks.Task RunCliAsync(InvocationContext context) + public static async System.Threading.Tasks.Task HandleCommandsAsync(InvocationContext context) { - string agent = context.ParseResult.GetValueForOption(s_agentOption)!; - - await RunCliAsync(agent); + await RunCliAsync(); } #region private - private static readonly Option s_agentOption = new( - "--agent", - getDefaultValue: () => "http://localhost:10000", - description: "Agent URL"); - - private static async System.Threading.Tasks.Task RunCliAsync(string agentUrl) + private static async System.Threading.Tasks.Task RunCliAsync() { // Set up the logging using var loggerFactory = LoggerFactory.Create(builder => @@ -55,14 +43,14 @@ private static async System.Threading.Tasks.Task RunCliAsync(string agentUrl) .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"; + 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/policy/ http://localhost:5000/invoice/ http://localhost:5000/logistics/"; // Create the Host agent var hostAgent = new HostClientAgent(logger); - await hostAgent.InitializeAgentAsync(modelId, apiKey, baseAddress); - + await hostAgent.InitializeAgentAsync(modelId, apiKey, agentUrls!.Split(" ")); + AgentThread thread = new ChatHistoryAgentThread(); try { while (true) @@ -81,12 +69,14 @@ private static async System.Threading.Tasks.Task RunCliAsync(string agentUrl) break; } - Console.ForegroundColor = ConsoleColor.Cyan; - await foreach (AgentResponseItem response in hostAgent.Agent!.InvokeAsync(message)) + await foreach (AgentResponseItem response in hostAgent.Agent!.InvokeAsync(message, thread)) { + Console.ForegroundColor = ConsoleColor.Cyan; Console.WriteLine($"Agent: {response.Message.Content}"); + Console.ResetColor(); + + thread = response.Thread; } - Console.ResetColor(); } } catch (Exception ex) diff --git a/dotnet/samples/Demos/A2AClientServer/A2AClient/README.md b/dotnet/samples/Demos/A2AClientServer/A2AClient/README.md new file mode 100644 index 000000000000..3f22e6bc5d69 --- /dev/null +++ b/dotnet/samples/Demos/A2AClientServer/A2AClient/README.md @@ -0,0 +1,26 @@ + +# A2A Client Sample +Show how to create an A2A Client with a command line interface which invokes agents using the A2A protocol. + +## Run the Sample + +To run the sample, follow these steps: + +1. Run the A2A client: + ```bash + cd A2AClient + dotnet run + ``` +2. Enter your request e.g. "Show me all invoices for Contoso?" + +## Set 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" +``` \ No newline at end of file diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.csproj b/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.csproj index c009fd4f4794..b82136c3d4ef 100644 --- a/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.csproj +++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.csproj @@ -16,6 +16,7 @@ + diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.http b/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.http index ddc720633dbf..67e3d5699858 100644 --- a/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.http +++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.http @@ -25,11 +25,11 @@ Content-Type: application/json } } -### Query agent card for the currency agent -GET {{host}}/currency/.well-known/agent.json +### Query agent card for the policy agent +GET {{host}}/policy/.well-known/agent.json -### Send a task to the currency agent -POST {{host}}/currency +### Send a task to the policy agent +POST {{host}}/policy Content-Type: application/json { @@ -43,7 +43,32 @@ Content-Type: application/json "parts": [ { "type": "text", - "text": "What is the current exchange rather for Dollars to Euro?" + "text": "What is the policy for short shipments?" + } + ] + } + } +} + +### Query agent card for the logistics agent +GET {{host}}/logistics/.well-known/agent.json + +### Send a task to the logistics agent +POST {{host}}/logistics +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 status for SHPMT-SAP-001" } ] } diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAICurrencyAgent.cs b/dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAICurrencyAgent.cs new file mode 100644 index 000000000000..df7a79d602af --- /dev/null +++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAICurrencyAgent.cs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft. All rights reserved. +using Azure.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents.A2A; +using Microsoft.SemanticKernel.Agents.AzureAI; +using SharpA2A.Core; + +namespace A2A; + +internal sealed class AzureAICurrencyAgent : A2AHostAgent, IDisposable +{ + internal AzureAICurrencyAgent(ILogger logger) : base(logger) + { + this._logger = logger; + this._httpClient = new HttpClient(); + + this._currencyPlugin = new CurrencyPlugin( + logger: new Logger(new LoggerFactory()), + httpClient: this._httpClient); + } + + public void Dispose() + { + this._httpClient.Dispose(); + + if (this.Agent is AzureAIAgent azureAIAgent && azureAIAgent is not null) + { + azureAIAgent.Client.DeleteAgent(azureAIAgent.Id); + } + } + + public async Task InitializeAgentAsync(string modelId, string connectionString) + { + try + { + this._logger.LogInformation("Initializing Semantic Kernel agent with model {ModelId}", modelId); + + // Define the CurrencyAgent + var projectClient = AzureAIAgent.CreateAzureAIClient(connectionString, new AzureCliCredential()); + var agentsClient = projectClient.GetAgentsClient(); + Azure.AI.Projects.Agent definition = await agentsClient.CreateAgentAsync( + modelId, + "CurrencyAgent", + null, + """ + You specialize in handling queries related to currency exchange rates. + """); + + this.Agent = new AzureAIAgent(definition, agentsClient); + + if (this._currencyPlugin is not null) + { + this.Agent.Kernel.Plugins.Add(KernelPluginFactory.CreateFromObject(this._currencyPlugin)); + } + } + catch (Exception ex) + { + this._logger.LogError(ex, "Failed to initialize AzureAICurrencyAgent"); + throw; + } + } + + 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; + + #endregion +} diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAIInvoiceAgent.cs b/dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAIInvoiceAgent.cs new file mode 100644 index 000000000000..10f81be6fe11 --- /dev/null +++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAIInvoiceAgent.cs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft. All rights reserved. +using Azure.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents.A2A; +using Microsoft.SemanticKernel.Agents.AzureAI; +using SharpA2A.Core; + +namespace A2A; + +internal sealed class AzureAIInvoiceAgent : A2AHostAgent +{ + internal AzureAIInvoiceAgent(ILogger logger) : base(logger) + { + this._logger = logger; + } + + public async Task InitializeAgentAsync(string modelId, string connectionString, string assistantId) + { + try + { + this._logger.LogInformation("Initializing AzureAIInvoiceAgent {AssistantId}", assistantId); + + // Define the InvoiceAgent + var projectClient = AzureAIAgent.CreateAzureAIClient(connectionString, new AzureCliCredential()); + var agentsClient = projectClient.GetAgentsClient(); + Azure.AI.Projects.Agent definition = await agentsClient.GetAgentAsync(assistantId); + + this.Agent = new AzureAIAgent(definition, agentsClient); + this.Agent.Kernel.Plugins.Add(KernelPluginFactory.CreateFromType()); + } + catch (Exception ex) + { + this._logger.LogError(ex, "Failed to initialize AzureAIInvoiceAgent"); + throw; + } + } + + public override AgentCard GetAgentCard(string agentUrl) + { + if (this.Agent is null) + { + throw new InvalidOperationException("Agent not initialized."); + } + + var capabilities = new AgentCapabilities() + { + Streaming = false, + PushNotifications = false, + }; + + var invoiceQuery = new AgentSkill() + { + Id = "id_invoice_agent", + Name = "InvoiceQuery", + Description = "Handles requests relating to invoices.", + Tags = ["invoice", "semantic-kernel"], + Examples = + [ + "List the latest invoices for Contoso.", + ], + }; + + return new AgentCard() + { + Name = this.Agent.Name ?? "InvoiceAgent", + Description = this.Agent.Description, + Url = agentUrl, + Version = "1.0.0", + DefaultInputModes = ["text"], + DefaultOutputModes = ["text"], + Capabilities = capabilities, + Skills = [invoiceQuery], + }; + } + + #region private + private readonly ILogger _logger; + #endregion +} diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAILogisticsAgent.cs b/dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAILogisticsAgent.cs new file mode 100644 index 000000000000..7c9181eaf8be --- /dev/null +++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAILogisticsAgent.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft. All rights reserved. +using Azure.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Agents.A2A; +using Microsoft.SemanticKernel.Agents.AzureAI; +using SharpA2A.Core; + +namespace A2A; + +internal sealed class AzureAILogisticsAgent : A2AHostAgent +{ + internal AzureAILogisticsAgent(ILogger logger) : base(logger) + { + this._logger = logger; + } + + public async Task InitializeAgentAsync(string modelId, string connectionString, string assistantId) + { + try + { + this._logger.LogInformation("Initializing AzureAILogisticsAgent {AssistantId}", assistantId); + + // Define the InvoiceAgent + var projectClient = AzureAIAgent.CreateAzureAIClient(connectionString, new AzureCliCredential()); + var agentsClient = projectClient.GetAgentsClient(); + Azure.AI.Projects.Agent definition = await agentsClient.GetAgentAsync(assistantId); + + this.Agent = new AzureAIAgent(definition, agentsClient); + } + catch (Exception ex) + { + this._logger.LogError(ex, "Failed to initialize AzureAILogisticsAgent"); + throw; + } + } + + public override AgentCard GetAgentCard(string agentUrl) + { + if (this.Agent is null) + { + throw new InvalidOperationException("Agent not initialized."); + } + + var capabilities = new AgentCapabilities() + { + Streaming = false, + PushNotifications = false, + }; + + var invoiceQuery = new AgentSkill() + { + Id = "id_invoice_agent", + Name = "LogisticsQuery", + Description = "Handles requests relating to logistics.", + Tags = ["logistics", "semantic-kernel"], + Examples = + [ + "What is the status for SHPMT-SAP-001", + ], + }; + + return new AgentCard() + { + Name = this.Agent.Name ?? "LogisticsAgent", + Description = this.Agent.Description, + Url = agentUrl, + Version = "1.0.0", + DefaultInputModes = ["text"], + DefaultOutputModes = ["text"], + Capabilities = capabilities, + Skills = [invoiceQuery], + }; + } + + #region private + private readonly ILogger _logger; + #endregion +} diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAIPolicyAgent.cs b/dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAIPolicyAgent.cs new file mode 100644 index 000000000000..136674a79922 --- /dev/null +++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAIPolicyAgent.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft. All rights reserved. +using Azure.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Agents.A2A; +using Microsoft.SemanticKernel.Agents.AzureAI; +using SharpA2A.Core; + +namespace A2A; + +internal sealed class AzureAIPolicyAgent : A2AHostAgent +{ + internal AzureAIPolicyAgent(ILogger logger) : base(logger) + { + this._logger = logger; + } + + public async Task InitializeAgentAsync(string modelId, string connectionString, string assistantId) + { + try + { + this._logger.LogInformation("Initializing AzureAIPolicyAgent {AssistantId}", assistantId); + + // Define the InvoiceAgent + var projectClient = AzureAIAgent.CreateAzureAIClient(connectionString, new AzureCliCredential()); + var agentsClient = projectClient.GetAgentsClient(); + Azure.AI.Projects.Agent definition = await agentsClient.GetAgentAsync(assistantId); + + this.Agent = new AzureAIAgent(definition, agentsClient); + } + catch (Exception ex) + { + this._logger.LogError(ex, "Failed to initialize AzureAIPolicyAgent"); + throw; + } + } + + public override AgentCard GetAgentCard(string agentUrl) + { + if (this.Agent is null) + { + throw new InvalidOperationException("Agent not initialized."); + } + + 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 = this.Agent.Name ?? "PolicyAgent", + Description = this.Agent.Description, + Url = agentUrl, + Version = "1.0.0", + DefaultInputModes = ["text"], + DefaultOutputModes = ["text"], + Capabilities = capabilities, + Skills = [invoiceQuery], + }; + } + + #region private + private readonly ILogger _logger; + #endregion +} diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/ChatCompletion/CurrencyAgent.cs b/dotnet/samples/Demos/A2AClientServer/A2AServer/ChatCompletion/CurrencyAgent.cs new file mode 100644 index 000000000000..98c2558634a2 --- /dev/null +++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/ChatCompletion/CurrencyAgent.cs @@ -0,0 +1,94 @@ +// 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 CurrencyAgent : A2AHostAgent, IDisposable +{ + internal CurrencyAgent(ILogger logger) : base(logger) + { + this._logger = logger; + this._httpClient = new HttpClient(); + + this._currencyPlugin = new CurrencyPlugin( + logger: new Logger(new LoggerFactory()), + httpClient: this._httpClient); + } + + public void Dispose() + { + this._httpClient.Dispose(); + } + public void InitializeAgent(string modelId, string apiKey) + { + try + { + this._logger.LogInformation("Initializing CurrencyAgent with model {ModelId}", modelId); + + // Define the CurrencyAgent + var builder = Kernel.CreateBuilder(); + builder.AddOpenAIChatCompletion(modelId, apiKey); + builder.Plugins.AddFromObject(this._currencyPlugin); + var kernel = builder.Build(); + + this.Agent = 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 CurrencyAgent"); + throw; + } + } + + 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; + #endregion +} diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/ChatCompletion/InvoiceAgent.cs b/dotnet/samples/Demos/A2AClientServer/A2AServer/ChatCompletion/InvoiceAgent.cs new file mode 100644 index 000000000000..f58e1a5edd65 --- /dev/null +++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/ChatCompletion/InvoiceAgent.cs @@ -0,0 +1,83 @@ +// 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 InvoiceAgent : A2AHostAgent +{ + internal InvoiceAgent(ILogger logger) : base(logger) + { + this._logger = logger; + } + + public void InitializeAgent(string modelId, string apiKey) + { + try + { + this._logger.LogInformation("Initializing InvoiceAgent with model {ModelId}", modelId); + + // Define the InvoiceAgent + var builder = Kernel.CreateBuilder(); + builder.AddOpenAIChatCompletion(modelId, apiKey); + builder.Plugins.AddFromType(); + var kernel = builder.Build(); + + this.Agent = 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; + } + } + + 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; + #endregion +} diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/ChatCompletion/LogisticsAgent.cs b/dotnet/samples/Demos/A2AClientServer/A2AServer/ChatCompletion/LogisticsAgent.cs new file mode 100644 index 000000000000..00d076c6e64e --- /dev/null +++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/ChatCompletion/LogisticsAgent.cs @@ -0,0 +1,90 @@ +// 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 LogisticsAgent : A2AHostAgent +{ + internal LogisticsAgent(ILogger logger) : base(logger) + { + this._logger = logger; + + // Add TextSearch over the shipping policies + } + + public void InitializeAgent(string modelId, string apiKey) + { + try + { + this._logger.LogInformation("Initializing LogisticAgent with model {ModelId}", modelId); + + // Define the TravelPlannerAgent + var builder = Kernel.CreateBuilder(); + builder.AddOpenAIChatCompletion(modelId, apiKey); + var kernel = builder.Build(); + + this.Agent = new ChatCompletionAgent() + { + Kernel = kernel, + Name = "LogisticsAgent", + Instructions = + """ + You specialize in handling queries related to logistics + + Always reply with exactly: + + Shipment number: SHPMT-SAP-001 + Item: TSHIRT-RED-L + Quantity: 900 + """, + Arguments = new KernelArguments(new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }), + }; + } + catch (Exception ex) + { + this._logger.LogError(ex, "Failed to initialize LogisticAgent"); + throw; + } + } + + public override AgentCard GetAgentCard(string agentUrl) + { + var capabilities = new AgentCapabilities() + { + Streaming = false, + PushNotifications = false, + }; + + var invoiceQuery = new AgentSkill() + { + Id = "id_policy_agent", + Name = "LogisticsAgent", + Description = "Handles requests relating to logistics.", + Tags = ["logistics", "semantic-kernel"], + Examples = + [ + "What is the status for SHPMT-SAP-001", + ], + }; + + return new AgentCard() + { + Name = "LogisticsAgent", + Description = "Handles requests relating to logistics.", + Url = agentUrl, + Version = "1.0.0", + DefaultInputModes = ["text"], + DefaultOutputModes = ["text"], + Capabilities = capabilities, + Skills = [invoiceQuery], + }; + } + + #region private + private readonly ILogger _logger; + #endregion +} diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/PolicyAgent.cs b/dotnet/samples/Demos/A2AClientServer/A2AServer/ChatCompletion/PolicyAgent.cs similarity index 57% rename from dotnet/samples/Demos/A2AClientServer/A2AServer/PolicyAgent.cs rename to dotnet/samples/Demos/A2AClientServer/A2AServer/ChatCompletion/PolicyAgent.cs index e36e42aa22ca..ec3c6a0a12a2 100644 --- a/dotnet/samples/Demos/A2AClientServer/A2AServer/PolicyAgent.cs +++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/ChatCompletion/PolicyAgent.cs @@ -9,13 +9,62 @@ namespace A2A; internal sealed class PolicyAgent : A2AHostAgent { - internal PolicyAgent(string modelId, string apiKey, ILogger logger) : base(logger) + internal PolicyAgent(ILogger logger) : base(logger) { this._logger = logger; // Add TextSearch over the shipping policies + } + + public void InitializeAgent(string modelId, string apiKey) + { + try + { + this._logger.LogInformation("Initializing PolicyAgent with model {ModelId}", modelId); + + // Define the TravelPlannerAgent + var builder = Kernel.CreateBuilder(); + builder.AddOpenAIChatCompletion(modelId, apiKey); + //builder.Plugins.AddFromObject(this._policyPlugin); + var kernel = builder.Build(); + + this.Agent = new ChatCompletionAgent() + { + Kernel = kernel, + Name = "PolicyAgent", + Instructions = + """ + You specialize in handling queries related to policies and customer communications. + + Always reply with exactly this text: + + Policy: Short Shipment Dispute Handling Policy V2.1 + + Summary: "For short shipments reported by customers, first verify internal shipment records + (SAP) and physical logistics scan data (BigQuery). If discrepancy is confirmed and logistics data + shows fewer items packed than invoiced, issue a credit for the missing items. Document the + resolution in SAP CRM and notify the customer via email within 2 business days, referencing the + original invoice and the credit memo number. Use the 'Formal Credit Notification' email + template." + Always reply with exactly this text: + + Policy: Short Shipment Dispute Handling Policy V2.1 - this.Agent = this.InitializeAgent(modelId, apiKey); + Summary: "For short shipments reported by customers, first verify internal shipment records + (SAP) and physical logistics scan data (BigQuery). If discrepancy is confirmed and logistics data + shows fewer items packed than invoiced, issue a credit for the missing items. Document the + resolution in SAP CRM and notify the customer via email within 2 business days, referencing the + original invoice and the credit memo number. Use the 'Formal Credit Notification' email + template." + """, + Arguments = new KernelArguments(new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }), + }; + } + catch (Exception ex) + { + this._logger.LogError(ex, "Failed to initialize PolicyAgent"); + throw; + } } public override AgentCard GetAgentCard(string agentUrl) @@ -53,35 +102,6 @@ public override AgentCard GetAgentCard(string agentUrl) #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.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 } @@ -101,29 +121,3 @@ 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 List GetPolicies => this._policies; - - public void AddPolicy(ShippingPolicy policy) - { - this._policies.Add(policy); - } -} diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/CurrencyAgent.cs b/dotnet/samples/Demos/A2AClientServer/A2AServer/Plugins/CurrencyPlugin.cs similarity index 59% rename from dotnet/samples/Demos/A2AClientServer/A2AServer/CurrencyAgent.cs rename to dotnet/samples/Demos/A2AClientServer/A2AServer/Plugins/CurrencyPlugin.cs index 32fc26e8f86c..8b39ab7a7594 100644 --- a/dotnet/samples/Demos/A2AClientServer/A2AServer/CurrencyAgent.cs +++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/Plugins/CurrencyPlugin.cs @@ -1,104 +1,14 @@ // 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 Polly.Retry; -using SharpA2A.Core; namespace A2A; -internal sealed class CurrencyAgent : A2AHostAgent, IDisposable -{ - 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 void Dispose() - { - this._httpClient.Dispose(); - } - - 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. diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/InvoiceAgent.cs b/dotnet/samples/Demos/A2AClientServer/A2AServer/Plugins/InvoiceQueryPlugin.cs similarity index 55% rename from dotnet/samples/Demos/A2AClientServer/A2AServer/InvoiceAgent.cs rename to dotnet/samples/Demos/A2AClientServer/A2AServer/Plugins/InvoiceQueryPlugin.cs index e508ed6f0661..453f339005f8 100644 --- a/dotnet/samples/Demos/A2AClientServer/A2AServer/InvoiceAgent.cs +++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/Plugins/InvoiceQueryPlugin.cs @@ -1,88 +1,9 @@ // 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 sealed 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. /// @@ -107,13 +28,15 @@ public decimal TotalPrice() public class Invoice { - public int InvoiceId { get; set; } + public string TransactionId { get; set; } + public string InvoiceId { get; set; } public string CompanyName { get; set; } public DateTime InvoiceDate { get; set; } public List Products { get; set; } // List of products - public Invoice(int invoiceId, string companyName, DateTime invoiceDate, List products) + public Invoice(string transactionId, string invoiceId, string companyName, DateTime invoiceDate, List products) { + this.TransactionId = transactionId; this.InvoiceId = invoiceId; this.CompanyName = companyName; this.InvoiceDate = invoiceDate; @@ -136,61 +59,61 @@ public InvoiceQueryPlugin() // Extended mock data with quantities and prices this._invoices = [ - new(1, "Contoso", GetRandomDateWithinLastTwoMonths(), new List + new("TICKET-XYZ987", "INV789", "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("TICKET-XYZ111", "INV111", "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("TICKET-XYZ222", "INV222", "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("TICKET-XYZ333", "INV333", "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("TICKET-XYZ444", "INV444", "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("TICKET-XYZ555", "INV555", "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("TICKET-XYZ666", "INV666", "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("TICKET-XYZ777", "INV777", "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("TICKET-XYZ888", "INV888", "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("TICKET-XYZ999", "INV999", "Contoso", GetRandomDateWithinLastTwoMonths(), new List { new("T-Shirts", 1400, 10.50m), new("Hats", 1100, 9.00m), @@ -233,4 +156,22 @@ public IEnumerable QueryInvoices(string companyName, DateTime? startDat return query.ToList(); } + + [KernelFunction] + [Description("Retrieves invoice using the transaction id")] + public IEnumerable QueryByTransactionId(string transactionId) + { + var query = this._invoices.Where(i => i.TransactionId.Equals(transactionId, StringComparison.OrdinalIgnoreCase)); + + return query.ToList(); + } + + [KernelFunction] + [Description("Retrieves invoice using the invoice id")] + public IEnumerable QueryByInvoiceId(string invoiceId) + { + var query = this._invoices.Where(i => i.InvoiceId.Equals(invoiceId, StringComparison.OrdinalIgnoreCase)); + + return query.ToList(); + } } diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/Program.cs b/dotnet/samples/Demos/A2AClientServer/A2AServer/Program.cs index bb44f83e2612..7f6ab0778cb7 100644 --- a/dotnet/samples/Demos/A2AClientServer/A2AServer/Program.cs +++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/Program.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Reflection; using A2A; using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using SharpA2A.AspNetCore; using SharpA2A.Core; @@ -9,22 +11,64 @@ 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"; +IConfigurationRoot configuration = new ConfigurationBuilder() + .AddEnvironmentVariables() + .AddUserSecrets(Assembly.GetExecutingAssembly()) + .Build(); -var invoiceAgent = new InvoiceAgent(modelId, apiKey, logger); -var invoiceTaskManager = new TaskManager(); -invoiceAgent.Attach(invoiceTaskManager); -app.MapA2A(invoiceTaskManager, "/invoice"); +string? apiKey = configuration["A2AServer:ApiKey"]; +string? connectionString = configuration["A2AServer:ConnectionString"]; +string modelId = configuration["A2AServer:ModelId"] ?? "gpt-4o-mini"; -var currencyAgent = new CurrencyAgent(modelId, apiKey, logger); -var currencyTaskManager = new TaskManager(); -currencyAgent.Attach(currencyTaskManager); -app.MapA2A(currencyTaskManager, "/currency"); +if (!string.IsNullOrEmpty(connectionString)) +{ + var invoiceAgent = new AzureAIInvoiceAgent(logger); + var invoiceAgentId = configuration["A2AServer:InvoiceAgentId"] ?? throw new ArgumentException("A2AServer:InvoiceAgentId must be provided"); + await invoiceAgent.InitializeAgentAsync(modelId, connectionString, invoiceAgentId); + var invoiceTaskManager = new TaskManager(); + invoiceAgent.Attach(invoiceTaskManager); + app.MapA2A(invoiceTaskManager, "/invoice"); + + var policyAgent = new AzureAIPolicyAgent(logger); + var policyAgentId = configuration["A2AServer:PolicyAgentId"] ?? throw new ArgumentException("A2AServer:PolicyAgentId must be provided"); + await policyAgent.InitializeAgentAsync(modelId, connectionString, policyAgentId); + var policyTaskManager = new TaskManager(); + policyAgent.Attach(policyTaskManager); + app.MapA2A(policyTaskManager, "/policy"); + + var logisticsAgent = new AzureAILogisticsAgent(logger); + var logisticsAgentId = configuration["A2AServer:LogisticsAgentId"] ?? throw new ArgumentException("A2AServer:LogisticsAgentId must be provided"); + await logisticsAgent.InitializeAgentAsync(modelId, connectionString, logisticsAgentId); + var logisticsTaskManager = new TaskManager(); + logisticsAgent.Attach(logisticsTaskManager); + app.MapA2A(logisticsTaskManager, "/logistics"); +} +else if (!string.IsNullOrEmpty(apiKey)) +{ + var invoiceAgent = new InvoiceAgent(logger); + invoiceAgent.InitializeAgent(modelId, apiKey); + var invoiceTaskManager = new TaskManager(); + invoiceAgent.Attach(invoiceTaskManager); + app.MapA2A(invoiceTaskManager, "/invoice"); + + var policyAgent = new PolicyAgent(logger); + policyAgent.InitializeAgent(modelId, apiKey); + var policyTaskManager = new TaskManager(); + policyAgent.Attach(policyTaskManager); + app.MapA2A(policyTaskManager, "/policy"); + + var logisticsAgent = new LogisticsAgent(logger); + logisticsAgent.InitializeAgent(modelId, apiKey); + var logisticsTaskManager = new TaskManager(); + logisticsAgent.Attach(logisticsTaskManager); + app.MapA2A(logisticsTaskManager, "/logistics"); +} +else +{ + Console.Error.WriteLine("Either A2AServer:ApiKey or A2AServer:ConnectionString must be provided"); +} await app.RunAsync(); diff --git a/dotnet/samples/Demos/A2AClientServer/README.md b/dotnet/samples/Demos/A2AClientServer/README.md index 281a694cc4a7..7596b89243bd 100644 --- a/dotnet/samples/Demos/A2AClientServer/README.md +++ b/dotnet/samples/Demos/A2AClientServer/README.md @@ -11,10 +11,50 @@ These samples are built with [SharpA2A.Core](https://www.nuget.org/packages/Shar ## Configuring Secrets or Environment Variables -The samples require an OpenAI API key. +The samples can be configured to use chat completion agents or Azure AI agents. -Create an environment variable need `OPENAI_API_KEY` with your OpenAI API key. +### Configuring for use with Chat Completion Agents +Provide your OpenAI API key via .Net secrets + +```bash +dotnet user-secrets set "A2AClient:ApiKey" "..." +``` + +Optionally if you want to use chat completion agents in the server then set the OpenAI key for the server to use. + +```bash +dotnet user-secrets set "A2AServer:ApiKey" "..." +``` + +### Configuring for use with Azure AI Agents + +You must create the agents in an Azure AI Foundry project and then provide the connection string and agents ids + +```bash +dotnet user-secrets set "A2AServer:ConnectionString" "..." +dotnet user-secrets set "A2AServer:InvoiceAgentId" "..." +dotnet user-secrets set "A2AServer:PolicyAgentId" "..." +dotnet user-secrets set "A2AServer:LogisticsAgentId" "..." +``` + +### Configuring Agents for the A2A Client + +The A2A client will connect to remote agents using the A2A protocol. + +By default the client will connect to the invoice, policy and logistics agents provided by the sample A2A Server. + +These are available at the following URL's: + +- http://localhost:5000/policy/ +- http://localhost:5000/invoice/ +- http://localhost:5000/logistics/ + +if you want to change which agents are using then set the agents url's as a space delimited string as follows: + +```bash +dotnet user-secrets set "A2AClient:AgentUrls" "http://localhost:5000/policy/ http://localhost:5000/invoice/ http://localhost:5000/logistics/" +``` ## Run the Sample @@ -30,4 +70,4 @@ To run the sample, follow these steps: cd A2AClient dotnet run ``` -3. Enter your request e.g. "Show me all invoices for Contoso?" \ No newline at end of file +3. Enter your request e.g. "Customer is disputing transaction TICKET-XYZ987 as they claim the received fewer t-shirts than ordered." \ No newline at end of file From 84acb0250513343fc9b3ee49f49a201db6744419 Mon Sep 17 00:00:00 2001 From: markwallace-microsoft <127216156+markwallace-microsoft@users.noreply.github.com> Date: Sat, 17 May 2025 13:53:20 +0100 Subject: [PATCH 13/14] Fix formatting --- .../samples/Demos/A2AClientServer/A2AClient/HostClientAgent.cs | 3 +-- dotnet/samples/Demos/A2AClientServer/README.md | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/dotnet/samples/Demos/A2AClientServer/A2AClient/HostClientAgent.cs b/dotnet/samples/Demos/A2AClientServer/A2AClient/HostClientAgent.cs index ed8723e84072..2224c09993b5 100644 --- a/dotnet/samples/Demos/A2AClientServer/A2AClient/HostClientAgent.cs +++ b/dotnet/samples/Demos/A2AClientServer/A2AClient/HostClientAgent.cs @@ -81,7 +81,7 @@ internal sealed class ConsoleOutputFunctionInvocationFilter() : IFunctionInvocat private static string IndentMultilineString(string multilineText, int indentLevel = 1, int spacesPerIndent = 4) { // Create the indentation string - string indentation = new string(' ', indentLevel * spacesPerIndent); + string indentation = new (' ', indentLevel * spacesPerIndent); // Split the text into lines, add indentation, and rejoin char[] NewLineChars = { '\r', '\n' }; @@ -118,4 +118,3 @@ public async Task OnFunctionInvocationAsync(FunctionInvocationContext context, F Console.ResetColor(); } } - diff --git a/dotnet/samples/Demos/A2AClientServer/README.md b/dotnet/samples/Demos/A2AClientServer/README.md index 7596b89243bd..de61510f67a4 100644 --- a/dotnet/samples/Demos/A2AClientServer/README.md +++ b/dotnet/samples/Demos/A2AClientServer/README.md @@ -34,7 +34,7 @@ You must create the agents in an Azure AI Foundry project and then provide the c ```bash dotnet user-secrets set "A2AServer:ConnectionString" "..." dotnet user-secrets set "A2AServer:InvoiceAgentId" "..." -dotnet user-secrets set "A2AServer:PolicyAgentId" "..." +dotnet user-secrets set "A2AServer:PolicyA:qgentId" "..." dotnet user-secrets set "A2AServer:LogisticsAgentId" "..." ``` From bec3c2d89f666982c2d1fdb860f1f074f8188b9e Mon Sep 17 00:00:00 2001 From: markwallace-microsoft <127216156+markwallace-microsoft@users.noreply.github.com> Date: Sat, 17 May 2025 13:57:37 +0100 Subject: [PATCH 14/14] Fix formatting --- .../samples/Demos/A2AClientServer/A2AClient/HostClientAgent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/samples/Demos/A2AClientServer/A2AClient/HostClientAgent.cs b/dotnet/samples/Demos/A2AClientServer/A2AClient/HostClientAgent.cs index 2224c09993b5..6fb3505c1c92 100644 --- a/dotnet/samples/Demos/A2AClientServer/A2AClient/HostClientAgent.cs +++ b/dotnet/samples/Demos/A2AClientServer/A2AClient/HostClientAgent.cs @@ -81,7 +81,7 @@ internal sealed class ConsoleOutputFunctionInvocationFilter() : IFunctionInvocat private static string IndentMultilineString(string multilineText, int indentLevel = 1, int spacesPerIndent = 4) { // Create the indentation string - string indentation = new (' ', indentLevel * spacesPerIndent); + var indentation = new string(' ', indentLevel * spacesPerIndent); // Split the text into lines, add indentation, and rejoin char[] NewLineChars = { '\r', '\n' };