// Copyright (c) Microsoft. All rights reserved. using System; using System.ClientModel; using System.ClientModel.Primitives; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Azure.AI.Extensions.OpenAI; using Azure.AI.Projects; using Microsoft.Extensions.AI; using Microsoft.Extensions.Logging; using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.AI.Foundry; /// /// Provides an that uses Microsoft Foundry for AI agent capabilities. /// /// /// /// connects to a pre-configured server-side agent in Microsoft Foundry, /// wrapping it as an for use with Agent Framework. Unlike the direct /// AIProjectClient.AsAIAgent(model, instructions) approach (which creates a local agent /// backed by the Responses API without any server-side agent definition), /// works with agents that are managed and versioned in the Foundry service. /// /// /// This class provides convenient access to Foundry-specific features such as server-side /// conversation management via . /// /// /// Instances can be created directly via public constructors or through /// AsAIAgent extension methods on . /// /// public sealed class FoundryAgent : DelegatingAIAgent { /// /// Initializes a new instance of the class using the direct Responses API path. /// /// The Microsoft Foundry project endpoint. /// The authentication credential. /// The model deployment name. /// The instructions that guide the agent's behavior. /// Optional configuration options for the . /// Optional name for the agent. /// Optional description for the agent. /// Optional tools to use when interacting with the agent. /// Provides a way to customize the creation of the underlying . /// Optional logger factory for creating loggers used by the agent. /// Optional service provider for resolving dependencies required by AI functions. public FoundryAgent( Uri projectEndpoint, AuthenticationTokenProvider credential, string model, string instructions, AIProjectClientOptions? clientOptions = null, string? name = null, string? description = null, IList? tools = null, Func? clientFactory = null, ILoggerFactory? loggerFactory = null, IServiceProvider? services = null) : base(CreateInnerAgent( CreateProjectClient(projectEndpoint, credential, clientOptions), model, instructions, name, description, tools, clientFactory, loggerFactory, services, out _)) { } /// /// Initializes a new instance of the class from an agent-specific endpoint. /// /// /// The agent-specific endpoint URI. Must be of the shape /// https://<host>/.../projects/<project>/agents/<agentName>/endpoint/protocols/openai. /// /// The authentication credential. /// /// Optional configuration for the underlying . When supplied: /// /// The instance is passed through to the per-agent client; pipeline policies added via AddPolicy(...) on it execute on the per-agent traffic. /// Endpoint and are owned by this constructor and are overwritten with values derived from ; any caller value is replaced. /// For the project-level conversations client a separate fresh options bag is built that copies only , , , and UserAgentApplicationId; pipeline policies added via AddPolicy(...) do not propagate to the conversations pipeline. /// /// /// Optional tools to use when interacting with the agent. /// Provides a way to customize the creation of the underlying . /// Optional service provider for resolving dependencies required by AI functions. /// or is null. /// does not match the expected agent-endpoint shape. /// /// This is the lightweight constructor for invoking an existing Foundry hosted agent when the /// caller already has the per-agent endpoint URL. It populates /// and from the agent name parsed out of the endpoint /// path; Description, Instructions, Temperature, and TopP are not /// populated. Callers that need those fields hydrated from server-side state should use /// AIProjectClient.AsAIAgent(ProjectsAgentVersion) or /// AIProjectClient.AsAIAgent(ProjectsAgentRecord) instead. /// public FoundryAgent( Uri agentEndpoint, AuthenticationTokenProvider credential, ProjectOpenAIClientOptions? clientOptions = null, IList? tools = null, Func? clientFactory = null, IServiceProvider? services = null) : base(CreateInnerAgentFromAgentEndpoint(agentEndpoint, credential, clientOptions, tools, clientFactory, services)) { } /// /// Internal constructor used by the AsAIAgent(this AIProjectClient, Uri, ...) /// extension where the caller already has an and the agent /// endpoint URI. Reuses the supplied client's pipeline (no new credential or transport is /// stamped) and surfaces the agent through a just like the /// public agent-endpoint ctor. /// internal FoundryAgent( AIProjectClient aiProjectClient, Uri agentEndpoint, IList? tools = null, Func? clientFactory = null, IServiceProvider? services = null) : base(CreateInnerAgentFromAgentEndpointReusingProjectClient(aiProjectClient, agentEndpoint, tools, clientFactory, services)) { } /// /// Internal constructor used by AsAIAgent extension methods that already have a /// configured . The inner agent already routes through a /// whose GetService<AIProjectClient>() surfaces /// the project client to downstream callers, so the agent does not also need a private /// reference here. /// internal FoundryAgent(ChatClientAgent innerAgent) : base(WireClientHeaders(Throw.IfNull(innerAgent))) { } #region Convenience methods /// /// Creates a new agent session instance using an existing conversation identifier to continue that conversation. /// /// The identifier of an existing conversation to continue. /// The to monitor for cancellation requests. /// /// A value task representing the asynchronous operation. The task result contains a new instance configured to work with the specified conversation. /// /// /// /// This method creates an that relies on server-side chat history storage, where the chat history /// is maintained by the underlying AI service rather than by a local . /// /// /// Agent sessions created with this method will only work with /// instances that support server-side conversation storage through their underlying . /// /// public ValueTask CreateSessionAsync(string conversationId, CancellationToken cancellationToken = default) => this.GetInnerChatClientAgent().CreateSessionAsync(conversationId, cancellationToken); /// /// Creates a server-side conversation session that appears in the Foundry Project UI. /// /// A token to monitor for cancellation requests. /// A linked to the newly created server-side conversation. public async Task CreateConversationSessionAsync(CancellationToken cancellationToken = default) { // The inner FoundryChatClient surfaces an AIProjectClient via GetService for all // three construction modes (Plan #2 Agent Endpoint mode materialization). Resolve it through the // delegating chain at call time instead of caching a private reference on this agent. var aiProjectClient = this.GetService() ?? throw new InvalidOperationException( "FoundryAgent inner chain does not expose an AIProjectClient; cannot create a project-level conversation session."); var conversationsClient = aiProjectClient.GetProjectOpenAIClient().GetProjectConversationsClient(); var conversation = (await conversationsClient.CreateProjectConversationAsync(options: null, cancellationToken).ConfigureAwait(false)).Value; return (ChatClientAgentSession)await this.GetInnerChatClientAgent().CreateSessionAsync(conversation.Id, cancellationToken).ConfigureAwait(false); } /// Walks the delegating chain to find the inner . private ChatClientAgent GetInnerChatClientAgent() => this.GetService() ?? throw new InvalidOperationException("FoundryAgent inner chain does not contain a ChatClientAgent."); #endregion #region Private helpers private static AIAgent CreateInnerAgent( AIProjectClient aiProjectClient, string model, string instructions, string? name, string? description, IList? tools, Func? clientFactory, ILoggerFactory? loggerFactory, IServiceProvider? services, out AIProjectClient outClient) { Throw.IfNullOrWhitespace(model); Throw.IfNullOrWhitespace(instructions); outClient = aiProjectClient; ChatClientAgentOptions options = new() { Name = name, Description = description, ChatOptions = new ChatOptions { ModelId = model, Instructions = instructions, Tools = tools, }, }; return CreateResponsesChatClientAgent(aiProjectClient, options, clientFactory, loggerFactory, services); } private static AIAgent CreateResponsesChatClientAgent( AIProjectClient aiProjectClient, ChatClientAgentOptions agentOptions, Func? clientFactory, ILoggerFactory? loggerFactory, IServiceProvider? services) { Throw.IfNull(aiProjectClient); Throw.IfNull(agentOptions); Throw.IfNull(agentOptions.ChatOptions); Throw.IfNullOrWhitespace(agentOptions.ChatOptions.ModelId); IChatClient chatClient = new FoundryChatClient(aiProjectClient, agentOptions.ChatOptions.ModelId); if (clientFactory is not null) { chatClient = clientFactory(chatClient); } return WireClientHeaders(new ChatClientAgent(chatClient, agentOptions, loggerFactory, services)); } /// /// Registers on the agent's underlying chat client (if it /// exposes ) and wraps the agent in a /// so per-call x-client-* headers stamped via /// reach /// the wire. Idempotent: if the chain already contains a , /// the original instance is returned unchanged. /// private static AIAgent WireClientHeaders(ChatClientAgent innerAgent) { if (innerAgent.GetService() is not null) { return innerAgent; } #pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. if (innerAgent.ChatClient.GetService() is { } policies) { OpenAIRequestPoliciesReflection.AddPolicyIfMissing( policies, ClientHeadersPolicy.Instance, PipelinePosition.PerCall); } #pragma warning restore MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. return new ClientHeadersAgent(innerAgent); } /// /// Builds the inner for the agent-endpoint constructor. The /// per-agent shape and URL parsing are owned by /// ; we just construct it in the Agent Endpoint mode (Mode 3) /// and pass the inner chat client through any caller-provided . /// private static AIAgent CreateInnerAgentFromAgentEndpoint( Uri agentEndpoint, AuthenticationTokenProvider credential, ProjectOpenAIClientOptions? clientOptions, IList? tools, Func? clientFactory, IServiceProvider? services) { Throw.IfNull(agentEndpoint); Throw.IfNull(credential); IChatClient chatClient = new FoundryChatClient(agentEndpoint, credential, clientOptions); var agentName = ((FoundryChatClient)chatClient).AgentName!; if (clientFactory is not null) { chatClient = clientFactory(chatClient); } ChatClientAgentOptions agentOptions = new() { Id = agentName, Name = agentName, ChatOptions = new() { Tools = tools }, }; return WireClientHeaders(new ChatClientAgent(chatClient, agentOptions, services: services)); } /// /// Variant of that reuses an existing /// 's pipeline instead of stamping a fresh credential. Used by /// the AsAIAgent(AIProjectClient, Uri agentEndpoint, ...) extension overload. /// private static AIAgent CreateInnerAgentFromAgentEndpointReusingProjectClient( AIProjectClient aiProjectClient, Uri agentEndpoint, IList? tools, Func? clientFactory, IServiceProvider? services) { Throw.IfNull(aiProjectClient); Throw.IfNull(agentEndpoint); IChatClient chatClient = new FoundryChatClient(aiProjectClient, agentEndpoint, clientOptions: null); var agentName = ((FoundryChatClient)chatClient).AgentName!; if (clientFactory is not null) { chatClient = clientFactory(chatClient); } ChatClientAgentOptions agentOptions = new() { Id = agentName, Name = agentName, ChatOptions = new() { Tools = tools }, }; return WireClientHeaders(new ChatClientAgent(chatClient, agentOptions, services: services)); } /// /// Parses an agent endpoint URI. Delegates to /// so the chat client and the agent share a single source of truth for the URL shape. /// internal static (string AgentName, Uri ProjectRoot) ParseAgentEndpoint(Uri agentEndpoint) => FoundryChatClient.ParseAgentEndpoint(agentEndpoint); /// /// Parses an agent endpoint URI of shape /// https://<host>/.../projects/<project>/agents/<agentName>/endpoint/protocols/openai /// and returns the agent name and the derived project-root URI. /// /// /// Single source of truth for both agent-name extraction and project-root derivation. /// Tolerates trailing slash, casing variants on /agents/ and the suffix segment, and /// strips query string and fragment. Throws for inputs that /// do not match the expected shape. /// private static AIProjectClient CreateProjectClient(Uri endpoint, AuthenticationTokenProvider credential, AIProjectClientOptions? clientOptions = null) { Throw.IfNull(endpoint); Throw.IfNull(credential); return new AIProjectClient(endpoint, credential, clientOptions ?? new AIProjectClientOptions()); } #endregion }