// 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