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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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' }; From 7940e930cf4bdacf122eb0933c606d3a8a1b6fa4 Mon Sep 17 00:00:00 2001 From: markwallace-microsoft <127216156+markwallace-microsoft@users.noreply.github.com> Date: Mon, 19 May 2025 10:56:38 +0100 Subject: [PATCH 15/23] Rename configu property --- dotnet/samples/GettingStartedWithAgents/A2A/Step01_A2AAgent.cs | 2 +- .../samples/InternalUtilities/TestConfiguration.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/samples/GettingStartedWithAgents/A2A/Step01_A2AAgent.cs b/dotnet/samples/GettingStartedWithAgents/A2A/Step01_A2AAgent.cs index 1b66f6cddd7b..178efd4b3f0e 100644 --- a/dotnet/samples/GettingStartedWithAgents/A2A/Step01_A2AAgent.cs +++ b/dotnet/samples/GettingStartedWithAgents/A2A/Step01_A2AAgent.cs @@ -20,7 +20,7 @@ public async Task UseA2AAgent() // Create an A2A agent instance using var httpClient = new HttpClient { - BaseAddress = new Uri(TestConfiguration.A2A.Agent) + BaseAddress = TestConfiguration.A2A.AgentUrl }; var client = new A2AClient(httpClient); var cardResolver = new A2ACardResolver(httpClient); diff --git a/dotnet/src/InternalUtilities/samples/InternalUtilities/TestConfiguration.cs b/dotnet/src/InternalUtilities/samples/InternalUtilities/TestConfiguration.cs index d5efd75000b1..97340d8142ce 100644 --- a/dotnet/src/InternalUtilities/samples/InternalUtilities/TestConfiguration.cs +++ b/dotnet/src/InternalUtilities/samples/InternalUtilities/TestConfiguration.cs @@ -361,6 +361,6 @@ public class BedrockAgentConfig public class A2AConfig { - public string Agent { get; set; } = "http://localhost:5000"; + public Uri AgentUrl { get; set; } = new Uri("http://localhost:5000"); } } From b2439f592556e54883dda27257e557da65f14379 Mon Sep 17 00:00:00 2001 From: markwallace-microsoft <127216156+markwallace-microsoft@users.noreply.github.com> Date: Mon, 19 May 2025 14:09:24 +0100 Subject: [PATCH 16/23] Start work on streaming support --- dotnet/src/Agents/A2A/A2AAgent.cs | 44 +++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Agents/A2A/A2AAgent.cs b/dotnet/src/Agents/A2A/A2AAgent.cs index 468031d58837..c3ec1b623eda 100644 --- a/dotnet/src/Agents/A2A/A2AAgent.cs +++ b/dotnet/src/Agents/A2A/A2AAgent.cs @@ -66,9 +66,42 @@ public override async IAsyncEnumerable> In } /// - public override IAsyncEnumerable> InvokeStreamingAsync(ICollection messages, AgentThread? thread = null, AgentInvokeOptions? options = null, CancellationToken cancellationToken = default) + public override async IAsyncEnumerable> InvokeStreamingAsync(ICollection messages, AgentThread? thread = null, AgentInvokeOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - throw new NotImplementedException(); + Verify.NotNull(messages); + + var agentThread = await this.EnsureThreadExistsWithMessagesAsync( + messages, + thread, + () => new A2AAgentThread(this.Client), + cancellationToken).ConfigureAwait(false); + + // Invoke the agent. + var chatMessages = new ChatHistory(); + var invokeResults = this.InternalInvokeStreamingAsync( + this.AgentCard.Name, + messages, + agentThread, + options ?? new AgentInvokeOptions(), + chatMessages, + cancellationToken); + + // Return the chunks to the caller. + await foreach (var result in invokeResults.ConfigureAwait(false)) + { + yield return new(result, agentThread); + } + + // Notify the thread of any new messages that were assembled from the streaming response. + foreach (var chatMessage in chatMessages) + { + await this.NotifyThreadOfNewMessage(agentThread, chatMessage, cancellationToken).ConfigureAwait(false); + + if (options?.OnIntermediateMessage is not null) + { + await options.OnIntermediateMessage(chatMessage).ConfigureAwait(false); + } + } } /// @@ -139,5 +172,12 @@ private async IAsyncEnumerable> InvokeAgen Console.WriteLine(); } } + + private IAsyncEnumerable> InternalInvokeStreamingAsync(string name, ICollection messages, A2AAgentThread thread, AgentInvokeOptions options, ChatHistory chatMessages, CancellationToken cancellationToken) + { + Verify.NotNull(messages); + + throw new NotImplementedException(); + } #endregion } From 945851136ece7e8642a892cb4fdaa7ffb405a067 Mon Sep 17 00:00:00 2001 From: markwallace-microsoft <127216156+markwallace-microsoft@users.noreply.github.com> Date: Tue, 10 Jun 2025 17:56:49 +0100 Subject: [PATCH 17/23] Update the .slnx file --- dotnet/SK-dotnet.slnx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dotnet/SK-dotnet.slnx b/dotnet/SK-dotnet.slnx index b24a49cbd285..9f7dbcab4973 100644 --- a/dotnet/SK-dotnet.slnx +++ b/dotnet/SK-dotnet.slnx @@ -47,6 +47,10 @@ + + + + @@ -85,6 +89,7 @@ + From 6844e8b749f6d94fa282e10b615571a10fcc448f Mon Sep 17 00:00:00 2001 From: markwallace-microsoft <127216156+markwallace-microsoft@users.noreply.github.com> Date: Wed, 11 Jun 2025 13:33:22 +0100 Subject: [PATCH 18/23] Update to new client API --- .../A2AServer/AzureAI/AzureAICurrencyAgent.cs | 10 +++++----- .../A2AServer/AzureAI/AzureAIInvoiceAgent.cs | 8 ++++---- .../A2AServer/AzureAI/AzureAILogisticsAgent.cs | 8 ++++---- .../A2AServer/AzureAI/AzureAIPolicyAgent.cs | 8 ++++---- .../samples/Demos/A2AClientServer/A2AServer/Program.cs | 10 +++++----- .../samples/InternalUtilities/TestConfiguration.cs | 2 +- 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAICurrencyAgent.cs b/dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAICurrencyAgent.cs index df7a79d602af..6eff54059efa 100644 --- a/dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAICurrencyAgent.cs +++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAICurrencyAgent.cs @@ -1,4 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. +using Azure.AI.Agents.Persistent; using Azure.Identity; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; @@ -26,20 +27,19 @@ public void Dispose() if (this.Agent is AzureAIAgent azureAIAgent && azureAIAgent is not null) { - azureAIAgent.Client.DeleteAgent(azureAIAgent.Id); + azureAIAgent.Client.Administration.DeleteAgent(azureAIAgent.Id); } } - public async Task InitializeAgentAsync(string modelId, string connectionString) + public async Task InitializeAgentAsync(string modelId, string endpoint) { 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( + var agentsClient = new PersistentAgentsClient(endpoint, new AzureCliCredential()); + PersistentAgent definition = await agentsClient.Administration.CreateAgentAsync( modelId, "CurrencyAgent", null, diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAIInvoiceAgent.cs b/dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAIInvoiceAgent.cs index 10f81be6fe11..9f22a8ad3d7e 100644 --- a/dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAIInvoiceAgent.cs +++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAIInvoiceAgent.cs @@ -1,4 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. +using Azure.AI.Agents.Persistent; using Azure.Identity; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; @@ -15,16 +16,15 @@ internal AzureAIInvoiceAgent(ILogger logger) : base(logger) this._logger = logger; } - public async Task InitializeAgentAsync(string modelId, string connectionString, string assistantId) + public async Task InitializeAgentAsync(string modelId, string endpoint, 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); + var agentsClient = new PersistentAgentsClient(endpoint, new AzureCliCredential()); + PersistentAgent definition = await agentsClient.Administration.GetAgentAsync(assistantId); this.Agent = new AzureAIAgent(definition, agentsClient); this.Agent.Kernel.Plugins.Add(KernelPluginFactory.CreateFromType()); diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAILogisticsAgent.cs b/dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAILogisticsAgent.cs index 7c9181eaf8be..099aca27a238 100644 --- a/dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAILogisticsAgent.cs +++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAILogisticsAgent.cs @@ -1,4 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. +using Azure.AI.Agents.Persistent; using Azure.Identity; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.Agents.A2A; @@ -14,16 +15,15 @@ internal AzureAILogisticsAgent(ILogger logger) : base(logger) this._logger = logger; } - public async Task InitializeAgentAsync(string modelId, string connectionString, string assistantId) + public async Task InitializeAgentAsync(string modelId, string endpoint, 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); + var agentsClient = new PersistentAgentsClient(endpoint, new AzureCliCredential()); + PersistentAgent definition = await agentsClient.Administration.GetAgentAsync(assistantId); this.Agent = new AzureAIAgent(definition, agentsClient); } diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAIPolicyAgent.cs b/dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAIPolicyAgent.cs index 136674a79922..9b0d5f2b60d8 100644 --- a/dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAIPolicyAgent.cs +++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAIPolicyAgent.cs @@ -1,4 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. +using Azure.AI.Agents.Persistent; using Azure.Identity; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.Agents.A2A; @@ -14,16 +15,15 @@ internal AzureAIPolicyAgent(ILogger logger) : base(logger) this._logger = logger; } - public async Task InitializeAgentAsync(string modelId, string connectionString, string assistantId) + public async Task InitializeAgentAsync(string modelId, string endpoint, 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); + var agentsClient = new PersistentAgentsClient(endpoint, new AzureCliCredential()); + PersistentAgent definition = await agentsClient.Administration.GetAgentAsync(assistantId); this.Agent = new AzureAIAgent(definition, agentsClient); } diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/Program.cs b/dotnet/samples/Demos/A2AClientServer/A2AServer/Program.cs index 7f6ab0778cb7..58b1726cdafd 100644 --- a/dotnet/samples/Demos/A2AClientServer/A2AServer/Program.cs +++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/Program.cs @@ -20,28 +20,28 @@ .Build(); string? apiKey = configuration["A2AServer:ApiKey"]; -string? connectionString = configuration["A2AServer:ConnectionString"]; +string? endpoint = configuration["A2AServer:Endpoint"]; string modelId = configuration["A2AServer:ModelId"] ?? "gpt-4o-mini"; -if (!string.IsNullOrEmpty(connectionString)) +if (!string.IsNullOrEmpty(endpoint)) { var invoiceAgent = new AzureAIInvoiceAgent(logger); var invoiceAgentId = configuration["A2AServer:InvoiceAgentId"] ?? throw new ArgumentException("A2AServer:InvoiceAgentId must be provided"); - await invoiceAgent.InitializeAgentAsync(modelId, connectionString, invoiceAgentId); + await invoiceAgent.InitializeAgentAsync(modelId, endpoint, 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); + await policyAgent.InitializeAgentAsync(modelId, endpoint, 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); + await logisticsAgent.InitializeAgentAsync(modelId, endpoint, logisticsAgentId); var logisticsTaskManager = new TaskManager(); logisticsAgent.Attach(logisticsTaskManager); app.MapA2A(logisticsTaskManager, "/logistics"); diff --git a/dotnet/src/InternalUtilities/samples/InternalUtilities/TestConfiguration.cs b/dotnet/src/InternalUtilities/samples/InternalUtilities/TestConfiguration.cs index 9d8082b23142..1228cfc1d05e 100644 --- a/dotnet/src/InternalUtilities/samples/InternalUtilities/TestConfiguration.cs +++ b/dotnet/src/InternalUtilities/samples/InternalUtilities/TestConfiguration.cs @@ -364,7 +364,7 @@ public class A2AConfig { public Uri AgentUrl { get; set; } = new Uri("http://localhost:5000"); } - + public class Mem0Config { public string? BaseAddress { get; set; } From bf32517b7ffa7d71fce4d3c74ce184df6e46c114 Mon Sep 17 00:00:00 2001 From: markwallace-microsoft <127216156+markwallace-microsoft@users.noreply.github.com> Date: Thu, 19 Jun 2025 10:05:56 +0100 Subject: [PATCH 19/23] Upgrade to latest SharpA2A --- dotnet/Directory.Packages.props | 4 +- .../A2AClientServer/A2AClient/Program.cs | 2 +- .../A2AClientServer/A2AServer/A2AServer.http | 32 ++++--- .../A2AServer/AzureAI/AzureAIHostAgent.cs | 46 ++++++++++ .../A2AServer/AzureAI/AzureAIInvoiceAgent.cs | 32 +------ .../AzureAI/AzureAILogisticsAgent.cs | 30 +------ .../A2AServer/AzureAI/AzureAIPolicyAgent.cs | 30 +------ .../ChatCompletion/ChatCompletionHostAgent.cs | 69 ++++++++++++++ .../A2AServer/ChatCompletion/InvoiceAgent.cs | 42 +-------- .../ChatCompletion/LogisticsAgent.cs | 54 +++-------- .../A2AServer/ChatCompletion/PolicyAgent.cs | 89 ++++++------------- .../A2AClientServer/A2AServer/Program.cs | 72 +++++++-------- .../GettingStartedWithAgents.csproj | 6 +- dotnet/src/Agents/A2A/A2AAgent.cs | 44 ++++++--- .../A2A/Extensions/AuthorRoleExtensions.cs | 33 +++++++ 15 files changed, 287 insertions(+), 298 deletions(-) create mode 100644 dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAIHostAgent.cs create mode 100644 dotnet/samples/Demos/A2AClientServer/A2AServer/ChatCompletion/ChatCompletionHostAgent.cs create mode 100644 dotnet/src/Agents/A2A/Extensions/AuthorRoleExtensions.cs diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 1163facf1720..1010b3c6920b 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -102,8 +102,8 @@ - - + + diff --git a/dotnet/samples/Demos/A2AClientServer/A2AClient/Program.cs b/dotnet/samples/Demos/A2AClientServer/A2AClient/Program.cs index 9213884d71a3..0002ab0f293d 100644 --- a/dotnet/samples/Demos/A2AClientServer/A2AClient/Program.cs +++ b/dotnet/samples/Demos/A2AClientServer/A2AClient/Program.cs @@ -45,7 +45,7 @@ private static async System.Threading.Tasks.Task RunCliAsync() .Build(); var apiKey = configRoot["A2AClient:ApiKey"] ?? throw new ArgumentException("A2AClient:ApiKey must be provided"); var modelId = configRoot["A2AClient:ModelId"] ?? "gpt-4.1"; - var agentUrls = configRoot["A2AClient:AgentUrls"] ?? "http://localhost:5000/policy/ http://localhost:5000/invoice/ http://localhost:5000/logistics/"; + var agentUrls = configRoot["A2AClient:AgentUrls"] ?? "http://localhost:5000/ http://localhost:5001/ http://localhost:5002/"; // Create the Host agent var hostAgent = new HostClientAgent(logger); diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.http b/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.http index 67e3d5699858..5c6b3cf01833 100644 --- a/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.http +++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.http @@ -1,23 +1,27 @@ -@host = http://localhost:5000 +### Each A2A agent is available at a different host address +@hostInvoice = http://localhost:5000 +@hostPolicy = http://localhost:5001 +@hostLogistics = http://localhost:5002 ### Query agent card for the invoice agent -GET {{host}}/invoice/.well-known/agent.json +GET {{hostInvoice}}/.well-known/agent.json ### Send a task to the invoice agent -POST {{host}}/invoice +POST {{hostInvoice}} Content-Type: application/json { "id": "1", "jsonrpc": "2.0", - "method": "task/send", + "method": "message/send", "params": { "id": "12345", "message": { "role": "user", + "messageId": "msg_1", "parts": [ { - "type": "text", + "kind": "text", "text": "Show me all invoices for Contoso?" } ] @@ -26,23 +30,24 @@ Content-Type: application/json } ### Query agent card for the policy agent -GET {{host}}/policy/.well-known/agent.json +GET {{hostPolicy}}/.well-known/agent.json ### Send a task to the policy agent -POST {{host}}/policy +POST {{hostPolicy}} Content-Type: application/json { "id": "1", "jsonrpc": "2.0", - "method": "task/send", + "method": "message/send", "params": { "id": "12345", "message": { "role": "user", + "messageId": "msg_1", "parts": [ { - "type": "text", + "kind": "text", "text": "What is the policy for short shipments?" } ] @@ -51,23 +56,24 @@ Content-Type: application/json } ### Query agent card for the logistics agent -GET {{host}}/logistics/.well-known/agent.json +GET {{hostLogistics}}/.well-known/agent.json ### Send a task to the logistics agent -POST {{host}}/logistics +POST {{hostLogistics}} Content-Type: application/json { "id": "1", "jsonrpc": "2.0", - "method": "task/send", + "method": "message/send", "params": { "id": "12345", "message": { "role": "user", + "messageId": "msg_1", "parts": [ { - "type": "text", + "kind": "text", "text": "What is the status for SHPMT-SAP-001" } ] diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAIHostAgent.cs b/dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAIHostAgent.cs new file mode 100644 index 000000000000..02de6d066e73 --- /dev/null +++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAIHostAgent.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Azure.AI.Agents.Persistent; +using Azure.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents.A2A; +using Microsoft.SemanticKernel.Agents.AzureAI; + +namespace A2A; + +internal abstract class AzureAIHostAgent : A2AHostAgent +{ + /// + /// Optional: The plugins associated with the agent. + /// + protected IEnumerable? Plugins { get; set; } + + internal AzureAIHostAgent(ILogger logger) : base(logger) + { + this._logger = logger; + } + + public async Task InitializeAgentAsync(string modelId, string endpoint, string assistantId) + { + try + { + this._logger.LogInformation("Initializing AzureAIPolicyAgent {AssistantId}", assistantId); + + // Define the InvoiceAgent + var agentsClient = new PersistentAgentsClient(endpoint, new AzureCliCredential()); + PersistentAgent definition = await agentsClient.Administration.GetAgentAsync(assistantId); + + this.Agent = new AzureAIAgent(definition, agentsClient, this.Plugins); + } + catch (Exception ex) + { + this._logger.LogError(ex, "Failed to initialize AzureAIHostAgent"); + throw; + } + } + + #region private + 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 index 9f22a8ad3d7e..e6806493b53b 100644 --- a/dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAIInvoiceAgent.cs +++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAIInvoiceAgent.cs @@ -1,39 +1,13 @@ // Copyright (c) Microsoft. All rights reserved. -using Azure.AI.Agents.Persistent; -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 sealed class AzureAIInvoiceAgent : AzureAIHostAgent { internal AzureAIInvoiceAgent(ILogger logger) : base(logger) { - this._logger = logger; - } - - public async Task InitializeAgentAsync(string modelId, string endpoint, string assistantId) - { - try - { - this._logger.LogInformation("Initializing AzureAIInvoiceAgent {AssistantId}", assistantId); - - // Define the InvoiceAgent - var agentsClient = new PersistentAgentsClient(endpoint, new AzureCliCredential()); - PersistentAgent definition = await agentsClient.Administration.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) @@ -73,8 +47,4 @@ public override AgentCard GetAgentCard(string agentUrl) 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 index 099aca27a238..c4bd525d08cb 100644 --- a/dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAILogisticsAgent.cs +++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAILogisticsAgent.cs @@ -1,37 +1,13 @@ // Copyright (c) Microsoft. All rights reserved. -using Azure.AI.Agents.Persistent; -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 sealed class AzureAILogisticsAgent : AzureAIHostAgent { internal AzureAILogisticsAgent(ILogger logger) : base(logger) { - this._logger = logger; - } - - public async Task InitializeAgentAsync(string modelId, string endpoint, string assistantId) - { - try - { - this._logger.LogInformation("Initializing AzureAILogisticsAgent {AssistantId}", assistantId); - - // Define the InvoiceAgent - var agentsClient = new PersistentAgentsClient(endpoint, new AzureCliCredential()); - PersistentAgent definition = await agentsClient.Administration.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) @@ -71,8 +47,4 @@ public override AgentCard GetAgentCard(string agentUrl) 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 index 9b0d5f2b60d8..682e3edc6aed 100644 --- a/dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAIPolicyAgent.cs +++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAIPolicyAgent.cs @@ -1,37 +1,13 @@ // Copyright (c) Microsoft. All rights reserved. -using Azure.AI.Agents.Persistent; -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 sealed class AzureAIPolicyAgent : AzureAIHostAgent { internal AzureAIPolicyAgent(ILogger logger) : base(logger) { - this._logger = logger; - } - - public async Task InitializeAgentAsync(string modelId, string endpoint, string assistantId) - { - try - { - this._logger.LogInformation("Initializing AzureAIPolicyAgent {AssistantId}", assistantId); - - // Define the InvoiceAgent - var agentsClient = new PersistentAgentsClient(endpoint, new AzureCliCredential()); - PersistentAgent definition = await agentsClient.Administration.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) @@ -71,8 +47,4 @@ public override AgentCard GetAgentCard(string agentUrl) Skills = [invoiceQuery], }; } - - #region private - private readonly ILogger _logger; - #endregion } diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/ChatCompletion/ChatCompletionHostAgent.cs b/dotnet/samples/Demos/A2AClientServer/A2AServer/ChatCompletion/ChatCompletionHostAgent.cs new file mode 100644 index 000000000000..81aef92c4c04 --- /dev/null +++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/ChatCompletion/ChatCompletionHostAgent.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.A2A; + +namespace A2A; + +internal abstract class ChatCompletionHostAgent : A2AHostAgent +{ + internal ChatCompletionHostAgent(ILogger logger) : base(logger) + { + this._logger = logger; + } + + /// + /// The name of the agent; + /// + protected string Name { get; set; } = "ChatCompletionHostAgent"; + + /// + /// The instructions for the agent; + /// + protected string Instructions { get; set; } = string.Empty; + + /// + /// Optional: The plugins associated with the agent. + /// + protected IEnumerable? Plugins { get; set; } + + public void InitializeAgent(string modelId, string apiKey) + { + try + { + this._logger.LogInformation("Initializing ChatCompletionHostAgent with model {ModelId}", modelId); + + // Define the InvoiceAgent + var builder = Kernel.CreateBuilder(); + builder.AddOpenAIChatCompletion(modelId, apiKey); + if (this.Plugins is not null) + { + foreach (var plugin in this.Plugins) + { + builder.Plugins.Add(plugin); + } + } + var kernel = builder.Build(); + + this.Agent = new ChatCompletionAgent() + { + Kernel = kernel, + Name = this.Name, + Instructions = this.Instructions, + Arguments = new KernelArguments(new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }), + }; + } + catch (Exception ex) + { + this._logger.LogError(ex, "Failed to initialize ChatCompletionHostAgent"); + throw; + } + } + + #region private + 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 index f58e1a5edd65..b109f95f2053 100644 --- a/dotnet/samples/Demos/A2AClientServer/A2AServer/ChatCompletion/InvoiceAgent.cs +++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/ChatCompletion/InvoiceAgent.cs @@ -1,47 +1,17 @@ // 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 sealed class InvoiceAgent : ChatCompletionHostAgent { 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; - } + this.Name = "InvoiceAgent"; + this.Instructions = "You specialize in handling queries related to invoices."; + this.Plugins = [KernelPluginFactory.CreateFromType()]; } public override AgentCard GetAgentCard(string agentUrl) @@ -76,8 +46,4 @@ public override AgentCard GetAgentCard(string agentUrl) 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 index 00d076c6e64e..846fec21f774 100644 --- a/dotnet/samples/Demos/A2AClientServer/A2AServer/ChatCompletion/LogisticsAgent.cs +++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/ChatCompletion/LogisticsAgent.cs @@ -1,54 +1,24 @@ // 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 sealed class LogisticsAgent : ChatCompletionHostAgent { internal LogisticsAgent(ILogger logger) : base(logger) { - this._logger = logger; + this.Name = "LogisticsAgent"; + this.Instructions = + """ + You specialize in handling queries related to logistics. - // 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(); + Always reply with exactly: - 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; - } + Shipment number: SHPMT-SAP-001 + Item: TSHIRT-RED-L + Quantity: 900 + """; } public override AgentCard GetAgentCard(string agentUrl) @@ -83,8 +53,4 @@ public override AgentCard GetAgentCard(string agentUrl) Skills = [invoiceQuery], }; } - - #region private - private readonly ILogger _logger; - #endregion } diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/ChatCompletion/PolicyAgent.cs b/dotnet/samples/Demos/A2AClientServer/A2AServer/ChatCompletion/PolicyAgent.cs index ec3c6a0a12a2..e25924895ea2 100644 --- a/dotnet/samples/Demos/A2AClientServer/A2AServer/ChatCompletion/PolicyAgent.cs +++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/ChatCompletion/PolicyAgent.cs @@ -1,70 +1,39 @@ // 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 PolicyAgent : A2AHostAgent +internal sealed class PolicyAgent : ChatCompletionHostAgent { 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 - - 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; - } + this.Name = "PolicyAgent"; + this.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 + + 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." + """; } public override AgentCard GetAgentCard(string agentUrl) @@ -99,10 +68,6 @@ public override AgentCard GetAgentCard(string agentUrl) Skills = [invoiceQuery], }; } - - #region private - private readonly ILogger _logger; - #endregion } public class ShippingPolicy diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/Program.cs b/dotnet/samples/Demos/A2AClientServer/A2AServer/Program.cs index 58b1726cdafd..2606c404d3ad 100644 --- a/dotnet/samples/Demos/A2AClientServer/A2AServer/Program.cs +++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/Program.cs @@ -7,6 +7,21 @@ using SharpA2A.AspNetCore; using SharpA2A.Core; +string agentId = string.Empty; +string agentType = string.Empty; + +for (var i = 0; i < args.Length; i++) +{ + if (args[i].StartsWith("--agentId", StringComparison.InvariantCultureIgnoreCase) && i + 1 < args.Length) + { + agentId = args[++i]; + } + else if (args[i].StartsWith("--agentType", StringComparison.InvariantCultureIgnoreCase) && i + 1 < args.Length) + { + agentType = args[++i]; + } +} + var builder = WebApplication.CreateBuilder(args); builder.Services.AddHttpClient().AddLogging(); var app = builder.Build(); @@ -25,46 +40,33 @@ if (!string.IsNullOrEmpty(endpoint)) { - var invoiceAgent = new AzureAIInvoiceAgent(logger); - var invoiceAgentId = configuration["A2AServer:InvoiceAgentId"] ?? throw new ArgumentException("A2AServer:InvoiceAgentId must be provided"); - await invoiceAgent.InitializeAgentAsync(modelId, endpoint, invoiceAgentId); - var invoiceTaskManager = new TaskManager(); - invoiceAgent.Attach(invoiceTaskManager); - app.MapA2A(invoiceTaskManager, "/invoice"); + AzureAIHostAgent hostAgent = agentType.ToUpperInvariant() switch + { + "INVOICE" => new AzureAIInvoiceAgent(logger), + "POLICY" => new AzureAIPolicyAgent(logger), + "LOGISTICS" => new AzureAILogisticsAgent(logger), + _ => throw new ArgumentException($"Unsupported agent type: {agentType}"), + }; - var policyAgent = new AzureAIPolicyAgent(logger); - var policyAgentId = configuration["A2AServer:PolicyAgentId"] ?? throw new ArgumentException("A2AServer:PolicyAgentId must be provided"); - await policyAgent.InitializeAgentAsync(modelId, endpoint, 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, endpoint, logisticsAgentId); - var logisticsTaskManager = new TaskManager(); - logisticsAgent.Attach(logisticsTaskManager); - app.MapA2A(logisticsTaskManager, "/logistics"); + await hostAgent.InitializeAgentAsync(modelId, endpoint, agentId); + var taskManager = new TaskManager(); + hostAgent.Attach(taskManager); + app.MapA2A(taskManager, ""); } 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"); + ChatCompletionHostAgent hostAgent = agentType.ToUpperInvariant() switch + { + "INVOICE" => new InvoiceAgent(logger), + "POLICY" => new PolicyAgent(logger), + "LOGISTICS" => new LogisticsAgent(logger), + _ => throw new ArgumentException($"Unsupported agent type: {agentType}"), + }; - 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"); + hostAgent.InitializeAgent(modelId, apiKey); + var invoiceTaskManager = new TaskManager(); + hostAgent.Attach(invoiceTaskManager); + app.MapA2A(invoiceTaskManager, ""); } else { diff --git a/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj b/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj index 406b061e61d6..c3cbe41ccdba 100644 --- a/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj +++ b/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj @@ -23,11 +23,11 @@ - + - - + + diff --git a/dotnet/src/Agents/A2A/A2AAgent.cs b/dotnet/src/Agents/A2A/A2AAgent.cs index c3ec1b623eda..3ea6df7b9307 100644 --- a/dotnet/src/Agents/A2A/A2AAgent.cs +++ b/dotnet/src/Agents/A2A/A2AAgent.cs @@ -139,13 +139,12 @@ private async IAsyncEnumerable> InternalIn private async IAsyncEnumerable> InvokeAgentAsync(string name, ChatMessageContent message, A2AAgentThread thread, AgentInvokeOptions options, [EnumeratorCancellation] CancellationToken cancellationToken) { - var taskSendParams = new TaskSendParams + var messageSendParams = new MessageSendParams { - Id = thread.Id!, - SessionId = thread.SessionId, Message = new Message { - Role = message.Role.ToString(), + MessageId = Guid.NewGuid().ToString(), + Role = message.Role.ToMessageRole(), Parts = [ new TextPart @@ -156,20 +155,43 @@ private async IAsyncEnumerable> InvokeAgen } }; - var agentTask = await this.Client.Send(taskSendParams).ConfigureAwait(false); - if (agentTask.Artifacts != null && agentTask.Artifacts.Count > 0) + A2AResponse response = await this.Client.SendMessageAsync(messageSendParams).ConfigureAwait(false); + if (response is AgentTask agentTask) { - foreach (var artifact in agentTask.Artifacts) + if (agentTask.Artifacts != null && agentTask.Artifacts.Count > 0) { - foreach (var part in artifact.Parts) + foreach (var artifact in agentTask.Artifacts) { - if (part is TextPart textPart) + foreach (var part in artifact.Parts) { - yield return new AgentResponseItem(new ChatMessageContent(AuthorRole.Assistant, textPart.Text), thread); + if (part is TextPart textPart) + { + yield return new AgentResponseItem(new ChatMessageContent(AuthorRole.Assistant, textPart.Text), thread); + } } } + Console.WriteLine(); } - Console.WriteLine(); + } + else if (response is Message messageResponse) + { + foreach (var part in messageResponse.Parts) + { + if (part is TextPart textPart) + { + yield return new AgentResponseItem( + new ChatMessageContent( + AuthorRole.Assistant, + textPart.Text, + encoding: message.Encoding, + metadata: message.Metadata), + thread); + } + } + } + else + { + throw new InvalidOperationException("Unexpected response type from A2A client."); } } diff --git a/dotnet/src/Agents/A2A/Extensions/AuthorRoleExtensions.cs b/dotnet/src/Agents/A2A/Extensions/AuthorRoleExtensions.cs new file mode 100644 index 000000000000..211a3cc9494d --- /dev/null +++ b/dotnet/src/Agents/A2A/Extensions/AuthorRoleExtensions.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Microsoft.SemanticKernel.ChatCompletion; +using SharpA2A.Core; + +namespace Microsoft.SemanticKernel.Agents.A2A; + +/// +/// Extensions for converting between amd . +/// +internal static class AuthorRoleExtensions +{ + public static AuthorRole ToAuthorRole(this MessageRole role) + { + return role switch + { + MessageRole.User => AuthorRole.User, + MessageRole.Agent => AuthorRole.Assistant, + _ => throw new ArgumentOutOfRangeException(nameof(role), role, "Invalid message role") + }; + } + + public static MessageRole ToMessageRole(this AuthorRole role) + { + return role.Label switch + { + "user" => MessageRole.User, + "assistant" => MessageRole.Agent, + _ => throw new ArgumentOutOfRangeException(nameof(role), role, "Invalid author role") + }; + } +} From a05b44d386b78f6cccbc0472b6b5e0fb194b5520 Mon Sep 17 00:00:00 2001 From: markwallace-microsoft <127216156+markwallace-microsoft@users.noreply.github.com> Date: Thu, 19 Jun 2025 13:45:46 +0100 Subject: [PATCH 20/23] Cleanup samples and test --- .../A2AClient/HostClientAgent.cs | 3 +- .../A2AClientServer/A2AServer/A2AServer.http | 2 +- .../A2AServer/AzureAI/AzureAICurrencyAgent.cs | 103 ------------------ .../A2AServer/AzureAI/AzureAIInvoiceAgent.cs | 2 + .../A2AServer/ChatCompletion/CurrencyAgent.cs | 94 ---------------- .../A2AServer/ChatCompletion/InvoiceAgent.cs | 2 +- .../ChatCompletion/LogisticsAgent.cs | 2 +- 7 files changed, 7 insertions(+), 201 deletions(-) delete mode 100644 dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAICurrencyAgent.cs delete mode 100644 dotnet/samples/Demos/A2AClientServer/A2AServer/ChatCompletion/CurrencyAgent.cs diff --git a/dotnet/samples/Demos/A2AClientServer/A2AClient/HostClientAgent.cs b/dotnet/samples/Demos/A2AClientServer/A2AClient/HostClientAgent.cs index 6fb3505c1c92..433e59354e74 100644 --- a/dotnet/samples/Demos/A2AClientServer/A2AClient/HostClientAgent.cs +++ b/dotnet/samples/Demos/A2AClientServer/A2AClient/HostClientAgent.cs @@ -107,13 +107,14 @@ public async Task OnFunctionInvocationAsync(FunctionInvocationContext context, F { Console.ForegroundColor = ConsoleColor.DarkGray; - Console.WriteLine($"Got Response from Agent {context.Function.Name}:"); + Console.WriteLine($"Response from Agent {context.Function.Name}:"); foreach (var message in chatMessages) { Console.ForegroundColor = ConsoleColor.Gray; Console.WriteLine(IndentMultilineString($"{message}")); } + Console.WriteLine($"Finished processing response from Agent {context.Function.Name}.\n"); } Console.ResetColor(); } diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.http b/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.http index 5c6b3cf01833..746e57358e52 100644 --- a/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.http +++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/A2AServer.http @@ -74,7 +74,7 @@ Content-Type: application/json "parts": [ { "kind": "text", - "text": "What is the status for SHPMT-SAP-001" + "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 deleted file mode 100644 index 6eff54059efa..000000000000 --- a/dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAICurrencyAgent.cs +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using Azure.AI.Agents.Persistent; -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.Administration.DeleteAgent(azureAIAgent.Id); - } - } - - public async Task InitializeAgentAsync(string modelId, string endpoint) - { - try - { - this._logger.LogInformation("Initializing Semantic Kernel agent with model {ModelId}", modelId); - - // Define the CurrencyAgent - var agentsClient = new PersistentAgentsClient(endpoint, new AzureCliCredential()); - PersistentAgent definition = await agentsClient.Administration.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 index e6806493b53b..799658938f92 100644 --- a/dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAIInvoiceAgent.cs +++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/AzureAI/AzureAIInvoiceAgent.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel; using SharpA2A.Core; namespace A2A; @@ -8,6 +9,7 @@ internal sealed class AzureAIInvoiceAgent : AzureAIHostAgent { internal AzureAIInvoiceAgent(ILogger logger) : base(logger) { + this.Plugins = [KernelPluginFactory.CreateFromType()]; } public override AgentCard GetAgentCard(string agentUrl) diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/ChatCompletion/CurrencyAgent.cs b/dotnet/samples/Demos/A2AClientServer/A2AServer/ChatCompletion/CurrencyAgent.cs deleted file mode 100644 index 98c2558634a2..000000000000 --- a/dotnet/samples/Demos/A2AClientServer/A2AServer/ChatCompletion/CurrencyAgent.cs +++ /dev/null @@ -1,94 +0,0 @@ -// 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 index b109f95f2053..5ffcff2f4f29 100644 --- a/dotnet/samples/Demos/A2AClientServer/A2AServer/ChatCompletion/InvoiceAgent.cs +++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/ChatCompletion/InvoiceAgent.cs @@ -30,7 +30,7 @@ public override AgentCard GetAgentCard(string agentUrl) Tags = ["invoice", "semantic-kernel"], Examples = [ - "List the latest invoices for Contoso.", + "List the latest invoices for Contoso?", ], }; diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/ChatCompletion/LogisticsAgent.cs b/dotnet/samples/Demos/A2AClientServer/A2AServer/ChatCompletion/LogisticsAgent.cs index 846fec21f774..01c2c3aa0cd8 100644 --- a/dotnet/samples/Demos/A2AClientServer/A2AServer/ChatCompletion/LogisticsAgent.cs +++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/ChatCompletion/LogisticsAgent.cs @@ -37,7 +37,7 @@ public override AgentCard GetAgentCard(string agentUrl) Tags = ["logistics", "semantic-kernel"], Examples = [ - "What is the status for SHPMT-SAP-001", + "What is the status for SHPMT-SAP-001?", ], }; From ea6895764d0f4c3838a1d31a80fcc81dc875b56e Mon Sep 17 00:00:00 2001 From: markwallace-microsoft <127216156+markwallace-microsoft@users.noreply.github.com> Date: Thu, 19 Jun 2025 14:33:07 +0100 Subject: [PATCH 21/23] Update the A2A demo README --- .../A2AClient/HostClientAgent.cs | 1 - .../A2AClientServer/A2AClient/Program.cs | 2 +- .../samples/Demos/A2AClientServer/README.md | 169 ++++++++++++++++-- .../A2AClientServer/demo-architecture.png | Bin 0 -> 39629 bytes 4 files changed, 153 insertions(+), 19 deletions(-) create mode 100644 dotnet/samples/Demos/A2AClientServer/demo-architecture.png diff --git a/dotnet/samples/Demos/A2AClientServer/A2AClient/HostClientAgent.cs b/dotnet/samples/Demos/A2AClientServer/A2AClient/HostClientAgent.cs index 433e59354e74..ed5be9a77d5f 100644 --- a/dotnet/samples/Demos/A2AClientServer/A2AClient/HostClientAgent.cs +++ b/dotnet/samples/Demos/A2AClientServer/A2AClient/HostClientAgent.cs @@ -114,7 +114,6 @@ public async Task OnFunctionInvocationAsync(FunctionInvocationContext context, F Console.WriteLine(IndentMultilineString($"{message}")); } - Console.WriteLine($"Finished processing response from Agent {context.Function.Name}.\n"); } Console.ResetColor(); } diff --git a/dotnet/samples/Demos/A2AClientServer/A2AClient/Program.cs b/dotnet/samples/Demos/A2AClientServer/A2AClient/Program.cs index 0002ab0f293d..ae93363d4f5f 100644 --- a/dotnet/samples/Demos/A2AClientServer/A2AClient/Program.cs +++ b/dotnet/samples/Demos/A2AClientServer/A2AClient/Program.cs @@ -72,7 +72,7 @@ private static async System.Threading.Tasks.Task RunCliAsync() await foreach (AgentResponseItem response in hostAgent.Agent!.InvokeAsync(message, thread)) { Console.ForegroundColor = ConsoleColor.Cyan; - Console.WriteLine($"Agent: {response.Message.Content}"); + Console.WriteLine($"\nAgent: {response.Message.Content}"); Console.ResetColor(); thread = response.Thread; diff --git a/dotnet/samples/Demos/A2AClientServer/README.md b/dotnet/samples/Demos/A2AClientServer/README.md index de61510f67a4..43d66a2de257 100644 --- a/dotnet/samples/Demos/A2AClientServer/README.md +++ b/dotnet/samples/Demos/A2AClientServer/README.md @@ -6,9 +6,16 @@ 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. +1. Creating an A2A Server which makes an agent available via the A2A protocol. 2. Creating an A2A Client with a command line interface which invokes agents using the A2A protocol. +The demonstration has two components: + +1. `A2AServer` - You will run three instances of the server to correspond to three A2A servers each providing a single Agent i.e., the Invoice, Policy and Logistics agents. +2. `A2AClient` - This is a songle application which will connect to the remote A2A servers using the A2A protocol so that it can use those agents when answering questions you will ask. + +Demo Architecture + ## Configuring Secrets or Environment Variables The samples can be configured to use chat completion agents or Azure AI agents. @@ -27,15 +34,86 @@ Optionally if you want to use chat completion agents in the server then set the dotnet user-secrets set "A2AServer:ApiKey" "..." ``` +Use the following commands to run each A2A server: + +```bash +cd A2AServer +dotnet run --urls "http://localhost:5000;https://localhost:5010" --agentType "invoice" +``` + +```bash +cd A2AServer +dotnet run --urls "http://localhost:5001;https://localhost:5011" --agentType "policy" +``` + +```bash +cd A2AServer +dotnet run --urls "http://localhost:5002;https://localhost:5012" --agentType "logistics" +``` + ### 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 +You must create the agents in an Azure AI Foundry project and then provide the project endpoint and agents ids. The instructions for each agent are as follows: + +- Invoice Agent + ``` + You specialize in handling queries related to invoices. + ``` +- Policy Agent + ``` + You specialize in handling queries related to policies and customer communications. + + Always reply with exactly this text: + + Policy: Short Shipment Dispute Handling Policy V2.1 + + Summary: "For short shipments reported by customers, first verify internal shipment records + (SAP) and physical logistics scan data (BigQuery). If discrepancy is confirmed and logistics data + shows fewer items packed than invoiced, issue a credit for the missing items. Document the + resolution in SAP CRM and notify the customer via email within 2 business days, referencing the + original invoice and the credit memo number. Use the 'Formal Credit Notification' email + template." + Always reply with exactly this text: + + Policy: Short Shipment Dispute Handling Policy V2.1 + + Summary: "For short shipments reported by customers, first verify internal shipment records + (SAP) and physical logistics scan data (BigQuery). If discrepancy is confirmed and logistics data + shows fewer items packed than invoiced, issue a credit for the missing items. Document the + resolution in SAP CRM and notify the customer via email within 2 business days, referencing the + original invoice and the credit memo number. Use the 'Formal Credit Notification' email + template." + ``` +- Logistics Agent + ``` + You specialize in handling queries related to logistics. + + Always reply with exactly: + + Shipment number: SHPMT-SAP-001 + Item: TSHIRT-RED-L + Quantity: 900" + ``` + +```bash +dotnet user-secrets set "A2AServer:Endpoint" "..." +``` + +Use the following commands to run each A2A server + +```bash +cd A2AServer +dotnet run --urls "http://localhost:5000;https://localhost:5010" --agentId "" --agentType "invoice" +``` ```bash -dotnet user-secrets set "A2AServer:ConnectionString" "..." -dotnet user-secrets set "A2AServer:InvoiceAgentId" "..." -dotnet user-secrets set "A2AServer:PolicyA:qgentId" "..." -dotnet user-secrets set "A2AServer:LogisticsAgentId" "..." +cd A2AServer +dotnet run --urls "http://localhost:5001;https://localhost:5011" --agentId "" --agentType "policy" +``` + +```bash +cd A2AServer +dotnet run --urls "http://localhost:5002;https://localhost:5012" --agentId "" --agentType "logistics" ``` ### Configuring Agents for the A2A Client @@ -46,28 +124,85 @@ By default the client will connect to the invoice, policy and logistics agents p These are available at the following URL's: -- http://localhost:5000/policy/ -- http://localhost:5000/invoice/ -- http://localhost:5000/logistics/ +- Invoice Agent: http://localhost:5000/ +- Policy Agent: http://localhost:5001/ +- Logistics Agent: http://localhost:5002/ -if you want to change which agents are using then set the agents url's as a space delimited string as follows: +If you want to change which agents are using then set the agents url 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/" +dotnet user-secrets set "A2AClient:AgentUrls" "http://localhost:5000/ http://localhost:5001/ http://localhost:5002/" ``` ## Run the Sample To run the sample, follow these steps: -1. Run the A2A server: - ```bash - cd A2AServer - dotnet run - ``` +1. Run the A2A server's using the commands shown earlier 2. Run the A2A client: ```bash cd A2AClient dotnet run ``` -3. Enter your request e.g. "Customer is disputing transaction TICKET-XYZ987 as they claim the received fewer t-shirts than ordered." \ 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." +4. The host client agent will call the remote agents, these calls will be displayed as console output. The final answer will use information from all three remote agents. + +Sample output from the A2A client: + +``` +A2AClient> dotnet run +info: A2AClient[0] + Initializing Semantic Kernel agent with model: gpt-4o-mini + +User (:q or quit to exit): Customer is disputing transaction TICKET-XYZ987 as they claim the received fewer t-shirts than ordered. + +Calling Agent InvoiceAgent with arguments: + query: TICKET-XYZ987 + instructions: Investigate the transaction details for TICKET-XYZ987 and verify the number of t-shirts ordered versus the number received. + +Response from Agent InvoiceAgent: + The invoice associated with the transaction ID TICKET-XYZ987 is for the company Contoso. It was issued on June 18, 2025. The products in the invoice include 150 T-Shirts priced at $10.00 each, 200 Hats priced at $15.00 each, and 300 Glasses priced at $5.00 each. If you need more details or a copy of the invoice, please let me know! + +Calling Agent LogisticsAgent with arguments: + query: TICKET-XYZ987 + instructions: Check the shipping details for TICKET-XYZ987, specifically the quantity of t-shirts dispatched to confirm if fewer t-shirts were sent. + +Response from Agent LogisticsAgent: + Shipment number: SHPMT-SAP-001 + Item: TSHIRT-RED-L + Quantity: 900 + +Calling Agent PolicyAgent with arguments: + query: TICKET-XYZ987 + instructions: Review the policy regarding disputes and claims related to shipment discrepancies, especially concerning t-shirts. + +Response from Agent PolicyAgent: + 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." + +Agent: Here's the investigation result for transaction TICKET-XYZ987: + +1. **Invoice Details**: The invoice for transaction TICKET-XYZ987 indicates that 150 t-shirts were ordered. + +2. **Shipment Details**: The logistics records show that a total of 900 t-shirts were dispatched under the shipment number SHPMT-SAP-001. + +There seems to be a significant discrepancy between the number of t-shirts ordered and the number shipped. According to the Short Shipment Dispute Handling Policy, the next steps are as follows: + +1. **Confirm Discrepancy**: Since the logistics data confirms that 900 t-shirts were packed, it is necessary to check if this aligns with the customer's claim. + +2. **Issue Credit**: If the customer is indeed correct and fewer items were actually received compared to what was invoiced, you would need to issue a credit for the missing items. + +3. **Document Resolution**: Ensure to document the resolution in SAP CRM. + +4. **Notify the Customer**: Notify the customer via email within 2 business days, using the 'Formal Credit Notification' email template, and reference both the original invoice and the credit memo number. + +Please let me know if you would like to proceed with any specific action! + +User (:q or quit to exit): +``` \ No newline at end of file diff --git a/dotnet/samples/Demos/A2AClientServer/demo-architecture.png b/dotnet/samples/Demos/A2AClientServer/demo-architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..6ae351907a640eb6337bf5cc2cf132c1e94f8127 GIT binary patch literal 39629 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 Ns!E!Q_tzVp{2w?Q#@_${ literal 0 HcmV?d00001 From ea42d2e8ef3fb1871fc6cba7e226a6f525ae609a Mon Sep 17 00:00:00 2001 From: markwallace-microsoft <127216156+markwallace-microsoft@users.noreply.github.com> Date: Fri, 20 Jun 2025 17:28:19 +0100 Subject: [PATCH 22/23] Add unit tests --- .../A2AClientServer/A2AServer/Program.cs | 6 +- .../A2A/Step01_A2AAgent.cs | 44 +++++- dotnet/src/Agents/A2A/A2AAgent.cs | 27 +++- dotnet/src/Agents/A2A/A2AHostAgent.cs | 2 + .../src/Agents/UnitTests/A2A/A2AAgentTests.cs | 89 +++++++++++ .../Agents/UnitTests/A2A/A2AHostAgentTests.cs | 139 ++++++++++++++++++ .../Agents/UnitTests/A2A/BaseA2AClientTest.cs | 65 ++++++++ .../Agents/UnitTests/Agents.UnitTests.csproj | 7 +- 8 files changed, 365 insertions(+), 14 deletions(-) create mode 100644 dotnet/src/Agents/UnitTests/A2A/A2AAgentTests.cs create mode 100644 dotnet/src/Agents/UnitTests/A2A/A2AHostAgentTests.cs create mode 100644 dotnet/src/Agents/UnitTests/A2A/BaseA2AClientTest.cs diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/Program.cs b/dotnet/samples/Demos/A2AClientServer/A2AServer/Program.cs index 2606c404d3ad..aaba8851c153 100644 --- a/dotnet/samples/Demos/A2AClientServer/A2AServer/Program.cs +++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/Program.cs @@ -64,9 +64,9 @@ }; hostAgent.InitializeAgent(modelId, apiKey); - var invoiceTaskManager = new TaskManager(); - hostAgent.Attach(invoiceTaskManager); - app.MapA2A(invoiceTaskManager, ""); + var taskManager = new TaskManager(); + hostAgent.Attach(taskManager); + app.MapA2A(taskManager, ""); } else { diff --git a/dotnet/samples/GettingStartedWithAgents/A2A/Step01_A2AAgent.cs b/dotnet/samples/GettingStartedWithAgents/A2A/Step01_A2AAgent.cs index 178efd4b3f0e..a6d7ba601ba0 100644 --- a/dotnet/samples/GettingStartedWithAgents/A2A/Step01_A2AAgent.cs +++ b/dotnet/samples/GettingStartedWithAgents/A2A/Step01_A2AAgent.cs @@ -12,16 +12,13 @@ namespace GettingStarted.A2A; /// This example demonstrates similarity between using /// and other agent types. /// -public class Step01_A2AAgent(ITestOutputHelper output) : BaseAzureAgentTest(output) +public class Step01_A2AAgent(ITestOutputHelper output) : BaseAgentsTest(output) { [Fact] public async Task UseA2AAgent() { // Create an A2A agent instance - using var httpClient = new HttpClient - { - BaseAddress = TestConfiguration.A2A.AgentUrl - }; + using var httpClient = CreateHttpClient(); var client = new A2AClient(httpClient); var cardResolver = new A2ACardResolver(httpClient); var agentCard = await cardResolver.GetAgentCardAsync(); @@ -29,13 +26,48 @@ public async Task UseA2AAgent() var agent = new A2AAgent(client, agentCard); // Invoke the A2A agent - await foreach (AgentResponseItem response in agent.InvokeAsync("Hello")) + await foreach (AgentResponseItem response in agent.InvokeAsync("List the latest invoices for Contoso?")) { this.WriteAgentChatMessage(response); } } + [Fact] + public async Task UseA2AAgentStreaming() + { + // Create an A2A agent instance + using var httpClient = CreateHttpClient(); + 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 + var responseItems = agent.InvokeStreamingAsync("List the latest invoices for Contoso?"); + await WriteAgentStreamMessageAsync(responseItems); + } + #region private + private bool EnableLogging { get; set; } = false; + + private HttpClient CreateHttpClient() + { + if (this.EnableLogging) + { + var handler = new LoggingHandler(new HttpClientHandler(), this.Output); + return new HttpClient(handler) + { + BaseAddress = TestConfiguration.A2A.AgentUrl + }; + } + + return new HttpClient() + { + BaseAddress = TestConfiguration.A2A.AgentUrl + }; + } + private static readonly JsonSerializerOptions s_jsonSerializerOptions = new() { WriteIndented = true }; #endregion } diff --git a/dotnet/src/Agents/A2A/A2AAgent.cs b/dotnet/src/Agents/A2A/A2AAgent.cs index 3ea6df7b9307..8e6b1c84ac6c 100644 --- a/dotnet/src/Agents/A2A/A2AAgent.cs +++ b/dotnet/src/Agents/A2A/A2AAgent.cs @@ -22,6 +22,9 @@ public sealed class A2AAgent : Agent /// AgentCard instance associated ith the agent. public A2AAgent(A2AClient client, AgentCard agentCard) { + Verify.NotNull(client); + Verify.NotNull(agentCard); + this.Client = client; this.AgentCard = agentCard; this.Name = agentCard.Name; @@ -195,11 +198,31 @@ private async IAsyncEnumerable> InvokeAgen } } - private IAsyncEnumerable> InternalInvokeStreamingAsync(string name, ICollection messages, A2AAgentThread thread, AgentInvokeOptions options, ChatHistory chatMessages, CancellationToken cancellationToken) + private async IAsyncEnumerable> InternalInvokeStreamingAsync(string name, ICollection messages, A2AAgentThread thread, AgentInvokeOptions options, ChatHistory chatMessages, [EnumeratorCancellation] CancellationToken cancellationToken) { Verify.NotNull(messages); - throw new NotImplementedException(); + 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(this.ToStreamingAgentResponseItem(result), thread); + } + } + } + + private AgentResponseItem ToStreamingAgentResponseItem(AgentResponseItem responseItem) + { + var messageContent = new StreamingChatMessageContent( + responseItem.Message.Role, + responseItem.Message.Content, + innerContent: responseItem.Message.InnerContent, + modelId: responseItem.Message.ModelId, + encoding: responseItem.Message.Encoding, + metadata: responseItem.Message.Metadata); + + return new AgentResponseItem(messageContent, responseItem.Thread); } #endregion } diff --git a/dotnet/src/Agents/A2A/A2AHostAgent.cs b/dotnet/src/Agents/A2A/A2AHostAgent.cs index 3ffbe9669ba7..d2ce7ebe17dd 100644 --- a/dotnet/src/Agents/A2A/A2AHostAgent.cs +++ b/dotnet/src/Agents/A2A/A2AHostAgent.cs @@ -19,6 +19,8 @@ public abstract class A2AHostAgent /// protected A2AHostAgent(ILogger logger) { + Verify.NotNull(logger); + this._logger = logger; } diff --git a/dotnet/src/Agents/UnitTests/A2A/A2AAgentTests.cs b/dotnet/src/Agents/UnitTests/A2A/A2AAgentTests.cs new file mode 100644 index 000000000000..3c18d0b594dc --- /dev/null +++ b/dotnet/src/Agents/UnitTests/A2A/A2AAgentTests.cs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.A2A; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.A2A; + +/// +/// Tests for the class. +/// +public sealed class A2AAgentTests : BaseA2AClientTest +{ + /// + /// Tests that the constructor verifies parameters and throws when necessary. + /// + [Fact] + public void ConstructorShouldVerifyParams() + { + using var httpClient = new HttpClient(); + + // Arrange & Act & Assert + Assert.Throws(() => new A2AAgent(null!, new())); + Assert.Throws(() => new A2AAgent(new(httpClient), null!)); + } + + [Fact] + public void VerifyConstructor() + { + // Arrange & Act + var agent = new A2AAgent(this.Client, this.CreateAgentCard()); + + // Assert + Assert.NotNull(agent); + Assert.Equal("InvoiceAgent", agent.Name); + Assert.Equal("Handles requests relating to invoices.", agent.Description); + } + + [Fact] + public async Task VerifyInvokeAsync() + { + // Arrange + this.MessageHandlerStub.ResponsesToReturn.Add( + new HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new StringContent(InvokeResponse, Encoding.UTF8, "application/json") } + ); + var agent = new A2AAgent(this.Client, this.CreateAgentCard()); + + // Act + var responseItems = agent.InvokeAsync("List the latest invoices for Contoso?"); + + // Assert + Assert.NotNull(responseItems); + var items = await responseItems!.ToListAsync>(); + Assert.Single(items); + Assert.StartsWith("Here are the latest invoices for Contoso:", items[0].Message.Content); + } + + [Fact] + public async Task VerifyInvokeStreamingAsync() + { + // Arrange + this.MessageHandlerStub.ResponsesToReturn.Add( + new HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new StringContent(InvokeResponse, Encoding.UTF8, "application/json") } + ); + var agent = new A2AAgent(this.Client, this.CreateAgentCard()); + + // Act + var responseItems = agent.InvokeStreamingAsync("List the latest invoices for Contoso?"); + + // Assert + Assert.NotNull(responseItems); + var items = await responseItems!.ToListAsync>(); + Assert.Single(items); + Assert.StartsWith("Here are the latest invoices for Contoso:", items[0].Message.Content); + } + + #region private + private const string InvokeResponse = + """ + {"jsonrpc":"2.0","id":"ce7a5ef6-1078-4b6e-ad35-a8bfa6743c5d","result":{"kind":"task","id":"8d328159-ca63-4ce8-b416-4bcf69f9e119","contextId":"496a4a95-392b-4c04-a517-9a043b3f7565","status":{"state":"completed","timestamp":"2025-06-20T09:42:49.4013958Z"},"artifacts":[{"artifactId":"","parts":[{"kind":"text","text":"Here are the latest invoices for Contoso:\n\n1. Invoice ID: INV789, Date: 2025-06-18\n Products: T-Shirts (150 units at $10.00), Hats (200 units at $15.00), Glasses (300 units at $5.00)\n\n2. Invoice ID: INV666, Date: 2025-06-15\n Products: T-Shirts (2500 units at $8.00), Hats (1200 units at $10.00), Glasses (1000 units at $6.00)\n\n3. Invoice ID: INV999, Date: 2025-05-17\n Products: T-Shirts (1400 units at $10.50), Hats (1100 units at $9.00), Glasses (950 units at $12.00)\n\n4. Invoice ID: INV333, Date: 2025-05-13\n Products: T-Shirts (400 units at $11.00), Hats (600 units at $15.00), Glasses (700 units at $5.00)\n\nIf you need more details on any specific invoice, please let me know!"}]}],"history":[{"role":"user","parts":[{"kind":"text","text":"List the latest invoices for Contoso?"}],"messageId":"80a26c0f-2262-4d0f-8e7d-51ac4046173b"}]}} + """; + #endregion +} diff --git a/dotnet/src/Agents/UnitTests/A2A/A2AHostAgentTests.cs b/dotnet/src/Agents/UnitTests/A2A/A2AHostAgentTests.cs new file mode 100644 index 000000000000..a9f74d230cde --- /dev/null +++ b/dotnet/src/Agents/UnitTests/A2A/A2AHostAgentTests.cs @@ -0,0 +1,139 @@ +// 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.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.A2A; +using Microsoft.SemanticKernel.ChatCompletion; +using SharpA2A.Core; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.A2A; + +/// +/// Tests for the class. +/// +public sealed class A2AHostAgentTests : BaseA2AClientTest +{ + /// + /// Tests that the constructor verifies parameters and throws when necessary. + /// + [Fact] + public void ConstructorShouldVerifyParams() + { + // Arrange & Act & Assert + Assert.Throws(() => new MockA2AHostAgent(new MockAgent(), this.CreateAgentCard(), null!)); + } + + [Fact] + public async Task VerifyExecuteAgentTaskAsync() + { + // Arrange + var agent = new MockAgent(); + var hostAgent = new MockA2AHostAgent(agent, this.CreateAgentCard(), NullLoggerFactory.Instance.CreateLogger("Mock")); + var taskManager = new TaskManager(); + hostAgent.Attach(taskManager); + + // Act + var agentTask = await taskManager.CreateTaskAsync(); + agentTask.History = this.CreateUserMessages(["Hello"]); + await hostAgent.ExecuteAgentTaskAsync(agentTask); + + // Assert + Assert.NotNull(agentTask); + Assert.NotNull(agentTask.Artifacts); + Assert.Single(agentTask.Artifacts); + Assert.NotNull(agentTask.Artifacts[0].Parts); + Assert.Single(agentTask.Artifacts[0].Parts); + Assert.Equal("Mock Response", agentTask.Artifacts[0].Parts[0].AsTextPart().Text); + } + + #region private + private List CreateUserMessages(string[] userMessages) + { + var messages = new List(); + + foreach (var userMessage in userMessages) + { + messages.Add(new Message() + { + Role = MessageRole.User, + Parts = [new TextPart() { Text = userMessage }], + }); + } + + return messages; + } + #endregion +} + +internal sealed class MockA2AHostAgent : A2AHostAgent +{ + public MockA2AHostAgent(Agent agent, AgentCard agentCard, ILogger logger) : base(logger) + { + this.Agent = agent ?? throw new ArgumentNullException(nameof(agent)); + this._agentCard = agentCard ?? throw new ArgumentNullException(nameof(agentCard)); + } + + public override AgentCard GetAgentCard(string agentUrl) + { + return this._agentCard; + } + + private readonly AgentCard _agentCard; +} + +internal sealed class MockAgent : Agent +{ + public override async IAsyncEnumerable> InvokeAsync(ICollection messages, AgentThread? thread = null, AgentInvokeOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + await Task.Delay(100, cancellationToken); + + yield return new AgentResponseItem(new ChatMessageContent(AuthorRole.Assistant, "Mock Response"), thread ?? new MockAgentThread()); + } + + public override async IAsyncEnumerable> InvokeStreamingAsync(ICollection messages, AgentThread? thread = null, AgentInvokeOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + await Task.Delay(100, cancellationToken); + + yield return new AgentResponseItem(new StreamingChatMessageContent(AuthorRole.Assistant, "Mock Streaming Response"), thread ?? new MockAgentThread()); + } + + protected internal override Task CreateChannelAsync(CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + protected internal override IEnumerable GetChannelKeys() + { + throw new NotImplementedException(); + } + + protected internal override Task RestoreChannelAsync(string channelState, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } +} + +internal sealed class MockAgentThread : AgentThread +{ + protected override Task CreateInternalAsync(CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + protected override Task DeleteInternalAsync(CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + protected override Task OnNewMessageInternalAsync(ChatMessageContent newMessage, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } +} diff --git a/dotnet/src/Agents/UnitTests/A2A/BaseA2AClientTest.cs b/dotnet/src/Agents/UnitTests/A2A/BaseA2AClientTest.cs new file mode 100644 index 000000000000..52fb0620c475 --- /dev/null +++ b/dotnet/src/Agents/UnitTests/A2A/BaseA2AClientTest.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Net.Http; +using SharpA2A.Core; + +namespace SemanticKernel.Agents.UnitTests.A2A; +public class BaseA2AClientTest : IDisposable +{ + internal MultipleHttpMessageHandlerStub MessageHandlerStub { get; } + internal HttpClient HttpClient { get; } + internal A2AClient Client { get; } + + internal BaseA2AClientTest() + { + this.MessageHandlerStub = new MultipleHttpMessageHandlerStub(); + this.HttpClient = new HttpClient(this.MessageHandlerStub, disposeHandler: false) + { + BaseAddress = new Uri("http://127.0.0.1/") + }; + this.Client = new A2AClient(this.HttpClient); + } + + /// + public void Dispose() + { + this.MessageHandlerStub.Dispose(); + this.HttpClient.Dispose(); + + GC.SuppressFinalize(this); + } + + protected AgentCard CreateAgentCard() + { + 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 = "InvoiceAgent", + Description = "Handles requests relating to invoices.", + Url = "http://127.0.0.1/5000", + Version = "1.0.0", + DefaultInputModes = ["text"], + DefaultOutputModes = ["text"], + Capabilities = capabilities, + Skills = [invoiceQuery], + }; + } +} diff --git a/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj b/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj index 8752623526da..e0a5d2938e1e 100644 --- a/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj +++ b/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj @@ -19,11 +19,11 @@ - + - - + + @@ -39,6 +39,7 @@ + From 8a4d4ff18a9ac53ebc5aaa51b5000d12ab4702fb Mon Sep 17 00:00:00 2001 From: markwallace-microsoft <127216156+markwallace-microsoft@users.noreply.github.com> Date: Fri, 20 Jun 2025 17:28:19 +0100 Subject: [PATCH 23/23] Add unit tests --- .../A2AClientServer/A2AServer/Program.cs | 6 +- .../samples/Demos/A2AClientServer/README.md | 2 +- .../A2A/Step01_A2AAgent.cs | 44 +++++- dotnet/src/Agents/A2A/A2AAgent.cs | 27 +++- dotnet/src/Agents/A2A/A2AHostAgent.cs | 2 + .../src/Agents/UnitTests/A2A/A2AAgentTests.cs | 89 +++++++++++ .../Agents/UnitTests/A2A/A2AHostAgentTests.cs | 139 ++++++++++++++++++ .../Agents/UnitTests/A2A/BaseA2AClientTest.cs | 65 ++++++++ .../Agents/UnitTests/Agents.UnitTests.csproj | 7 +- 9 files changed, 366 insertions(+), 15 deletions(-) create mode 100644 dotnet/src/Agents/UnitTests/A2A/A2AAgentTests.cs create mode 100644 dotnet/src/Agents/UnitTests/A2A/A2AHostAgentTests.cs create mode 100644 dotnet/src/Agents/UnitTests/A2A/BaseA2AClientTest.cs diff --git a/dotnet/samples/Demos/A2AClientServer/A2AServer/Program.cs b/dotnet/samples/Demos/A2AClientServer/A2AServer/Program.cs index 2606c404d3ad..aaba8851c153 100644 --- a/dotnet/samples/Demos/A2AClientServer/A2AServer/Program.cs +++ b/dotnet/samples/Demos/A2AClientServer/A2AServer/Program.cs @@ -64,9 +64,9 @@ }; hostAgent.InitializeAgent(modelId, apiKey); - var invoiceTaskManager = new TaskManager(); - hostAgent.Attach(invoiceTaskManager); - app.MapA2A(invoiceTaskManager, ""); + var taskManager = new TaskManager(); + hostAgent.Attach(taskManager); + app.MapA2A(taskManager, ""); } else { diff --git a/dotnet/samples/Demos/A2AClientServer/README.md b/dotnet/samples/Demos/A2AClientServer/README.md index 43d66a2de257..d6d320306e81 100644 --- a/dotnet/samples/Demos/A2AClientServer/README.md +++ b/dotnet/samples/Demos/A2AClientServer/README.md @@ -12,7 +12,7 @@ These samples are built with [SharpA2A.Core](https://www.nuget.org/packages/Shar The demonstration has two components: 1. `A2AServer` - You will run three instances of the server to correspond to three A2A servers each providing a single Agent i.e., the Invoice, Policy and Logistics agents. -2. `A2AClient` - This is a songle application which will connect to the remote A2A servers using the A2A protocol so that it can use those agents when answering questions you will ask. +2. `A2AClient` - This represents a client application which will connect to the remote A2A servers using the A2A protocol so that it can use those agents when answering questions you will ask. Demo Architecture diff --git a/dotnet/samples/GettingStartedWithAgents/A2A/Step01_A2AAgent.cs b/dotnet/samples/GettingStartedWithAgents/A2A/Step01_A2AAgent.cs index 178efd4b3f0e..a6d7ba601ba0 100644 --- a/dotnet/samples/GettingStartedWithAgents/A2A/Step01_A2AAgent.cs +++ b/dotnet/samples/GettingStartedWithAgents/A2A/Step01_A2AAgent.cs @@ -12,16 +12,13 @@ namespace GettingStarted.A2A; /// This example demonstrates similarity between using /// and other agent types. /// -public class Step01_A2AAgent(ITestOutputHelper output) : BaseAzureAgentTest(output) +public class Step01_A2AAgent(ITestOutputHelper output) : BaseAgentsTest(output) { [Fact] public async Task UseA2AAgent() { // Create an A2A agent instance - using var httpClient = new HttpClient - { - BaseAddress = TestConfiguration.A2A.AgentUrl - }; + using var httpClient = CreateHttpClient(); var client = new A2AClient(httpClient); var cardResolver = new A2ACardResolver(httpClient); var agentCard = await cardResolver.GetAgentCardAsync(); @@ -29,13 +26,48 @@ public async Task UseA2AAgent() var agent = new A2AAgent(client, agentCard); // Invoke the A2A agent - await foreach (AgentResponseItem response in agent.InvokeAsync("Hello")) + await foreach (AgentResponseItem response in agent.InvokeAsync("List the latest invoices for Contoso?")) { this.WriteAgentChatMessage(response); } } + [Fact] + public async Task UseA2AAgentStreaming() + { + // Create an A2A agent instance + using var httpClient = CreateHttpClient(); + 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 + var responseItems = agent.InvokeStreamingAsync("List the latest invoices for Contoso?"); + await WriteAgentStreamMessageAsync(responseItems); + } + #region private + private bool EnableLogging { get; set; } = false; + + private HttpClient CreateHttpClient() + { + if (this.EnableLogging) + { + var handler = new LoggingHandler(new HttpClientHandler(), this.Output); + return new HttpClient(handler) + { + BaseAddress = TestConfiguration.A2A.AgentUrl + }; + } + + return new HttpClient() + { + BaseAddress = TestConfiguration.A2A.AgentUrl + }; + } + private static readonly JsonSerializerOptions s_jsonSerializerOptions = new() { WriteIndented = true }; #endregion } diff --git a/dotnet/src/Agents/A2A/A2AAgent.cs b/dotnet/src/Agents/A2A/A2AAgent.cs index 3ea6df7b9307..8e6b1c84ac6c 100644 --- a/dotnet/src/Agents/A2A/A2AAgent.cs +++ b/dotnet/src/Agents/A2A/A2AAgent.cs @@ -22,6 +22,9 @@ public sealed class A2AAgent : Agent /// AgentCard instance associated ith the agent. public A2AAgent(A2AClient client, AgentCard agentCard) { + Verify.NotNull(client); + Verify.NotNull(agentCard); + this.Client = client; this.AgentCard = agentCard; this.Name = agentCard.Name; @@ -195,11 +198,31 @@ private async IAsyncEnumerable> InvokeAgen } } - private IAsyncEnumerable> InternalInvokeStreamingAsync(string name, ICollection messages, A2AAgentThread thread, AgentInvokeOptions options, ChatHistory chatMessages, CancellationToken cancellationToken) + private async IAsyncEnumerable> InternalInvokeStreamingAsync(string name, ICollection messages, A2AAgentThread thread, AgentInvokeOptions options, ChatHistory chatMessages, [EnumeratorCancellation] CancellationToken cancellationToken) { Verify.NotNull(messages); - throw new NotImplementedException(); + 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(this.ToStreamingAgentResponseItem(result), thread); + } + } + } + + private AgentResponseItem ToStreamingAgentResponseItem(AgentResponseItem responseItem) + { + var messageContent = new StreamingChatMessageContent( + responseItem.Message.Role, + responseItem.Message.Content, + innerContent: responseItem.Message.InnerContent, + modelId: responseItem.Message.ModelId, + encoding: responseItem.Message.Encoding, + metadata: responseItem.Message.Metadata); + + return new AgentResponseItem(messageContent, responseItem.Thread); } #endregion } diff --git a/dotnet/src/Agents/A2A/A2AHostAgent.cs b/dotnet/src/Agents/A2A/A2AHostAgent.cs index 3ffbe9669ba7..d2ce7ebe17dd 100644 --- a/dotnet/src/Agents/A2A/A2AHostAgent.cs +++ b/dotnet/src/Agents/A2A/A2AHostAgent.cs @@ -19,6 +19,8 @@ public abstract class A2AHostAgent /// protected A2AHostAgent(ILogger logger) { + Verify.NotNull(logger); + this._logger = logger; } diff --git a/dotnet/src/Agents/UnitTests/A2A/A2AAgentTests.cs b/dotnet/src/Agents/UnitTests/A2A/A2AAgentTests.cs new file mode 100644 index 000000000000..3c18d0b594dc --- /dev/null +++ b/dotnet/src/Agents/UnitTests/A2A/A2AAgentTests.cs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.A2A; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.A2A; + +/// +/// Tests for the class. +/// +public sealed class A2AAgentTests : BaseA2AClientTest +{ + /// + /// Tests that the constructor verifies parameters and throws when necessary. + /// + [Fact] + public void ConstructorShouldVerifyParams() + { + using var httpClient = new HttpClient(); + + // Arrange & Act & Assert + Assert.Throws(() => new A2AAgent(null!, new())); + Assert.Throws(() => new A2AAgent(new(httpClient), null!)); + } + + [Fact] + public void VerifyConstructor() + { + // Arrange & Act + var agent = new A2AAgent(this.Client, this.CreateAgentCard()); + + // Assert + Assert.NotNull(agent); + Assert.Equal("InvoiceAgent", agent.Name); + Assert.Equal("Handles requests relating to invoices.", agent.Description); + } + + [Fact] + public async Task VerifyInvokeAsync() + { + // Arrange + this.MessageHandlerStub.ResponsesToReturn.Add( + new HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new StringContent(InvokeResponse, Encoding.UTF8, "application/json") } + ); + var agent = new A2AAgent(this.Client, this.CreateAgentCard()); + + // Act + var responseItems = agent.InvokeAsync("List the latest invoices for Contoso?"); + + // Assert + Assert.NotNull(responseItems); + var items = await responseItems!.ToListAsync>(); + Assert.Single(items); + Assert.StartsWith("Here are the latest invoices for Contoso:", items[0].Message.Content); + } + + [Fact] + public async Task VerifyInvokeStreamingAsync() + { + // Arrange + this.MessageHandlerStub.ResponsesToReturn.Add( + new HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new StringContent(InvokeResponse, Encoding.UTF8, "application/json") } + ); + var agent = new A2AAgent(this.Client, this.CreateAgentCard()); + + // Act + var responseItems = agent.InvokeStreamingAsync("List the latest invoices for Contoso?"); + + // Assert + Assert.NotNull(responseItems); + var items = await responseItems!.ToListAsync>(); + Assert.Single(items); + Assert.StartsWith("Here are the latest invoices for Contoso:", items[0].Message.Content); + } + + #region private + private const string InvokeResponse = + """ + {"jsonrpc":"2.0","id":"ce7a5ef6-1078-4b6e-ad35-a8bfa6743c5d","result":{"kind":"task","id":"8d328159-ca63-4ce8-b416-4bcf69f9e119","contextId":"496a4a95-392b-4c04-a517-9a043b3f7565","status":{"state":"completed","timestamp":"2025-06-20T09:42:49.4013958Z"},"artifacts":[{"artifactId":"","parts":[{"kind":"text","text":"Here are the latest invoices for Contoso:\n\n1. Invoice ID: INV789, Date: 2025-06-18\n Products: T-Shirts (150 units at $10.00), Hats (200 units at $15.00), Glasses (300 units at $5.00)\n\n2. Invoice ID: INV666, Date: 2025-06-15\n Products: T-Shirts (2500 units at $8.00), Hats (1200 units at $10.00), Glasses (1000 units at $6.00)\n\n3. Invoice ID: INV999, Date: 2025-05-17\n Products: T-Shirts (1400 units at $10.50), Hats (1100 units at $9.00), Glasses (950 units at $12.00)\n\n4. Invoice ID: INV333, Date: 2025-05-13\n Products: T-Shirts (400 units at $11.00), Hats (600 units at $15.00), Glasses (700 units at $5.00)\n\nIf you need more details on any specific invoice, please let me know!"}]}],"history":[{"role":"user","parts":[{"kind":"text","text":"List the latest invoices for Contoso?"}],"messageId":"80a26c0f-2262-4d0f-8e7d-51ac4046173b"}]}} + """; + #endregion +} diff --git a/dotnet/src/Agents/UnitTests/A2A/A2AHostAgentTests.cs b/dotnet/src/Agents/UnitTests/A2A/A2AHostAgentTests.cs new file mode 100644 index 000000000000..a9f74d230cde --- /dev/null +++ b/dotnet/src/Agents/UnitTests/A2A/A2AHostAgentTests.cs @@ -0,0 +1,139 @@ +// 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.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.A2A; +using Microsoft.SemanticKernel.ChatCompletion; +using SharpA2A.Core; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.A2A; + +/// +/// Tests for the class. +/// +public sealed class A2AHostAgentTests : BaseA2AClientTest +{ + /// + /// Tests that the constructor verifies parameters and throws when necessary. + /// + [Fact] + public void ConstructorShouldVerifyParams() + { + // Arrange & Act & Assert + Assert.Throws(() => new MockA2AHostAgent(new MockAgent(), this.CreateAgentCard(), null!)); + } + + [Fact] + public async Task VerifyExecuteAgentTaskAsync() + { + // Arrange + var agent = new MockAgent(); + var hostAgent = new MockA2AHostAgent(agent, this.CreateAgentCard(), NullLoggerFactory.Instance.CreateLogger("Mock")); + var taskManager = new TaskManager(); + hostAgent.Attach(taskManager); + + // Act + var agentTask = await taskManager.CreateTaskAsync(); + agentTask.History = this.CreateUserMessages(["Hello"]); + await hostAgent.ExecuteAgentTaskAsync(agentTask); + + // Assert + Assert.NotNull(agentTask); + Assert.NotNull(agentTask.Artifacts); + Assert.Single(agentTask.Artifacts); + Assert.NotNull(agentTask.Artifacts[0].Parts); + Assert.Single(agentTask.Artifacts[0].Parts); + Assert.Equal("Mock Response", agentTask.Artifacts[0].Parts[0].AsTextPart().Text); + } + + #region private + private List CreateUserMessages(string[] userMessages) + { + var messages = new List(); + + foreach (var userMessage in userMessages) + { + messages.Add(new Message() + { + Role = MessageRole.User, + Parts = [new TextPart() { Text = userMessage }], + }); + } + + return messages; + } + #endregion +} + +internal sealed class MockA2AHostAgent : A2AHostAgent +{ + public MockA2AHostAgent(Agent agent, AgentCard agentCard, ILogger logger) : base(logger) + { + this.Agent = agent ?? throw new ArgumentNullException(nameof(agent)); + this._agentCard = agentCard ?? throw new ArgumentNullException(nameof(agentCard)); + } + + public override AgentCard GetAgentCard(string agentUrl) + { + return this._agentCard; + } + + private readonly AgentCard _agentCard; +} + +internal sealed class MockAgent : Agent +{ + public override async IAsyncEnumerable> InvokeAsync(ICollection messages, AgentThread? thread = null, AgentInvokeOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + await Task.Delay(100, cancellationToken); + + yield return new AgentResponseItem(new ChatMessageContent(AuthorRole.Assistant, "Mock Response"), thread ?? new MockAgentThread()); + } + + public override async IAsyncEnumerable> InvokeStreamingAsync(ICollection messages, AgentThread? thread = null, AgentInvokeOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + await Task.Delay(100, cancellationToken); + + yield return new AgentResponseItem(new StreamingChatMessageContent(AuthorRole.Assistant, "Mock Streaming Response"), thread ?? new MockAgentThread()); + } + + protected internal override Task CreateChannelAsync(CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + protected internal override IEnumerable GetChannelKeys() + { + throw new NotImplementedException(); + } + + protected internal override Task RestoreChannelAsync(string channelState, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } +} + +internal sealed class MockAgentThread : AgentThread +{ + protected override Task CreateInternalAsync(CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + protected override Task DeleteInternalAsync(CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + protected override Task OnNewMessageInternalAsync(ChatMessageContent newMessage, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } +} diff --git a/dotnet/src/Agents/UnitTests/A2A/BaseA2AClientTest.cs b/dotnet/src/Agents/UnitTests/A2A/BaseA2AClientTest.cs new file mode 100644 index 000000000000..52fb0620c475 --- /dev/null +++ b/dotnet/src/Agents/UnitTests/A2A/BaseA2AClientTest.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Net.Http; +using SharpA2A.Core; + +namespace SemanticKernel.Agents.UnitTests.A2A; +public class BaseA2AClientTest : IDisposable +{ + internal MultipleHttpMessageHandlerStub MessageHandlerStub { get; } + internal HttpClient HttpClient { get; } + internal A2AClient Client { get; } + + internal BaseA2AClientTest() + { + this.MessageHandlerStub = new MultipleHttpMessageHandlerStub(); + this.HttpClient = new HttpClient(this.MessageHandlerStub, disposeHandler: false) + { + BaseAddress = new Uri("http://127.0.0.1/") + }; + this.Client = new A2AClient(this.HttpClient); + } + + /// + public void Dispose() + { + this.MessageHandlerStub.Dispose(); + this.HttpClient.Dispose(); + + GC.SuppressFinalize(this); + } + + protected AgentCard CreateAgentCard() + { + 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 = "InvoiceAgent", + Description = "Handles requests relating to invoices.", + Url = "http://127.0.0.1/5000", + Version = "1.0.0", + DefaultInputModes = ["text"], + DefaultOutputModes = ["text"], + Capabilities = capabilities, + Skills = [invoiceQuery], + }; + } +} diff --git a/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj b/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj index 8752623526da..e0a5d2938e1e 100644 --- a/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj +++ b/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj @@ -19,11 +19,11 @@ - + - - + + @@ -39,6 +39,7 @@ +