// Copyright (c) Microsoft. All rights reserved. using System; using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Threading.Tasks; using Azure.AI.AgentServer.Responses; using Azure.Core; using Azure.Identity; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Agents.AI.Foundry.Hosting; /// /// Extension methods for registering agent-framework agents as Foundry Hosted Agents /// using the Azure AI Responses Server SDK. /// [Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)] public static class FoundryHostingExtensions { /// /// Registers the Azure AI Responses Server SDK and /// as the . Agents are resolved from keyed DI services /// using the agent.name or metadata["entity_id"] from incoming requests. /// /// /// /// This method calls AddResponsesServer() internally, so you do not need to /// call it separately. Register your instances before calling this. /// /// /// Example: /// /// builder.AddAIAgent("my-agent", ...); /// builder.Services.AddFoundryResponses(); /// /// var app = builder.Build(); /// app.MapFoundryResponses(); /// /// /// /// The service collection. /// The service collection for chaining. public static IServiceCollection AddFoundryResponses(this IServiceCollection services) { ArgumentNullException.ThrowIfNull(services); services.AddResponsesServer(); services.TryAddSingleton(); services.TryAddSingleton(); return services; } /// /// Registers the Azure AI Responses Server SDK and a specific /// as the handler for all incoming requests, regardless of the agent.name in the request. /// /// /// /// Use this overload when hosting a single agent. The provided agent instance is /// registered as both a keyed service and the default . /// This method calls AddResponsesServer() internally. /// /// /// Example: /// /// builder.Services.AddFoundryResponses(myAgent); /// /// var app = builder.Build(); /// app.MapFoundryResponses(); /// /// /// /// The service collection. /// The agent instance to register. /// The agent session store to use for managing agent sessions server-side. If null, an in-memory session store will be used. /// The service collection for chaining. public static IServiceCollection AddFoundryResponses(this IServiceCollection services, AIAgent agent, AgentSessionStore? agentSessionStore = null) { ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(agent); services.AddResponsesServer(); agentSessionStore ??= new InMemoryAgentSessionStore(); if (!string.IsNullOrWhiteSpace(agent.Name)) { services.TryAddKeyedSingleton(agent.Name, agent); services.TryAddKeyedSingleton(agent.Name, agentSessionStore); } // Also register as the default (non-keyed) agent so requests // without an agent name can resolve it (e.g., local dev tooling). services.TryAddSingleton(agent); services.TryAddSingleton(agentSessionStore); services.TryAddSingleton(); return services; } /// /// Registers the Foundry Toolbox service, which eagerly connects to the Foundry Toolboxes /// MCP proxy at startup and provides MCP tools to . /// /// /// /// Each string in is a toolbox name registered in the Foundry /// project. The proxy URL per toolbox is constructed as: /// {FOUNDRY_AGENT_TOOLSET_ENDPOINT}/{toolboxName}/mcp?api-version=2025-05-01-preview /// /// /// When FOUNDRY_AGENT_TOOLSET_ENDPOINT is absent, startup succeeds without error and /// no tools are loaded (the container remains healthy per spec §2). /// /// /// Example: /// /// builder.Services.AddFoundryToolboxes("my-toolbox", "another-toolbox"); /// /// /// /// The service collection. /// Names of the Foundry toolboxes to connect to. /// The service collection for chaining. public static IServiceCollection AddFoundryToolboxes( this IServiceCollection services, params string[] toolboxNames) => services.AddFoundryToolboxes(configureOptions: null, toolboxNames); /// /// Registers the Foundry Toolbox service with additional options configuration. /// /// The service collection. /// Callback to further configure (e.g. set ). /// Names of the Foundry toolboxes to pre-register at startup. /// The service collection for chaining. public static IServiceCollection AddFoundryToolboxes( this IServiceCollection services, Action? configureOptions, params string[] toolboxNames) { ArgumentNullException.ThrowIfNull(services); services.Configure(opt => { foreach (var name in toolboxNames) { if (!string.IsNullOrWhiteSpace(name)) { opt.ToolboxNames.Add(name); } } configureOptions?.Invoke(opt); }); // Register DefaultAzureCredential as the default TokenCredential if not already registered services.TryAddSingleton(_ => new DefaultAzureCredential()); // Register FoundryToolboxService as a singleton so it can be injected into the handler services.TryAddSingleton(); // AddHostedService uses TryAddEnumerable internally, so calling AddFoundryToolboxes // multiple times will not invoke StartAsync twice on the same singleton. services.AddHostedService(sp => sp.GetRequiredService()); return services; } /// /// Maps the Responses API routes for the agent-framework handler to the endpoint routing pipeline. /// /// The endpoint route builder. /// Optional route prefix (e.g., "/openai/v1"). Default: empty (routes at /responses). /// The endpoint route builder for chaining. public static IEndpointRouteBuilder MapFoundryResponses(this IEndpointRouteBuilder endpoints, string prefix = "") { ArgumentNullException.ThrowIfNull(endpoints); endpoints.MapResponsesServer(prefix); if (endpoints is IApplicationBuilder app) { // Ensure the middleware is added to the pipeline app.UseMiddleware(); } return endpoints; } /// /// The ActivitySource name for the Responses hosting pipeline. /// Matches the value previously exposed by AgentHostTelemetry.ResponsesSourceName /// in Azure.AI.AgentServer.Core. /// private const string ResponsesSourceName = "Azure.AI.AgentServer.Responses"; /// /// Wraps with instrumentation /// so that agent invocations emit spans into the pipeline registered by /// Azure.AI.AgentServer.Core's AddAgentHostTelemetry(). /// If the agent is already instrumented the original instance is returned unchanged. /// internal static AIAgent ApplyOpenTelemetry(AIAgent agent) { if (agent.GetService() is not null) { return agent; } return agent.AsBuilder() .UseOpenTelemetry(sourceName: ResponsesSourceName) .Build(); } private sealed class AgentFrameworkUserAgentMiddleware(RequestDelegate next) { private static readonly string s_userAgentValue = CreateUserAgentValue(); public async Task InvokeAsync(HttpContext context) { var headers = context.Request.Headers; var userAgent = headers.UserAgent.ToString(); if (string.IsNullOrEmpty(userAgent)) { headers.UserAgent = s_userAgentValue; } else if (!userAgent.Contains(s_userAgentValue, StringComparison.OrdinalIgnoreCase)) { headers.UserAgent = $"{userAgent} {s_userAgentValue}"; } await next(context).ConfigureAwait(false); } private static string CreateUserAgentValue() { const string Name = "agent-framework-dotnet"; if (typeof(AgentFrameworkUserAgentMiddleware).Assembly.GetCustomAttribute()?.InformationalVersion is string version) { int pos = version.IndexOf('+'); if (pos >= 0) { version = version.Substring(0, pos); } if (version.Length > 0) { return $"{Name}/{version}"; } } return Name; } } }