diff --git a/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs b/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs index 571b07b1d..ba979a186 100644 --- a/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs +++ b/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs @@ -20,7 +20,7 @@ // Configure the chat model and our agent. builder.AddKeyedChatClient("chat-model"); -builder.AddAIAgent( +var pirateAgent = builder.AddAIAgent( "pirate", instructions: "You are a pirate. Speak like a pirate", description: "An agent that speaks like a pirate.", @@ -78,8 +78,20 @@ Once the user has deduced what type (knight or knave) both Alice and Bob are, te description: "An agent that helps with literature.", chatClientServiceKey: "chat-model"); -builder.AddSequentialWorkflow("science-sequential-workflow", [chemistryAgent, mathsAgent, literatureAgent]).AddAsAIAgent(); -builder.AddConcurrentWorkflow("science-concurrent-workflow", [chemistryAgent, mathsAgent, literatureAgent]).AddAsAIAgent(); +var scienceSequentialWorkflow = builder.AddWorkflow("science-sequential-workflow", (sp, key) => +{ + List usedAgents = [chemistryAgent, mathsAgent, literatureAgent]; + var agents = usedAgents.Select(ab => sp.GetRequiredKeyedService(ab.Name)); + return AgentWorkflowBuilder.BuildSequential(workflowName: key, agents: agents); +}).AddAsAIAgent(); + +var scienceConcurrentWorkflow = builder.AddWorkflow("science-concurrent-workflow", (sp, key) => +{ + List usedAgents = [chemistryAgent, mathsAgent, literatureAgent]; + var agents = usedAgents.Select(ab => sp.GetRequiredKeyedService(ab.Name)); + return AgentWorkflowBuilder.BuildConcurrent(workflowName: key, agents: agents); +}).AddAsAIAgent(); + builder.AddOpenAIResponses(); var app = builder.Build(); @@ -91,7 +103,7 @@ Once the user has deduced what type (knight or knave) both Alice and Bob are, te app.UseExceptionHandler(); // attach a2a with simple message communication -app.MapA2A(agentName: "pirate", path: "/a2a/pirate"); +app.MapA2A(pirateAgent, path: "/a2a/pirate"); app.MapA2A(agentName: "knights-and-knaves", path: "/a2a/knights-and-knaves", agentCard: new() { Name = "Knights and Knaves", @@ -104,8 +116,10 @@ Once the user has deduced what type (knight or knave) both Alice and Bob are, te app.MapOpenAIResponses(); -app.MapOpenAIChatCompletions("pirate"); +app.MapOpenAIChatCompletions(pirateAgent); app.MapOpenAIChatCompletions("knights-and-knaves"); +app.MapOpenAIChatCompletions(scienceSequentialWorkflow); +app.MapOpenAIChatCompletions(scienceConcurrentWorkflow); // Map the agents HTTP endpoints app.MapAgentDiscovery("/agents"); diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/EndpointRouteBuilderExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/EndpointRouteBuilderExtensions.cs index cae980114..aad7b985b 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/EndpointRouteBuilderExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A.AspNetCore/EndpointRouteBuilderExtensions.cs @@ -18,6 +18,21 @@ namespace Microsoft.AspNetCore.Builder; /// public static class MicrosoftAgentAIHostingA2AEndpointRouteBuilderExtensions { + /// + /// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application. + /// + /// The to add the A2A endpoints to. + /// The configuration builder for . + /// The route group to use for A2A endpoints. + /// Configured for A2A integration. + /// + /// This method can be used to access A2A agents that support the + /// Curated Registries (Catalog-Based Discovery) + /// discovery mechanism. + /// + public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, IHostedAgentBuilder agentBuilder, string path) + => endpoints.MapA2A(agentBuilder, path, _ => { }); + /// /// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application. /// @@ -28,6 +43,25 @@ public static class MicrosoftAgentAIHostingA2AEndpointRouteBuilderExtensions public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path) => endpoints.MapA2A(agentName, path, _ => { }); + /// + /// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application. + /// + /// The to add the A2A endpoints to. + /// The configuration builder for . + /// The route group to use for A2A endpoints. + /// The callback to configure . + /// Configured for A2A integration. + /// + /// This method can be used to access A2A agents that support the + /// Curated Registries (Catalog-Based Discovery) + /// discovery mechanism. + /// + public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, IHostedAgentBuilder agentBuilder, string path, Action configureTaskManager) + { + ArgumentNullException.ThrowIfNull(agentBuilder); + return endpoints.MapA2A(agentBuilder.Name, path, configureTaskManager); + } + /// /// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application. /// @@ -42,6 +76,22 @@ public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpo return endpoints.MapA2A(agent, path, configureTaskManager); } + /// + /// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application. + /// + /// The to add the A2A endpoints to. + /// The configuration builder for . + /// The route group to use for A2A endpoints. + /// Agent card info to return on query. + /// Configured for A2A integration. + /// + /// This method can be used to access A2A agents that support the + /// Curated Registries (Catalog-Based Discovery) + /// discovery mechanism. + /// + public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, IHostedAgentBuilder agentBuilder, string path, AgentCard agentCard) + => endpoints.MapA2A(agentBuilder, path, agentCard, _ => { }); + /// /// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application. /// @@ -58,6 +108,26 @@ public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpo public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path, AgentCard agentCard) => endpoints.MapA2A(agentName, path, agentCard, _ => { }); + /// + /// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application. + /// + /// The to add the A2A endpoints to. + /// The configuration builder for . + /// The route group to use for A2A endpoints. + /// Agent card info to return on query. + /// The callback to configure . + /// Configured for A2A integration. + /// + /// This method can be used to access A2A agents that support the + /// Curated Registries (Catalog-Based Discovery) + /// discovery mechanism. + /// + public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, IHostedAgentBuilder agentBuilder, string path, AgentCard agentCard, Action configureTaskManager) + { + ArgumentNullException.ThrowIfNull(agentBuilder); + return endpoints.MapA2A(agentBuilder.Name, path, agentCard, configureTaskManager); + } + /// /// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application. /// diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/EndpointRouteBuilderExtensions.ChatCompletions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/EndpointRouteBuilderExtensions.ChatCompletions.cs index 5fe39019f..7885a763c 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/EndpointRouteBuilderExtensions.ChatCompletions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/EndpointRouteBuilderExtensions.ChatCompletions.cs @@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis; using System.Threading; using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Hosting; using Microsoft.Agents.AI.Hosting.OpenAI.ChatCompletions; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; @@ -16,13 +17,28 @@ namespace Microsoft.AspNetCore.Builder; public static partial class MicrosoftAgentAIHostingOpenAIEndpointRouteBuilderExtensions { + /// + /// Maps OpenAI ChatCompletions API endpoints to the specified for the given . + /// + /// The to add the OpenAI ChatCompletions endpoints to. + /// The builder for specific to map OpenAI ChatCompletions to. + /// Custom route path for the chat completions endpoint. + public static IEndpointConventionBuilder MapOpenAIChatCompletions( + this IEndpointRouteBuilder endpoints, + IHostedAgentBuilder agentBuilder, + [StringSyntax("Route")] string? path = null) + { + ArgumentNullException.ThrowIfNull(agentBuilder); + return endpoints.MapOpenAIChatCompletions(agentBuilder.Name, path); + } + /// /// Maps OpenAI ChatCompletions API endpoints to the specified for the given . /// /// The to add the OpenAI ChatCompletions endpoints to. /// The name of the AI agent service registered in the dependency injection container. This name is used to resolve the instance from the keyed services. /// Custom route path for the chat completions endpoint. - public static void MapOpenAIChatCompletions( + public static IEndpointConventionBuilder MapOpenAIChatCompletions( this IEndpointRouteBuilder endpoints, string agentName, [StringSyntax("Route")] string? path = null) @@ -39,6 +55,8 @@ public static void MapOpenAIChatCompletions( path ??= $"/{agentName}/v1/chat/completions"; var chatCompletionsRouteGroup = endpoints.MapGroup(path); MapChatCompletions(chatCompletionsRouteGroup, agent); + + return chatCompletionsRouteGroup; } private static void MapChatCompletions(IEndpointRouteBuilder routeGroup, AIAgent agent) diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/EndpointRouteBuilderExtensions.Responses.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/EndpointRouteBuilderExtensions.Responses.cs index 0b8934016..92d2ed737 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/EndpointRouteBuilderExtensions.Responses.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/EndpointRouteBuilderExtensions.Responses.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using System.Threading; using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Hosting; using Microsoft.Agents.AI.Hosting.OpenAI.Responses; using Microsoft.Agents.AI.Hosting.OpenAI.Responses.Models; using Microsoft.AspNetCore.Http; @@ -18,6 +19,30 @@ namespace Microsoft.AspNetCore.Builder; /// public static partial class MicrosoftAgentAIHostingOpenAIEndpointRouteBuilderExtensions { + /// + /// Maps OpenAI Responses API endpoints to the specified for the given . + /// + /// The to add the OpenAI Responses endpoints to. + /// The builder for to map the OpenAI Responses endpoints for. + /// Custom route path for the responses endpoint. + public static IEndpointConventionBuilder MapOpenAIResponses(this IEndpointRouteBuilder endpoints, IHostedAgentBuilder agentBuilder, [StringSyntax("Route")] string? responsesPath = null) + { + ArgumentNullException.ThrowIfNull(agentBuilder); + return MapOpenAIResponses(endpoints, agentBuilder.Name, responsesPath); + } + + /// + /// Maps OpenAI Responses API endpoints to the specified for the given . + /// + /// The to add the OpenAI Responses endpoints to. + /// The name of to map the OpenAI Responses endpoints for. + /// Custom route path for the responses endpoint. + public static IEndpointConventionBuilder MapOpenAIResponses(this IEndpointRouteBuilder endpoints, string agentName, [StringSyntax("Route")] string? responsesPath = null) + { + var agent = endpoints.ServiceProvider.GetRequiredKeyedService(agentName); + return endpoints.MapOpenAIResponses(agent, responsesPath); + } + /// /// Maps OpenAI Responses API endpoints to the specified for the given . /// diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/HostApplicationBuilderWorkflowExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/HostApplicationBuilderWorkflowExtensions.cs index ac7887768..2215a52a6 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting/HostApplicationBuilderWorkflowExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting/HostApplicationBuilderWorkflowExtensions.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using System.Collections.Generic; using System.Linq; using Microsoft.Agents.AI.Hosting.Local; using Microsoft.Agents.AI.Workflows; @@ -16,46 +15,6 @@ namespace Microsoft.Agents.AI.Hosting; /// public static class HostApplicationBuilderWorkflowExtensions { - /// - /// Registers a concurrent workflow that executes multiple agents in parallel. - /// - /// The to configure. - /// The unique name for the workflow. - /// A collection of instances representing agents to execute concurrently. - /// An that can be used to further configure the workflow. - /// Thrown when , , or is null. - /// Thrown when or is empty. - public static IHostedWorkflowBuilder AddConcurrentWorkflow(this IHostApplicationBuilder builder, string name, IEnumerable agentBuilders) - { - Throw.IfNullOrEmpty(agentBuilders); - - return builder.AddWorkflow(name, (sp, key) => - { - var agents = agentBuilders.Select(ab => sp.GetRequiredKeyedService(ab.Name)); - return AgentWorkflowBuilder.BuildConcurrent(workflowName: name, agents: agents); - }); - } - - /// - /// Registers a sequential workflow that executes agents in a specific order. - /// - /// The to configure. - /// The unique name for the workflow. - /// A collection of instances representing agents to execute in sequence. - /// An that can be used to further configure the workflow. - /// Thrown when , , or is null. - /// Thrown when or is empty. - public static IHostedWorkflowBuilder AddSequentialWorkflow(this IHostApplicationBuilder builder, string name, IEnumerable agentBuilders) - { - Throw.IfNullOrEmpty(agentBuilders); - - return builder.AddWorkflow(name, (sp, key) => - { - var agents = agentBuilders.Select(ab => sp.GetRequiredKeyedService(ab.Name)); - return AgentWorkflowBuilder.BuildSequential(workflowName: name, agents: agents); - }); - } - /// /// Registers a custom workflow using a factory delegate. /// diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/HostApplicationBuilderWorkflowExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/HostApplicationBuilderWorkflowExtensionsTests.cs index 6c4250943..1f617afea 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/HostApplicationBuilderWorkflowExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/HostApplicationBuilderWorkflowExtensionsTests.cs @@ -137,102 +137,6 @@ public void AddWorkflow_ValidSpecialCharactersInName_Succeeds(string name) Assert.NotNull(descriptor); } - /// - /// Verifies that providing a null builder to AddConcurrentWorkflow throws an ArgumentNullException. - /// - [Fact] - public void AddConcurrentWorkflow_NullBuilder_ThrowsArgumentNullException() - { - Assert.Throws(() => - HostApplicationBuilderWorkflowExtensions.AddConcurrentWorkflow(null!, "workflow", [null!])); - } - - /// - /// Verifies that AddConcurrentWorkflow throws ArgumentNullException for null name. - /// - [Fact] - public void AddConcurrentWorkflow_NullName_ThrowsArgumentNullException() - { - var builder = new HostApplicationBuilder(); - - var exception = Assert.Throws(() => - builder.AddConcurrentWorkflow(null!, [new HostedAgentBuilder("test", builder)])); - Assert.Equal("name", exception.ParamName); - } - - /// - /// Verifies that AddConcurrentWorkflow throws ArgumentNullException for null agent builders. - /// - [Fact] - public void AddConcurrentWorkflow_NullAgentBuilders_ThrowsArgumentNullException() - { - var builder = new HostApplicationBuilder(); - - var exception = Assert.Throws(() => - builder.AddConcurrentWorkflow("workflowName", null!)); - Assert.Equal("agentBuilders", exception.ParamName); - } - - /// - /// Verifies that AddConcurrentWorkflow returns IHostWorkflowBuilder instance. - /// - [Fact] - public void AddConcurrentWorkflow_ValidParameters_ReturnsBuilder() - { - var builder = new HostApplicationBuilder(); - - var result = builder.AddConcurrentWorkflow("concurrentWorkflow", [new HostedAgentBuilder("test", builder)]); - - Assert.NotNull(result); - Assert.IsAssignableFrom(result); - } - - /// - /// Verifies that providing a null builder to AddSequentialWorkflow throws an ArgumentNullException. - /// - [Fact] - public void AddSequentialWorkflow_NullBuilder_ThrowsArgumentNullException() - { - Assert.Throws(() => - HostApplicationBuilderWorkflowExtensions.AddSequentialWorkflow(null!, "workflow", [null!])); - } - - /// - /// Verifies that AddSequentialWorkflow throws ArgumentNullException for null name. - /// - [Fact] - public void AddSequentialWorkflow_NullName_ThrowsArgumentNullException() - { - var builder = new HostApplicationBuilder(); - - var exception = Assert.Throws(() => - builder.AddSequentialWorkflow(null!, [new HostedAgentBuilder("test", builder)])); - Assert.Equal("name", exception.ParamName); - } - - /// - /// Verifies that AddSequentialWorkflow throws ArgumentNullException for null agent builders. - /// - [Fact] - public void AddSequentialWorkflow_NullAgentBuilders_ThrowsArgumentNullException() - { - var builder = new HostApplicationBuilder(); - - var exception = Assert.Throws(() => - builder.AddSequentialWorkflow("workflowName", null!)); - Assert.Equal("agentBuilders", exception.ParamName); - } - - [Fact] - public void AddSequentialWorkflow_EmptyAgentBuilders_Throws() - { - var builder = new HostApplicationBuilder(); - - var exception = Assert.Throws(() => - builder.AddSequentialWorkflow("sequentialWorkflow", Array.Empty())); - Assert.Equal("agentBuilders", exception.ParamName); - } - /// /// Helper method to create a simple test workflow with a given name. ///