diff --git a/.github/workflows/dotnet-format.yml b/.github/workflows/dotnet-format.yml index a9fe090013..bb55dbde86 100644 --- a/.github/workflows/dotnet-format.yml +++ b/.github/workflows/dotnet-format.yml @@ -7,7 +7,7 @@ name: dotnet-format on: workflow_dispatch: pull_request: - branches: ["main", "feature*"] + branches: ["main"] paths: - dotnet/** - '.github/workflows/dotnet-format.yml' diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 8967bad109..ad7880001d 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -17,8 +17,8 @@ - - + + diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index cf1e367293..2bc4067e55 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -28,6 +28,7 @@ + @@ -263,6 +264,7 @@ + diff --git a/dotnet/nuget.config b/dotnet/nuget.config index 62c87f185c..ae2bf13b3a 100644 --- a/dotnet/nuget.config +++ b/dotnet/nuget.config @@ -3,14 +3,14 @@ - + - - + + \ No newline at end of file diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureAIAgent/Agent_With_AzureAIAgent.csproj b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureAIAgent/Agent_With_AzureAIAgent.csproj new file mode 100644 index 0000000000..2d475becd4 --- /dev/null +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureAIAgent/Agent_With_AzureAIAgent.csproj @@ -0,0 +1,21 @@ + + + + Exe + net9.0 + + enable + enable + $(NoWarn);IDE0059 + + + + + + + + + + + + diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureAIAgent/Program.cs b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureAIAgent/Program.cs new file mode 100644 index 0000000000..d6eb139ebb --- /dev/null +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureAIAgent/Program.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. All rights reserved. + +// This sample shows how to create and use a simple AI agent with Azure Foundry Agents as the backend. + +using Azure.AI.Agents; +using Azure.Identity; +using Microsoft.Agents.AI; + +var endpoint = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_FOUNDRY_PROJECT_ENDPOINT is not set."); +var deploymentName = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME") ?? "gpt-4o-mini"; + +const string JokerInstructions = "You are good at telling jokes."; +const string JokerName = "JokerAgent"; + +// Get a client to create/retrieve server side agents with. +var agentsClient = new AgentsClient(new Uri(endpoint), new AzureCliCredential()); + +// Define the agent you want to create. +var agentDefinition = new PromptAgentDefinition(model: deploymentName) { Instructions = JokerInstructions }; + +// You can create a server side agent with the Azure.AI.Agents SDK. +var agentVersion = agentsClient.CreateAgentVersion(agentName: JokerName, definition: agentDefinition).Value; + +// You can retrieve an already created server side agent as an AIAgent. +AIAgent existingAgent = await agentsClient.GetAIAgentAsync(deploymentName, agentVersion.Name); + +// You can also create a server side persistent agent and return it as an AIAgent directly. +var createdAgent = agentsClient.CreateAIAgent(deploymentName, name: JokerName, instructions: JokerInstructions); + +// You can then invoke the agent like any other AIAgent. +AgentThread thread = existingAgent.GetNewThread(); +Console.WriteLine(await existingAgent.RunAsync("Tell me a joke about a pirate.", thread)); + +// Cleanup by agent name (removes both agent versions created by existingAgent + createdAgent). +await agentsClient.DeleteAgentAsync(agentVersion.Name); diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureAIAgent/README.md b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureAIAgent/README.md new file mode 100644 index 0000000000..df0854ba2f --- /dev/null +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_AzureAIAgent/README.md @@ -0,0 +1,16 @@ +# Prerequisites + +Before you begin, ensure you have the following prerequisites: + +- .NET 8.0 SDK or later +- Azure Foundry service endpoint and deployment configured +- Azure CLI installed and authenticated (for Azure credential authentication) + +**Note**: This demo uses Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Azure Foundry resource. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively). + +Set the following environment variables: + +```powershell +$env:AZURE_FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project" # Replace with your Azure Foundry resource endpoint +$env:AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME="gpt-4o-mini" # Optional, defaults to gpt-4o-mini +``` diff --git a/dotnet/src/Microsoft.Agents.AI.AzureAI/Microsoft.Agents.AI.AzureAI.csproj b/dotnet/src/Microsoft.Agents.AI.AzureAI/Microsoft.Agents.AI.AzureAI.csproj index 5300c98b14..6d1cc23a1c 100644 --- a/dotnet/src/Microsoft.Agents.AI.AzureAI/Microsoft.Agents.AI.AzureAI.csproj +++ b/dotnet/src/Microsoft.Agents.AI.AzureAI/Microsoft.Agents.AI.AzureAI.csproj @@ -1,4 +1,4 @@ - + $(ProjectsTargetFrameworks) @@ -11,9 +11,7 @@ - - diff --git a/dotnet/src/Microsoft.Agents.AI.AzureAI/New/PersistentAgentsClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.AzureAI/New/PersistentAgentsClientExtensions.cs deleted file mode 100644 index a08d5badce..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.AzureAI/New/PersistentAgentsClientExtensions.cs +++ /dev/null @@ -1,665 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.Agents.AI; -using Microsoft.Extensions.AI; -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 Azure.AI.Agents.Persistent; - -/// -/// Provides extension methods for . -/// -public static class AgentsClientExtensions -{ - /* - /// - /// Gets a runnable agent instance from the provided response containing persistent agent metadata. - /// - /// The client used to interact with persistent agents. Cannot be . - /// The response containing the persistent agent to be converted. Cannot be . - /// The default to use when interacting with the agent. - /// Provides a way to customize the creation of the underlying used by the agent. - /// A instance that can be used to perform operations on the persistent agent. - public static ChatClientAgent GetAIAgent(this AgentsClient client, Response persistentAgentResponse, ChatOptions? chatOptions = null, Func? clientFactory = null) - { - if (persistentAgentResponse is null) - { - throw new ArgumentNullException(nameof(persistentAgentResponse)); - } - - return GetAIAgent(client, persistentAgentResponse.Value, chatOptions, clientFactory); - } - - /// - /// Gets a runnable agent instance from a containing metadata about a persistent agent. - /// - /// The client used to interact with persistent agents. Cannot be . - /// The persistent agent metadata to be converted. Cannot be . - /// The default to use when interacting with the agent. - /// Provides a way to customize the creation of the underlying used by the agent. - /// A instance that can be used to perform operations on the persistent agent. - public static ChatClientAgent GetAIAgent(this PersistentAgentsClient persistentAgentsClient, PersistentAgent persistentAgentMetadata, ChatOptions? chatOptions = null, Func? clientFactory = null) - { - if (persistentAgentMetadata is null) - { - throw new ArgumentNullException(nameof(persistentAgentMetadata)); - } - - if (persistentAgentsClient is null) - { - throw new ArgumentNullException(nameof(persistentAgentsClient)); - } - - var chatClient = persistentAgentsClient.AsIChatClient(persistentAgentMetadata.Id); - - if (clientFactory is not null) - { - chatClient = clientFactory(chatClient); - } - - return new ChatClientAgent(chatClient, options: new() - { - Id = persistentAgentMetadata.Id, - Name = persistentAgentMetadata.Name, - Description = persistentAgentMetadata.Description, - Instructions = persistentAgentMetadata.Instructions, - ChatOptions = chatOptions - }); - } - - /// - /// Retrieves an existing server side agent, wrapped as a using the provided . - /// - /// The to create the with. - /// A for the persistent agent. - /// The ID of the server side agent to create a for. - /// Options that should apply to all runs of the agent. - /// Provides a way to customize the creation of the underlying used by the agent. - /// The to monitor for cancellation requests. The default is . - /// A instance that can be used to perform operations on the persistent agent. - public static ChatClientAgent GetAIAgent( - this PersistentAgentsClient persistentAgentsClient, - string agentId, - ChatOptions? chatOptions = null, - Func? clientFactory = null, - CancellationToken cancellationToken = default) - { - if (persistentAgentsClient is null) - { - throw new ArgumentNullException(nameof(persistentAgentsClient)); - } - - if (string.IsNullOrWhiteSpace(agentId)) - { - throw new ArgumentException($"{nameof(agentId)} should not be null or whitespace.", nameof(agentId)); - } - - var persistentAgentResponse = persistentAgentsClient.Administration.GetAgent(agentId, cancellationToken); - return persistentAgentsClient.GetAIAgent(persistentAgentResponse, chatOptions, clientFactory); - } - - /// - /// Retrieves an existing server side agent, wrapped as a using the provided . - /// - /// The to create the with. - /// A for the persistent agent. - /// The ID of the server side agent to create a for. - /// Options that should apply to all runs of the agent. - /// Provides a way to customize the creation of the underlying used by the agent. - /// The to monitor for cancellation requests. The default is . - /// A instance that can be used to perform operations on the persistent agent. - public static async Task GetAIAgentAsync( - this PersistentAgentsClient persistentAgentsClient, - string agentId, - ChatOptions? chatOptions = null, - Func? clientFactory = null, - CancellationToken cancellationToken = default) - { - if (persistentAgentsClient is null) - { - throw new ArgumentNullException(nameof(persistentAgentsClient)); - } - - if (string.IsNullOrWhiteSpace(agentId)) - { - throw new ArgumentException($"{nameof(agentId)} should not be null or whitespace.", nameof(agentId)); - } - - var persistentAgentResponse = await persistentAgentsClient.Administration.GetAgentAsync(agentId, cancellationToken).ConfigureAwait(false); - return persistentAgentsClient.GetAIAgent(persistentAgentResponse, chatOptions, clientFactory); - } - - /// - /// Gets a runnable agent instance from the provided response containing persistent agent metadata. - /// - /// The client used to interact with persistent agents. Cannot be . - /// The response containing the persistent agent to be converted. Cannot be . - /// Full set of options to configure the agent. - /// Provides a way to customize the creation of the underlying used by the agent. - /// A instance that can be used to perform operations on the persistent agent. - /// Thrown when or is . - public static ChatClientAgent GetAIAgent(this PersistentAgentsClient persistentAgentsClient, Response persistentAgentResponse, ChatClientAgentOptions options, Func? clientFactory = null) - { - if (persistentAgentResponse is null) - { - throw new ArgumentNullException(nameof(persistentAgentResponse)); - } - - return GetAIAgent(persistentAgentsClient, persistentAgentResponse.Value, options, clientFactory); - } - - /// - /// Gets a runnable agent instance from a containing metadata about a persistent agent. - /// - /// The client used to interact with persistent agents. Cannot be . - /// The persistent agent metadata to be converted. Cannot be . - /// Full set of options to configure the agent. - /// Provides a way to customize the creation of the underlying used by the agent. - /// A instance that can be used to perform operations on the persistent agent. - /// Thrown when or is . - public static ChatClientAgent GetAIAgent(this PersistentAgentsClient persistentAgentsClient, PersistentAgent persistentAgentMetadata, ChatClientAgentOptions options, Func? clientFactory = null) - { - if (persistentAgentMetadata is null) - { - throw new ArgumentNullException(nameof(persistentAgentMetadata)); - } - - if (persistentAgentsClient is null) - { - throw new ArgumentNullException(nameof(persistentAgentsClient)); - } - - if (options is null) - { - throw new ArgumentNullException(nameof(options)); - } - - var chatClient = persistentAgentsClient.AsIChatClient(persistentAgentMetadata.Id); - - if (clientFactory is not null) - { - chatClient = clientFactory(chatClient); - } - - var agentOptions = new ChatClientAgentOptions() - { - Id = persistentAgentMetadata.Id, - Name = options.Name ?? persistentAgentMetadata.Name, - Description = options.Description ?? persistentAgentMetadata.Description, - Instructions = options.Instructions ?? persistentAgentMetadata.Instructions, - ChatOptions = options.ChatOptions, - AIContextProviderFactory = options.AIContextProviderFactory, - ChatMessageStoreFactory = options.ChatMessageStoreFactory, - UseProvidedChatClientAsIs = options.UseProvidedChatClientAsIs - }; - - return new ChatClientAgent(chatClient, agentOptions); - } - - /// - /// Retrieves an existing server side agent, wrapped as a using the provided . - /// - /// The to create the with. - /// The ID of the server side agent to create a for. - /// Full set of options to configure the agent. - /// Provides a way to customize the creation of the underlying used by the agent. - /// The to monitor for cancellation requests. The default is . - /// A instance that can be used to perform operations on the persistent agent. - /// Thrown when or is . - /// Thrown when is empty or whitespace. - public static ChatClientAgent GetAIAgent( - this PersistentAgentsClient persistentAgentsClient, - string agentId, - ChatClientAgentOptions options, - Func? clientFactory = null, - CancellationToken cancellationToken = default) - { - if (persistentAgentsClient is null) - { - throw new ArgumentNullException(nameof(persistentAgentsClient)); - } - - if (string.IsNullOrWhiteSpace(agentId)) - { - throw new ArgumentException($"{nameof(agentId)} should not be null or whitespace.", nameof(agentId)); - } - - if (options is null) - { - throw new ArgumentNullException(nameof(options)); - } - - var persistentAgentResponse = persistentAgentsClient.Administration.GetAgent(agentId, cancellationToken); - return persistentAgentsClient.GetAIAgent(persistentAgentResponse, options, clientFactory); - } - - /// - /// Retrieves an existing server side agent, wrapped as a using the provided . - /// - /// The to create the with. - /// The ID of the server side agent to create a for. - /// Full set of options to configure the agent. - /// Provides a way to customize the creation of the underlying used by the agent. - /// The to monitor for cancellation requests. The default is . - /// A instance that can be used to perform operations on the persistent agent. - /// Thrown when or is . - /// Thrown when is empty or whitespace. - public static async Task GetAIAgentAsync( - this PersistentAgentsClient persistentAgentsClient, - string agentId, - ChatClientAgentOptions options, - Func? clientFactory = null, - CancellationToken cancellationToken = default) - { - if (persistentAgentsClient is null) - { - throw new ArgumentNullException(nameof(persistentAgentsClient)); - } - - if (string.IsNullOrWhiteSpace(agentId)) - { - throw new ArgumentException($"{nameof(agentId)} should not be null or whitespace.", nameof(agentId)); - } - - if (options is null) - { - throw new ArgumentNullException(nameof(options)); - } - - var persistentAgentResponse = await persistentAgentsClient.Administration.GetAgentAsync(agentId, cancellationToken).ConfigureAwait(false); - return persistentAgentsClient.GetAIAgent(persistentAgentResponse, options, clientFactory); - }*/ - - /// - /// Creates a new server side agent using the provided . - /// - /// The to create the agent with. - /// The model to be used by the agent. - /// The name of the agent. - /// The instructions for the agent. - /// The tools to be used by the agent. - /// The temperature setting for the agent. - /// The top-p setting for the agent. - /// The responsible AI config - /// The reasoning options for the agent. - /// The text options for the agent. - /// The structured inputs for the agent. - /// The metadata for the agent. - /// Provides a way to customize the creation of the underlying used by the agent. - /// The to monitor for cancellation requests. The default is . - /// A instance that can be used to perform operations on the newly created agent. - public static async Task CreateAIAgentAsync( - this AgentsClient client, - string model, - string? name = null, - string? instructions = null, - IEnumerable? tools = null, - float? temperature = null, - float? topP = null, - RaiConfig? raiConfig = null, - ResponseReasoningOptions? reasoningOptions = null, - ResponseTextOptions? textOptions = null, - IDictionary? structuredInputs = null, - IReadOnlyDictionary? metadata = null, - Func? clientFactory = null, - CancellationToken cancellationToken = default) - { - if (client is null) - { - throw new ArgumentNullException(nameof(client)); - } - - var openAIClient = client.GetOpenAIClient(); - var chatClient = openAIClient.GetOpenAIResponseClient(model).AsIChatClient(); - - var promptAgentDefinition = new PromptAgentDefinition(model) - { - Instructions = instructions, - Temperature = temperature, - TopP = topP, - RaiConfig = raiConfig, - ReasoningOptions = reasoningOptions, - TextOptions = textOptions, - }; - - var versionCreation = new AgentVersionCreationOptions(); - if (metadata is not null) - { - foreach (var kvp in metadata) - { - versionCreation.Metadata.Add(kvp.Key, kvp.Value); - } - } - - AgentVersion newAgentVersion = await client.CreateAgentVersionAsync(name, promptAgentDefinition, versionCreation, cancellationToken).ConfigureAwait(false); - - if (tools is not null) - { - if (promptAgentDefinition.Tools is List toolsList) - { - toolsList.AddRange(tools); - } - else - { - foreach (var tool in tools) - { - promptAgentDefinition.Tools.Add(tool); - } - } - } - - if (structuredInputs is not null) - { - foreach (var kvp in structuredInputs) - { - promptAgentDefinition.StructuredInputs.Add(kvp.Key, kvp.Value); - } - } - - var agent = new ChatClientAgent(chatClient); - agent.AsBuilder().Use(FoundryAgentMiddlewareAsync).Build(); - - async Task FoundryAgentMiddlewareAsync(IEnumerable messages, AgentThread? thread, AgentRunOptions? options, Func, AgentThread?, AgentRunOptions?, CancellationToken, Task> sharedFunc, CancellationToken cancellationToken) - { - if (options is not ChatClientAgentRunOptions chatClientOptions) - { - throw new InvalidOperationException("The provided AgentRunOptions is not of type ChatClientAgentRunOptions."); - } - - ChatClientAgentThread? chatClientThread = null; - if (thread is not null) - { - if (thread is not ChatClientAgentThread asChatClientAgentThread) - { - throw new InvalidOperationException("The provided AgentThread is not of type ChatClientAgentThread."); - } - - if (string.IsNullOrWhiteSpace(asChatClientAgentThread.ConversationId)) - { - throw new InvalidOperationException("The ChatClientAgentThread does not have a valid ConversationId."); - } - - chatClientThread = asChatClientAgentThread; - } - - var conversation = (chatClientThread is not null) - ? await client.GetConversationsClient().GetConversationAsync(chatClientThread.ConversationId, cancellationToken).ConfigureAwait(false) - : await client.GetConversationsClient().CreateConversationAsync(cancellationToken: cancellationToken).ConfigureAwait(false); - - chatClientOptions.ChatOptions ??= new(); - chatClientOptions.ChatOptions.RawRepresentationFactory = (client) => - { - var rawRepresentationFactory = chatClientOptions.ChatOptions?.RawRepresentationFactory; - ResponseCreationOptions? responseCreationOptions = null; - - if (rawRepresentationFactory is not null) - { - responseCreationOptions = rawRepresentationFactory.Invoke(chatClient) as ResponseCreationOptions; - - if (responseCreationOptions is null) - { - throw new InvalidOperationException("The RawRepresentationFactory did not return a valid ResponseCreationOptions instance."); - } - } - else - { - responseCreationOptions = new ResponseCreationOptions(); - } - - responseCreationOptions.SetAgentReference(name); - responseCreationOptions.SetConversationReference(conversation); - - return responseCreationOptions; - }; - - await sharedFunc(messages, thread, options, cancellationToken).ConfigureAwait(false); - } - - return agent; - } - - /// - /// Creates a new server side agent using the provided . - /// - /// The to create the agent with. - /// The model to be used by the agent. - /// The name of the agent. - /// The description of the agent. - /// The instructions for the agent. - /// The tools to be used by the agent. - /// The resources for the tools. - /// The temperature setting for the agent. - /// The top-p setting for the agent. - /// The response format for the agent. - /// The metadata for the agent. - /// Provides a way to customize the creation of the underlying used by the agent. - /// The to monitor for cancellation requests. The default is . - /// A instance that can be used to perform operations on the newly created agent. - public static ChatClientAgent CreateAIAgent( - this PersistentAgentsClient persistentAgentsClient, - string model, - string? name = null, - string? description = null, - string? instructions = null, - IEnumerable? tools = null, - ToolResources? toolResources = null, - float? temperature = null, - float? topP = null, - BinaryData? responseFormat = null, - IReadOnlyDictionary? metadata = null, - Func? clientFactory = null, - CancellationToken cancellationToken = default) - { - if (persistentAgentsClient is null) - { - throw new ArgumentNullException(nameof(persistentAgentsClient)); - } - - var createPersistentAgentResponse = persistentAgentsClient.Administration.CreateAgent( - model: model, - name: name, - description: description, - instructions: instructions, - tools: tools, - toolResources: toolResources, - temperature: temperature, - topP: topP, - responseFormat: responseFormat, - metadata: metadata, - cancellationToken: cancellationToken); - - // Get a local proxy for the agent to work with. - return persistentAgentsClient.GetAIAgent(createPersistentAgentResponse.Value.Id, clientFactory: clientFactory, cancellationToken: cancellationToken); - } - - /// - /// Creates a new server side agent using the provided . - /// - /// The to create the agent with. - /// The model to be used by the agent. - /// Full set of options to configure the agent. - /// Provides a way to customize the creation of the underlying used by the agent. - /// The to monitor for cancellation requests. The default is . - /// A instance that can be used to perform operations on the newly created agent. - /// Thrown when or or is . - /// Thrown when is empty or whitespace. - public static ChatClientAgent CreateAIAgent( - this PersistentAgentsClient persistentAgentsClient, - string model, - ChatClientAgentOptions options, - Func? clientFactory = null, - CancellationToken cancellationToken = default) - { - if (persistentAgentsClient is null) - { - throw new ArgumentNullException(nameof(persistentAgentsClient)); - } - - if (string.IsNullOrWhiteSpace(model)) - { - throw new ArgumentException($"{nameof(model)} should not be null or whitespace.", nameof(model)); - } - - if (options is null) - { - throw new ArgumentNullException(nameof(options)); - } - - var toolDefinitionsAndResources = ConvertAIToolsToToolDefinitions(options.ChatOptions?.Tools); - - var createPersistentAgentResponse = persistentAgentsClient.Administration.CreateAgent( - model: model, - name: options.Name, - description: options.Description, - instructions: options.Instructions, - tools: toolDefinitionsAndResources.ToolDefinitions, - toolResources: toolDefinitionsAndResources.ToolResources, - temperature: null, - topP: null, - responseFormat: null, - metadata: null, - cancellationToken: cancellationToken); - - if (options.ChatOptions?.Tools is { Count: > 0 } && (toolDefinitionsAndResources.FunctionToolsAndOtherTools is null || options.ChatOptions.Tools.Count != toolDefinitionsAndResources.FunctionToolsAndOtherTools.Count)) - { - options = options.Clone(); - options.ChatOptions!.Tools = toolDefinitionsAndResources.FunctionToolsAndOtherTools; - } - - // Get a local proxy for the agent to work with. - return persistentAgentsClient.GetAIAgent(createPersistentAgentResponse.Value.Id, options, clientFactory: clientFactory, cancellationToken: cancellationToken); - } - - /// - /// Creates a new server side agent using the provided . - /// - /// The to create the agent with. - /// The model to be used by the agent. - /// Full set of options to configure the agent. - /// Provides a way to customize the creation of the underlying used by the agent. - /// The to monitor for cancellation requests. The default is . - /// A instance that can be used to perform operations on the newly created agent. - /// Thrown when or or is . - /// Thrown when is empty or whitespace. - public static async Task CreateAIAgentAsync( - this PersistentAgentsClient persistentAgentsClient, - string model, - ChatClientAgentOptions options, - Func? clientFactory = null, - CancellationToken cancellationToken = default) - { - if (persistentAgentsClient is null) - { - throw new ArgumentNullException(nameof(persistentAgentsClient)); - } - - if (string.IsNullOrWhiteSpace(model)) - { - throw new ArgumentException($"{nameof(model)} should not be null or whitespace.", nameof(model)); - } - - if (options is null) - { - throw new ArgumentNullException(nameof(options)); - } - - var toolDefinitionsAndResources = ConvertAIToolsToToolDefinitions(options.ChatOptions?.Tools); - - var createPersistentAgentResponse = await persistentAgentsClient.Administration.CreateAgentAsync( - model: model, - name: options.Name, - description: options.Description, - instructions: options.Instructions, - tools: toolDefinitionsAndResources.ToolDefinitions, - toolResources: toolDefinitionsAndResources.ToolResources, - temperature: null, - topP: null, - responseFormat: null, - metadata: null, - cancellationToken: cancellationToken).ConfigureAwait(false); - - if (options.ChatOptions?.Tools is { Count: > 0 } && (toolDefinitionsAndResources.FunctionToolsAndOtherTools is null || options.ChatOptions.Tools.Count != toolDefinitionsAndResources.FunctionToolsAndOtherTools.Count)) - { - options = options.Clone(); - options.ChatOptions!.Tools = toolDefinitionsAndResources.FunctionToolsAndOtherTools; - } - - // Get a local proxy for the agent to work with. - return await persistentAgentsClient.GetAIAgentAsync(createPersistentAgentResponse.Value.Id, options, clientFactory: clientFactory, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - private static (List? ToolDefinitions, ToolResources? ToolResources, List? FunctionToolsAndOtherTools) ConvertAIToolsToToolDefinitions(IList? tools) - { - List? toolDefinitions = null; - ToolResources? toolResources = null; - List? functionToolsAndOtherTools = null; - - if (tools is not null) - { - foreach (AITool tool in tools) - { - switch (tool) - { - case HostedCodeInterpreterTool codeTool: - - toolDefinitions ??= new(); - toolDefinitions.Add(new CodeInterpreterToolDefinition()); - - if (codeTool.Inputs is { Count: > 0 }) - { - foreach (var input in codeTool.Inputs) - { - switch (input) - { - case HostedFileContent hostedFile: - // If the input is a HostedFileContent, we can use its ID directly. - toolResources ??= new(); - toolResources.CodeInterpreter ??= new(); - toolResources.CodeInterpreter.FileIds.Add(hostedFile.FileId); - break; - } - } - } - break; - - case HostedFileSearchTool fileSearchTool: - toolDefinitions ??= new(); - toolDefinitions.Add(new FileSearchToolDefinition - { - FileSearch = new() { MaxNumResults = fileSearchTool.MaximumResultCount } - }); - - if (fileSearchTool.Inputs is { Count: > 0 }) - { - foreach (var input in fileSearchTool.Inputs) - { - switch (input) - { - case HostedVectorStoreContent hostedVectorStore: - toolResources ??= new(); - toolResources.FileSearch ??= new(); - toolResources.FileSearch.VectorStoreIds.Add(hostedVectorStore.VectorStoreId); - break; - } - } - } - break; - - case HostedWebSearchTool webSearch when webSearch.AdditionalProperties?.TryGetValue("connectionId", out object? connectionId) is true: - toolDefinitions ??= new(); - toolDefinitions.Add(new BingGroundingToolDefinition(new BingGroundingSearchToolParameters([new BingGroundingSearchConfiguration(connectionId!.ToString())]))); - break; - - default: - functionToolsAndOtherTools ??= new(); - functionToolsAndOtherTools.Add(tool); - break; - } - } - } - - return (toolDefinitions, toolResources, functionToolsAndOtherTools); - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.AzureAIAgents/AgentsClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.AzureAIAgents/AgentsClientExtensions.cs new file mode 100644 index 0000000000..4795ff1403 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.AzureAIAgents/AgentsClientExtensions.cs @@ -0,0 +1,893 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.AzureAIAgents; +using Microsoft.Extensions.AI; +using Microsoft.Shared.Diagnostics; +using OpenAI; +using OpenAI.Responses; + +#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#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 Azure.AI.Agents; + +/// +/// Provides extension methods for . +/// +public static class AgentsClientExtensions +{ + /// + /// Gets a runnable agent instance from the provided agent record. + /// + /// The client used to interact with Azure AI Agents. Cannot be . + /// The model to be used by the agent. + /// The agent record to be converted. The latest version will be used. Cannot be . + /// The default to use when interacting with the agent. + /// Provides a way to customize the creation of the underlying used by the agent. + /// An optional for configuring the underlying OpenAI client. + /// The to monitor for cancellation requests. The default is . + /// A instance that can be used to perform operations based on the latest version of the Azure AI Agent. + public static ChatClientAgent GetAIAgent( + this AgentsClient agentsClient, + string model, + AgentRecord agentRecord, + ChatOptions? chatOptions = null, + Func? clientFactory = null, + OpenAIClientOptions? openAIClientOptions = null, + CancellationToken cancellationToken = default) + { + Throw.IfNull(agentsClient); + Throw.IfNullOrWhitespace(model); + Throw.IfNull(agentRecord); + + return GetAIAgent(agentsClient, model, agentRecord.Versions.Latest, chatOptions, clientFactory, openAIClientOptions, cancellationToken); + } + + /// + /// Gets a runnable agent instance from the provided agent record. + /// + /// The client used to interact with Azure AI Agents. Cannot be . + /// The model to be used by the agent. Cannot be . + /// The agent version to be converted. Cannot be . + /// The default to use when interacting with the agent. + /// Provides a way to customize the creation of the underlying used by the agent. + /// An optional for configuring the underlying OpenAI client. + /// The to monitor for cancellation requests. The default is . + /// A instance that can be used to perform operations based on the specified version of the Azure AI Agent. + /// Thrown when , , or is . + public static ChatClientAgent GetAIAgent( + this AgentsClient agentsClient, + string model, + AgentVersion agentVersion, + ChatOptions? chatOptions = null, + Func? clientFactory = null, + OpenAIClientOptions? openAIClientOptions = null, + CancellationToken cancellationToken = default) + { + Throw.IfNull(agentsClient); + Throw.IfNullOrWhitespace(model); + Throw.IfNull(agentVersion); + + return GetAIAgent(agentsClient, model, agentVersion, new ChatClientAgentOptions() { ChatOptions = chatOptions }, clientFactory, openAIClientOptions, cancellationToken); + } + + /// + /// Retrieves an existing server side agent, wrapped as a using the provided . + /// + /// The to create the with. Cannot be . + /// The model to be used by the agent. Cannot be or whitespace. + /// The name of the server side agent to create a for. Cannot be or whitespace. + /// The default to use when interacting with the agent. + /// Provides a way to customize the creation of the underlying used by the agent. + /// An optional for configuring the underlying OpenAI client. + /// The to monitor for cancellation requests. The default is . + /// A instance that can be used to perform operations based on the latest version of the named Azure AI Agent. + /// Thrown when , , or is . + /// Thrown when or is empty or whitespace, or when the agent with the specified name was not found. + /// The agent with the specified name was not found. + public static ChatClientAgent GetAIAgent( + this AgentsClient agentsClient, + string model, + string name, + ChatOptions? chatOptions = null, + Func? clientFactory = null, + OpenAIClientOptions? openAIClientOptions = null, + CancellationToken cancellationToken = default) + { + Throw.IfNull(agentsClient); + Throw.IfNullOrWhitespace(model); + Throw.IfNullOrWhitespace(name); + + var agentRecord = agentsClient.GetAgent(name, cancellationToken) + ?? throw new InvalidOperationException($"Agent with name '{name}' not found."); + + return GetAIAgent(agentsClient, model, agentRecord, chatOptions, clientFactory, openAIClientOptions, cancellationToken); + } + + /// + /// Asynchronously retrieves an existing server side agent, wrapped as a using the provided . + /// + /// The to create the with. Cannot be . + /// The model to be used by the agent. Cannot be or whitespace. + /// The name of the server side agent to create a for. Cannot be or whitespace. + /// The default to use when interacting with the agent. + /// Provides a way to customize the creation of the underlying used by the agent. + /// An optional for configuring the underlying OpenAI client. + /// The to monitor for cancellation requests. The default is . + /// A instance that can be used to perform operations based on the latest version of the named Azure AI Agent. + /// Thrown when , , or is . + /// Thrown when or is empty or whitespace, or when the agent with the specified name was not found. + /// The agent with the specified name was not found. + public static async Task GetAIAgentAsync( + this AgentsClient agentsClient, + string model, + string name, + ChatOptions? chatOptions = null, + Func? clientFactory = null, + OpenAIClientOptions? openAIClientOptions = null, + CancellationToken cancellationToken = default) + { + Throw.IfNull(agentsClient); + Throw.IfNullOrWhitespace(model); + Throw.IfNullOrWhitespace(name); + + var agentRecord = await agentsClient.GetAgentAsync(name, cancellationToken).ConfigureAwait(false) + ?? throw new InvalidOperationException($"Agent with name '{name}' not found."); + + return GetAIAgent(agentsClient, model, agentRecord, chatOptions, clientFactory, openAIClientOptions, cancellationToken); + } + + /// + /// Gets a runnable agent instance from a containing metadata about an Azure AI Agent. + /// + /// The client used to interact with Azure AI Agents. Cannot be . + /// The model to be used by the agent. + /// The agent record to be converted. The latest version will be used. Cannot be . + /// Full set of options to configure the agent. + /// Provides a way to customize the creation of the underlying used by the agent. + /// An optional for configuring the underlying OpenAI client. + /// The to monitor for cancellation requests. The default is . + /// A instance that can be used to perform operations based on the latest version of the Azure AI Agent record. + /// Thrown when , , or is . + public static ChatClientAgent GetAIAgent( + this AgentsClient agentsClient, + string model, + AgentRecord agentRecord, + ChatClientAgentOptions? options = null, + Func? clientFactory = null, + OpenAIClientOptions? openAIClientOptions = null, + CancellationToken cancellationToken = default) + { + Throw.IfNull(agentsClient); + Throw.IfNullOrWhitespace(model); + Throw.IfNull(agentRecord); + + return GetAIAgent( + agentsClient, + model, + agentRecord.Versions.Latest, + options, + clientFactory, + openAIClientOptions, + cancellationToken); + } + + /// + /// Gets a runnable agent instance from a containing metadata about an Azure AI Agent. + /// + /// The client used to interact with Azure AI Agents. Cannot be . + /// The model to be used by the agent. + /// The agent version to be converted. Cannot be . + /// Full set of options to configure the agent. + /// Provides a way to customize the creation of the underlying used by the agent. + /// An optional for configuring the underlying OpenAI client. + /// The to monitor for cancellation requests. The default is . + /// A instance that can be used to perform operations based on the provided version of the Azure AI Agent. + /// Thrown when , , or is . + public static ChatClientAgent GetAIAgent( + this AgentsClient agentsClient, + string model, + AgentVersion agentVersion, + ChatClientAgentOptions? options = null, + Func? clientFactory = null, + OpenAIClientOptions? openAIClientOptions = null, + CancellationToken cancellationToken = default) + { + Throw.IfNull(agentsClient); + Throw.IfNullOrWhitespace(model); + Throw.IfNull(agentVersion); + IChatClient chatClient = new AzureAIAgentChatClient(agentsClient, agentVersion, model, openAIClientOptions); + + if (clientFactory is not null) + { + chatClient = clientFactory(chatClient); + } + + ChatClientAgentOptions? agentOptions; + + // If options are null, populate from agentRecord definition + var version = agentVersion; + + if (options is null) + { + agentOptions = new(); + agentOptions.Id = GetAgentId(agentVersion); + agentOptions.Name = agentVersion.Name; + + agentOptions.Description = version.Description; + + if (version.Definition is PromptAgentDefinition promptDef && promptDef.Tools is { Count: > 0 }) + { + agentOptions.ChatOptions = new ChatOptions(); + agentOptions.ChatOptions.Tools = []; + agentOptions.Instructions = promptDef.Instructions; + + foreach (var tool in promptDef.Tools) + { + agentOptions.ChatOptions.Tools.Add(tool); + } + } + } + else + { + // When agent options it is used when available otherwise fallback to the agent definition used for the agent record. + agentOptions = new ChatClientAgentOptions() + { + Id = options.Id ?? GetAgentId(agentVersion), + Name = options.Name ?? agentVersion.Name, + Description = options.Description ?? version.Description, + Instructions = options.Instructions ?? options.ChatOptions?.Instructions ?? (version.Definition as PromptAgentDefinition)?.Instructions, + ChatOptions = options.ChatOptions, + AIContextProviderFactory = options.AIContextProviderFactory, + ChatMessageStoreFactory = options.ChatMessageStoreFactory, + UseProvidedChatClientAsIs = options.UseProvidedChatClientAsIs + }; + + // If no tools were provided in options, but exist in the agent definition, use those. + if (agentOptions.ChatOptions?.Tools is null or { Count: 0 } && version.Definition is PromptAgentDefinition promptDef && promptDef.Tools is { Count: > 0 }) + { + agentOptions.ChatOptions ??= new ChatOptions(); + agentOptions.ChatOptions.Tools ??= []; + + foreach (var tool in promptDef.Tools) + { + agentOptions.ChatOptions.Tools.Add(tool); + } + } + } + + return new ChatClientAgent(chatClient, agentOptions); + } + + /// + /// Retrieves an existing server side agent, wrapped as a using the provided . + /// + /// The to create the with. + /// The model to be used by the agent. + /// The ID of the server side agent to create a for. + /// Full set of options to configure the agent. + /// Provides a way to customize the creation of the underlying used by the agent. + /// An optional for configuring the underlying OpenAI client. + /// The to monitor for cancellation requests. The default is . + /// A instance that can be used to perform operations based on the latest version of named the Azure AI Agent. + /// Thrown when or is . + /// Thrown when is empty or whitespace. + public static async Task GetAIAgentAsync( + this AgentsClient agentsClient, + string model, + string name, + ChatClientAgentOptions options, + Func? clientFactory = null, + OpenAIClientOptions? openAIClientOptions = null, + CancellationToken cancellationToken = default) + { + Throw.IfNull(agentsClient); + Throw.IfNullOrWhitespace(name); + Throw.IfNull(options); + + var agentRecord = await agentsClient.GetAgentAsync(name, cancellationToken).ConfigureAwait(false) + ?? throw new InvalidOperationException($"Agent with name '{name}' not found."); + + return GetAIAgent(agentsClient, model, agentRecord, options, clientFactory, openAIClientOptions, cancellationToken); + } + + /// + /// Creates a new server side agent using the provided . + /// + /// The to create the agent with. + /// The model to be used by the agent. + /// The name of the agent. + /// The instructions for the agent. + /// The description for the agent. + /// The tools to be used by the agent. + /// The temperature setting for the agent. + /// The top-p setting for the agent. + /// The responsible AI configuration for the agent. + /// The reasoning options for the agent. + /// The text options for the agent. + /// The structured inputs for the agent. + /// The metadata for the agent. + /// A factory function to customize the creation of the chat client used by the agent. + /// An optional for configuring the underlying OpenAI client. + /// The to monitor for cancellation requests. The default is . + /// A instance that can be used to perform operations on the newly created agent. + public static ChatClientAgent CreateAIAgent( + this AgentsClient agentsClient, + string model, + string name, + string? instructions = null, + string? description = null, + IList? tools = null, + float? temperature = null, + float? topP = null, + RaiConfig? raiConfig = null, + ResponseReasoningOptions? reasoningOptions = null, + ResponseTextOptions? textOptions = null, + IDictionary? structuredInputs = null, + IReadOnlyDictionary? metadata = null, + Func? clientFactory = null, + OpenAIClientOptions? openAIClientOptions = null, + CancellationToken cancellationToken = default) + { + Throw.IfNull(agentsClient); + Throw.IfNullOrWhitespace(model); + Throw.IfNullOrWhitespace(name); + + var (promptAgentDefinition, versionCreationOptions, chatClientAgentOptions) = CreatePromptAgentDefinitionAndOptions( + name, + model, + instructions, + description, + temperature, + topP, + raiConfig, + reasoningOptions, + textOptions, + tools, + structuredInputs, + metadata); + + AgentVersion agentVersion = agentsClient.CreateAgentVersion(name, promptAgentDefinition, versionCreationOptions, cancellationToken); + IChatClient chatClient = new AzureAIAgentChatClient(agentsClient, agentVersion, model, openAIClientOptions); + + if (clientFactory is not null) + { + chatClient = clientFactory(chatClient); + } + + return new ChatClientAgent(chatClient, chatClientAgentOptions); + } + + /// + /// Creates a new AI agent using the specified agent definition and optional configuration parameters. + /// + /// The client used to manage and interact with AI agents. Cannot be . + /// The name for the agent. + /// The definition that specifies the configuration and behavior of the agent to create. Cannot be . + /// The name of the model to use for the agent. Model must be provided either directly or as part of a specialization property. + /// Settings that control the creation of the agent. + /// A factory function to customize the creation of the chat client used by the agent. + /// An optional for configuring the underlying OpenAI client. + /// A token to monitor for cancellation requests. + /// A instance that can be used to perform operations on the newly created agent. + /// Thrown when or is . + /// Thrown if neither the 'model' parameter nor a model in the agent definition is provided. + public static ChatClientAgent CreateAIAgent( + this AgentsClient agentsClient, + string name, + AgentDefinition agentDefinition, + string? model = null, + AgentCreationOptions? creationOptions = null, + Func? clientFactory = null, + OpenAIClientOptions? openAIClientOptions = null, + CancellationToken cancellationToken = default) + { + Throw.IfNull(agentsClient); + Throw.IfNullOrWhitespace(name); + Throw.IfNull(agentDefinition); + + model ??= (agentDefinition as PromptAgentDefinition)?.Model; + if (string.IsNullOrWhiteSpace(model)) + { + throw new ArgumentException("Model must be provided either directly or as part of a PromptAgentDefinition specialization.", nameof(model)); + } + + AgentRecord agentRecord = agentsClient.CreateAgent(name, agentDefinition, creationOptions, cancellationToken); + IChatClient chatClient = new AzureAIAgentChatClient(agentsClient, agentRecord, model, openAIClientOptions); + + if (clientFactory is not null) + { + chatClient = clientFactory(chatClient); + } + + return new ChatClientAgent(chatClient); + } + + /// + /// Creates a new Prompt AI Agent using the provided and options. + /// + /// The client used to manage and interact with AI agents. Cannot be . + /// The name of the model to use for the agent. Cannot be or whitespace. + /// The options for creating the agent. Cannot be . + /// A factory function to customize the creation of the chat client used by the agent. + /// An optional for configuring the underlying OpenAI client. + /// A to cancel the operation if needed. + /// A instance that can be used to perform operations on the newly created agent. + /// Thrown when , , or is . + /// Thrown when is empty or whitespace, or when the agent name is not provided in the options. + public static ChatClientAgent CreateAIAgent( + this AgentsClient agentsClient, + string model, + ChatClientAgentOptions options, + Func? clientFactory = null, + OpenAIClientOptions? openAIClientOptions = null, + CancellationToken cancellationToken = default) + { + Throw.IfNull(agentsClient); + Throw.IfNullOrWhitespace(model); + Throw.IfNull(options); + + if (string.IsNullOrWhiteSpace(options.Name)) + { + throw new ArgumentException("Agent name must be provided in the options.Name property", nameof(options)); + } + + var (agentDefinition, versionCreationOptions, chatClientAgentOptions) = CreatePromptAgentDefinitionAndOptions( + options.Name, + model, + options.Instructions, + options.Description); + + if (options.ChatOptions?.Tools is { Count: > 0 }) + { + foreach (var tool in options.ChatOptions.Tools) + { + agentDefinition.Tools.Add(ToResponseTool(tool, options.ChatOptions)); + } + } + + AgentVersion agentVersion = agentsClient.CreateAgentVersion(options.Name, agentDefinition, versionCreationOptions, cancellationToken); + IChatClient chatClient = new AzureAIAgentChatClient(agentsClient, agentVersion, model, openAIClientOptions); + + if (clientFactory is not null) + { + chatClient = clientFactory(chatClient); + } + + chatClientAgentOptions.Id = GetAgentId(agentVersion); + + return new ChatClientAgent(chatClient, chatClientAgentOptions); + } + + /// + /// Asynchronously creates a new AI agent using the specified agent definition and optional configuration + /// parameters. + /// + /// The client used to manage and interact with AI agents. Cannot be . + /// The definition that specifies the configuration and behavior of the agent to create. Cannot be . + /// The name of the model to use for the agent. If not specified, the model must be provided as part of the agent definition. + /// The name for the agent. + /// Settings that control the creation of the agent. + /// A factory function to customize the creation of the chat client used by the agent. + /// An optional for configuring the underlying OpenAI client. + /// A token to monitor for cancellation requests. + /// A instance that can be used to perform operations on the newly created agent. + /// Thrown when or is . + /// Thrown if neither the 'model' parameter nor a model in the agent definition is provided. + public static async Task CreateAIAgentAsync( + this AgentsClient agentsClient, + AgentDefinition agentDefinition, + string? model = null, + string? name = null, + AgentVersionCreationOptions? agentVersionCreationOptions = null, + Func? clientFactory = null, + OpenAIClientOptions? openAIClientOptions = null, + CancellationToken cancellationToken = default) + { + Throw.IfNull(agentsClient); + Throw.IfNull(agentDefinition); + + model ??= (agentDefinition as PromptAgentDefinition)?.Model; + if (string.IsNullOrWhiteSpace(model)) + { + throw new ArgumentException("Model must be provided either directly or as part of a PromptAgentDefinition specialization.", nameof(model)); + } + + AgentVersion agentVersion = await agentsClient.CreateAgentVersionAsync(name, agentDefinition, agentVersionCreationOptions, cancellationToken).ConfigureAwait(false); + IChatClient chatClient = new AzureAIAgentChatClient(agentsClient, agentVersion, model, openAIClientOptions); + + if (clientFactory is not null) + { + chatClient = clientFactory(chatClient); + } + + List? aiTools = null; + if (agentDefinition is PromptAgentDefinition { Tools: { Count: > 0 } definitionTools }) + { + aiTools = definitionTools.Select(rt => rt.AsAITool()).ToList(); + } + + return new ChatClientAgent(chatClient, new ChatClientAgentOptions() + { + Id = GetAgentId(agentVersion), + Name = name, + Description = agentVersionCreationOptions?.Description, + Instructions = (agentDefinition as PromptAgentDefinition)?.Instructions, + ChatOptions = new ChatOptions() + { + Tools = aiTools + } + }); + } + + /// + /// Creates a new server side prompt agent using the provided . + /// + /// The to create the agent with. + /// The model to be used by the agent. + /// The name of the agent. + /// The instructions for the agent. + /// The description for the agent. + /// The tools to be used by the agent. + /// The temperature setting for the agent. + /// The top-p setting for the agent. + /// The responsible AI configuration for the agent. + /// The reasoning options for the agent. + /// The text options for the agent. + /// The structured inputs for the agent. + /// The metadata for the agent. + /// A factory function to customize the creation of the underlying used by the agent. + /// An optional for configuring the underlying OpenAI client. + /// The to monitor for cancellation requests. + /// A instance that can be used to perform operations on the newly created agent. + public static async Task CreateAIAgentAsync( + this AgentsClient agentsClient, + string model, + string name, + string? instructions = null, + string? description = null, + IList? tools = null, + float? temperature = null, + float? topP = null, + RaiConfig? raiConfig = null, + ResponseReasoningOptions? reasoningOptions = null, + ResponseTextOptions? textOptions = null, + IDictionary? structuredInputs = null, + IReadOnlyDictionary? metadata = null, + Func? clientFactory = null, + OpenAIClientOptions? openAIClientOptions = null, + CancellationToken cancellationToken = default) + { + Throw.IfNull(agentsClient); + Throw.IfNullOrWhitespace(model); + + var (promptAgentDefinition, versionCreationOptions, chatClientAgentOptions) = CreatePromptAgentDefinitionAndOptions(name, model, instructions, description, temperature, topP, raiConfig, reasoningOptions, textOptions, tools, structuredInputs, metadata); + + AgentVersion agentVersion = await agentsClient.CreateAgentVersionAsync(name, promptAgentDefinition, versionCreationOptions, cancellationToken).ConfigureAwait(false); + IChatClient chatClient = new AzureAIAgentChatClient(agentsClient, agentVersion, model, openAIClientOptions); + + if (clientFactory is not null) + { + chatClient = clientFactory(chatClient); + } + + chatClientAgentOptions.Id = GetAgentId(agentVersion); + + return new ChatClientAgent(chatClient, chatClientAgentOptions); + } + + #region Private + + private static (PromptAgentDefinition, AgentVersionCreationOptions?, ChatClientAgentOptions) CreatePromptAgentDefinitionAndOptions( + string name, + string model, + string? instructions, + string? description, + float? temperature = null, + float? topP = null, + RaiConfig? raiConfig = null, + ResponseReasoningOptions? reasoningOptions = null, + ResponseTextOptions? textOptions = null, + IList? tools = null, + IDictionary? structuredInputs = null, + IReadOnlyDictionary? metadata = null) + { + PromptAgentDefinition promptAgentDefinition = new(model) + { + Model = model, + Instructions = instructions, + Temperature = temperature, + TopP = topP, + RaiConfig = raiConfig, + ReasoningOptions = reasoningOptions, + TextOptions = textOptions, + }; + + var chatOptions = new ChatOptions() + { + TopP = topP, + Temperature = temperature, + Instructions = instructions, + }; + + AgentVersionCreationOptions? versionCreationOptions = null; + if (metadata is not null) + { + versionCreationOptions ??= new(); + foreach (var kvp in metadata) + { + versionCreationOptions.Metadata.Add(kvp.Key, kvp.Value); + } + } + + if (!string.IsNullOrWhiteSpace(description)) + { + (versionCreationOptions ??= new()).Description = description; + } + + if (tools is { Count: > 0 }) + { + chatOptions.Tools ??= []; + + foreach (var tool in tools) + { + chatOptions.Tools.Add(tool); + promptAgentDefinition.Tools.Add(tool); + } + } + + if (structuredInputs is not null) + { + foreach (var kvp in structuredInputs) + { + promptAgentDefinition.StructuredInputs.Add(kvp.Key, kvp.Value); + } + } + + var chatClientAgentOptions = new ChatClientAgentOptions() + { + Name = name, + Instructions = instructions, + Description = description, + ChatOptions = chatOptions + }; + + return (promptAgentDefinition, versionCreationOptions, chatClientAgentOptions); + } + + private static string GetAgentId(AgentVersion agentVersion) + => $"{agentVersion.Name}:{agentVersion.Id}"; + + #endregion + + #region Polyfill from MEAI.OpenAI for AITool -> ResponseTool conversion + + // This code will be removed and replaced by the utility tool made public in the PR below for Microsoft.Extensions.AI.OpenAI package + // PR https://github.com/dotnet/extensions/pull/6958 + + /// Key into AdditionalProperties used to store a strict option. + private const string StrictKey = "strictJsonSchema"; + + private static FunctionTool ToResponseFunctionTool(AIFunctionDeclaration aiFunction, ChatOptions? options = null) + { + bool? strict = + HasStrict(aiFunction.AdditionalProperties) ?? + HasStrict(options?.AdditionalProperties); + + return ResponseTool.CreateFunctionTool( + aiFunction.Name, + ToOpenAIFunctionParameters(aiFunction, strict), + strict, + aiFunction.Description); + } + + /// Gets whether the properties specify that strict schema handling is desired. + private static bool? HasStrict(IReadOnlyDictionary? additionalProperties) => + additionalProperties?.TryGetValue(StrictKey, out object? strictObj) is true && + strictObj is bool strictValue ? + strictValue : null; + + /// Extracts from an the parameters and strictness setting for use with OpenAI's APIs. + private static BinaryData ToOpenAIFunctionParameters(AIFunctionDeclaration aiFunction, bool? strict) + { + // Perform any desirable transformations on the function's JSON schema, if it'll be used in a strict setting. + JsonElement jsonSchema = strict is true ? + GetStrictSchemaTransformCache().GetOrCreateTransformedSchema(aiFunction) : + aiFunction.JsonSchema; + + // Roundtrip the schema through the ToolJson model type to remove extra properties + // and force missing ones into existence, then return the serialized UTF8 bytes as BinaryData. + var tool = JsonSerializer.Deserialize(jsonSchema, AgentsClientJsonContext.Default.ToolJson)!; + return BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(tool, AgentsClientJsonContext.Default.ToolJson)); + } + + /// + /// Gets the JSON schema transformer cache conforming to OpenAI strict / structured output restrictions per + /// https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#supported-schemas. + /// + private static AIJsonSchemaTransformCache GetStrictSchemaTransformCache() => new(new() + { + DisallowAdditionalProperties = true, + ConvertBooleanSchemas = true, + MoveDefaultKeywordToDescription = true, + RequireAllProperties = true, + TransformSchemaNode = (ctx, node) => + { + // Move content from common but unsupported properties to description. In particular, we focus on properties that + // the AIJsonUtilities schema generator might produce and/or that are explicitly mentioned in the OpenAI documentation. + + if (node is JsonObject schemaObj) + { + StringBuilder? additionalDescription = null; + + ReadOnlySpan unsupportedProperties = + [ + // Produced by AIJsonUtilities but not in allow list at https://platform.openai.com/docs/guides/structured-outputs#supported-properties: + "contentEncoding", "contentMediaType", "not", + + // Explicitly mentioned at https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#key-ordering as being unsupported with some models: + "minLength", "maxLength", "pattern", "format", + "minimum", "maximum", "multipleOf", + "patternProperties", + "minItems", "maxItems", + + // Explicitly mentioned at https://learn.microsoft.com/azure/ai-services/openai/how-to/structured-outputs?pivots=programming-language-csharp&tabs=python-secure%2Cdotnet-entra-id#unsupported-type-specific-keywords + // as being unsupported with Azure OpenAI: + "unevaluatedProperties", "propertyNames", "minProperties", "maxProperties", + "unevaluatedItems", "contains", "minContains", "maxContains", "uniqueItems", + ]; + + foreach (string propName in unsupportedProperties) + { + if (schemaObj[propName] is { } propNode) + { + _ = schemaObj.Remove(propName); + AppendLine(ref additionalDescription, propName, propNode); + } + } + + if (additionalDescription is not null) + { + schemaObj["description"] = schemaObj["description"] is { } descriptionNode && descriptionNode.GetValueKind() == JsonValueKind.String ? + $"{descriptionNode.GetValue()}{Environment.NewLine}{additionalDescription}" : + additionalDescription.ToString(); + } + + return node; + + static void AppendLine(ref StringBuilder? sb, string propName, JsonNode propNode) + { + sb ??= new(); + + if (sb.Length > 0) + { + _ = sb.AppendLine(); + } + + _ = sb.Append(propName).Append(": ").Append(propNode); + } + } + + return node; + }, + }); + + private static ResponseTool ToResponseTool(AITool tool, ChatOptions options) + { + switch (tool) + { + case AIFunctionDeclaration aiFunction: + return ToResponseFunctionTool(aiFunction, options); + + case HostedWebSearchTool webSearchTool: + WebSearchToolLocation? location = null; + if (webSearchTool.AdditionalProperties.TryGetValue(nameof(WebSearchToolLocation), out object? objLocation)) + { + location = objLocation as WebSearchToolLocation; + } + + WebSearchToolContextSize? size = null; + if (webSearchTool.AdditionalProperties.TryGetValue(nameof(WebSearchToolContextSize), out object? objSize) && + objSize is WebSearchToolContextSize) + { + size = (WebSearchToolContextSize)objSize; + } + + return ResponseTool.CreateWebSearchTool(location, size); + + case HostedFileSearchTool fileSearchTool: + return ResponseTool.CreateFileSearchTool( + fileSearchTool.Inputs?.OfType().Select(c => c.VectorStoreId) ?? [], + fileSearchTool.MaximumResultCount); + + case HostedCodeInterpreterTool codeTool: + return ResponseTool.CreateCodeInterpreterTool( + new CodeInterpreterToolContainer(codeTool.Inputs?.OfType().Select(f => f.FileId).ToList() is { Count: > 0 } ids ? + CodeInterpreterToolContainerConfiguration.CreateAutomaticContainerConfiguration(ids) : + new())); + + case HostedMcpServerTool mcpTool: + McpTool responsesMcpTool = Uri.TryCreate(mcpTool.ServerAddress, UriKind.Absolute, out Uri? url) ? + ResponseTool.CreateMcpTool( + mcpTool.ServerName, + url, + mcpTool.AuthorizationToken, + mcpTool.ServerDescription) : + ResponseTool.CreateMcpTool( + mcpTool.ServerName, + new McpToolConnectorId(mcpTool.ServerAddress), + mcpTool.AuthorizationToken, + mcpTool.ServerDescription); + + if (mcpTool.AllowedTools is not null) + { + responsesMcpTool.AllowedTools = new(); + AddAllMcpFilters(mcpTool.AllowedTools, responsesMcpTool.AllowedTools); + } + + switch (mcpTool.ApprovalMode) + { + case HostedMcpServerToolAlwaysRequireApprovalMode: + responsesMcpTool.ToolCallApprovalPolicy = new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.AlwaysRequireApproval); + break; + + case HostedMcpServerToolNeverRequireApprovalMode: + responsesMcpTool.ToolCallApprovalPolicy = new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.NeverRequireApproval); + break; + + case HostedMcpServerToolRequireSpecificApprovalMode specificMode: + responsesMcpTool.ToolCallApprovalPolicy = new McpToolCallApprovalPolicy(new CustomMcpToolCallApprovalPolicy()); + + if (specificMode.AlwaysRequireApprovalToolNames is { Count: > 0 } alwaysRequireToolNames) + { + responsesMcpTool.ToolCallApprovalPolicy.CustomPolicy.ToolsAlwaysRequiringApproval = new(); + AddAllMcpFilters(alwaysRequireToolNames, responsesMcpTool.ToolCallApprovalPolicy.CustomPolicy.ToolsAlwaysRequiringApproval); + } + + if (specificMode.NeverRequireApprovalToolNames is { Count: > 0 } neverRequireToolNames) + { + responsesMcpTool.ToolCallApprovalPolicy.CustomPolicy.ToolsNeverRequiringApproval = new(); + AddAllMcpFilters(neverRequireToolNames, responsesMcpTool.ToolCallApprovalPolicy.CustomPolicy.ToolsNeverRequiringApproval); + } + + break; + } + + return responsesMcpTool; + + default: + throw new NotSupportedException($"Tool of type '{tool.GetType().FullName}' is not supported."); + } + } + + private static void AddAllMcpFilters(IList toolNames, McpToolFilter filter) + { + foreach (var toolName in toolNames) + { + filter.ToolNames.Add(toolName); + } + } + + /// Used to create the JSON payload for an OpenAI tool description. + internal sealed class ToolJson + { + [JsonPropertyName("type")] + public string Type { get; set; } = "object"; + + [JsonPropertyName("required")] + public HashSet Required { get; set; } = []; + + [JsonPropertyName("properties")] + public Dictionary Properties { get; set; } = []; + + [JsonPropertyName("additionalProperties")] + public bool AdditionalProperties { get; set; } + } + + #endregion +} diff --git a/dotnet/src/Microsoft.Agents.AI.AzureAIAgents/AgentsClientJsonContext.cs b/dotnet/src/Microsoft.Agents.AI.AzureAIAgents/AgentsClientJsonContext.cs new file mode 100644 index 0000000000..d54bd5ff8a --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.AzureAIAgents/AgentsClientJsonContext.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json; +using System.Text.Json.Serialization; + +#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#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 Azure.AI.Agents; + +/// Source-generated JSON type information for use by all OpenAI implementations. +[JsonSourceGenerationOptions(JsonSerializerDefaults.Web, + UseStringEnumConverter = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + WriteIndented = true)] +[JsonSerializable(typeof(AgentsClientExtensions.ToolJson))] +internal sealed partial class AgentsClientJsonContext : JsonSerializerContext; diff --git a/dotnet/src/Microsoft.Agents.AI.AzureAIAgents/AzureAIAgentChatClient.cs b/dotnet/src/Microsoft.Agents.AI.AzureAIAgents/AzureAIAgentChatClient.cs new file mode 100644 index 0000000000..9177a63e06 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.AzureAIAgents/AzureAIAgentChatClient.cs @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Runtime.CompilerServices; +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.AzureAIAgents; + +/// +/// 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 AgentsClient _agentsClient; + private readonly AgentVersion _agentVersion; + + /// + /// 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 AI model to use for the chat client. + /// An optional for configuring the underlying OpenAI client. + /// + /// The provided should be decorated with a for proper functionality. + /// + internal AzureAIAgentChatClient(AgentsClient agentsClient, AgentRecord agentRecord, string model, OpenAIClientOptions? openAIClientOptions = null) + : this(agentsClient, Throw.IfNull(agentRecord).Versions.Latest, model, openAIClientOptions) + { + } + + internal AzureAIAgentChatClient(AgentsClient agentsClient, AgentVersion agentVersion, string model, OpenAIClientOptions? openAIClientOptions = null) + : base(agentsClient.GetOpenAIClient(openAIClientOptions).GetOpenAIResponseClient(model).AsIChatClient()) + { + this._agentsClient = Throw.IfNull(agentsClient); + this._agentVersion = Throw.IfNull(agentVersion); + this._metadata = new ChatClientMetadata("azure.ai.agents"); + } + + /// + public override object? GetService(Type serviceType, object? serviceKey = null) + { + return (serviceKey is null && serviceType == typeof(ChatClientMetadata)) + ? this._metadata + : (serviceKey is null && serviceType == typeof(AgentsClient)) + ? this._agentsClient + : (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 conversation = await this.GetOrCreateConversationAsync(messages, options, cancellationToken).ConfigureAwait(false); + var conversationOptions = this.GetConversationEnabledChatOptions(options, conversation); + + return await base.GetResponseAsync(messages, conversationOptions, cancellationToken).ConfigureAwait(false); + } + + /// + public async override IAsyncEnumerable GetStreamingResponseAsync(IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var conversation = await this.GetOrCreateConversationAsync(messages, options, cancellationToken).ConfigureAwait(false); + var conversationOptions = this.GetConversationEnabledChatOptions(options, conversation); + + await foreach (var chunk in base.GetStreamingResponseAsync(messages, conversationOptions, cancellationToken).ConfigureAwait(false)) + { + yield return chunk; + } + } + + private async Task GetOrCreateConversationAsync(IEnumerable messages, ChatOptions? options, CancellationToken cancellationToken) + => string.IsNullOrWhiteSpace(options?.ConversationId) + ? await this._agentsClient.GetConversationClient().CreateConversationAsync(cancellationToken: cancellationToken).ConfigureAwait(false) + : await this._agentsClient.GetConversationClient().GetConversationAsync(options.ConversationId, cancellationToken: cancellationToken).ConfigureAwait(false); + + private ChatOptions GetConversationEnabledChatOptions(ChatOptions? chatOptions, AgentConversation agentConversation) + { + var conversationChatOptions = chatOptions is null ? new ChatOptions() : chatOptions.Clone(); + + var originalFactory = conversationChatOptions.RawRepresentationFactory; + conversationChatOptions.RawRepresentationFactory = (client) => + { + if (originalFactory?.Invoke(this) is not ResponseCreationOptions responseCreationOptions) + { + responseCreationOptions = new ResponseCreationOptions(); + } + + responseCreationOptions.SetAgentReference(this._agentVersion.Name); + responseCreationOptions.SetConversationReference(agentConversation); + + return responseCreationOptions; + }; + + // Clear out the conversation ID to prevent the inner client from attempting to use it as a PreviousResponseId + conversationChatOptions.ConversationId = null; + // Clear out any instructions to avoid conflicts with the agent's instructions + conversationChatOptions.Instructions = null; + + return conversationChatOptions; + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.AzureAIAgents/Microsoft.Agents.AI.AzureAIAgents.csproj b/dotnet/src/Microsoft.Agents.AI.AzureAIAgents/Microsoft.Agents.AI.AzureAIAgents.csproj new file mode 100644 index 0000000000..6f662416e2 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.AzureAIAgents/Microsoft.Agents.AI.AzureAIAgents.csproj @@ -0,0 +1,29 @@ + + + + $(ProjectsTargetFrameworks) + $(ProjectsDebugTargetFrameworks) + alpha + enable + true + + + + + + + + + + + + + + + + + Microsoft Agent Framework Azure AI Agents + Provides Microsoft Agent Framework support for Azure AI Agents. + + + diff --git a/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseFixture.cs b/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseFixture.cs index d223a65e28..fbb087a153 100644 --- a/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseFixture.cs +++ b/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseFixture.cs @@ -68,7 +68,7 @@ public async Task CreateChatClientAgentAsync( string name = "HelpfulAssistant", string instructions = "You are a helpful assistant.", IList? aiTools = null) => - new ChatClientAgent( + new( this._openAIResponseClient.AsIChatClient(), options: new() {