// Copyright (c) Microsoft. All rights reserved. using System.ClientModel.Primitives; using System.Runtime.CompilerServices; using System.Text; using Azure.AI.Agents; using Microsoft.Extensions.AI; using Microsoft.Shared.Diagnostics; using OpenAI; using OpenAI.Responses; #pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. namespace Microsoft.Agents.AI.AzureAI; /// /// Provides a chat client implementation that integrates with Azure AI Agents, enabling chat interactions using /// Azure-specific agent capabilities. /// internal sealed class AzureAIAgentChatClient : DelegatingChatClient { private readonly ChatClientMetadata? _metadata; private readonly AgentClient _agentClient; private readonly AgentVersion _agentVersion; private readonly ChatOptions? _chatOptions; /// /// The usage of a no-op model is a necessary change to avoid OpenAIClients to throw exceptions when /// used with Azure AI Agents as the model used is now defined at the agent creation time. /// private const string NoOpModel = "no-op"; /// /// 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. /// An optional for configuring the underlying OpenAI client. /// /// The provided should be decorated with a for proper functionality. /// internal AzureAIAgentChatClient(AgentClient agentClient, AgentRecord agentRecord, ChatOptions? chatOptions, OpenAIClientOptions? openAIClientOptions = null) : this(agentClient, Throw.IfNull(agentRecord).Versions.Latest, chatOptions, openAIClientOptions) { } internal AzureAIAgentChatClient(AgentClient agentClient, AgentVersion agentVersion, ChatOptions? chatOptions, OpenAIClientOptions? openAIClientOptions = null) : base(agentClient .GetOpenAIClient(openAIClientOptions) .GetOpenAIResponseClient((agentVersion.Definition as PromptAgentDefinition)?.Model ?? NoOpModel) .AsIChatClient()) { this._agentClient = Throw.IfNull(agentClient); this._agentVersion = Throw.IfNull(agentVersion); this._metadata = new ChatClientMetadata("azure.ai.agents"); this._chatOptions = chatOptions; } /// public override object? GetService(Type serviceType, object? serviceKey = null) { return (serviceKey is null && serviceType == typeof(ChatClientMetadata)) ? this._metadata : (serviceKey is null && serviceType == typeof(AgentClient)) ? this._agentClient : (serviceKey is null && serviceType == typeof(AgentVersion)) ? this._agentVersion : 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 async override 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; // 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 ResponseCreationOptions responseCreationOptions) { responseCreationOptions = new ResponseCreationOptions(); } SetAgentReference(responseCreationOptions, this._agentVersion); return responseCreationOptions; }; return agentEnabledChatOptions; } // Since the SetAdditionalProperty/SetAgentReference/SetConversationReference extensions in Azure.AI.Agents does not yet support the recent updates in OpenAI 2.6.0 // The methods below are copied and adapted to the new OpenAI SDK 2.6.0 structure where the Patch property is now exposed directly on ResponseCreationOptions and // may be removed once the Azure.AI.Agents package is updated to support OpenAI SDK 2.6+. #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. private static void SetAdditionalProperty(ResponseCreationOptions responseCreationOptions, string key, BinaryData value) { responseCreationOptions.Patch.Set([.. "$."u8, .. Encoding.UTF8.GetBytes(key)], value); } private static void SetAgentReference(ResponseCreationOptions responseCreationOptions, AgentVersion agentVersion) { var agentReference = new AgentReference(agentVersion.Name) { Version = agentVersion.Version }; SetAdditionalProperty(responseCreationOptions, "agent", ModelReaderWriter.Write(agentReference, new ModelReaderWriterOptions("W"), AzureAIAgentsContext.Default)); responseCreationOptions.Patch.Remove([.. "$."u8, .. Encoding.UTF8.GetBytes("model")]); } #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. }