// Copyright (c) Microsoft. All rights reserved. using System.ClientModel; using System.ClientModel.Primitives; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; 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.AzureAI; using Microsoft.Extensions.AI; using Microsoft.Extensions.Logging; using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; using OpenAI; 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); } /// /// Asynchronously retrieves an existing server side agent, wrapped as a using the provided . /// /// The to create the with. Cannot be . /// The name of the server side agent to create a for. Cannot be or whitespace. /// 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. /// The to monitor for cancellation requests. The default is . /// A instance that can be used to perform operations based on the latest version of the named Azure AI Agent. /// Thrown when or is . /// Thrown when is empty or whitespace, or when the agent with the specified name was not found. /// The agent with the specified name was not found. [Obsolete("Use native AIProjectClient agent APIs and AsAIAgent(AgentRecord/AgentVersion) instead.")] public static async Task GetAIAgentAsync( this AIProjectClient aiProjectClient, string name, IList? tools = null, Func? clientFactory = null, IServiceProvider? services = null, CancellationToken cancellationToken = default) { Throw.IfNull(aiProjectClient); ThrowIfInvalidAgentName(name); AgentRecord agentRecord = await GetAgentRecordByNameAsync(aiProjectClient, name, cancellationToken).ConfigureAwait(false); return AsAIAgent( aiProjectClient, agentRecord, tools, clientFactory, services); } /// /// 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, AgentRecord 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, AgentVersion 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); } /// /// Asynchronously retrieves an existing server side agent, wrapped as a using the provided . /// /// The client used to manage and interact with AI agents. Cannot be . /// The options for creating the agent. Cannot be . /// A factory function to customize the creation of the chat client used by the agent. /// An optional to use for resolving services required by the instances being invoked. /// A to cancel the operation if needed. /// A instance that can be used to perform operations on the newly created agent. /// Thrown when or is . [Obsolete("Use native AIProjectClient agent APIs and AsAIAgent(AgentRecord/AgentVersion) instead.")] public static async Task GetAIAgentAsync( this AIProjectClient aiProjectClient, ChatClientAgentOptions options, Func? clientFactory = null, IServiceProvider? services = null, CancellationToken cancellationToken = default) { Throw.IfNull(aiProjectClient); Throw.IfNull(options); if (string.IsNullOrWhiteSpace(options.Name)) { throw new ArgumentException("Agent name must be provided in the options.Name property", nameof(options)); } ThrowIfInvalidAgentName(options.Name); AgentRecord agentRecord = await GetAgentRecordByNameAsync(aiProjectClient, options.Name, cancellationToken).ConfigureAwait(false); var agentVersion = agentRecord.GetLatestVersion(); var agentOptions = CreateChatClientAgentOptions(agentVersion, options, requireInvocableTools: !options.UseProvidedChatClientAsIs); return new FoundryAgent( aiProjectClient, AsChatClientAgent(aiProjectClient, agentVersion, agentOptions, clientFactory, services)); } /// /// Creates a new Prompt AI agent in the Foundry service using the specified configuration parameters, and exposes it as a . /// /// The client used to manage and interact with AI agents. Cannot be . /// The name for the agent. /// The name of the model to use for the agent. Cannot be or whitespace. /// The instructions that guide the agent's behavior. Cannot be or whitespace. /// The description for the agent. /// The tools to use when interacting with the agent, this is required when using prompt agent definitions with tools. /// A factory function to customize the creation of the chat client used by the agent. /// An optional to use for resolving services required by the instances being invoked. /// A token to monitor for cancellation requests. /// A instance that can be used to perform operations on the newly created agent. /// Thrown when , , or is . /// Thrown when or is empty or whitespace. /// When using prompt agent definitions with tools the parameter needs to be provided. [Obsolete("Use native AIProjectClient.Agents APIs instead.")] public static Task CreateAIAgentAsync( this AIProjectClient aiProjectClient, string name, string model, string instructions, string? description = null, IList? tools = null, Func? clientFactory = null, IServiceProvider? services = null, CancellationToken cancellationToken = default) { Throw.IfNull(aiProjectClient); ThrowIfInvalidAgentName(name); Throw.IfNullOrWhitespace(model); Throw.IfNullOrWhitespace(instructions); return CreateAIAgentAsync( aiProjectClient, name, tools, new AgentVersionCreationOptions(new PromptAgentDefinition(model) { Instructions = instructions }) { Description = description }, clientFactory, services, cancellationToken); } /// /// Creates a new Prompt AI agent in the Foundry service using the specified configuration parameters, and exposes it as a . /// /// The client used to manage and interact with AI agents. Cannot be . /// The name of the model to use for the agent. Cannot be or whitespace. /// The options for creating the agent. Cannot be . /// A factory function to customize the creation of the chat client used by the agent. /// An optional to use for resolving services required by the instances being invoked. /// A to cancel the operation if needed. /// A instance that can be used to perform operations on the newly created agent. /// Thrown when or is . /// Thrown when is empty or whitespace, or when the agent name is not provided in the options. [Obsolete("Use native AIProjectClient.Agents APIs instead.")] public static async Task CreateAIAgentAsync( this AIProjectClient aiProjectClient, string model, ChatClientAgentOptions options, Func? clientFactory = null, IServiceProvider? services = null, CancellationToken cancellationToken = default) { Throw.IfNull(aiProjectClient); Throw.IfNull(options); Throw.IfNullOrWhitespace(model); if (string.IsNullOrWhiteSpace(options.Name)) { throw new ArgumentException("Agent name must be provided in the options.Name property", nameof(options)); } ThrowIfInvalidAgentName(options.Name); AgentVersion agentVersion = await CreateAgentVersionFromOptionsAsync(aiProjectClient, model, options, cancellationToken).ConfigureAwait(false); var agentOptions = CreateChatClientAgentOptions(agentVersion, options, requireInvocableTools: true); return new FoundryAgent( aiProjectClient, AsChatClientAgent(aiProjectClient, agentVersion, agentOptions, clientFactory, services)); } /// /// Creates a new Prompt AI agent in the Foundry service using the specified configuration parameters, and exposes it as a . /// parameters. /// /// The client used to manage and interact with AI agents. Cannot be . /// The name for the agent. /// Settings that control the creation of the agent. /// A factory function to customize the creation of the chat client used by the agent. /// A token to monitor for cancellation requests. /// A instance that can be used to perform operations on the newly created agent. /// Thrown when or is . /// /// When using this extension method with a the tools are only declarative and not invocable. /// Invocation of any in-process tools will need to be handled manually. /// [Obsolete("Use native AIProjectClient.Agents APIs instead.")] public static Task CreateAIAgentAsync( this AIProjectClient aiProjectClient, string name, AgentVersionCreationOptions creationOptions, Func? clientFactory = null, CancellationToken cancellationToken = default) { Throw.IfNull(aiProjectClient); ThrowIfInvalidAgentName(name); Throw.IfNull(creationOptions); return CreateAIAgentAsync( aiProjectClient, name, tools: null, creationOptions, clientFactory, services: null, cancellationToken); } /// /// 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 FoundryAgent 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 new FoundryAgent(aiProjectClient, 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 FoundryAgent AsAIAgent( this AIProjectClient aiProjectClient, ChatClientAgentOptions options, Func? clientFactory = null, ILoggerFactory? loggerFactory = null, IServiceProvider? services = null) { Throw.IfNull(aiProjectClient); Throw.IfNull(options); return new FoundryAgent(aiProjectClient, CreateResponsesChatClientAgent(aiProjectClient, options, clientFactory, loggerFactory, services)); } #region Private private static readonly ModelReaderWriterOptions s_modelWriterOptionsWire = new("W"); /// /// Asynchronously retrieves an agent record by name using the protocol method to inject user-agent headers. /// internal static async Task GetAgentRecordByNameAsync(AIProjectClient aiProjectClient, string agentName, CancellationToken cancellationToken) { ClientResult protocolResponse = await aiProjectClient.Agents.GetAgentAsync(agentName, cancellationToken.ToRequestOptions(false)).ConfigureAwait(false); var rawResponse = protocolResponse.GetRawResponse(); AgentRecord? result = ModelReaderWriter.Read(rawResponse.Content, s_modelWriterOptionsWire, AzureAIProjectsAgentsContext.Default); return result ?? throw new InvalidOperationException($"Agent with name '{agentName}' not found."); } /// /// Asynchronously creates an agent version using the protocol method to inject user-agent headers. /// internal static async Task CreateAgentVersionWithProtocolAsync(AIProjectClient aiProjectClient, string agentName, AgentVersionCreationOptions creationOptions, CancellationToken cancellationToken) { BinaryData serializedOptions = ModelReaderWriter.Write(creationOptions, s_modelWriterOptionsWire, AzureAIProjectsAgentsContext.Default); BinaryContent content = BinaryContent.Create(serializedOptions); ClientResult protocolResponse = await aiProjectClient.Agents.CreateAgentVersionAsync(agentName, content, foundryFeatures: null, cancellationToken.ToRequestOptions(false)).ConfigureAwait(false); var rawResponse = protocolResponse.GetRawResponse(); AgentVersion? result = ModelReaderWriter.Read(rawResponse.Content, s_modelWriterOptionsWire, AzureAIProjectsAgentsContext.Default); return result ?? throw new InvalidOperationException($"Failed to create agent version for agent '{agentName}'."); } private static async Task CreateAIAgentAsync( this AIProjectClient aiProjectClient, string name, IList? tools, AgentVersionCreationOptions creationOptions, Func? clientFactory, IServiceProvider? services, CancellationToken cancellationToken) { var allowDeclarativeMode = tools is not { Count: > 0 }; if (!allowDeclarativeMode) { ApplyToolsToAgentDefinition(creationOptions.Definition, tools); } AgentVersion agentVersion = await CreateAgentVersionWithProtocolAsync(aiProjectClient, name, creationOptions, cancellationToken).ConfigureAwait(false); return new FoundryAgent(aiProjectClient, AsChatClientAgent(aiProjectClient, agentVersion, tools, clientFactory, !allowDeclarativeMode, services)); } /// /// Creates an agent version with optional tool application, using the protocol method to inject user-agent headers. /// internal static async Task CreateAgentVersionWithProtocolAsync(AIProjectClient aiProjectClient, string agentName, AgentVersionCreationOptions creationOptions, IList? tools, CancellationToken cancellationToken) { if (tools is { Count: > 0 }) { ApplyToolsToAgentDefinition(creationOptions.Definition, tools); } return await CreateAgentVersionWithProtocolAsync(aiProjectClient, agentName, creationOptions, cancellationToken).ConfigureAwait(false); } /// /// Creates an agent version from , mapping options to a . /// internal static async Task CreateAgentVersionFromOptionsAsync( AIProjectClient aiProjectClient, string model, ChatClientAgentOptions options, CancellationToken cancellationToken) { PromptAgentDefinition agentDefinition = new(model) { Instructions = options.ChatOptions?.Instructions, Temperature = options.ChatOptions?.Temperature, TopP = options.ChatOptions?.TopP, TextOptions = new() { TextFormat = ToOpenAIResponseTextFormat(options.ChatOptions?.ResponseFormat, options.ChatOptions) } }; if (options.ChatOptions?.Reasoning is { } reasoning) { agentDefinition.ReasoningOptions = ToResponseReasoningOptions(reasoning); } else if (options.ChatOptions?.RawRepresentationFactory?.Invoke(new NoOpChatClient()) is CreateResponseOptions respCreationOptions) { agentDefinition.ReasoningOptions = respCreationOptions.ReasoningOptions; } ApplyToolsToAgentDefinition(agentDefinition, options.ChatOptions?.Tools); AgentVersionCreationOptions creationOptions = new(agentDefinition); if (!string.IsNullOrWhiteSpace(options.Description)) { creationOptions.Description = options.Description; } return await CreateAgentVersionWithProtocolAsync(aiProjectClient, options.Name!, creationOptions, cancellationToken).ConfigureAwait(false); } /// Creates a with the specified options. internal static ChatClientAgent CreateChatClientAgent( AIProjectClient aiProjectClient, AgentVersion 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); } internal 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 = new AzureAIProjectResponsesChatClient(aiProjectClient, 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, AgentVersion 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, AgentRecord 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, AgentVersion 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, AgentRecord 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. /// internal static ChatClientAgentOptions CreateChatClientAgentOptions(AgentVersion agentVersion, ChatOptions? chatOptions, bool requireInvocableTools) { var agentDefinition = agentVersion.Definition; List? agentTools = null; if (agentDefinition is PromptAgentDefinition { 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 PromptAgentDefinition 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; } /// /// Creates a new instance of configured for the specified agent version and /// optional base options. /// /// The agent version to use when configuring the chat client agent options. /// An optional instance whose relevant properties will be copied to the /// returned options. If , only default values are used. /// Specifies whether the returned options must include invocable tools. Set to to require /// invocable tools; otherwise, . /// A instance configured according to the specified parameters. internal static ChatClientAgentOptions CreateChatClientAgentOptions(AgentVersion agentVersion, ChatClientAgentOptions? options, bool requireInvocableTools) { var agentOptions = CreateChatClientAgentOptions(agentVersion, options?.ChatOptions, requireInvocableTools); if (options is not null) { agentOptions.AIContextProviders = options.AIContextProviders; agentOptions.ChatHistoryProvider = options.ChatHistoryProvider; agentOptions.UseProvidedChatClientAsIs = options.UseProvidedChatClientAsIs; } return agentOptions; } /// /// Adds the specified AI tools to a prompt agent definition, while also ensuring that all invocable tools are provided. /// /// The agent definition to which the tools will be applied. Must be a PromptAgentDefinition to support tools. /// A list of AI tools to add to the agent definition. If null or empty, no tools are added. /// Thrown if tools were provided but is not a . /// When providing functions, they need to be invokable AIFunctions. private static void ApplyToolsToAgentDefinition(AgentDefinition agentDefinition, IList? tools) { if (tools is { Count: > 0 }) { if (agentDefinition is not PromptAgentDefinition promptAgentDefinition) { throw new ArgumentException("Only prompt agent definitions support tools.", nameof(agentDefinition)); } // When tools are provided, those should represent the complete set of tools for the agent definition. // This is particularly important for existing agents so no duplication happens for what was already defined. promptAgentDefinition.Tools.Clear(); foreach (var tool in tools) { // Ensure that any AIFunctions provided are In-Proc, not just the declarations. if (tool is not AIFunction && ( tool.GetService() is not null // Declarative FunctionTool converted as AsAITool() || tool is AIFunctionDeclaration)) // AIFunctionDeclaration type { throw new InvalidOperationException("When providing functions, they need to be invokable AIFunctions. AIFunctions can be created correctly using AIFunctionFactory.Create"); } promptAgentDefinition.Tools.Add( // If this is a converted ResponseTool as AITool, we can directly retrieve the ResponseTool instance from GetService. tool.GetService() // Otherwise we should be able to convert existing MEAI Tool abstractions into OpenAI ResponseTools ?? tool.AsOpenAIResponseTool() ?? throw new InvalidOperationException("The provided AITool could not be converted to a ResponseTool, ensure that the AITool was created using responseTool.AsAITool() extension.")); } } } private static ResponseTextFormat? ToOpenAIResponseTextFormat(ChatResponseFormat? format, ChatOptions? options = null) => format switch { ChatResponseFormatText => ResponseTextFormat.CreateTextFormat(), ChatResponseFormatJson jsonFormat when StrictSchemaTransformCache.GetOrCreateTransformedSchema(jsonFormat) is { } jsonSchema => ResponseTextFormat.CreateJsonSchemaFormat( jsonFormat.SchemaName ?? "json_schema", BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(jsonSchema, AgentClientJsonContext.Default.JsonElement)), jsonFormat.SchemaDescription, HasStrict(options?.AdditionalProperties)), ChatResponseFormatJson => ResponseTextFormat.CreateJsonObjectFormat(), _ => null, }; /// Key into AdditionalProperties used to store a strict option. private const string StrictKey = "strictJsonSchema"; /// Gets whether the properties specify that strict schema handling is desired. private static bool? HasStrict(IReadOnlyDictionary? additionalProperties) => additionalProperties?.TryGetValue(StrictKey, out object? strictObj) is true && strictObj is bool strictValue ? strictValue : null; /// /// Gets the JSON schema transformer cache conforming to OpenAI strict / structured output restrictions per /// https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#supported-schemas. /// private static AIJsonSchemaTransformCache StrictSchemaTransformCache { get; } = new(new() { DisallowAdditionalProperties = true, ConvertBooleanSchemas = true, MoveDefaultKeywordToDescription = true, RequireAllProperties = true, TransformSchemaNode = (ctx, node) => { // Move content from common but unsupported properties to description. In particular, we focus on properties that // the AIJsonUtilities schema generator might produce and/or that are explicitly mentioned in the OpenAI documentation. if (node is JsonObject schemaObj) { StringBuilder? additionalDescription = null; ReadOnlySpan unsupportedProperties = [ // Produced by AIJsonUtilities but not in allow list at https://platform.openai.com/docs/guides/structured-outputs#supported-properties: "contentEncoding", "contentMediaType", "not", // Explicitly mentioned at https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#key-ordering as being unsupported with some models: "minLength", "maxLength", "pattern", "format", "minimum", "maximum", "multipleOf", "patternProperties", "minItems", "maxItems", // Explicitly mentioned at https://learn.microsoft.com/azure/ai-services/openai/how-to/structured-outputs?pivots=programming-language-csharp&tabs=python-secure%2Cdotnet-entra-id#unsupported-type-specific-keywords // as being unsupported with Azure OpenAI: "unevaluatedProperties", "propertyNames", "minProperties", "maxProperties", "unevaluatedItems", "contains", "minContains", "maxContains", "uniqueItems", ]; foreach (string propName in unsupportedProperties) { if (schemaObj[propName] is { } propNode) { _ = schemaObj.Remove(propName); AppendLine(ref additionalDescription, propName, propNode); } } if (additionalDescription is not null) { schemaObj["description"] = schemaObj["description"] is { } descriptionNode && descriptionNode.GetValueKind() == JsonValueKind.String ? $"{descriptionNode.GetValue()}{Environment.NewLine}{additionalDescription}" : additionalDescription.ToString(); } return node; static void AppendLine(ref StringBuilder? sb, string propName, JsonNode propNode) { sb ??= new(); if (sb.Length > 0) { _ = sb.AppendLine(); } _ = sb.Append(propName).Append(": ").Append(propNode); } } return node; }, }); /// /// This class is a no-op implementation of to be used to honor the argument passed /// while triggering avoiding any unexpected exception on the caller implementation. /// private sealed class NoOpChatClient : IChatClient { public void Dispose() { } public Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) => Task.FromResult(new ChatResponse()); public object? GetService(Type serviceType, object? serviceKey = null) => null; public async IAsyncEnumerable GetStreamingResponseAsync(IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { yield return new ChatResponseUpdate(); } } #endregion #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; } private static ResponseReasoningOptions? ToResponseReasoningOptions(ReasoningOptions reasoning) { ResponseReasoningEffortLevel? effortLevel = reasoning.Effort switch { ReasoningEffort.Low => ResponseReasoningEffortLevel.Low, ReasoningEffort.Medium => ResponseReasoningEffortLevel.Medium, ReasoningEffort.High => ResponseReasoningEffortLevel.High, ReasoningEffort.ExtraHigh => ResponseReasoningEffortLevel.High, _ => null, }; ResponseReasoningSummaryVerbosity? summary = reasoning.Output switch { ReasoningOutput.Summary => ResponseReasoningSummaryVerbosity.Concise, ReasoningOutput.Full => ResponseReasoningSummaryVerbosity.Detailed, _ => null, }; if (effortLevel is null && summary is null) { return null; } return new ResponseReasoningOptions { ReasoningEffortLevel = effortLevel, ReasoningSummaryVerbosity = summary, }; } } [JsonSerializable(typeof(JsonElement))] internal sealed partial class AgentClientJsonContext : JsonSerializerContext;