diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/AGUIEndpointRouteBuilderExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/AGUIEndpointRouteBuilderExtensions.cs
index e20d1ab448..1c3891e54b 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/AGUIEndpointRouteBuilderExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/AGUIEndpointRouteBuilderExtensions.cs
@@ -1,11 +1,14 @@
// Copyright (c) Microsoft. All rights reserved.
+using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.Hosting;
+using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.Shared;
-using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
@@ -14,40 +17,106 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
-namespace Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
+namespace Microsoft.AspNetCore.Builder;
///
/// Provides extension methods for mapping AG-UI agents to ASP.NET Core endpoints.
///
-public static class AGUIEndpointRouteBuilderExtensions
+public static class MicrosoftAgentAIHostingAGUIEndpointRouteBuilderExtensions
{
///
- /// Maps an AG-UI agent endpoint.
+ /// Maps AG-UI endpoints to the specified for the given .
///
- /// The endpoint route builder.
- /// The URL pattern for the endpoint.
- /// The agent instance.
+ /// The to add the AG-UI endpoints to.
+ /// The builder for to map the AG-UI endpoints for.
+ /// An for the mapped endpoint.
+ public static IEndpointConventionBuilder MapAGUI(this IEndpointRouteBuilder endpoints, IHostedAgentBuilder agentBuilder)
+ => MapAGUI(endpoints, agentBuilder, path: null);
+
+ ///
+ /// Maps AG-UI endpoints to the specified for the given .
+ ///
+ /// The to add the AG-UI endpoints to.
+ /// The builder for to map the AG-UI endpoints for.
+ /// Custom route path for the AG-UI endpoint.
+ /// An for the mapped endpoint.
+ public static IEndpointConventionBuilder MapAGUI(this IEndpointRouteBuilder endpoints, IHostedAgentBuilder agentBuilder, [StringSyntax("Route")] string? path)
+ {
+ ArgumentNullException.ThrowIfNull(endpoints);
+ ArgumentNullException.ThrowIfNull(agentBuilder);
+
+ AIAgent agent = endpoints.ServiceProvider.GetRequiredKeyedService(agentBuilder.Name);
+ return MapAGUI(endpoints, agent, path);
+ }
+
+ ///
+ /// Maps AG-UI endpoints to the specified for the given agent name.
+ ///
+ /// The to add the AG-UI endpoints to.
+ /// The name of the agent to map the AG-UI endpoints for.
+ /// An for the mapped endpoint.
+ public static IEndpointConventionBuilder MapAGUI(this IEndpointRouteBuilder endpoints, string agentName)
+ => MapAGUI(endpoints, agentName, path: null);
+
+ ///
+ /// Maps AG-UI endpoints to the specified for the given agent name.
+ ///
+ /// The to add the AG-UI endpoints to.
+ /// The name of the agent to map the AG-UI endpoints for.
+ /// Custom route path for the AG-UI endpoint.
+ /// An for the mapped endpoint.
+ public static IEndpointConventionBuilder MapAGUI(this IEndpointRouteBuilder endpoints, string agentName, [StringSyntax("Route")] string? path)
+ {
+ ArgumentNullException.ThrowIfNull(endpoints);
+ ArgumentException.ThrowIfNullOrWhiteSpace(agentName);
+
+ AIAgent agent = endpoints.ServiceProvider.GetRequiredKeyedService(agentName);
+ return MapAGUI(endpoints, agent, path);
+ }
+
+ ///
+ /// Maps AG-UI endpoints to the specified for the given .
+ ///
+ /// The to add the AG-UI endpoints to.
+ /// The instance to map the AG-UI endpoints for.
+ /// An for the mapped endpoint.
+ public static IEndpointConventionBuilder MapAGUI(this IEndpointRouteBuilder endpoints, AIAgent agent)
+ => MapAGUI(endpoints, agent, path: null);
+
+ ///
+ /// Maps AG-UI endpoints to the specified for the given .
+ ///
+ /// The to add the AG-UI endpoints to.
+ /// The instance to map the AG-UI endpoints for.
+ /// Custom route path for the AG-UI endpoint.
/// An for the mapped endpoint.
public static IEndpointConventionBuilder MapAGUI(
this IEndpointRouteBuilder endpoints,
- [StringSyntax("route")] string pattern,
- AIAgent aiAgent)
+ AIAgent agent,
+ [StringSyntax("Route")] string? path)
{
- return endpoints.MapPost(pattern, async ([FromBody] RunAgentInput? input, HttpContext context, CancellationToken cancellationToken) =>
+ ArgumentNullException.ThrowIfNull(endpoints);
+ ArgumentNullException.ThrowIfNull(agent);
+ ArgumentException.ThrowIfNullOrWhiteSpace(agent.Name, nameof(agent.Name));
+ ValidateAgentName(agent.Name);
+
+ path ??= $"/{agent.Name}/agui";
+
+ return endpoints.MapPost(path, async ([FromBody] RunAgentInput? input, HttpContext context, CancellationToken cancellationToken) =>
{
if (input is null)
{
return Results.BadRequest();
}
- var jsonOptions = context.RequestServices.GetRequiredService>();
- var jsonSerializerOptions = jsonOptions.Value.SerializerOptions;
+ IOptions jsonOptions = context.RequestServices.GetRequiredService>();
+ System.Text.Json.JsonSerializerOptions jsonSerializerOptions = jsonOptions.Value.SerializerOptions;
- var messages = input.Messages.AsChatMessages(jsonSerializerOptions);
- var clientTools = input.Tools?.AsAITools().ToList();
+ IEnumerable messages = input.Messages.AsChatMessages(jsonSerializerOptions);
+ List? clientTools = input.Tools?.AsAITools().ToList();
// Create run options with AG-UI context in AdditionalProperties
- var runOptions = new ChatClientAgentRunOptions
+ ChatClientAgentRunOptions runOptions = new()
{
ChatOptions = new ChatOptions
{
@@ -64,7 +133,7 @@ public static class AGUIEndpointRouteBuilderExtensions
};
// Run the agent and convert to AG-UI events
- var events = aiAgent.RunStreamingAsync(
+ IAsyncEnumerable events = agent.RunStreamingAsync(
messages,
options: runOptions,
cancellationToken: cancellationToken)
@@ -76,8 +145,17 @@ public static class AGUIEndpointRouteBuilderExtensions
jsonSerializerOptions,
cancellationToken);
- var sseLogger = context.RequestServices.GetRequiredService>();
+ ILogger sseLogger = context.RequestServices.GetRequiredService>();
return new AGUIServerSentEventsResult(events, sseLogger);
});
}
+
+ private static void ValidateAgentName([NotNull] string agentName)
+ {
+ string escaped = Uri.EscapeDataString(agentName);
+ if (!string.Equals(escaped, agentName, StringComparison.OrdinalIgnoreCase))
+ {
+ throw new ArgumentException($"Agent name '{agentName}' contains characters invalid for URL routes.", nameof(agentName));
+ }
+ }
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/AGUIEndpointRouteBuilderExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/AGUIEndpointRouteBuilderExtensionsTests.cs
index 78a3048747..ed42c4d96d 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/AGUIEndpointRouteBuilderExtensionsTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/AGUIEndpointRouteBuilderExtensionsTests.cs
@@ -20,7 +20,7 @@ using Moq;
namespace Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests;
///
-/// Unit tests for the class.
+/// Unit tests for the class.
///
public sealed class AGUIEndpointRouteBuilderExtensionsTests
{
@@ -38,7 +38,7 @@ public sealed class AGUIEndpointRouteBuilderExtensionsTests
AIAgent agent = new TestAgent();
// Act
- IEndpointConventionBuilder? result = endpointsMock.Object.MapAGUI(Pattern, agent);
+ IEndpointConventionBuilder? result = endpointsMock.Object.MapAGUI(agent, Pattern);
// Assert
Assert.NotNull(result);
@@ -423,6 +423,8 @@ public sealed class AGUIEndpointRouteBuilderExtensionsTests
{
public override string Id => "multi-response-agent";
+ public override string Name => "multi-response-agent";
+
public override string? Description => "Agent that produces multiple text chunks";
public override AgentThread GetNewThread() => new TestInMemoryAgentThread();
@@ -512,6 +514,8 @@ public sealed class AGUIEndpointRouteBuilderExtensionsTests
{
public override string Id => "test-agent";
+ public override string Name => "test-agent";
+
public override string? Description => "Test agent";
public override AgentThread GetNewThread() => new TestInMemoryAgentThread();
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/MapAGUIEndpointRouteBuilderExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/MapAGUIEndpointRouteBuilderExtensionsTests.cs
new file mode 100644
index 0000000000..750958b912
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/MapAGUIEndpointRouteBuilderExtensionsTests.cs
@@ -0,0 +1,479 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests;
+
+///
+/// Unit tests for the MapAGUI extension methods.
+///
+public sealed class MapAGUIEndpointRouteBuilderExtensionsTests
+{
+ ///
+ /// Verifies that MapAGUI throws ArgumentNullException for null endpoints when using IHostedAgentBuilder.
+ ///
+ [Fact]
+ public void MapAGUI_WithAgentBuilder_NullEndpoints_ThrowsArgumentNullException()
+ {
+ // Arrange
+ IEndpointRouteBuilder endpoints = null!;
+ WebApplicationBuilder builder = WebApplication.CreateBuilder();
+ IChatClient mockChatClient = new SimpleMockChatClient();
+ builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
+ IHostedAgentBuilder agentBuilder = builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
+
+ // Act & Assert
+ ArgumentNullException exception = Assert.Throws(() =>
+ endpoints.MapAGUI(agentBuilder));
+
+ Assert.Equal("endpoints", exception.ParamName);
+ }
+
+ ///
+ /// Verifies that MapAGUI throws ArgumentNullException for null agentBuilder.
+ ///
+ [Fact]
+ public void MapAGUI_WithAgentBuilder_NullAgentBuilder_ThrowsArgumentNullException()
+ {
+ // Arrange
+ WebApplicationBuilder builder = WebApplication.CreateBuilder();
+ IChatClient mockChatClient = new SimpleMockChatClient();
+ builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
+ builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
+ builder.Services.AddAGUI();
+ using WebApplication app = builder.Build();
+ IHostedAgentBuilder agentBuilder = null!;
+
+ // Act & Assert
+ ArgumentNullException exception = Assert.Throws(() =>
+ app.MapAGUI(agentBuilder));
+
+ Assert.Equal("agentBuilder", exception.ParamName);
+ }
+
+ ///
+ /// Verifies that MapAGUI with IHostedAgentBuilder correctly maps the agent.
+ ///
+ [Fact]
+ public void MapAGUI_WithAgentBuilder_Succeeds()
+ {
+ // Arrange
+ WebApplicationBuilder builder = WebApplication.CreateBuilder();
+ IChatClient mockChatClient = new SimpleMockChatClient();
+ builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
+ IHostedAgentBuilder agentBuilder = builder.AddAIAgent("agent", "Instructions", chatClientServiceKey: "chat-client");
+ builder.Services.AddAGUI();
+ using WebApplication app = builder.Build();
+
+ // Act & Assert - Should not throw
+ app.MapAGUI(agentBuilder);
+ Assert.NotNull(app);
+ }
+
+ ///
+ /// Verifies that MapAGUI with IHostedAgentBuilder and custom path works correctly.
+ ///
+ [Fact]
+ public void MapAGUI_WithAgentBuilder_CustomPath_Succeeds()
+ {
+ // Arrange
+ WebApplicationBuilder builder = WebApplication.CreateBuilder();
+ IChatClient mockChatClient = new SimpleMockChatClient();
+ builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
+ IHostedAgentBuilder agentBuilder = builder.AddAIAgent("my-agent", "Instructions", chatClientServiceKey: "chat-client");
+ builder.Services.AddAGUI();
+ using WebApplication app = builder.Build();
+
+ // Act & Assert - Should not throw
+ app.MapAGUI(agentBuilder, path: "/agents/my-agent/agui");
+ Assert.NotNull(app);
+ }
+
+ ///
+ /// Verifies that multiple agents can be mapped using IHostedAgentBuilder.
+ ///
+ [Fact]
+ public void MapAGUI_WithAgentBuilder_MultipleAgents_Succeeds()
+ {
+ // Arrange
+ WebApplicationBuilder builder = WebApplication.CreateBuilder();
+ IChatClient mockChatClient = new SimpleMockChatClient();
+ builder.Services.AddKeyedSingleton("chat-client", mockChatClient);
+ IHostedAgentBuilder agent1Builder = builder.AddAIAgent("agent1", "Instructions1", chatClientServiceKey: "chat-client");
+ IHostedAgentBuilder agent2Builder = builder.AddAIAgent("agent2", "Instructions2", chatClientServiceKey: "chat-client");
+ builder.Services.AddAGUI();
+ using WebApplication app = builder.Build();
+
+ // Act & Assert - Should not throw
+ app.MapAGUI(agent1Builder);
+ app.MapAGUI(agent2Builder);
+ Assert.NotNull(app);
+ }
+
+ ///
+ /// Verifies that IHostedAgentBuilder overload validates agent name characters.
+ ///
+ [Theory]
+ [InlineData("agent with spaces")]
+ [InlineData("agent