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