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