// Copyright (c) Microsoft. All rights reserved. using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using Azure.AI.Projects; using Azure.AI.Projects.OpenAI; using Microsoft.Extensions.AI; using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; using OpenAI.Responses; namespace Microsoft.Agents.AI.AzureAI; /// /// Provides a chat client implementation that integrates with Azure AI Agents, enabling chat interactions using /// Azure-specific agent capabilities. /// [Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)] internal sealed class AzureAIProjectChatClient : DelegatingChatClient { private readonly ChatClientMetadata? _metadata; private readonly AIProjectClient _agentClient; private readonly AgentVersion? _agentVersion; private readonly AgentRecord? _agentRecord; private readonly ChatOptions? _chatOptions; private readonly AgentReference _agentReference; /// /// Initializes a new instance of the class. /// /// An instance of to interact with Azure AI Agents services. /// An instance of representing the specific agent to use. /// The default model to use for the agent, if applicable. /// An instance of representing the options on how the agent was predefined. /// /// The provided should be decorated with a for proper functionality. /// internal AzureAIProjectChatClient(AIProjectClient aiProjectClient, AgentReference agentReference, string? defaultModelId, ChatOptions? chatOptions) : base(Throw.IfNull(aiProjectClient) .GetProjectOpenAIClient() .GetProjectResponsesClientForAgent(agentReference) .AsIChatClient()) { this._agentClient = aiProjectClient; this._agentReference = Throw.IfNull(agentReference); this._metadata = new ChatClientMetadata("azure.ai.agents", defaultModelId: defaultModelId); this._chatOptions = chatOptions; } /// /// Initializes a new instance of the class. /// /// An instance of to interact with Azure AI Agents services. /// An instance of representing the specific agent to use. /// An instance of representing the options on how the agent was predefined. /// /// The provided should be decorated with a for proper functionality. /// internal AzureAIProjectChatClient(AIProjectClient aiProjectClient, AgentRecord agentRecord, ChatOptions? chatOptions) : this(aiProjectClient, Throw.IfNull(agentRecord).Versions.Latest, chatOptions) { this._agentRecord = agentRecord; } internal AzureAIProjectChatClient(AIProjectClient aiProjectClient, AgentVersion agentVersion, ChatOptions? chatOptions) : this( aiProjectClient, CreateAgentReference(Throw.IfNull(agentVersion)), (agentVersion.Definition as PromptAgentDefinition)?.Model, chatOptions) { this._agentVersion = agentVersion; } /// /// Creates an from an . /// Uses the agent version's version if available, otherwise defaults to "latest". /// /// The agent version to create a reference from. /// An for the specified agent version. private static AgentReference CreateAgentReference(AgentVersion agentVersion) { // If the version is null, empty, or whitespace, use "latest" as the default. // This handles cases where hosted agents (like MCP agents) may not have a version assigned. var version = string.IsNullOrWhiteSpace(agentVersion.Version) ? "latest" : agentVersion.Version; return new AgentReference(agentVersion.Name, version); } /// public override object? GetService(Type serviceType, object? serviceKey = null) { return (serviceKey is null && serviceType == typeof(ChatClientMetadata)) ? this._metadata : (serviceKey is null && serviceType == typeof(AIProjectClient)) ? this._agentClient : (serviceKey is null && serviceType == typeof(AgentVersion)) ? this._agentVersion : (serviceKey is null && serviceType == typeof(AgentRecord)) ? this._agentRecord : (serviceKey is null && serviceType == typeof(AgentReference)) ? this._agentReference : base.GetService(serviceType, serviceKey); } /// public override async Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) { var agentOptions = this.GetAgentEnabledChatOptions(options); return await base.GetResponseAsync(messages, agentOptions, cancellationToken).ConfigureAwait(false); } /// public override async IAsyncEnumerable GetStreamingResponseAsync(IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { var agentOptions = this.GetAgentEnabledChatOptions(options); await foreach (var chunk in base.GetStreamingResponseAsync(messages, agentOptions, cancellationToken).ConfigureAwait(false)) { yield return chunk; } } private ChatOptions GetAgentEnabledChatOptions(ChatOptions? options) { // Start with a clone of the base chat options defined for the agent, if any. ChatOptions agentEnabledChatOptions = this._chatOptions?.Clone() ?? new(); // Ignore per-request all options that can't be overridden. agentEnabledChatOptions.Instructions = null; agentEnabledChatOptions.Tools = null; agentEnabledChatOptions.Temperature = null; agentEnabledChatOptions.TopP = null; agentEnabledChatOptions.PresencePenalty = null; agentEnabledChatOptions.ResponseFormat = null; // Use the conversation from the request, or the one defined at the client level. agentEnabledChatOptions.ConversationId = options?.ConversationId ?? this._chatOptions?.ConversationId; // Preserve the original RawRepresentationFactory var originalFactory = options?.RawRepresentationFactory; agentEnabledChatOptions.RawRepresentationFactory = (client) => { if (originalFactory?.Invoke(this) is not CreateResponseOptions responseCreationOptions) { responseCreationOptions = new CreateResponseOptions(); } responseCreationOptions.Agent = this._agentReference; #pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. responseCreationOptions.Patch.Remove("$.model"u8); #pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. return responseCreationOptions; }; return agentEnabledChatOptions; } }