.NET: Azure.AI.Agents Package Split + Initial Extensions (#1657)

* Move packages

* Update nuget.config

* Address Xmldoc

* Remove format from branches checks

* Address Xmldocs

* Add more details to the implementation

* Moving Agent logic to ChatClient

* Adding Name and Id overrides to AzureAIAgent

* Updating extensions

* Add GetAiAgent extensions

* Adding support for version as name can conflict 409 using the Agents API with same name

* Addressing more updates to the extensions

* More improvements

* Remove debugging code from sample

* Address copilot feedback

* Apply suggestions from co-pilot code review
This commit is contained in:
Roger Barreto
2025-10-29 17:24:19 +00:00
Unverified
parent 904e17473f
commit 7e23140ca9
14 changed files with 1130 additions and 675 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ name: dotnet-format
on:
workflow_dispatch:
pull_request:
branches: ["main", "feature*"]
branches: ["main"]
paths:
- dotnet/**
- '.github/workflows/dotnet-format.yml'
+2 -2
View File
@@ -17,8 +17,8 @@
<PackageVersion Include="Aspire.Microsoft.Azure.Cosmos" Version="$(AspireAppHostSdkVersion)" />
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="9.8.0" />
<!-- Azure.* -->
<PackageVersion Include="Azure.AI.Agents" Version="2.0.0-alpha.20251016.2" />
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.7" />
<PackageVersion Include="Azure.AI.Agents" Version="2.0.0-alpha.20251024.3" />
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.6" />
<PackageVersion Include="Azure.AI.OpenAI" Version="2.5.0-beta.1" />
<PackageVersion Include="Azure.Identity" Version="1.17.0" />
<PackageVersion Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.4.0" />
+1
View File
@@ -32,6 +32,7 @@
<Folder Name="/Samples/GettingStarted/AgentProviders/">
<File Path="samples/GettingStarted/AgentProviders/README.md" />
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_A2A/Agent_With_A2A.csproj" />
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_AzureAIAgent/Agent_With_AzureAIAgent.csproj" />
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_AzureFoundryAgent/Agent_With_AzureFoundryAgent.csproj" />
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_AzureFoundryModel/Agent_With_AzureFoundryModel.csproj" />
<Project Path="samples/GettingStarted/AgentProviders/Agent_With_AzureOpenAIChatCompletion/Agent_With_AzureOpenAIChatCompletion.csproj" />
+3 -3
View File
@@ -3,14 +3,14 @@
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="local" value="C:\localpackages" />
<add key="azure-sdk-for-net-pr" value="https://pkgs.dev.azure.com/azure-sdk/internal/_packaging/azure-sdk-for-net-pr/nuget/v3/index.json" />
</packageSources>
<packageSourceMapping>
<packageSource key="nuget.org">
<package pattern="*" />
</packageSource>
<packageSource key="local">
<package pattern="*" />
<packageSource key="azure-sdk-for-net-pr">
<package pattern="Azure.AI.Agents" />
</packageSource>
</packageSourceMapping>
</configuration>
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);IDE0059</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.Agents" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.AzureAIAgents\Microsoft.Agents.AI.AzureAIAgents.csproj" />
</ItemGroup>
</Project>
@@ -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);
@@ -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
```
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(ProjectsTargetFrameworks)</TargetFrameworks>
@@ -11,9 +11,7 @@
<ItemGroup>
<PackageReference Include="Azure.AI.Agents.Persistent" />
<PackageReference Include="Azure.AI.Agents" />
<PackageReference Include="Microsoft.Extensions.AI" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
</ItemGroup>
<ItemGroup>
@@ -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;
/// <summary>
/// Provides extension methods for <see cref="PersistentAgentsClient"/>.
/// </summary>
public static class AgentsClientExtensions
{
/*
/// <summary>
/// Gets a runnable agent instance from the provided response containing persistent agent metadata.
/// </summary>
/// <param name="client">The client used to interact with persistent agents. Cannot be <see langword="null"/>.</param>
/// <param name="persistentAgentResponse">The response containing the persistent agent to be converted. Cannot be <see langword="null"/>.</param>
/// <param name="chatOptions">The default <see cref="ChatOptions"/> to use when interacting with the agent.</param>
/// <param name="clientFactory">Provides a way to customize the creation of the underlying <see cref="IChatClient"/> used by the agent.</param>
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations on the persistent agent.</returns>
public static ChatClientAgent GetAIAgent(this AgentsClient client, Response<PersistentAgent> persistentAgentResponse, ChatOptions? chatOptions = null, Func<IChatClient, IChatClient>? clientFactory = null)
{
if (persistentAgentResponse is null)
{
throw new ArgumentNullException(nameof(persistentAgentResponse));
}
return GetAIAgent(client, persistentAgentResponse.Value, chatOptions, clientFactory);
}
/// <summary>
/// Gets a runnable agent instance from a <see cref="PersistentAgent"/> containing metadata about a persistent agent.
/// </summary>
/// <param name="persistentAgentsClient">The client used to interact with persistent agents. Cannot be <see langword="null"/>.</param>
/// <param name="persistentAgentMetadata">The persistent agent metadata to be converted. Cannot be <see langword="null"/>.</param>
/// <param name="chatOptions">The default <see cref="ChatOptions"/> to use when interacting with the agent.</param>
/// <param name="clientFactory">Provides a way to customize the creation of the underlying <see cref="IChatClient"/> used by the agent.</param>
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations on the persistent agent.</returns>
public static ChatClientAgent GetAIAgent(this PersistentAgentsClient persistentAgentsClient, PersistentAgent persistentAgentMetadata, ChatOptions? chatOptions = null, Func<IChatClient, IChatClient>? 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
});
}
/// <summary>
/// Retrieves an existing server side agent, wrapped as a <see cref="ChatClientAgent"/> using the provided <see cref="PersistentAgentsClient"/>.
/// </summary>
/// <param name="persistentAgentsClient">The <see cref="PersistentAgentsClient"/> to create the <see cref="ChatClientAgent"/> with.</param>
/// <returns>A <see cref="ChatClientAgent"/> for the persistent agent.</returns>
/// <param name="agentId"> The ID of the server side agent to create a <see cref="ChatClientAgent"/> for.</param>
/// <param name="chatOptions">Options that should apply to all runs of the agent.</param>
/// <param name="clientFactory">Provides a way to customize the creation of the underlying <see cref="IChatClient"/> used by the agent.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations on the persistent agent.</returns>
public static ChatClientAgent GetAIAgent(
this PersistentAgentsClient persistentAgentsClient,
string agentId,
ChatOptions? chatOptions = null,
Func<IChatClient, IChatClient>? 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);
}
/// <summary>
/// Retrieves an existing server side agent, wrapped as a <see cref="ChatClientAgent"/> using the provided <see cref="PersistentAgentsClient"/>.
/// </summary>
/// <param name="persistentAgentsClient">The <see cref="PersistentAgentsClient"/> to create the <see cref="ChatClientAgent"/> with.</param>
/// <returns>A <see cref="ChatClientAgent"/> for the persistent agent.</returns>
/// <param name="agentId"> The ID of the server side agent to create a <see cref="ChatClientAgent"/> for.</param>
/// <param name="chatOptions">Options that should apply to all runs of the agent.</param>
/// <param name="clientFactory">Provides a way to customize the creation of the underlying <see cref="IChatClient"/> used by the agent.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations on the persistent agent.</returns>
public static async Task<ChatClientAgent> GetAIAgentAsync(
this PersistentAgentsClient persistentAgentsClient,
string agentId,
ChatOptions? chatOptions = null,
Func<IChatClient, IChatClient>? 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);
}
/// <summary>
/// Gets a runnable agent instance from the provided response containing persistent agent metadata.
/// </summary>
/// <param name="persistentAgentsClient">The client used to interact with persistent agents. Cannot be <see langword="null"/>.</param>
/// <param name="persistentAgentResponse">The response containing the persistent agent to be converted. Cannot be <see langword="null"/>.</param>
/// <param name="options">Full set of options to configure the agent.</param>
/// <param name="clientFactory">Provides a way to customize the creation of the underlying <see cref="IChatClient"/> used by the agent.</param>
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations on the persistent agent.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="persistentAgentResponse"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
public static ChatClientAgent GetAIAgent(this PersistentAgentsClient persistentAgentsClient, Response<PersistentAgent> persistentAgentResponse, ChatClientAgentOptions options, Func<IChatClient, IChatClient>? clientFactory = null)
{
if (persistentAgentResponse is null)
{
throw new ArgumentNullException(nameof(persistentAgentResponse));
}
return GetAIAgent(persistentAgentsClient, persistentAgentResponse.Value, options, clientFactory);
}
/// <summary>
/// Gets a runnable agent instance from a <see cref="PersistentAgent"/> containing metadata about a persistent agent.
/// </summary>
/// <param name="persistentAgentsClient">The client used to interact with persistent agents. Cannot be <see langword="null"/>.</param>
/// <param name="persistentAgentMetadata">The persistent agent metadata to be converted. Cannot be <see langword="null"/>.</param>
/// <param name="options">Full set of options to configure the agent.</param>
/// <param name="clientFactory">Provides a way to customize the creation of the underlying <see cref="IChatClient"/> used by the agent.</param>
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations on the persistent agent.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="persistentAgentMetadata"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
public static ChatClientAgent GetAIAgent(this PersistentAgentsClient persistentAgentsClient, PersistentAgent persistentAgentMetadata, ChatClientAgentOptions options, Func<IChatClient, IChatClient>? 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);
}
/// <summary>
/// Retrieves an existing server side agent, wrapped as a <see cref="ChatClientAgent"/> using the provided <see cref="PersistentAgentsClient"/>.
/// </summary>
/// <param name="persistentAgentsClient">The <see cref="PersistentAgentsClient"/> to create the <see cref="ChatClientAgent"/> with.</param>
/// <param name="agentId">The ID of the server side agent to create a <see cref="ChatClientAgent"/> for.</param>
/// <param name="options">Full set of options to configure the agent.</param>
/// <param name="clientFactory">Provides a way to customize the creation of the underlying <see cref="IChatClient"/> used by the agent.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations on the persistent agent.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="persistentAgentsClient"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="agentId"/> is empty or whitespace.</exception>
public static ChatClientAgent GetAIAgent(
this PersistentAgentsClient persistentAgentsClient,
string agentId,
ChatClientAgentOptions options,
Func<IChatClient, IChatClient>? 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);
}
/// <summary>
/// Retrieves an existing server side agent, wrapped as a <see cref="ChatClientAgent"/> using the provided <see cref="PersistentAgentsClient"/>.
/// </summary>
/// <param name="persistentAgentsClient">The <see cref="PersistentAgentsClient"/> to create the <see cref="ChatClientAgent"/> with.</param>
/// <param name="agentId">The ID of the server side agent to create a <see cref="ChatClientAgent"/> for.</param>
/// <param name="options">Full set of options to configure the agent.</param>
/// <param name="clientFactory">Provides a way to customize the creation of the underlying <see cref="IChatClient"/> used by the agent.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations on the persistent agent.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="persistentAgentsClient"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="agentId"/> is empty or whitespace.</exception>
public static async Task<ChatClientAgent> GetAIAgentAsync(
this PersistentAgentsClient persistentAgentsClient,
string agentId,
ChatClientAgentOptions options,
Func<IChatClient, IChatClient>? 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);
}*/
/// <summary>
/// Creates a new server side agent using the provided <see cref="PersistentAgentsClient"/>.
/// </summary>
/// <param name="client">The <see cref="AgentsClient"/> to create the agent with.</param>
/// <param name="model">The model to be used by the agent.</param>
/// <param name="name">The name of the agent.</param>
/// <param name="instructions">The instructions for the agent.</param>
/// <param name="tools">The tools to be used by the agent.</param>
/// <param name="temperature">The temperature setting for the agent.</param>
/// <param name="topP">The top-p setting for the agent.</param>
/// <param name="raiConfig">The responsible AI config</param>
/// <param name="reasoningOptions">The reasoning options for the agent.</param>
/// <param name="textOptions">The text options for the agent.</param>
/// <param name="structuredInputs">The structured inputs for the agent.</param>
/// <param name="metadata">The metadata for the agent.</param>
/// <param name="clientFactory">Provides a way to customize the creation of the underlying <see cref="IChatClient"/> used by the agent.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations on the newly created agent.</returns>
public static async Task<ChatClientAgent> CreateAIAgentAsync(
this AgentsClient client,
string model,
string? name = null,
string? instructions = null,
IEnumerable<ResponseTool>? tools = null,
float? temperature = null,
float? topP = null,
RaiConfig? raiConfig = null,
ResponseReasoningOptions? reasoningOptions = null,
ResponseTextOptions? textOptions = null,
IDictionary<string, StructuredInputDefinition>? structuredInputs = null,
IReadOnlyDictionary<string, string>? metadata = null,
Func<IChatClient, IChatClient>? 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<ResponseTool> 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<ChatMessage> messages, AgentThread? thread, AgentRunOptions? options, Func<IEnumerable<ChatMessage>, 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;
}
/// <summary>
/// Creates a new server side agent using the provided <see cref="PersistentAgentsClient"/>.
/// </summary>
/// <param name="persistentAgentsClient">The <see cref="PersistentAgentsClient"/> to create the agent with.</param>
/// <param name="model">The model to be used by the agent.</param>
/// <param name="name">The name of the agent.</param>
/// <param name="description">The description of the agent.</param>
/// <param name="instructions">The instructions for the agent.</param>
/// <param name="tools">The tools to be used by the agent.</param>
/// <param name="toolResources">The resources for the tools.</param>
/// <param name="temperature">The temperature setting for the agent.</param>
/// <param name="topP">The top-p setting for the agent.</param>
/// <param name="responseFormat">The response format for the agent.</param>
/// <param name="metadata">The metadata for the agent.</param>
/// <param name="clientFactory">Provides a way to customize the creation of the underlying <see cref="IChatClient"/> used by the agent.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations on the newly created agent.</returns>
public static ChatClientAgent CreateAIAgent(
this PersistentAgentsClient persistentAgentsClient,
string model,
string? name = null,
string? description = null,
string? instructions = null,
IEnumerable<ToolDefinition>? tools = null,
ToolResources? toolResources = null,
float? temperature = null,
float? topP = null,
BinaryData? responseFormat = null,
IReadOnlyDictionary<string, string>? metadata = null,
Func<IChatClient, IChatClient>? 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);
}
/// <summary>
/// Creates a new server side agent using the provided <see cref="PersistentAgentsClient"/>.
/// </summary>
/// <param name="persistentAgentsClient">The <see cref="PersistentAgentsClient"/> to create the agent with.</param>
/// <param name="model">The model to be used by the agent.</param>
/// <param name="options">Full set of options to configure the agent.</param>
/// <param name="clientFactory">Provides a way to customize the creation of the underlying <see cref="IChatClient"/> used by the agent.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations on the newly created agent.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="persistentAgentsClient"/> or <paramref name="model"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="model"/> is empty or whitespace.</exception>
public static ChatClientAgent CreateAIAgent(
this PersistentAgentsClient persistentAgentsClient,
string model,
ChatClientAgentOptions options,
Func<IChatClient, IChatClient>? 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);
}
/// <summary>
/// Creates a new server side agent using the provided <see cref="PersistentAgentsClient"/>.
/// </summary>
/// <param name="persistentAgentsClient">The <see cref="PersistentAgentsClient"/> to create the agent with.</param>
/// <param name="model">The model to be used by the agent.</param>
/// <param name="options">Full set of options to configure the agent.</param>
/// <param name="clientFactory">Provides a way to customize the creation of the underlying <see cref="IChatClient"/> used by the agent.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations on the newly created agent.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="persistentAgentsClient"/> or <paramref name="model"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="model"/> is empty or whitespace.</exception>
public static async Task<ChatClientAgent> CreateAIAgentAsync(
this PersistentAgentsClient persistentAgentsClient,
string model,
ChatClientAgentOptions options,
Func<IChatClient, IChatClient>? 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<ToolDefinition>? ToolDefinitions, ToolResources? ToolResources, List<AITool>? FunctionToolsAndOtherTools) ConvertAIToolsToToolDefinitions(IList<AITool>? tools)
{
List<ToolDefinition>? toolDefinitions = null;
ToolResources? toolResources = null;
List<AITool>? 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);
}
}
@@ -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;
/// <summary>
/// Provides extension methods for <see cref="AgentsClient"/>.
/// </summary>
public static class AgentsClientExtensions
{
/// <summary>
/// Gets a runnable agent instance from the provided agent record.
/// </summary>
/// <param name="agentsClient">The client used to interact with Azure AI Agents. Cannot be <see langword="null"/>.</param>
/// <param name="model">The model to be used by the agent.</param>
/// <param name="agentRecord">The agent record to be converted. The latest version will be used. Cannot be <see langword="null"/>.</param>
/// <param name="chatOptions">The default <see cref="ChatOptions"/> to use when interacting with the agent.</param>
/// <param name="clientFactory">Provides a way to customize the creation of the underlying <see cref="IChatClient"/> used by the agent.</param>
/// <param name="openAIClientOptions">An optional <see cref="OpenAIClientOptions"/> for configuring the underlying OpenAI client.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations based on the latest version of the Azure AI Agent.</returns>
public static ChatClientAgent GetAIAgent(
this AgentsClient agentsClient,
string model,
AgentRecord agentRecord,
ChatOptions? chatOptions = null,
Func<IChatClient, IChatClient>? 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);
}
/// <summary>
/// Gets a runnable agent instance from the provided agent record.
/// </summary>
/// <param name="agentsClient">The client used to interact with Azure AI Agents. Cannot be <see langword="null"/>.</param>
/// <param name="model">The model to be used by the agent. Cannot be <see langword="null"/>.</param>
/// <param name="agentVersion">The agent version to be converted. Cannot be <see langword="null"/>.</param>
/// <param name="chatOptions">The default <see cref="ChatOptions"/> to use when interacting with the agent.</param>
/// <param name="clientFactory">Provides a way to customize the creation of the underlying <see cref="IChatClient"/> used by the agent.</param>
/// <param name="openAIClientOptions">An optional <see cref="OpenAIClientOptions"/> for configuring the underlying OpenAI client.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations based on the specified version of the Azure AI Agent.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="agentsClient"/>, <paramref name="model"/>, or <paramref name="agentVersion"/> is <see langword="null"/>.</exception>
public static ChatClientAgent GetAIAgent(
this AgentsClient agentsClient,
string model,
AgentVersion agentVersion,
ChatOptions? chatOptions = null,
Func<IChatClient, IChatClient>? 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);
}
/// <summary>
/// Retrieves an existing server side agent, wrapped as a <see cref="ChatClientAgent"/> using the provided <see cref="AgentsClient"/>.
/// </summary>
/// <param name="agentsClient">The <see cref="AgentsClient"/> to create the <see cref="ChatClientAgent"/> with. Cannot be <see langword="null"/>.</param>
/// <param name="model">The model to be used by the agent. Cannot be <see langword="null"/> or whitespace.</param>
/// <param name="name">The name of the server side agent to create a <see cref="ChatClientAgent"/> for. Cannot be <see langword="null"/> or whitespace.</param>
/// <param name="chatOptions">The default <see cref="ChatOptions"/> to use when interacting with the agent.</param>
/// <param name="clientFactory">Provides a way to customize the creation of the underlying <see cref="IChatClient"/> used by the agent.</param>
/// <param name="openAIClientOptions">An optional <see cref="OpenAIClientOptions"/> for configuring the underlying OpenAI client.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations based on the latest version of the named Azure AI Agent.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="agentsClient"/>, <paramref name="model"/>, or <paramref name="name"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="model"/> or <paramref name="name"/> is empty or whitespace, or when the agent with the specified name was not found.</exception>
/// <exception cref="InvalidOperationException">The agent with the specified name was not found.</exception>
public static ChatClientAgent GetAIAgent(
this AgentsClient agentsClient,
string model,
string name,
ChatOptions? chatOptions = null,
Func<IChatClient, IChatClient>? 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);
}
/// <summary>
/// Asynchronously retrieves an existing server side agent, wrapped as a <see cref="ChatClientAgent"/> using the provided <see cref="AgentsClient"/>.
/// </summary>
/// <param name="agentsClient">The <see cref="AgentsClient"/> to create the <see cref="ChatClientAgent"/> with. Cannot be <see langword="null"/>.</param>
/// <param name="model">The model to be used by the agent. Cannot be <see langword="null"/> or whitespace.</param>
/// <param name="name">The name of the server side agent to create a <see cref="ChatClientAgent"/> for. Cannot be <see langword="null"/> or whitespace.</param>
/// <param name="chatOptions">The default <see cref="ChatOptions"/> to use when interacting with the agent.</param>
/// <param name="clientFactory">Provides a way to customize the creation of the underlying <see cref="IChatClient"/> used by the agent.</param>
/// <param name="openAIClientOptions">An optional <see cref="OpenAIClientOptions"/> for configuring the underlying OpenAI client.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations based on the latest version of the named Azure AI Agent.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="agentsClient"/>, <paramref name="model"/>, or <paramref name="name"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="model"/> or <paramref name="name"/> is empty or whitespace, or when the agent with the specified name was not found.</exception>
/// <exception cref="InvalidOperationException">The agent with the specified name was not found.</exception>
public static async Task<ChatClientAgent> GetAIAgentAsync(
this AgentsClient agentsClient,
string model,
string name,
ChatOptions? chatOptions = null,
Func<IChatClient, IChatClient>? 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);
}
/// <summary>
/// Gets a runnable agent instance from a <see cref="AgentVersion"/> containing metadata about an Azure AI Agent.
/// </summary>
/// <param name="agentsClient">The client used to interact with Azure AI Agents. Cannot be <see langword="null"/>.</param>
/// <param name="model">The model to be used by the agent.</param>
/// <param name="agentRecord">The agent record to be converted. The latest version will be used. Cannot be <see langword="null"/>.</param>
/// <param name="options">Full set of options to configure the agent.</param>
/// <param name="clientFactory">Provides a way to customize the creation of the underlying <see cref="IChatClient"/> used by the agent.</param>
/// <param name="openAIClientOptions">An optional <see cref="OpenAIClientOptions"/> for configuring the underlying OpenAI client.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations based on the latest version of the Azure AI Agent record.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="agentsClient"/>, <paramref name="model"/>, <paramref name="agentRecord"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
public static ChatClientAgent GetAIAgent(
this AgentsClient agentsClient,
string model,
AgentRecord agentRecord,
ChatClientAgentOptions? options = null,
Func<IChatClient, IChatClient>? 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);
}
/// <summary>
/// Gets a runnable agent instance from a <see cref="AgentVersion"/> containing metadata about an Azure AI Agent.
/// </summary>
/// <param name="agentsClient">The client used to interact with Azure AI Agents. Cannot be <see langword="null"/>.</param>
/// <param name="model">The model to be used by the agent.</param>
/// <param name="agentVersion">The agent version to be converted. Cannot be <see langword="null"/>.</param>
/// <param name="options">Full set of options to configure the agent.</param>
/// <param name="clientFactory">Provides a way to customize the creation of the underlying <see cref="IChatClient"/> used by the agent.</param>
/// <param name="openAIClientOptions">An optional <see cref="OpenAIClientOptions"/> for configuring the underlying OpenAI client.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations based on the provided version of the Azure AI Agent.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="agentsClient"/>, <paramref name="model"/>, <paramref name="agentVersion"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
public static ChatClientAgent GetAIAgent(
this AgentsClient agentsClient,
string model,
AgentVersion agentVersion,
ChatClientAgentOptions? options = null,
Func<IChatClient, IChatClient>? 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);
}
/// <summary>
/// Retrieves an existing server side agent, wrapped as a <see cref="ChatClientAgent"/> using the provided <see cref="AgentsClient"/>.
/// </summary>
/// <param name="agentsClient">The <see cref="AgentsClient"/> to create the <see cref="ChatClientAgent"/> with.</param>
/// <param name="model">The model to be used by the agent.</param>
/// <param name="name">The ID of the server side agent to create a <see cref="ChatClientAgent"/> for.</param>
/// <param name="options">Full set of options to configure the agent.</param>
/// <param name="clientFactory">Provides a way to customize the creation of the underlying <see cref="IChatClient"/> used by the agent.</param>
/// <param name="openAIClientOptions">An optional <see cref="OpenAIClientOptions"/> for configuring the underlying OpenAI client.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations based on the latest version of named the Azure AI Agent.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="agentsClient"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is empty or whitespace.</exception>
public static async Task<ChatClientAgent> GetAIAgentAsync(
this AgentsClient agentsClient,
string model,
string name,
ChatClientAgentOptions options,
Func<IChatClient, IChatClient>? 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);
}
/// <summary>
/// Creates a new server side agent using the provided <see cref="AgentsClient"/>.
/// </summary>
/// <param name="agentsClient">The <see cref="AgentsClient"/> to create the agent with.</param>
/// <param name="model">The model to be used by the agent.</param>
/// <param name="name">The name of the agent.</param>
/// <param name="instructions">The instructions for the agent.</param>
/// <param name="description">The description for the agent.</param>
/// <param name="tools">The tools to be used by the agent.</param>
/// <param name="temperature">The temperature setting for the agent.</param>
/// <param name="topP">The top-p setting for the agent.</param>
/// <param name="raiConfig">The responsible AI configuration for the agent.</param>
/// <param name="reasoningOptions">The reasoning options for the agent.</param>
/// <param name="textOptions">The text options for the agent.</param>
/// <param name="structuredInputs">The structured inputs for the agent.</param>
/// <param name="metadata">The metadata for the agent.</param>
/// <param name="clientFactory">A factory function to customize the creation of the chat client used by the agent.</param>
/// <param name="openAIClientOptions">An optional <see cref="OpenAIClientOptions"/> for configuring the underlying OpenAI client.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations on the newly created agent.</returns>
public static ChatClientAgent CreateAIAgent(
this AgentsClient agentsClient,
string model,
string name,
string? instructions = null,
string? description = null,
IList<ResponseTool>? tools = null,
float? temperature = null,
float? topP = null,
RaiConfig? raiConfig = null,
ResponseReasoningOptions? reasoningOptions = null,
ResponseTextOptions? textOptions = null,
IDictionary<string, StructuredInputDefinition>? structuredInputs = null,
IReadOnlyDictionary<string, string>? metadata = null,
Func<IChatClient, IChatClient>? 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);
}
/// <summary>
/// Creates a new AI agent using the specified agent definition and optional configuration parameters.
/// </summary>
/// <param name="agentsClient">The client used to manage and interact with AI agents. Cannot be <see langword="null"/>.</param>
/// <param name="name">The name for the agent.</param>
/// <param name="agentDefinition">The definition that specifies the configuration and behavior of the agent to create. Cannot be <see langword="null"/>.</param>
/// <param name="model">The name of the model to use for the agent. Model must be provided either directly or as part of a <see cref="PromptAgentDefinition.Model"/> specialization property.</param>
/// <param name="creationOptions">Settings that control the creation of the agent.</param>
/// <param name="clientFactory">A factory function to customize the creation of the chat client used by the agent.</param>
/// <param name="openAIClientOptions">An optional <see cref="OpenAIClientOptions"/> for configuring the underlying OpenAI client.</param>
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations on the newly created agent.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="agentsClient"/> or <paramref name="agentDefinition"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">Thrown if neither the 'model' parameter nor a model in the agent definition is provided.</exception>
public static ChatClientAgent CreateAIAgent(
this AgentsClient agentsClient,
string name,
AgentDefinition agentDefinition,
string? model = null,
AgentCreationOptions? creationOptions = null,
Func<IChatClient, IChatClient>? 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);
}
/// <summary>
/// Creates a new Prompt AI Agent using the provided <see cref="AgentsClient"/> and options.
/// </summary>
/// <param name="agentsClient">The client used to manage and interact with AI agents. Cannot be <see langword="null"/>.</param>
/// <param name="model">The name of the model to use for the agent. Cannot be <see langword="null"/> or whitespace.</param>
/// <param name="options">The options for creating the agent. Cannot be <see langword="null"/>.</param>
/// <param name="clientFactory">A factory function to customize the creation of the chat client used by the agent.</param>
/// <param name="openAIClientOptions">An optional <see cref="OpenAIClientOptions"/> for configuring the underlying OpenAI client.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to cancel the operation if needed.</param>
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations on the newly created agent.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="agentsClient"/>, <paramref name="model"/>, or <paramref name="options"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="model"/> is empty or whitespace, or when the agent name is not provided in the options.</exception>
public static ChatClientAgent CreateAIAgent(
this AgentsClient agentsClient,
string model,
ChatClientAgentOptions options,
Func<IChatClient, IChatClient>? 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);
}
/// <summary>
/// Asynchronously creates a new AI agent using the specified agent definition and optional configuration
/// parameters.
/// </summary>
/// <param name="agentsClient">The client used to manage and interact with AI agents. Cannot be <see langword="null"/>.</param>
/// <param name="agentDefinition">The definition that specifies the configuration and behavior of the agent to create. Cannot be <see langword="null"/>.</param>
/// <param name="model">The name of the model to use for the agent. If not specified, the model must be provided as part of the agent definition.</param>
/// <param name="name">The name for the agent.</param>
/// <param name="agentVersionCreationOptions">Settings that control the creation of the agent.</param>
/// <param name="clientFactory">A factory function to customize the creation of the chat client used by the agent.</param>
/// <param name="openAIClientOptions">An optional <see cref="OpenAIClientOptions"/> for configuring the underlying OpenAI client.</param>
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations on the newly created agent.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="agentsClient"/> or <paramref name="agentDefinition"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">Thrown if neither the 'model' parameter nor a model in the agent definition is provided.</exception>
public static async Task<ChatClientAgent> CreateAIAgentAsync(
this AgentsClient agentsClient,
AgentDefinition agentDefinition,
string? model = null,
string? name = null,
AgentVersionCreationOptions? agentVersionCreationOptions = null,
Func<IChatClient, IChatClient>? 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<AITool>? 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
}
});
}
/// <summary>
/// Creates a new server side prompt agent using the provided <see cref="AgentsClient"/>.
/// </summary>
/// <param name="agentsClient">The <see cref="AgentsClient"/> to create the agent with.</param>
/// <param name="model">The model to be used by the agent.</param>
/// <param name="name">The name of the agent.</param>
/// <param name="instructions">The instructions for the agent.</param>
/// <param name="description">The description for the agent.</param>
/// <param name="tools">The tools to be used by the agent.</param>
/// <param name="temperature">The temperature setting for the agent.</param>
/// <param name="topP">The top-p setting for the agent.</param>
/// <param name="raiConfig">The responsible AI configuration for the agent.</param>
/// <param name="reasoningOptions">The reasoning options for the agent.</param>
/// <param name="textOptions">The text options for the agent.</param>
/// <param name="structuredInputs">The structured inputs for the agent.</param>
/// <param name="metadata">The metadata for the agent.</param>
/// <param name="clientFactory">A factory function to customize the creation of the underlying <see cref="IChatClient"/> used by the agent.</param>
/// <param name="openAIClientOptions">An optional <see cref="OpenAIClientOptions"/> for configuring the underlying OpenAI client.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
/// <returns>A <see cref="ChatClientAgent"/> instance that can be used to perform operations on the newly created agent.</returns>
public static async Task<ChatClientAgent> CreateAIAgentAsync(
this AgentsClient agentsClient,
string model,
string name,
string? instructions = null,
string? description = null,
IList<ResponseTool>? tools = null,
float? temperature = null,
float? topP = null,
RaiConfig? raiConfig = null,
ResponseReasoningOptions? reasoningOptions = null,
ResponseTextOptions? textOptions = null,
IDictionary<string, StructuredInputDefinition>? structuredInputs = null,
IReadOnlyDictionary<string, string>? metadata = null,
Func<IChatClient, IChatClient>? 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<ResponseTool>? tools = null,
IDictionary<string, StructuredInputDefinition>? structuredInputs = null,
IReadOnlyDictionary<string, string>? 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
/// <summary>Key into AdditionalProperties used to store a strict option.</summary>
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);
}
/// <summary>Gets whether the properties specify that strict schema handling is desired.</summary>
private static bool? HasStrict(IReadOnlyDictionary<string, object?>? additionalProperties) =>
additionalProperties?.TryGetValue(StrictKey, out object? strictObj) is true &&
strictObj is bool strictValue ?
strictValue : null;
/// <summary>Extracts from an <see cref="AIFunctionDeclaration"/> the parameters and strictness setting for use with OpenAI's APIs.</summary>
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));
}
/// <summary>
/// Gets the JSON schema transformer cache conforming to OpenAI <b>strict</b> / structured output restrictions per
/// https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#supported-schemas.
/// </summary>
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<string> 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<string>()}{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<HostedVectorStoreContent>().Select(c => c.VectorStoreId) ?? [],
fileSearchTool.MaximumResultCount);
case HostedCodeInterpreterTool codeTool:
return ResponseTool.CreateCodeInterpreterTool(
new CodeInterpreterToolContainer(codeTool.Inputs?.OfType<HostedFileContent>().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<string> toolNames, McpToolFilter filter)
{
foreach (var toolName in toolNames)
{
filter.ToolNames.Add(toolName);
}
}
/// <summary>Used to create the JSON payload for an OpenAI tool description.</summary>
internal sealed class ToolJson
{
[JsonPropertyName("type")]
public string Type { get; set; } = "object";
[JsonPropertyName("required")]
public HashSet<string> Required { get; set; } = [];
[JsonPropertyName("properties")]
public Dictionary<string, JsonElement> Properties { get; set; } = [];
[JsonPropertyName("additionalProperties")]
public bool AdditionalProperties { get; set; }
}
#endregion
}
@@ -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;
/// <summary>Source-generated JSON type information for use by all OpenAI implementations.</summary>
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web,
UseStringEnumConverter = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
WriteIndented = true)]
[JsonSerializable(typeof(AgentsClientExtensions.ToolJson))]
internal sealed partial class AgentsClientJsonContext : JsonSerializerContext;
@@ -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;
/// <summary>
/// Provides a chat client implementation that integrates with Azure AI Agents, enabling chat interactions using
/// Azure-specific agent capabilities.
/// </summary>
internal sealed class AzureAIAgentChatClient : DelegatingChatClient
{
private readonly ChatClientMetadata? _metadata;
private readonly AgentsClient _agentsClient;
private readonly AgentVersion _agentVersion;
/// <summary>
/// Initializes a new instance of the <see cref="AzureAIAgentChatClient"/> class.
/// </summary>
/// <param name="agentsClient">An instance of <see cref="AgentsClient"/> to interact with Azure AI Agents services.</param>
/// <param name="agentRecord">An instance of <see cref="AgentRecord"/> representing the specific agent to use.</param>
/// <param name="model">The AI model to use for the chat client.</param>
/// <param name="openAIClientOptions">An optional <see cref="OpenAIClientOptions"/> for configuring the underlying OpenAI client.</param>
/// <remarks>
/// The <see cref="IChatClient"/> provided should be decorated with a <see cref="AzureAIAgentChatClient"/> for proper functionality.
/// </remarks>
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");
}
/// <inheritdoc/>
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);
}
/// <inheritdoc/>
public override async Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> 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);
}
/// <inheritdoc/>
public async override IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(IEnumerable<ChatMessage> 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<AgentConversation> GetOrCreateConversationAsync(IEnumerable<ChatMessage> 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;
}
}
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(ProjectsTargetFrameworks)</TargetFrameworks>
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
<VersionSuffix>alpha</VersionSuffix>
<ImplicitUsings>enable</ImplicitUsings>
<InjectSharedThrow>true</InjectSharedThrow>
</PropertyGroup>
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
<ItemGroup>
<PackageReference Include="Azure.AI.Agents" />
<PackageReference Include="Microsoft.Extensions.AI" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
</ItemGroup>
<PropertyGroup>
<!-- NuGet Package Settings -->
<Title>Microsoft Agent Framework Azure AI Agents</Title>
<Description>Provides Microsoft Agent Framework support for Azure AI Agents.</Description>
</PropertyGroup>
</Project>
@@ -68,7 +68,7 @@ public class OpenAIResponseFixture(bool store) : IChatClientAgentFixture
string name = "HelpfulAssistant",
string instructions = "You are a helpful assistant.",
IList<AITool>? aiTools = null) =>
new ChatClientAgent(
new(
this._openAIResponseClient.AsIChatClient(),
options: new()
{