// Copyright (c) Microsoft. All rights reserved. using System; using System.ClientModel.Primitives; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using Azure.AI.AgentServer.Responses; using Azure.Core; using Azure.Identity; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.AI; 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.Services.AddKeyedSingleton<AIAgent>("my-agent", myAgent); /// 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(_ => FileSystemAgentSessionStore.CreateDefault()); 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, a file-system session store is used, rooted at /.checkpoints when running in a Foundry hosted environment and {cwd}/.checkpoints locally. /// 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 ??= FileSystemAgentSessionStore.CreateDefault(); 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); return endpoints; } /// /// The ActivitySource name for the Responses hosting pipeline. /// 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(); } /// /// Registers the hosted-agent User-Agent supplement policy /// () on the agent's underlying chat client via the /// MEAI 10.5.1 hook so every outgoing OpenAI Responses /// request carries the segment foundry-hosting/agent-framework-dotnet/{version}. /// /// /// /// Best-effort and idempotent. The method is a no-op when: /// /// exposes no ; /// the chat client is not OpenAI-backed (the service lookup returns ); /// the policy was already registered on this client by a prior invocation (deduped via reflection on OpenAIRequestPolicies._entries). /// /// /// /// Returns the same instance unchanged. The policy is installed /// on the chat client; the agent itself is not wrapped. /// /// internal static AIAgent TryApplyUserAgent(AIAgent agent) { var chatClient = agent.GetService(); if (chatClient?.GetService() is { } policies) { // Hosted agents are typically singletons resolved per request, so AddPolicy must be // called at most once per OpenAIRequestPolicies instance to avoid unbounded growth of // the policy list (each entry adds per-request CPU work even though the User-Agent // value stays stable). Track which instances we have already wired with a // ConditionalWeakTable keyed on the OpenAIRequestPolicies reference; the table holds // weak references so it does not extend the lifetime of the chat client. if (s_userAgentRegistrations.TryAdd(policies, s_boxedTrue)) { policies.AddPolicy(HostedAgentUserAgentPolicy.Instance, PipelinePosition.PerCall); } } return agent; } private static readonly object s_boxedTrue = new(); private static readonly ConditionalWeakTable s_userAgentRegistrations = new(); }