// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using Azure.AI.Extensions.OpenAI;
using Azure.AI.Projects.Agents;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Foundry;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;
using OpenAI.Responses;
namespace Azure.AI.Projects;
///
/// Provides extension methods for .
///
[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)]
public static partial class AzureAIProjectChatClientExtensions
{
///
/// Uses an existing server side agent, wrapped as a using the provided and .
///
/// The to create the with. Cannot be .
/// The representing the name and version of the server side agent to create a for. Cannot be .
/// The tools to use when interacting with the agent. This is required when using prompt agent definitions with tools.
/// Provides a way to customize the creation of the underlying used by the agent.
/// An optional to use for resolving services required by the instances being invoked.
/// A instance that can be used to perform operations based on the latest version of the named Azure AI Agent.
/// Thrown when or is .
/// The agent with the specified name was not found.
///
/// When instantiating a by using an , minimal information will be available about the agent in the instance level, and any logic that relies
/// on to retrieve information about the agent like will receive as the result.
///
public static FoundryAgent AsAIAgent(
this AIProjectClient aiProjectClient,
AgentReference agentReference,
IList? tools = null,
Func? clientFactory = null,
IServiceProvider? services = null)
{
Throw.IfNull(aiProjectClient);
Throw.IfNull(agentReference);
ThrowIfInvalidAgentName(agentReference.Name);
var innerAgent = AsChatClientAgent(
aiProjectClient,
agentReference,
new ChatClientAgentOptions()
{
Id = $"{agentReference.Name}:{agentReference.Version}",
Name = agentReference.Name,
ChatOptions = new() { Tools = tools },
},
clientFactory,
services);
return new FoundryAgent(aiProjectClient, innerAgent);
}
///
/// Uses an existing server side agent, wrapped as a using the provided and .
///
/// The client used to interact with Azure AI Agents. Cannot be .
/// The agent record to be converted. The latest version will be used. Cannot be .
/// The tools to use when interacting with the agent. This is required when using prompt agent definitions with tools.
/// Provides a way to customize the creation of the underlying used by the agent.
/// An optional to use for resolving services required by the instances being invoked.
/// A instance that can be used to perform operations based on the latest version of the Azure AI Agent.
/// Thrown when or is .
public static FoundryAgent AsAIAgent(
this AIProjectClient aiProjectClient,
ProjectsAgentRecord agentRecord,
IList? tools = null,
Func? clientFactory = null,
IServiceProvider? services = null)
{
Throw.IfNull(aiProjectClient);
Throw.IfNull(agentRecord);
var allowDeclarativeMode = tools is not { Count: > 0 };
var innerAgent = AsChatClientAgent(
aiProjectClient,
agentRecord,
tools,
clientFactory,
!allowDeclarativeMode,
services);
return new FoundryAgent(aiProjectClient, innerAgent);
}
///
/// Uses an existing server side agent, wrapped as a using the provided and .
///
/// The client used to interact with Azure AI Agents. Cannot be .
/// The agent version to be converted. Cannot be .
/// In-process invocable tools to be provided. If no tools are provided manual handling will be necessary to invoke in-process tools.
/// Provides a way to customize the creation of the underlying used by the agent.
/// An optional to use for resolving services required by the instances being invoked.
/// A instance that can be used to perform operations based on the provided version of the Azure AI Agent.
/// Thrown when or is .
public static FoundryAgent AsAIAgent(
this AIProjectClient aiProjectClient,
ProjectsAgentVersion agentVersion,
IList? tools = null,
Func? clientFactory = null,
IServiceProvider? services = null)
{
Throw.IfNull(aiProjectClient);
Throw.IfNull(agentVersion);
var allowDeclarativeMode = tools is not { Count: > 0 };
var innerAgent = AsChatClientAgent(
aiProjectClient,
agentVersion,
tools,
clientFactory,
!allowDeclarativeMode,
services);
return new FoundryAgent(aiProjectClient, innerAgent);
}
///
/// Creates a non-versioned backed by the project's Responses API using the specified model and instructions.
///
/// The to use for Responses API calls. Cannot be .
/// The model deployment name to use for the agent. Cannot be or whitespace.
/// The instructions that guide the agent's behavior. Cannot be or whitespace.
/// Optional name for the agent.
/// Optional human-readable description for the agent.
/// Optional collection of tools that the agent can invoke during conversations.
/// Provides a way to customize the creation of the underlying used by the agent.
/// Optional logger factory for creating loggers used by the agent.
/// An optional to use for resolving services required by the instances being invoked.
/// A backed by the project's Responses API.
/// Thrown when is .
/// Thrown when or is empty or whitespace.
public static ChatClientAgent AsAIAgent(
this AIProjectClient aiProjectClient,
string model,
string instructions,
string? name = null,
string? description = null,
IList? tools = null,
Func? clientFactory = null,
ILoggerFactory? loggerFactory = null,
IServiceProvider? services = null)
{
Throw.IfNull(aiProjectClient);
Throw.IfNullOrWhitespace(model);
Throw.IfNullOrWhitespace(instructions);
ChatClientAgentOptions options = new()
{
Name = name,
Description = description,
ChatOptions = new ChatOptions
{
ModelId = model,
Instructions = instructions,
Tools = tools,
},
};
return CreateResponsesChatClientAgent(aiProjectClient, options, clientFactory, loggerFactory, services);
}
///
/// Creates a non-versioned backed by the project's Responses API using the specified options.
///
/// The to use for Responses API calls. Cannot be .
/// Configuration options that control the agent's behavior. is required.
/// Provides a way to customize the creation of the underlying used by the agent.
/// Optional logger factory for creating loggers used by the agent.
/// An optional to use for resolving services required by the instances being invoked.
/// A backed by the project's Responses API.
/// Thrown when or is .
/// Thrown when does not specify .
public static ChatClientAgent AsAIAgent(
this AIProjectClient aiProjectClient,
ChatClientAgentOptions options,
Func? clientFactory = null,
ILoggerFactory? loggerFactory = null,
IServiceProvider? services = null)
{
Throw.IfNull(aiProjectClient);
Throw.IfNull(options);
return CreateResponsesChatClientAgent(aiProjectClient, options, clientFactory, loggerFactory, services);
}
#region Private
/// Creates a with the specified options.
private static ChatClientAgent CreateChatClientAgent(
AIProjectClient aiProjectClient,
ProjectsAgentVersion agentVersion,
ChatClientAgentOptions agentOptions,
Func? clientFactory,
IServiceProvider? services)
{
IChatClient chatClient = new AzureAIProjectChatClient(aiProjectClient, agentVersion, agentOptions.ChatOptions);
if (clientFactory is not null)
{
chatClient = clientFactory(chatClient);
}
return new ChatClientAgent(chatClient, agentOptions, services: services);
}
private static ChatClientAgent 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 = aiProjectClient
.GetProjectOpenAIClient()
.GetResponsesClient()
.AsIChatClient(agentOptions.ChatOptions.ModelId);
if (clientFactory is not null)
{
chatClient = clientFactory(chatClient);
}
return new ChatClientAgent(chatClient, agentOptions, loggerFactory, services);
}
/// This method creates an with the specified ChatClientAgentOptions.
private static ChatClientAgent AsChatClientAgent(
AIProjectClient aiProjectClient,
ProjectsAgentVersion agentVersion,
ChatClientAgentOptions agentOptions,
Func? clientFactory,
IServiceProvider? services)
=> CreateChatClientAgent(aiProjectClient, agentVersion, agentOptions, clientFactory, services);
/// This method creates an with the specified ChatClientAgentOptions.
private static ChatClientAgent AsChatClientAgent(
AIProjectClient aiProjectClient,
ProjectsAgentRecord agentRecord,
ChatClientAgentOptions agentOptions,
Func? clientFactory,
IServiceProvider? services)
{
IChatClient chatClient = new AzureAIProjectChatClient(aiProjectClient, agentRecord, agentOptions.ChatOptions);
if (clientFactory is not null)
{
chatClient = clientFactory(chatClient);
}
return new ChatClientAgent(chatClient, agentOptions, services: services);
}
/// This method creates an with the specified ChatClientAgentOptions.
private static ChatClientAgent AsChatClientAgent(
AIProjectClient aiProjectClient,
AgentReference agentReference,
ChatClientAgentOptions agentOptions,
Func? clientFactory,
IServiceProvider? services)
{
IChatClient chatClient = new AzureAIProjectChatClient(aiProjectClient, agentReference, defaultModelId: null, agentOptions.ChatOptions);
if (clientFactory is not null)
{
chatClient = clientFactory(chatClient);
}
return new ChatClientAgent(chatClient, agentOptions, services: services);
}
/// This method creates an with a auto-generated ChatClientAgentOptions from the specified configuration parameters.
private static ChatClientAgent AsChatClientAgent(
AIProjectClient aiProjectClient,
ProjectsAgentVersion agentVersion,
IList? tools,
Func? clientFactory,
bool requireInvocableTools,
IServiceProvider? services)
=> AsChatClientAgent(
aiProjectClient,
agentVersion,
CreateChatClientAgentOptions(agentVersion, new ChatOptions() { Tools = tools }, requireInvocableTools),
clientFactory,
services);
/// This method creates an with a auto-generated ChatClientAgentOptions from the specified configuration parameters.
private static ChatClientAgent AsChatClientAgent(
AIProjectClient aiProjectClient,
ProjectsAgentRecord agentRecord,
IList? tools,
Func? clientFactory,
bool requireInvocableTools,
IServiceProvider? services)
=> AsChatClientAgent(
aiProjectClient,
agentRecord,
CreateChatClientAgentOptions(agentRecord.GetLatestVersion(), new ChatOptions() { Tools = tools }, requireInvocableTools),
clientFactory,
services);
///
/// This method creates for the specified and the provided tools.
///
/// The agent version.
/// The to use when interacting with the agent.
/// Indicates whether to enforce the presence of invocable tools when the AIAgent is created with an agent definition that uses them.
/// The created .
/// Thrown when the agent definition requires in-process tools but none were provided.
/// Thrown when the agent definition required tools were not provided.
///
/// This method rebuilds the agent options from the agent definition returned by the version and combine with the in-proc tools when provided
/// this ensures that all required tools are provided and the definition of the agent options are consistent with the agent definition coming from the server.
///
private static ChatClientAgentOptions CreateChatClientAgentOptions(ProjectsAgentVersion agentVersion, ChatOptions? chatOptions, bool requireInvocableTools)
{
var agentDefinition = agentVersion.Definition;
List? agentTools = null;
if (agentDefinition is DeclarativeAgentDefinition { Tools: { Count: > 0 } definitionTools })
{
// Check if no tools were provided while the agent definition requires in-proc tools.
if (requireInvocableTools && chatOptions?.Tools is not { Count: > 0 } && definitionTools.Any(t => t is FunctionTool))
{
throw new ArgumentException("The agent definition in-process tools must be provided in the extension method tools parameter.");
}
// Agregate all missing tools for a single error message.
List? missingTools = null;
// Check function tools
foreach (ResponseTool responseTool in definitionTools)
{
if (responseTool is FunctionTool functionTool)
{
// Check if a tool with the same type and name exists in the provided tools.
// Always prefer matching AIFunction when available, regardless of requireInvocableTools.
var matchingTool = chatOptions?.Tools?.FirstOrDefault(t => t is AIFunction tf && functionTool.FunctionName == tf.Name);
if (matchingTool is not null)
{
(agentTools ??= []).Add(matchingTool!);
continue;
}
if (requireInvocableTools)
{
(missingTools ??= []).Add($"Function tool: {functionTool.FunctionName}");
continue;
}
}
(agentTools ??= []).Add(responseTool.AsAITool());
}
if (requireInvocableTools && missingTools is { Count: > 0 })
{
throw new InvalidOperationException($"The following prompt agent definition required tools were not provided: {string.Join(", ", missingTools)}");
}
}
// Use the agent version's ID if available, otherwise generate one from name and version.
// This handles cases where hosted agents (like MCP agents) may not have an ID assigned.
var version = string.IsNullOrWhiteSpace(agentVersion.Version) ? "latest" : agentVersion.Version;
var agentId = string.IsNullOrWhiteSpace(agentVersion.Id)
? $"{agentVersion.Name}:{version}"
: agentVersion.Id;
var agentOptions = new ChatClientAgentOptions()
{
Id = agentId,
Name = agentVersion.Name,
Description = agentVersion.Description,
};
if (agentDefinition is DeclarativeAgentDefinition promptAgentDefinition)
{
agentOptions.ChatOptions ??= chatOptions?.Clone() ?? new();
agentOptions.ChatOptions.Instructions = promptAgentDefinition.Instructions;
agentOptions.ChatOptions.Temperature = promptAgentDefinition.Temperature;
agentOptions.ChatOptions.TopP = promptAgentDefinition.TopP;
}
if (agentTools is { Count: > 0 })
{
agentOptions.ChatOptions ??= chatOptions?.Clone() ?? new();
agentOptions.ChatOptions.Tools = agentTools;
}
return agentOptions;
}
#if NET
[GeneratedRegex("^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$")]
private static partial Regex AgentNameValidationRegex();
#else
private static Regex AgentNameValidationRegex() => new("^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$");
#endif
internal static string ThrowIfInvalidAgentName(string? name)
{
Throw.IfNullOrWhitespace(name);
if (!AgentNameValidationRegex().IsMatch(name))
{
throw new ArgumentException("Agent name must be 1-63 characters long, start and end with an alphanumeric character, and can only contain alphanumeric characters or hyphens.", nameof(name));
}
return name;
}
}
[JsonSerializable(typeof(JsonElement))]
internal sealed partial class AgentClientJsonContext : JsonSerializerContext;
#endregion