Add MapAGUI hosting overloads with IHostedAgentBuilder and agent name support

This adds the same hosting patterns from A2A and OpenAI to AGUI:
- MapAGUI(IHostedAgentBuilder) and MapAGUI(IHostedAgentBuilder, string? path)
- MapAGUI(string agentName) and MapAGUI(string agentName, string? path)
- MapAGUI(AIAgent) and MapAGUI(AIAgent, string? path)
- ValidateAgentName for URL-safe validation
- Updated namespace to Microsoft.AspNetCore.Builder
- Renamed class to MicrosoftAgentAIHostingAGUIEndpointRouteBuilderExtensions
- Added comprehensive unit tests
This commit is contained in:
Javier Calvarro Nelson
2025-12-01 13:07:20 +01:00
Unverified
parent 5c70e143a0
commit 5160d381b1
4 changed files with 665 additions and 19 deletions
@@ -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;
/// <summary>
/// Provides extension methods for mapping AG-UI agents to ASP.NET Core endpoints.
/// </summary>
public static class AGUIEndpointRouteBuilderExtensions
public static class MicrosoftAgentAIHostingAGUIEndpointRouteBuilderExtensions
{
/// <summary>
/// Maps an AG-UI agent endpoint.
/// Maps AG-UI endpoints to the specified <see cref="IEndpointRouteBuilder"/> for the given <see cref="IHostedAgentBuilder"/>.
/// </summary>
/// <param name="endpoints">The endpoint route builder.</param>
/// <param name="pattern">The URL pattern for the endpoint.</param>
/// <param name="aiAgent">The agent instance.</param>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the AG-UI endpoints to.</param>
/// <param name="agentBuilder">The builder for <see cref="AIAgent"/> to map the AG-UI endpoints for.</param>
/// <returns>An <see cref="IEndpointConventionBuilder"/> for the mapped endpoint.</returns>
public static IEndpointConventionBuilder MapAGUI(this IEndpointRouteBuilder endpoints, IHostedAgentBuilder agentBuilder)
=> MapAGUI(endpoints, agentBuilder, path: null);
/// <summary>
/// Maps AG-UI endpoints to the specified <see cref="IEndpointRouteBuilder"/> for the given <see cref="IHostedAgentBuilder"/>.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the AG-UI endpoints to.</param>
/// <param name="agentBuilder">The builder for <see cref="AIAgent"/> to map the AG-UI endpoints for.</param>
/// <param name="path">Custom route path for the AG-UI endpoint.</param>
/// <returns>An <see cref="IEndpointConventionBuilder"/> for the mapped endpoint.</returns>
public static IEndpointConventionBuilder MapAGUI(this IEndpointRouteBuilder endpoints, IHostedAgentBuilder agentBuilder, [StringSyntax("Route")] string? path)
{
ArgumentNullException.ThrowIfNull(endpoints);
ArgumentNullException.ThrowIfNull(agentBuilder);
AIAgent agent = endpoints.ServiceProvider.GetRequiredKeyedService<AIAgent>(agentBuilder.Name);
return MapAGUI(endpoints, agent, path);
}
/// <summary>
/// Maps AG-UI endpoints to the specified <see cref="IEndpointRouteBuilder"/> for the given agent name.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the AG-UI endpoints to.</param>
/// <param name="agentName">The name of the agent to map the AG-UI endpoints for.</param>
/// <returns>An <see cref="IEndpointConventionBuilder"/> for the mapped endpoint.</returns>
public static IEndpointConventionBuilder MapAGUI(this IEndpointRouteBuilder endpoints, string agentName)
=> MapAGUI(endpoints, agentName, path: null);
/// <summary>
/// Maps AG-UI endpoints to the specified <see cref="IEndpointRouteBuilder"/> for the given agent name.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the AG-UI endpoints to.</param>
/// <param name="agentName">The name of the agent to map the AG-UI endpoints for.</param>
/// <param name="path">Custom route path for the AG-UI endpoint.</param>
/// <returns>An <see cref="IEndpointConventionBuilder"/> for the mapped endpoint.</returns>
public static IEndpointConventionBuilder MapAGUI(this IEndpointRouteBuilder endpoints, string agentName, [StringSyntax("Route")] string? path)
{
ArgumentNullException.ThrowIfNull(endpoints);
ArgumentException.ThrowIfNullOrWhiteSpace(agentName);
AIAgent agent = endpoints.ServiceProvider.GetRequiredKeyedService<AIAgent>(agentName);
return MapAGUI(endpoints, agent, path);
}
/// <summary>
/// Maps AG-UI endpoints to the specified <see cref="IEndpointRouteBuilder"/> for the given <see cref="AIAgent"/>.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the AG-UI endpoints to.</param>
/// <param name="agent">The <see cref="AIAgent"/> instance to map the AG-UI endpoints for.</param>
/// <returns>An <see cref="IEndpointConventionBuilder"/> for the mapped endpoint.</returns>
public static IEndpointConventionBuilder MapAGUI(this IEndpointRouteBuilder endpoints, AIAgent agent)
=> MapAGUI(endpoints, agent, path: null);
/// <summary>
/// Maps AG-UI endpoints to the specified <see cref="IEndpointRouteBuilder"/> for the given <see cref="AIAgent"/>.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the AG-UI endpoints to.</param>
/// <param name="agent">The <see cref="AIAgent"/> instance to map the AG-UI endpoints for.</param>
/// <param name="path">Custom route path for the AG-UI endpoint.</param>
/// <returns>An <see cref="IEndpointConventionBuilder"/> for the mapped endpoint.</returns>
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<IOptions<Microsoft.AspNetCore.Http.Json.JsonOptions>>();
var jsonSerializerOptions = jsonOptions.Value.SerializerOptions;
IOptions<Microsoft.AspNetCore.Http.Json.JsonOptions> jsonOptions = context.RequestServices.GetRequiredService<IOptions<Microsoft.AspNetCore.Http.Json.JsonOptions>>();
System.Text.Json.JsonSerializerOptions jsonSerializerOptions = jsonOptions.Value.SerializerOptions;
var messages = input.Messages.AsChatMessages(jsonSerializerOptions);
var clientTools = input.Tools?.AsAITools().ToList();
IEnumerable<ChatMessage> messages = input.Messages.AsChatMessages(jsonSerializerOptions);
List<AITool>? 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<BaseEvent> events = agent.RunStreamingAsync(
messages,
options: runOptions,
cancellationToken: cancellationToken)
@@ -76,8 +145,17 @@ public static class AGUIEndpointRouteBuilderExtensions
jsonSerializerOptions,
cancellationToken);
var sseLogger = context.RequestServices.GetRequiredService<ILogger<AGUIServerSentEventsResult>>();
ILogger<AGUIServerSentEventsResult> sseLogger = context.RequestServices.GetRequiredService<ILogger<AGUIServerSentEventsResult>>();
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));
}
}
}