// Copyright (c) Microsoft. All rights reserved. using System; using System.ClientModel.Primitives; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Net.Http; using System.Runtime.CompilerServices; using System.Text.Json.Nodes; using System.Threading; using System.Threading.Tasks; using Azure.AI.Projects; using Azure.AI.Projects.OpenAI; using Azure.Core; using Microsoft.Extensions.AI; using OpenAI.Responses; namespace Microsoft.Agents.AI.Workflows.Declarative; /// /// Provides functionality to interact with Foundry agents within a specified project context. /// /// This class is used to retrieve and manage AI agents associated with a Foundry project. It requires a /// project endpoint and credentials to authenticate requests. /// A instance representing the endpoint URL of the Foundry project. This must be a valid, non-null URI pointing to the project. /// The credentials used to authenticate with the Foundry project. This must be a valid instance of . public sealed class AzureAgentProvider(Uri projectEndpoint, TokenCredential projectCredentials) : ResponseAgentProvider { private readonly Dictionary _versionCache = []; private readonly Dictionary _agentCache = []; private AIProjectClient? _agentClient; private ProjectConversationsClient? _conversationClient; /// /// Optional options used when creating the . /// public AIProjectClientOptions? AIProjectClientOptions { get; init; } /// /// Optional options used when invoking the . /// public ProjectOpenAIClientOptions? OpenAIClientOptions { get; init; } /// /// An optional instance to be used for making HTTP requests. /// If not provided, a default client will be used. /// public HttpClient? HttpClient { get; init; } /// public override async Task CreateConversationAsync(CancellationToken cancellationToken = default) { ProjectConversation conversation = await this.GetConversationClient() .CreateProjectConversationAsync(options: null, cancellationToken).ConfigureAwait(false); return conversation.Id; } /// public override async Task CreateMessageAsync(string conversationId, ChatMessage conversationMessage, CancellationToken cancellationToken = default) { ReadOnlyCollection newItems = await this.GetConversationClient().CreateProjectConversationItemsAsync( conversationId, items: GetResponseItems(), include: null, cancellationToken).ConfigureAwait(false); return newItems.AsChatMessages().Single(); IEnumerable GetResponseItems() { IEnumerable messages = [conversationMessage]; foreach (ResponseItem item in messages.AsOpenAIResponseItems()) { if (string.IsNullOrEmpty(item.Id)) { yield return item; } else { yield return new ReferenceResponseItem(item.Id); } } } } /// public override async IAsyncEnumerable InvokeAgentAsync( string agentId, string? agentVersion, string? conversationId, IEnumerable? messages, IDictionary? inputArguments, [EnumeratorCancellation] CancellationToken cancellationToken = default) { AgentVersion agentVersionResult = await this.QueryAgentAsync(agentId, agentVersion, cancellationToken).ConfigureAwait(false); AIAgent agent = await this.GetAgentAsync(agentVersionResult, cancellationToken).ConfigureAwait(false); ChatOptions chatOptions = new() { ConversationId = conversationId, AllowMultipleToolCalls = this.AllowMultipleToolCalls, }; if (inputArguments is not null) { JsonNode jsonNode = ConvertDictionaryToJson(inputArguments); CreateResponseOptions responseCreationOptions = new(); #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.Set("$.structured_inputs"u8, BinaryData.FromString(jsonNode.ToJsonString())); #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. chatOptions.RawRepresentationFactory = (_) => responseCreationOptions; } ChatClientAgentRunOptions runOptions = new(chatOptions); IAsyncEnumerable agentResponse = messages is not null ? agent.RunStreamingAsync([.. messages], null, runOptions, cancellationToken) : agent.RunStreamingAsync([], null, runOptions, cancellationToken); await foreach (AgentResponseUpdate update in agentResponse.ConfigureAwait(false)) { update.AuthorName = agentVersionResult.Name; yield return update; } } private async Task QueryAgentAsync(string agentName, string? agentVersion, CancellationToken cancellationToken = default) { string agentKey = $"{agentName}:{agentVersion}"; if (this._versionCache.TryGetValue(agentKey, out AgentVersion? targetAgent)) { return targetAgent; } AIProjectClient client = this.GetAgentClient(); if (string.IsNullOrEmpty(agentVersion)) { AgentRecord agentRecord = await client.Agents.GetAgentAsync( agentName, cancellationToken).ConfigureAwait(false); targetAgent = agentRecord.Versions.Latest; } else { targetAgent = await client.Agents.GetAgentVersionAsync( agentName, agentVersion, cancellationToken).ConfigureAwait(false); } this._versionCache[agentKey] = targetAgent; return targetAgent; } private async Task GetAgentAsync(AgentVersion agentVersion, CancellationToken cancellationToken = default) { if (this._agentCache.TryGetValue(agentVersion.Id, out AIAgent? agent)) { return agent; } AIProjectClient client = this.GetAgentClient(); agent = client.AsAIAgent(agentVersion, tools: null, clientFactory: null, services: null); FunctionInvokingChatClient? functionInvokingClient = agent.GetService(); if (functionInvokingClient is not null) { // Allow concurrent invocations if configured functionInvokingClient.AllowConcurrentInvocation = this.AllowConcurrentInvocation; // Allows the caller to respond with function responses functionInvokingClient.TerminateOnUnknownCalls = true; // Make functions available for execution. Doesn't change what tool is available for any given agent. if (this.Functions is not null) { if (functionInvokingClient.AdditionalTools is null) { functionInvokingClient.AdditionalTools = [.. this.Functions]; } else { functionInvokingClient.AdditionalTools = [.. functionInvokingClient.AdditionalTools, .. this.Functions]; } } } this._agentCache[agentVersion.Id] = agent; return agent; } /// public override async Task GetMessageAsync(string conversationId, string messageId, CancellationToken cancellationToken = default) { AgentResponseItem responseItem = await this.GetConversationClient().GetProjectConversationItemAsync(conversationId, messageId, include: null, cancellationToken).ConfigureAwait(false); ResponseItem[] items = [responseItem.AsResponseResultItem()]; return items.AsChatMessages().Single(); } /// public override async IAsyncEnumerable GetMessagesAsync( string conversationId, int? limit = null, string? after = null, string? before = null, bool newestFirst = false, [EnumeratorCancellation] CancellationToken cancellationToken = default) { AgentListOrder order = newestFirst ? AgentListOrder.Ascending : AgentListOrder.Descending; await foreach (AgentResponseItem responseItem in this.GetConversationClient().GetProjectConversationItemsAsync(conversationId, null, limit, order.ToString(), after, before, include: null, cancellationToken).ConfigureAwait(false)) { ResponseItem[] items = [responseItem.AsResponseResultItem()]; foreach (ChatMessage message in items.AsChatMessages()) { yield return message; } } } private AIProjectClient GetAgentClient() { if (this._agentClient is null) { AIProjectClientOptions clientOptions = this.AIProjectClientOptions ?? new(); if (this.HttpClient is not null) { clientOptions.Transport = new HttpClientPipelineTransport(this.HttpClient); } AIProjectClient newClient = new(projectEndpoint, projectCredentials, clientOptions); Interlocked.CompareExchange(ref this._agentClient, newClient, null); } return this._agentClient; } private ProjectConversationsClient GetConversationClient() { if (this._conversationClient is null) { ProjectConversationsClient conversationClient = this.GetAgentClient().GetProjectOpenAIClient().GetProjectConversationsClient(); Interlocked.CompareExchange(ref this._conversationClient, conversationClient, null); } return this._conversationClient; } }