// Copyright (c) Microsoft. All rights reserved.
using System;
using System.ClientModel;
using System.ClientModel.Primitives;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
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.DiagnosticIds;
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 .
///
///
[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)]
public sealed class FoundryAgent : DelegatingAIAgent
{
///
/// Default OAuth scope for the Azure AI resource. Matches the scope used by
/// Azure.AI.Extensions.OpenAI's internal authentication helper so the bearer token is
/// accepted by the Foundry control plane.
///
private const string AzureAiResourceScope = "https://ai.azure.com/.default";
///
/// The cached when one was supplied or constructed by the active
/// constructor. Null when the agent was constructed via the agent-endpoint constructor, which
/// does not build a full .
///
private readonly AIProjectClient? _aiProjectClient;
///
/// Project-scoped . Always non-null. Used for project-level
/// operations such as .
/// In agent-endpoint mode this is built directly from the project root derived from the
/// supplied agent endpoint; in project-endpoint mode it is the cached client returned by
/// .
///
private readonly ProjectOpenAIClient _projectOpenAIClient;
///
/// 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 var aiProjectClient))
{
this._aiProjectClient = aiProjectClient;
this._projectOpenAIClient = aiProjectClient.GetProjectOpenAIClient();
}
///
/// 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))
{
this._projectOpenAIClient = CreateProjectLevelOpenAIClientFromAgentEndpoint(agentEndpoint, credential, clientOptions);
}
///
/// Internal constructor used by AsAIAgent extension methods that already have an and a configured .
///
internal FoundryAgent(AIProjectClient aiProjectClient, ChatClientAgent innerAgent)
: base(WireClientHeaders(Throw.IfNull(innerAgent)))
{
this._aiProjectClient = Throw.IfNull(aiProjectClient);
this._projectOpenAIClient = aiProjectClient.GetProjectOpenAIClient();
}
#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)
{
var conversationsClient = this._projectOpenAIClient.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
///
public override object? GetService(Type serviceType, object? serviceKey = null)
{
if (serviceKey is null && serviceType == typeof(AIProjectClient))
{
return this._aiProjectClient;
}
if (serviceKey is null && serviceType == typeof(ProjectOpenAIClient))
{
return this._projectOpenAIClient;
}
return base.GetService(serviceType, serviceKey);
}
#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 AzureAIProjectResponsesChatClient(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;
}
if (innerAgent.ChatClient.GetService() is { } policies)
{
OpenAIRequestPoliciesReflection.AddPolicyIfMissing(
policies,
ClientHeadersPolicy.Instance,
PipelinePosition.PerCall);
}
return new ClientHeadersAgent(innerAgent);
}
///
/// Builds the inner for the agent-endpoint constructor by
/// constructing a per-agent via the
/// ProjectOpenAIClient(AuthenticationPolicy, ProjectOpenAIClientOptions)
/// constructor with set. This routes the
/// outbound URL through the per-agent endpoint shape that the Foundry service expects for
/// hosted agents and lets the SDK auto-append the api-version query string.
/// Caller-supplied are passed through to the per-agent
/// client with Endpoint and
/// overridden by values derived from
/// ; any policies the caller added via AddPolicy
/// remain in effect on the per-agent pipeline. The MEAI user-agent policy is appended last.
///
private static AIAgent CreateInnerAgentFromAgentEndpoint(
Uri agentEndpoint,
AuthenticationTokenProvider credential,
ProjectOpenAIClientOptions? clientOptions,
IList? tools,
Func? clientFactory,
IServiceProvider? services)
{
Throw.IfNull(agentEndpoint);
Throw.IfNull(credential);
var (agentName, _) = ParseAgentEndpoint(agentEndpoint);
var perAgentOptions = clientOptions ?? new ProjectOpenAIClientOptions();
perAgentOptions.Endpoint = agentEndpoint;
perAgentOptions.AgentName = agentName;
perAgentOptions.AddPolicy(RequestOptionsExtensions.UserAgentPolicy, PipelinePosition.PerCall);
var authPolicy = new BearerTokenPolicy(credential, AzureAiResourceScope);
var perAgentClient = new ProjectOpenAIClient(authPolicy, perAgentOptions);
IChatClient chatClient = perAgentClient.GetProjectResponsesClient().AsIChatClient();
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));
}
///
/// Builds the project-scoped for the agent-endpoint
/// constructor by deriving the project root from the supplied agent endpoint and constructing
/// a fresh client without so the SDK
/// appends the standard /openai/v1 suffix expected for project-level surfaces such as
/// conversations.
///
///
/// Only the four observable primitive properties (,
/// , ,
/// and UserAgentApplicationId) are copied from the caller's options bag. Pipeline
/// policies added via AddPolicy on the caller bag do not propagate because
/// does not publicly enumerate its policies. The MEAI
/// user-agent policy is appended last.
///
private static ProjectOpenAIClient CreateProjectLevelOpenAIClientFromAgentEndpoint(
Uri agentEndpoint,
AuthenticationTokenProvider credential,
ProjectOpenAIClientOptions? clientOptions)
{
var (_, projectRoot) = ParseAgentEndpoint(agentEndpoint);
var projectOptions = new ProjectOpenAIClientOptions();
if (clientOptions is not null)
{
if (clientOptions.RetryPolicy is not null)
{
projectOptions.RetryPolicy = clientOptions.RetryPolicy;
}
if (clientOptions.NetworkTimeout is not null)
{
projectOptions.NetworkTimeout = clientOptions.NetworkTimeout;
}
if (clientOptions.Transport is not null)
{
projectOptions.Transport = clientOptions.Transport;
}
if (!string.IsNullOrEmpty(clientOptions.UserAgentApplicationId))
{
projectOptions.UserAgentApplicationId = clientOptions.UserAgentApplicationId;
}
}
projectOptions.AddPolicy(RequestOptionsExtensions.UserAgentPolicy, PipelinePosition.PerCall);
return new ProjectOpenAIClient(projectRoot, credential, projectOptions);
}
///
/// 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.
///
///
/// The endpoint is missing the /agents/ segment, has an empty agent name, or has a
/// suffix other than /endpoint/protocols/openai.
///
internal static (string AgentName, Uri ProjectRoot) ParseAgentEndpoint(Uri agentEndpoint)
{
Throw.IfNull(agentEndpoint);
const string AgentsSegment = "/agents/";
const string ExpectedSuffix = "/endpoint/protocols/openai";
var path = agentEndpoint.AbsolutePath.TrimEnd('/');
var idx = path.IndexOf(AgentsSegment, StringComparison.OrdinalIgnoreCase);
if (idx < 0)
{
throw new ArgumentException(
$"Expected an agent endpoint of shape 'https:///.../projects//agents//endpoint/protocols/openai' but got '{agentEndpoint}'. " +
"If you want to construct a FoundryAgent against a project endpoint, use the (Uri projectEndpoint, AuthenticationTokenProvider credential, string model, string instructions, ...) constructor instead.",
nameof(agentEndpoint));
}
var afterAgents = path.Substring(idx + AgentsSegment.Length);
var nextSlash = afterAgents.IndexOf('/');
if (nextSlash <= 0)
{
throw new ArgumentException(
$"Agent endpoint '{agentEndpoint}' is missing the '{ExpectedSuffix}' suffix.",
nameof(agentEndpoint));
}
var agentName = afterAgents.Substring(0, nextSlash);
var suffix = afterAgents.Substring(nextSlash);
if (!string.Equals(suffix, ExpectedSuffix, StringComparison.OrdinalIgnoreCase))
{
throw new ArgumentException(
$"Agent endpoint '{agentEndpoint}' has an unexpected suffix '{suffix}'. Expected '{ExpectedSuffix}'.",
nameof(agentEndpoint));
}
var rootPath = path.Substring(0, idx);
var projectRoot = new UriBuilder(agentEndpoint)
{
Path = rootPath,
Query = string.Empty,
Fragment = string.Empty,
}.Uri;
return (agentName, projectRoot);
}
private static AIProjectClient CreateProjectClient(Uri endpoint, AuthenticationTokenProvider credential, AIProjectClientOptions? clientOptions = null)
{
Throw.IfNull(endpoint);
Throw.IfNull(credential);
clientOptions ??= new AIProjectClientOptions();
clientOptions.AddPolicy(RequestOptionsExtensions.UserAgentPolicy, PipelinePosition.PerCall);
return new AIProjectClient(endpoint, credential, clientOptions);
}
#endregion
}