.NET: Add support for getting and creating Assistant and Foundry agents with ChatClientAgentOptions (#1304)

* Add support for getting and creating Assistant and Foundry agents with ChatClientAgentOptions

* Fix options cloning and agent creation

* Fix inconsistency

* Add support for mapping more tools and integration tests for ensuring CreateAIAgent works with those tools.

* Add support for additional openai tools with tests.

* Remove special casing for function tools, since it's either not supported yet, or requires a lot of code duplication.

* Removed unused using.

* Fix broken unit tests

* Change integration test to reduce flakiness.
This commit is contained in:
westey
2025-10-16 16:39:48 +01:00
committed by GitHub
Unverified
parent 3b9193c15e
commit 54c3eb726a
10 changed files with 1730 additions and 63 deletions
+2 -1
View File
@@ -1,4 +1,5 @@
{
"dotnet.defaultSolution": "agent-framework-dotnet.slnx",
"git.openRepositoryInParentFolders": "always"
"git.openRepositoryInParentFolders": "always",
"chat.agent.enabled": true
}
@@ -127,6 +127,147 @@ public static class PersistentAgentsClientExtensions
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>
@@ -234,4 +375,193 @@ public static class PersistentAgentsClientExtensions
// 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);
}
}
@@ -143,6 +143,155 @@ public static class OpenAIAssistantClientExtensions
return assistantClient.GetAIAgent(assistantResponse, chatOptions, clientFactory);
}
/// <summary>
/// Gets a <see cref="ChatClientAgent"/> from a <see cref="ClientResult{Assistant}"/>.
/// </summary>
/// <param name="assistantClient">The assistant client.</param>
/// <param name="assistantClientResult">The client result containing the assistant.</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 assistant.</returns>
/// <exception cref="ArgumentNullException"><paramref name="assistantClientResult"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
public static ChatClientAgent GetAIAgent(
this AssistantClient assistantClient,
ClientResult<Assistant> assistantClientResult,
ChatClientAgentOptions options,
Func<IChatClient, IChatClient>? clientFactory = null)
{
if (assistantClientResult is null)
{
throw new ArgumentNullException(nameof(assistantClientResult));
}
return assistantClient.GetAIAgent(assistantClientResult.Value, options, clientFactory);
}
/// <summary>
/// Gets a <see cref="ChatClientAgent"/> from an <see cref="Assistant"/>.
/// </summary>
/// <param name="assistantClient">The assistant client.</param>
/// <param name="assistantMetadata">The assistant metadata.</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 assistant.</returns>
/// <exception cref="ArgumentNullException"><paramref name="assistantMetadata"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
public static ChatClientAgent GetAIAgent(
this AssistantClient assistantClient,
Assistant assistantMetadata,
ChatClientAgentOptions options,
Func<IChatClient, IChatClient>? clientFactory = null)
{
if (assistantMetadata is null)
{
throw new ArgumentNullException(nameof(assistantMetadata));
}
if (assistantClient is null)
{
throw new ArgumentNullException(nameof(assistantClient));
}
if (options is null)
{
throw new ArgumentNullException(nameof(options));
}
var chatClient = assistantClient.AsIChatClient(assistantMetadata.Id);
if (clientFactory is not null)
{
chatClient = clientFactory(chatClient);
}
var mergedOptions = new ChatClientAgentOptions()
{
Id = assistantMetadata.Id,
Name = options.Name ?? assistantMetadata.Name,
Description = options.Description ?? assistantMetadata.Description,
Instructions = options.Instructions ?? assistantMetadata.Instructions,
ChatOptions = options.ChatOptions,
AIContextProviderFactory = options.AIContextProviderFactory,
ChatMessageStoreFactory = options.ChatMessageStoreFactory,
UseProvidedChatClientAsIs = options.UseProvidedChatClientAsIs
};
return new ChatClientAgent(chatClient, mergedOptions);
}
/// <summary>
/// Retrieves an existing server side agent, wrapped as a <see cref="ChatClientAgent"/> using the provided <see cref="AssistantClient"/>.
/// </summary>
/// <param name="assistantClient">The <see cref="AssistantClient"/> 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 assistant agent.</returns>
/// <exception cref="ArgumentNullException"><paramref name="assistantClient"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="agentId"/> is empty or whitespace.</exception>
public static ChatClientAgent GetAIAgent(
this AssistantClient assistantClient,
string agentId,
ChatClientAgentOptions options,
Func<IChatClient, IChatClient>? clientFactory = null,
CancellationToken cancellationToken = default)
{
if (assistantClient is null)
{
throw new ArgumentNullException(nameof(assistantClient));
}
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 assistant = assistantClient.GetAssistant(agentId, cancellationToken);
return assistantClient.GetAIAgent(assistant, options, clientFactory);
}
/// <summary>
/// Retrieves an existing server side agent, wrapped as a <see cref="ChatClientAgent"/> using the provided <see cref="AssistantClient"/>.
/// </summary>
/// <param name="assistantClient">The <see cref="AssistantClient"/> 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 assistant agent.</returns>
/// <exception cref="ArgumentNullException"><paramref name="assistantClient"/> or <paramref name="options"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="agentId"/> is empty or whitespace.</exception>
public static async Task<ChatClientAgent> GetAIAgentAsync(
this AssistantClient assistantClient,
string agentId,
ChatClientAgentOptions options,
Func<IChatClient, IChatClient>? clientFactory = null,
CancellationToken cancellationToken = default)
{
if (assistantClient is null)
{
throw new ArgumentNullException(nameof(assistantClient));
}
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 assistantResponse = await assistantClient.GetAssistantAsync(agentId, cancellationToken).ConfigureAwait(false);
return assistantClient.GetAIAgent(assistantResponse, options, clientFactory);
}
/// <summary>
/// Creates an AI agent from an <see cref="AssistantClient"/> using the OpenAI Assistant API.
/// </summary>
@@ -210,48 +359,34 @@ public static class OpenAIAssistantClientExtensions
Instructions = options.Instructions,
};
if (options.ChatOptions?.Tools is not null)
// Convert AITools to ToolDefinitions and ToolResources
var toolDefinitionsAndResources = ConvertAIToolsToToolDefinitions(options.ChatOptions?.Tools);
if (toolDefinitionsAndResources.ToolDefinitions is { Count: > 0 })
{
foreach (AITool tool in options.ChatOptions.Tools)
{
switch (tool)
{
// Attempting to set the tools at the agent level throws
// https://github.com/dotnet/extensions/issues/6743
//case AIFunction aiFunction:
// assistantOptions.Tools.Add(ToOpenAIAssistantsFunctionToolDefinition(aiFunction));
// break;
case HostedCodeInterpreterTool:
var codeInterpreterToolDefinition = new CodeInterpreterToolDefinition();
assistantOptions.Tools.Add(codeInterpreterToolDefinition);
break;
}
}
toolDefinitionsAndResources.ToolDefinitions.ForEach(x => assistantOptions.Tools.Add(x));
}
if (toolDefinitionsAndResources.ToolResources is not null)
{
assistantOptions.ToolResources = toolDefinitionsAndResources.ToolResources;
}
// Create the assistant in the assistant service.
var assistantCreateResult = client.CreateAssistant(model, assistantOptions);
var assistantId = assistantCreateResult.Value.Id;
var agentOptions = new ChatClientAgentOptions()
{
Id = assistantId,
Name = options.Name,
Description = options.Description,
Instructions = options.Instructions,
ChatOptions = options.ChatOptions?.Tools is null ? null : new ChatOptions()
{
Tools = options.ChatOptions.Tools,
}
};
// Build the local agent object.
var chatClient = client.AsIChatClient(assistantId);
if (clientFactory is not null)
{
chatClient = clientFactory(chatClient);
}
var agentOptions = options.Clone();
agentOptions.Id = assistantId;
options.ChatOptions ??= new ChatOptions();
options.ChatOptions!.Tools = toolDefinitionsAndResources.FunctionToolsAndOtherTools;
return new ChatClientAgent(chatClient, agentOptions, loggerFactory);
}
@@ -321,48 +456,101 @@ public static class OpenAIAssistantClientExtensions
Instructions = options.Instructions,
};
if (options.ChatOptions?.Tools is not null)
// Convert AITools to ToolDefinitions and ToolResources
var toolDefinitionsAndResources = ConvertAIToolsToToolDefinitions(options.ChatOptions?.Tools);
if (toolDefinitionsAndResources.ToolDefinitions is { Count: > 0 } toolDefinitions)
{
foreach (AITool tool in options.ChatOptions.Tools)
{
switch (tool)
{
// Attempting to set the tools at the agent level throws
// https://github.com/dotnet/extensions/issues/6743
//case AIFunction aiFunction:
// assistantOptions.Tools.Add(ToOpenAIAssistantsFunctionToolDefinition(aiFunction));
// break;
case HostedCodeInterpreterTool:
var codeInterpreterToolDefinition = new CodeInterpreterToolDefinition();
assistantOptions.Tools.Add(codeInterpreterToolDefinition);
break;
}
}
toolDefinitions.ForEach(x => assistantOptions.Tools.Add(x));
}
if (toolDefinitionsAndResources.ToolResources is not null)
{
assistantOptions.ToolResources = toolDefinitionsAndResources.ToolResources;
}
// Create the assistant in the assistant service.
var assistantCreateResult = await client.CreateAssistantAsync(model, assistantOptions).ConfigureAwait(false);
var assistantId = assistantCreateResult.Value.Id;
var agentOptions = new ChatClientAgentOptions()
{
Id = assistantId,
Name = options.Name,
Description = options.Description,
Instructions = options.Instructions,
ChatOptions = options.ChatOptions?.Tools is null ? null : new ChatOptions()
{
Tools = options.ChatOptions.Tools,
}
};
// Build the local agent object.
var chatClient = client.AsIChatClient(assistantId);
if (clientFactory is not null)
{
chatClient = clientFactory(chatClient);
}
var agentOptions = options.Clone();
agentOptions.Id = assistantId;
options.ChatOptions ??= new ChatOptions();
options.ChatOptions!.Tools = toolDefinitionsAndResources.FunctionToolsAndOtherTools;
return new ChatClientAgent(chatClient, agentOptions, loggerFactory);
}
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
{
MaxResults = 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;
default:
functionToolsAndOtherTools ??= new();
functionToolsAndOtherTools.Add(tool);
break;
}
}
}
return (toolDefinitions, toolResources, functionToolsAndOtherTools);
}
}
@@ -101,7 +101,7 @@ public class ChatClientAgentOptions
/// <summary>
/// Creates a new instance of <see cref="ChatClientAgentOptions"/> with the same values as this instance.
/// </summary>
internal ChatClientAgentOptions Clone()
public ChatClientAgentOptions Clone()
=> new()
{
Id = this.Id,
@@ -7,6 +7,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.AzureAI\Microsoft.Agents.AI.AzureAI.csproj" />
<ProjectReference Include="..\AgentConformance.IntegrationTests\AgentConformance.IntegrationTests.csproj" />
</ItemGroup>
@@ -0,0 +1,262 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.IO;
using System.Threading.Tasks;
using AgentConformance.IntegrationTests.Support;
using Azure.AI.Agents.Persistent;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Shared.IntegrationTests;
namespace AzureAIAgentsPersistent.IntegrationTests;
public class AzureAIAgentsPersistentCreateTests
{
private static readonly AzureAIConfiguration s_config = TestConfiguration.LoadSection<AzureAIConfiguration>();
private readonly PersistentAgentsClient _persistentAgentsClient = new(s_config.Endpoint, new AzureCliCredential());
[Theory]
[InlineData("CreateWithChatClientAgentOptionsAsync")]
[InlineData("CreateWithChatClientAgentOptionsSync")]
[InlineData("CreateWithFoundryOptionsAsync")]
[InlineData("CreateWithFoundryOptionsSync")]
public async Task CreateAgent_CreatesAgentWithCorrectMetadataAsync(string createMechanism)
{
// Arrange.
const string AgentName = "IntegrationTestAgent";
const string AgentDescription = "An agent created during integration tests";
const string AgentInstructions = "You are an integration test agent";
// Act.
var agent = createMechanism switch
{
"CreateWithChatClientAgentOptionsAsync" => await this._persistentAgentsClient.CreateAIAgentAsync(
s_config.DeploymentName,
options: new ChatClientAgentOptions(
instructions: AgentInstructions,
name: AgentName,
description: AgentDescription)),
"CreateWithChatClientAgentOptionsSync" => this._persistentAgentsClient.CreateAIAgent(
s_config.DeploymentName,
options: new ChatClientAgentOptions(
instructions: AgentInstructions,
name: AgentName,
description: AgentDescription)),
"CreateWithFoundryOptionsAsync" => await this._persistentAgentsClient.CreateAIAgentAsync(
s_config.DeploymentName,
instructions: AgentInstructions,
name: AgentName,
description: AgentDescription),
"CreateWithFoundryOptionsSync" => this._persistentAgentsClient.CreateAIAgent(
s_config.DeploymentName,
instructions: AgentInstructions,
name: AgentName,
description: AgentDescription),
_ => throw new InvalidOperationException($"Unknown create mechanism: {createMechanism}")
};
try
{
// Assert.
Assert.NotNull(agent);
Assert.Equal(AgentName, agent.Name);
Assert.Equal(AgentDescription, agent.Description);
Assert.Equal(AgentInstructions, agent.Instructions);
var retrievedAgentMetadata = await this._persistentAgentsClient.Administration.GetAgentAsync(agent.Id);
Assert.NotNull(retrievedAgentMetadata);
Assert.Equal(AgentName, retrievedAgentMetadata.Value.Name);
Assert.Equal(AgentDescription, retrievedAgentMetadata.Value.Description);
Assert.Equal(AgentInstructions, retrievedAgentMetadata.Value.Instructions);
}
finally
{
// Cleanup.
await this._persistentAgentsClient.Administration.DeleteAgentAsync(agent.Id);
}
}
[Theory]
[InlineData("CreateWithChatClientAgentOptionsAsync")]
[InlineData("CreateWithChatClientAgentOptionsSync")]
[InlineData("CreateWithFoundryOptionsAsync")]
[InlineData("CreateWithFoundryOptionsSync")]
public async Task CreateAgent_CreatesAgentWithVectorStoresAsync(string createMechanism)
{
// Arrange.
const string AgentInstructions = """
You are a helpful agent that can help fetch data from files you know about.
Use the File Search Tool to look up codes for words.
Do not answer a question unless you can find the answer using the File Search Tool.
""";
// Create a vector store.
var searchFilePath = Path.GetTempFileName() + "wordcodelookup.txt";
File.WriteAllText(
path: searchFilePath,
contents: "The word 'apple' uses the code 442345, while the word 'banana' uses the code 673457."
);
PersistentAgentFileInfo uploadedAgentFile = this._persistentAgentsClient.Files.UploadFile(
filePath: searchFilePath,
purpose: PersistentAgentFilePurpose.Agents
);
var vectorStoreMetadata = await this._persistentAgentsClient.VectorStores.CreateVectorStoreAsync([uploadedAgentFile.Id], name: "WordCodeLookup_VectorStore");
// Act.
var agent = createMechanism switch
{
"CreateWithChatClientAgentOptionsAsync" => await this._persistentAgentsClient.CreateAIAgentAsync(
s_config.DeploymentName,
options: new ChatClientAgentOptions(
instructions: AgentInstructions,
tools: [new HostedFileSearchTool() { Inputs = [new HostedVectorStoreContent(vectorStoreMetadata.Value.Id)] }])),
"CreateWithChatClientAgentOptionsSync" => this._persistentAgentsClient.CreateAIAgent(
s_config.DeploymentName,
options: new ChatClientAgentOptions(
instructions: AgentInstructions,
tools: [new HostedFileSearchTool() { Inputs = [new HostedVectorStoreContent(vectorStoreMetadata.Value.Id)] }])),
"CreateWithFoundryOptionsAsync" => await this._persistentAgentsClient.CreateAIAgentAsync(
s_config.DeploymentName,
instructions: AgentInstructions,
tools: [new FileSearchToolDefinition()],
toolResources: new ToolResources() { FileSearch = new([vectorStoreMetadata.Value.Id], null) }),
"CreateWithFoundryOptionsSync" => this._persistentAgentsClient.CreateAIAgent(
s_config.DeploymentName,
instructions: AgentInstructions,
tools: [new FileSearchToolDefinition()],
toolResources: new ToolResources() { FileSearch = new([vectorStoreMetadata.Value.Id], null) }),
_ => throw new InvalidOperationException($"Unknown create mechanism: {createMechanism}")
};
try
{
// Assert.
// Verify that the agent can use the vector store to answer a question.
var result = await agent.RunAsync("Can you give me the documented code for 'banana'?");
Assert.Contains("673457", result.ToString());
}
finally
{
// Cleanup.
await this._persistentAgentsClient.Administration.DeleteAgentAsync(agent.Id);
await this._persistentAgentsClient.VectorStores.DeleteVectorStoreAsync(vectorStoreMetadata.Value.Id);
await this._persistentAgentsClient.Files.DeleteFileAsync(uploadedAgentFile.Id);
File.Delete(searchFilePath);
}
}
[Theory]
[InlineData("CreateWithChatClientAgentOptionsAsync")]
[InlineData("CreateWithChatClientAgentOptionsSync")]
[InlineData("CreateWithFoundryOptionsAsync")]
[InlineData("CreateWithFoundryOptionsSync")]
public async Task CreateAgent_CreatesAgentWithCodeInterpreterAsync(string createMechanism)
{
// Arrange.
const string AgentInstructions = """
You are a helpful coding agent. A Python file is provided. Use the Code Interpreter Tool to run the file
and report the SECRET_NUMBER value it prints. Respond only with the number.
""";
// Create a python file that prints a known value.
var codeFilePath = Path.GetTempFileName() + "secret_number.py";
File.WriteAllText(
path: codeFilePath,
contents: "print(\"SECRET_NUMBER=24601\")" // Deterministic output we will look for.
);
PersistentAgentFileInfo uploadedCodeFile = this._persistentAgentsClient.Files.UploadFile(
filePath: codeFilePath,
purpose: PersistentAgentFilePurpose.Agents
);
CodeInterpreterToolResource toolResource = new();
toolResource.FileIds.Add(uploadedCodeFile.Id);
// Act.
var agent = createMechanism switch
{
// Hosted tool path (tools supplied via ChatClientAgentOptions)
"CreateWithChatClientAgentOptionsAsync" => await this._persistentAgentsClient.CreateAIAgentAsync(
s_config.DeploymentName,
options: new ChatClientAgentOptions(
instructions: AgentInstructions,
tools: [new HostedCodeInterpreterTool() { Inputs = [new HostedFileContent(uploadedCodeFile.Id)] }])),
"CreateWithChatClientAgentOptionsSync" => this._persistentAgentsClient.CreateAIAgent(
s_config.DeploymentName,
options: new ChatClientAgentOptions(
instructions: AgentInstructions,
tools: [new HostedCodeInterpreterTool() { Inputs = [new HostedFileContent(uploadedCodeFile.Id)] }])),
// Foundry (definitions + resources provided directly)
"CreateWithFoundryOptionsAsync" => await this._persistentAgentsClient.CreateAIAgentAsync(
s_config.DeploymentName,
instructions: AgentInstructions,
tools: [new CodeInterpreterToolDefinition()],
toolResources: new ToolResources() { CodeInterpreter = toolResource }),
"CreateWithFoundryOptionsSync" => this._persistentAgentsClient.CreateAIAgent(
s_config.DeploymentName,
instructions: AgentInstructions,
tools: [new CodeInterpreterToolDefinition()],
toolResources: new ToolResources() { CodeInterpreter = toolResource }),
_ => throw new InvalidOperationException($"Unknown create mechanism: {createMechanism}")
};
try
{
// Assert.
var result = await agent.RunAsync("What is the SECRET_NUMBER?");
// We expect the model to run the code and surface the number.
Assert.Contains("24601", result.ToString());
}
finally
{
// Cleanup.
await this._persistentAgentsClient.Administration.DeleteAgentAsync(agent.Id);
await this._persistentAgentsClient.Files.DeleteFileAsync(uploadedCodeFile.Id);
File.Delete(codeFilePath);
}
}
[Theory]
[InlineData("CreateWithChatClientAgentOptionsAsync")]
[InlineData("CreateWithChatClientAgentOptionsSync")]
public async Task CreateAgent_CreatesAgentWithAIFunctionToolsAsync(string createMechanism)
{
// Arrange.
const string AgentInstructions = "You are a helpful weather assistant. Always call the GetWeather function to answer questions about weather.";
static string GetWeather(string location) => $"The weather in {location} is sunny with a high of 23C.";
var weatherFunction = AIFunctionFactory.Create(GetWeather);
ChatClientAgent agent = createMechanism switch
{
"CreateWithChatClientAgentOptionsAsync" => await this._persistentAgentsClient.CreateAIAgentAsync(
s_config.DeploymentName,
options: new ChatClientAgentOptions(
instructions: AgentInstructions,
tools: [weatherFunction])),
"CreateWithChatClientAgentOptionsSync" => this._persistentAgentsClient.CreateAIAgent(
s_config.DeploymentName,
options: new ChatClientAgentOptions(
instructions: AgentInstructions,
tools: [weatherFunction])),
_ => throw new InvalidOperationException($"Unknown create mechanism: {createMechanism}")
};
try
{
// Act.
var response = await agent.RunAsync("What is the weather like in Amsterdam?");
// Assert - ensure function was invoked and its output surfaced.
var text = response.Text;
Assert.Contains("Amsterdam", text, StringComparison.OrdinalIgnoreCase);
Assert.Contains("sunny", text, StringComparison.OrdinalIgnoreCase);
Assert.Contains("23", text, StringComparison.OrdinalIgnoreCase);
}
finally
{
await this._persistentAgentsClient.Administration.DeleteAgentAsync(agent.Id);
}
}
}
@@ -294,6 +294,438 @@ public sealed class PersistentAgentsClientExtensionsTests
Assert.Null(retrievedTestClient);
}
/// <summary>
/// Verify that GetAIAgent with Response and options works correctly.
/// </summary>
[Fact]
public void GetAIAgent_WithResponseAndOptions_WorksCorrectly()
{
// Arrange
var client = CreateFakePersistentAgentsClient();
var persistentAgent = ModelReaderWriter.Read<PersistentAgent>(BinaryData.FromString("""{"id": "agent_abc123", "name": "Original Name", "description": "Original Description", "instructions": "Original Instructions"}"""))!;
var response = Response.FromValue(persistentAgent, new FakeResponse());
var options = new ChatClientAgentOptions
{
Name = "Override Name",
Description = "Override Description",
Instructions = "Override Instructions"
};
// Act
var agent = client.GetAIAgent(response, options);
// Assert
Assert.NotNull(agent);
Assert.Equal("Override Name", agent.Name);
Assert.Equal("Override Description", agent.Description);
Assert.Equal("Override Instructions", agent.Instructions);
}
/// <summary>
/// Verify that GetAIAgent with PersistentAgent and options works correctly.
/// </summary>
[Fact]
public void GetAIAgent_WithPersistentAgentAndOptions_WorksCorrectly()
{
// Arrange
var client = CreateFakePersistentAgentsClient();
var persistentAgent = ModelReaderWriter.Read<PersistentAgent>(BinaryData.FromString("""{"id": "agent_abc123", "name": "Original Name", "description": "Original Description", "instructions": "Original Instructions"}"""))!;
var options = new ChatClientAgentOptions
{
Name = "Override Name",
Description = "Override Description",
Instructions = "Override Instructions"
};
// Act
var agent = client.GetAIAgent(persistentAgent, options);
// Assert
Assert.NotNull(agent);
Assert.Equal("Override Name", agent.Name);
Assert.Equal("Override Description", agent.Description);
Assert.Equal("Override Instructions", agent.Instructions);
}
/// <summary>
/// Verify that GetAIAgent with PersistentAgent and options falls back to agent metadata when options are null.
/// </summary>
[Fact]
public void GetAIAgent_WithPersistentAgentAndOptionsWithNullFields_FallsBackToAgentMetadata()
{
// Arrange
var client = CreateFakePersistentAgentsClient();
var persistentAgent = ModelReaderWriter.Read<PersistentAgent>(BinaryData.FromString("""{"id": "agent_abc123", "name": "Original Name", "description": "Original Description", "instructions": "Original Instructions"}"""))!;
var options = new ChatClientAgentOptions(); // Empty options
// Act
var agent = client.GetAIAgent(persistentAgent, options);
// Assert
Assert.NotNull(agent);
Assert.Equal("Original Name", agent.Name);
Assert.Equal("Original Description", agent.Description);
Assert.Equal("Original Instructions", agent.Instructions);
}
/// <summary>
/// Verify that GetAIAgent with agentId and options works correctly.
/// </summary>
[Fact]
public void GetAIAgent_WithAgentIdAndOptions_WorksCorrectly()
{
// Arrange
var client = CreateFakePersistentAgentsClient();
const string AgentId = "agent_abc123";
var options = new ChatClientAgentOptions
{
Name = "Override Name",
Description = "Override Description",
Instructions = "Override Instructions"
};
// Act
var agent = client.GetAIAgent(AgentId, options);
// Assert
Assert.NotNull(agent);
Assert.Equal("Override Name", agent.Name);
Assert.Equal("Override Description", agent.Description);
Assert.Equal("Override Instructions", agent.Instructions);
}
/// <summary>
/// Verify that GetAIAgentAsync with agentId and options works correctly.
/// </summary>
[Fact]
public async Task GetAIAgentAsync_WithAgentIdAndOptions_WorksCorrectlyAsync()
{
// Arrange
var client = CreateFakePersistentAgentsClient();
const string AgentId = "agent_abc123";
var options = new ChatClientAgentOptions
{
Name = "Override Name",
Description = "Override Description",
Instructions = "Override Instructions"
};
// Act
var agent = await client.GetAIAgentAsync(AgentId, options);
// Assert
Assert.NotNull(agent);
Assert.Equal("Override Name", agent.Name);
Assert.Equal("Override Description", agent.Description);
Assert.Equal("Override Instructions", agent.Instructions);
}
/// <summary>
/// Verify that GetAIAgent with clientFactory parameter correctly applies the factory.
/// </summary>
[Fact]
public void GetAIAgent_WithOptionsAndClientFactory_AppliesFactoryCorrectly()
{
// Arrange
var client = CreateFakePersistentAgentsClient();
var persistentAgent = ModelReaderWriter.Read<PersistentAgent>(BinaryData.FromString("""{"id": "agent_abc123", "name": "Test Agent"}"""))!;
var testChatClient = new TestChatClient(client.AsIChatClient("agent_abc123"));
var options = new ChatClientAgentOptions
{
Name = "Test Agent"
};
// Act
var agent = client.GetAIAgent(
persistentAgent,
options,
clientFactory: (innerClient) => testChatClient);
// Assert
Assert.NotNull(agent);
Assert.Equal("Test Agent", agent.Name);
// Verify that the custom chat client can be retrieved from the agent's service collection
var retrievedTestClient = agent.GetService<TestChatClient>();
Assert.NotNull(retrievedTestClient);
Assert.Same(testChatClient, retrievedTestClient);
}
/// <summary>
/// Verify that GetAIAgent throws ArgumentNullException when response is null.
/// </summary>
[Fact]
public void GetAIAgent_WithNullResponse_ThrowsArgumentNullException()
{
// Arrange
var client = CreateFakePersistentAgentsClient();
var options = new ChatClientAgentOptions();
// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(() =>
client.GetAIAgent((Response<PersistentAgent>)null!, options));
Assert.Equal("persistentAgentResponse", exception.ParamName);
}
/// <summary>
/// Verify that GetAIAgent throws ArgumentNullException when persistentAgent is null.
/// </summary>
[Fact]
public void GetAIAgent_WithNullPersistentAgent_ThrowsArgumentNullException()
{
// Arrange
var client = CreateFakePersistentAgentsClient();
var options = new ChatClientAgentOptions();
// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(() =>
client.GetAIAgent((PersistentAgent)null!, options));
Assert.Equal("persistentAgentMetadata", exception.ParamName);
}
/// <summary>
/// Verify that GetAIAgent throws ArgumentNullException when options is null.
/// </summary>
[Fact]
public void GetAIAgent_WithNullOptions_ThrowsArgumentNullException()
{
// Arrange
var client = CreateFakePersistentAgentsClient();
var persistentAgent = ModelReaderWriter.Read<PersistentAgent>(BinaryData.FromString("""{"id": "agent_abc123"}"""))!;
// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(() =>
client.GetAIAgent(persistentAgent, (ChatClientAgentOptions)null!));
Assert.Equal("options", exception.ParamName);
}
/// <summary>
/// Verify that GetAIAgent throws ArgumentException when agentId is empty.
/// </summary>
[Fact]
public void GetAIAgent_WithOptionsAndEmptyAgentId_ThrowsArgumentException()
{
// Arrange
var client = CreateFakePersistentAgentsClient();
var options = new ChatClientAgentOptions();
// Act & Assert
var exception = Assert.Throws<ArgumentException>(() =>
client.GetAIAgent(string.Empty, options));
Assert.Equal("agentId", exception.ParamName);
}
/// <summary>
/// Verify that GetAIAgentAsync throws ArgumentException when agentId is empty.
/// </summary>
[Fact]
public async Task GetAIAgentAsync_WithOptionsAndEmptyAgentId_ThrowsArgumentExceptionAsync()
{
// Arrange
var client = CreateFakePersistentAgentsClient();
var options = new ChatClientAgentOptions();
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentException>(() =>
client.GetAIAgentAsync(string.Empty, options));
Assert.Equal("agentId", exception.ParamName);
}
/// <summary>
/// Verify that CreateAIAgent with options works correctly.
/// </summary>
[Fact]
public void CreateAIAgent_WithOptions_WorksCorrectly()
{
// Arrange
var client = CreateFakePersistentAgentsClient();
const string Model = "test-model";
var options = new ChatClientAgentOptions
{
Name = "Test Agent",
Description = "Test description",
Instructions = "Test instructions"
};
// Act
var agent = client.CreateAIAgent(Model, options);
// Assert
Assert.NotNull(agent);
Assert.Equal("Test Agent", agent.Name);
Assert.Equal("Test description", agent.Description);
Assert.Equal("Test instructions", agent.Instructions);
}
/// <summary>
/// Verify that CreateAIAgentAsync with options works correctly.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithOptions_WorksCorrectlyAsync()
{
// Arrange
var client = CreateFakePersistentAgentsClient();
const string Model = "test-model";
var options = new ChatClientAgentOptions
{
Name = "Test Agent",
Description = "Test description",
Instructions = "Test instructions"
};
// Act
var agent = await client.CreateAIAgentAsync(Model, options);
// Assert
Assert.NotNull(agent);
Assert.Equal("Test Agent", agent.Name);
Assert.Equal("Test description", agent.Description);
Assert.Equal("Test instructions", agent.Instructions);
}
/// <summary>
/// Verify that CreateAIAgent with options and clientFactory applies the factory correctly.
/// </summary>
[Fact]
public void CreateAIAgent_WithOptionsAndClientFactory_AppliesFactoryCorrectly()
{
// Arrange
var client = CreateFakePersistentAgentsClient();
TestChatClient? testChatClient = null;
const string Model = "test-model";
var options = new ChatClientAgentOptions
{
Name = "Test Agent"
};
// Act
var agent = client.CreateAIAgent(
Model,
options,
clientFactory: (innerClient) => testChatClient = new TestChatClient(innerClient));
// Assert
Assert.NotNull(agent);
Assert.Equal("Test Agent", agent.Name);
// Verify that the custom chat client can be retrieved from the agent's service collection
var retrievedTestClient = agent.GetService<TestChatClient>();
Assert.NotNull(retrievedTestClient);
Assert.Same(testChatClient, retrievedTestClient);
}
/// <summary>
/// Verify that CreateAIAgentAsync with options and clientFactory applies the factory correctly.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithOptionsAndClientFactory_AppliesFactoryCorrectlyAsync()
{
// Arrange
var client = CreateFakePersistentAgentsClient();
TestChatClient? testChatClient = null;
const string Model = "test-model";
var options = new ChatClientAgentOptions
{
Name = "Test Agent"
};
// Act
var agent = await client.CreateAIAgentAsync(
Model,
options,
clientFactory: (innerClient) => testChatClient = new TestChatClient(innerClient));
// Assert
Assert.NotNull(agent);
Assert.Equal("Test Agent", agent.Name);
// Verify that the custom chat client can be retrieved from the agent's service collection
var retrievedTestClient = agent.GetService<TestChatClient>();
Assert.NotNull(retrievedTestClient);
Assert.Same(testChatClient, retrievedTestClient);
}
/// <summary>
/// Verify that CreateAIAgent throws ArgumentNullException when options is null.
/// </summary>
[Fact]
public void CreateAIAgent_WithNullOptions_ThrowsArgumentNullException()
{
// Arrange
var client = CreateFakePersistentAgentsClient();
// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(() =>
client.CreateAIAgent("test-model", (ChatClientAgentOptions)null!));
Assert.Equal("options", exception.ParamName);
}
/// <summary>
/// Verify that CreateAIAgentAsync throws ArgumentNullException when options is null.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithNullOptions_ThrowsArgumentNullExceptionAsync()
{
// Arrange
var client = CreateFakePersistentAgentsClient();
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentNullException>(() =>
client.CreateAIAgentAsync("test-model", (ChatClientAgentOptions)null!));
Assert.Equal("options", exception.ParamName);
}
/// <summary>
/// Verify that CreateAIAgent throws ArgumentException when model is empty.
/// </summary>
[Fact]
public void CreateAIAgent_WithEmptyModel_ThrowsArgumentException()
{
// Arrange
var client = CreateFakePersistentAgentsClient();
var options = new ChatClientAgentOptions();
// Act & Assert
var exception = Assert.Throws<ArgumentException>(() =>
client.CreateAIAgent(string.Empty, options));
Assert.Equal("model", exception.ParamName);
}
/// <summary>
/// Verify that CreateAIAgentAsync throws ArgumentException when model is empty.
/// </summary>
[Fact]
public async Task CreateAIAgentAsync_WithEmptyModel_ThrowsArgumentExceptionAsync()
{
// Arrange
var client = CreateFakePersistentAgentsClient();
var options = new ChatClientAgentOptions();
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentException>(() =>
client.CreateAIAgentAsync(string.Empty, options));
Assert.Equal("model", exception.ParamName);
}
/// <summary>
/// Test custom chat client that can be used to verify clientFactory functionality.
/// </summary>
@@ -207,6 +207,254 @@ public sealed class OpenAIAssistantClientExtensionsTests
Assert.Equal("options", exception.ParamName);
}
/// <summary>
/// Verify that GetAIAgent with ClientResult and options works correctly.
/// </summary>
[Fact]
public void GetAIAgent_WithClientResultAndOptions_WorksCorrectly()
{
// Arrange
var assistantClient = new TestAssistantClient();
var assistant = ModelReaderWriter.Read<Assistant>(BinaryData.FromString("""{"id": "asst_abc123", "name": "Original Name", "description": "Original Description", "instructions": "Original Instructions"}"""))!;
var clientResult = ClientResult.FromValue(assistant, new FakePipelineResponse());
var options = new ChatClientAgentOptions
{
Name = "Override Name",
Description = "Override Description",
Instructions = "Override Instructions"
};
// Act
var agent = assistantClient.GetAIAgent(clientResult, options);
// Assert
Assert.NotNull(agent);
Assert.Equal("Override Name", agent.Name);
Assert.Equal("Override Description", agent.Description);
Assert.Equal("Override Instructions", agent.Instructions);
}
/// <summary>
/// Verify that GetAIAgent with Assistant and options works correctly.
/// </summary>
[Fact]
public void GetAIAgent_WithAssistantAndOptions_WorksCorrectly()
{
// Arrange
var assistantClient = new TestAssistantClient();
var assistant = ModelReaderWriter.Read<Assistant>(BinaryData.FromString("""{"id": "asst_abc123", "name": "Original Name", "description": "Original Description", "instructions": "Original Instructions"}"""))!;
var options = new ChatClientAgentOptions
{
Name = "Override Name",
Description = "Override Description",
Instructions = "Override Instructions"
};
// Act
var agent = assistantClient.GetAIAgent(assistant, options);
// Assert
Assert.NotNull(agent);
Assert.Equal("Override Name", agent.Name);
Assert.Equal("Override Description", agent.Description);
Assert.Equal("Override Instructions", agent.Instructions);
}
/// <summary>
/// Verify that GetAIAgent with Assistant and options falls back to assistant metadata when options are null.
/// </summary>
[Fact]
public void GetAIAgent_WithAssistantAndOptionsWithNullFields_FallsBackToAssistantMetadata()
{
// Arrange
var assistantClient = new TestAssistantClient();
var assistant = ModelReaderWriter.Read<Assistant>(BinaryData.FromString("""{"id": "asst_abc123", "name": "Original Name", "description": "Original Description", "instructions": "Original Instructions"}"""))!;
var options = new ChatClientAgentOptions(); // Empty options
// Act
var agent = assistantClient.GetAIAgent(assistant, options);
// Assert
Assert.NotNull(agent);
Assert.Equal("Original Name", agent.Name);
Assert.Equal("Original Description", agent.Description);
Assert.Equal("Original Instructions", agent.Instructions);
}
/// <summary>
/// Verify that GetAIAgent with agentId and options works correctly.
/// </summary>
[Fact]
public void GetAIAgent_WithAgentIdAndOptions_WorksCorrectly()
{
// Arrange
var assistantClient = new TestAssistantClient();
const string AgentId = "asst_abc123";
var options = new ChatClientAgentOptions
{
Name = "Override Name",
Description = "Override Description",
Instructions = "Override Instructions"
};
// Act
var agent = assistantClient.GetAIAgent(AgentId, options);
// Assert
Assert.NotNull(agent);
Assert.Equal("Override Name", agent.Name);
Assert.Equal("Override Description", agent.Description);
Assert.Equal("Override Instructions", agent.Instructions);
}
/// <summary>
/// Verify that GetAIAgentAsync with agentId and options works correctly.
/// </summary>
[Fact]
public async Task GetAIAgentAsync_WithAgentIdAndOptions_WorksCorrectlyAsync()
{
// Arrange
var assistantClient = new TestAssistantClient();
const string AgentId = "asst_abc123";
var options = new ChatClientAgentOptions
{
Name = "Override Name",
Description = "Override Description",
Instructions = "Override Instructions"
};
// Act
var agent = await assistantClient.GetAIAgentAsync(AgentId, options);
// Assert
Assert.NotNull(agent);
Assert.Equal("Override Name", agent.Name);
Assert.Equal("Override Description", agent.Description);
Assert.Equal("Override Instructions", agent.Instructions);
}
/// <summary>
/// Verify that GetAIAgent with clientFactory parameter correctly applies the factory.
/// </summary>
[Fact]
public void GetAIAgent_WithClientFactory_AppliesFactoryCorrectly()
{
// Arrange
var assistantClient = new TestAssistantClient();
var assistant = ModelReaderWriter.Read<Assistant>(BinaryData.FromString("""{"id": "asst_abc123", "name": "Test Agent"}"""))!;
var testChatClient = new TestChatClient(assistantClient.AsIChatClient("asst_abc123"));
var options = new ChatClientAgentOptions
{
Name = "Test Agent"
};
// Act
var agent = assistantClient.GetAIAgent(
assistant,
options,
clientFactory: (innerClient) => testChatClient);
// Assert
Assert.NotNull(agent);
Assert.Equal("Test Agent", agent.Name);
// Verify that the custom chat client can be retrieved from the agent's service collection
var retrievedTestClient = agent.GetService<TestChatClient>();
Assert.NotNull(retrievedTestClient);
Assert.Same(testChatClient, retrievedTestClient);
}
/// <summary>
/// Verify that GetAIAgent throws ArgumentNullException when assistantClientResult is null.
/// </summary>
[Fact]
public void GetAIAgent_WithNullClientResult_ThrowsArgumentNullException()
{
// Arrange
var assistantClient = new TestAssistantClient();
var options = new ChatClientAgentOptions();
// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(() =>
assistantClient.GetAIAgent((ClientResult<Assistant>)null!, options));
Assert.Equal("assistantClientResult", exception.ParamName);
}
/// <summary>
/// Verify that GetAIAgent throws ArgumentNullException when assistant is null.
/// </summary>
[Fact]
public void GetAIAgent_WithNullAssistant_ThrowsArgumentNullException()
{
// Arrange
var assistantClient = new TestAssistantClient();
var options = new ChatClientAgentOptions();
// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(() =>
assistantClient.GetAIAgent((Assistant)null!, options));
Assert.Equal("assistantMetadata", exception.ParamName);
}
/// <summary>
/// Verify that GetAIAgent throws ArgumentNullException when options is null.
/// </summary>
[Fact]
public void GetAIAgent_WithNullOptions_ThrowsArgumentNullException()
{
// Arrange
var assistantClient = new TestAssistantClient();
var assistant = ModelReaderWriter.Read<Assistant>(BinaryData.FromString("""{"id": "asst_abc123"}"""))!;
// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(() =>
assistantClient.GetAIAgent(assistant, (ChatClientAgentOptions)null!));
Assert.Equal("options", exception.ParamName);
}
/// <summary>
/// Verify that GetAIAgent throws ArgumentException when agentId is empty.
/// </summary>
[Fact]
public void GetAIAgent_WithEmptyAgentId_ThrowsArgumentException()
{
// Arrange
var assistantClient = new TestAssistantClient();
var options = new ChatClientAgentOptions();
// Act & Assert
var exception = Assert.Throws<ArgumentException>(() =>
assistantClient.GetAIAgent(string.Empty, options));
Assert.Equal("agentId", exception.ParamName);
}
/// <summary>
/// Verify that GetAIAgentAsync throws ArgumentException when agentId is empty.
/// </summary>
[Fact]
public async Task GetAIAgentAsync_WithEmptyAgentId_ThrowsArgumentExceptionAsync()
{
// Arrange
var assistantClient = new TestAssistantClient();
var options = new ChatClientAgentOptions();
// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentException>(() =>
assistantClient.GetAIAgentAsync(string.Empty, options));
Assert.Equal("agentId", exception.ParamName);
}
/// <summary>
/// Creates a test AssistantClient implementation for testing.
/// </summary>
@@ -220,6 +468,17 @@ public sealed class OpenAIAssistantClientExtensionsTests
{
return ClientResult.FromValue(ModelReaderWriter.Read<Assistant>(BinaryData.FromString("""{"id": "asst_abc123"}""")), new FakePipelineResponse())!;
}
public override ClientResult<Assistant> GetAssistant(string assistantId, CancellationToken cancellationToken = default)
{
return ClientResult.FromValue(ModelReaderWriter.Read<Assistant>(BinaryData.FromString("""{"id": "asst_abc123", "name": "Original Name", "description": "Original Description", "instructions": "Original Instructions"}""")), new FakePipelineResponse())!;
}
public override async Task<ClientResult<Assistant>> GetAssistantAsync(string assistantId, CancellationToken cancellationToken = default)
{
await Task.Delay(1, cancellationToken); // Simulate async operation
return ClientResult.FromValue(ModelReaderWriter.Read<Assistant>(BinaryData.FromString("""{"id": "asst_abc123", "name": "Original Name", "description": "Original Description", "instructions": "Original Instructions"}""")), new FakePipelineResponse())!;
}
}
private sealed class TestChatClient : DelegatingChatClient
@@ -1,7 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(ProjectsTargetFrameworks)</TargetFrameworks>
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
<InjectSharedIntegrationTestCode>True</InjectSharedIntegrationTestCode>
<NoWarn>$(NoWarn);OPENAI001;</NoWarn>
</PropertyGroup>
@@ -12,6 +13,7 @@
<ItemGroup>
<ProjectReference Include="..\AgentConformance.IntegrationTests\AgentConformance.IntegrationTests.csproj" />
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,192 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.IO;
using System.Threading.Tasks;
using AgentConformance.IntegrationTests.Support;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI;
using OpenAI.Assistants;
using OpenAI.Files;
using OpenAI.VectorStores;
using Shared.IntegrationTests;
namespace OpenAIAssistant.IntegrationTests;
public class OpenAIAssistantClientExtensionsTests
{
private static readonly OpenAIConfiguration s_config = TestConfiguration.LoadSection<OpenAIConfiguration>();
private readonly AssistantClient _assistantClient = new OpenAIClient(s_config.ApiKey).GetAssistantClient();
private readonly OpenAIFileClient _fileClient = new OpenAIClient(s_config.ApiKey).GetOpenAIFileClient();
[Theory]
[InlineData("CreateWithChatClientAgentOptionsAsync")]
[InlineData("CreateWithChatClientAgentOptionsSync")]
[InlineData("CreateWithParamsAsync")]
public async Task CreateAIAgentAsync_WithAIFunctionTool_InvokesFunctionAsync(string createMechanism)
{
// Arrange
const string AgentInstructions = "You are a helpful weather assistant. Always call the GetWeather function to answer questions about weather.";
static string GetWeather(string location) => $"The weather in {location} is sunny with a high of 23C.";
var weatherFunction = AIFunctionFactory.Create(GetWeather, nameof(GetWeather));
// Act
var agent = createMechanism switch
{
"CreateWithChatClientAgentOptionsAsync" => await this._assistantClient.CreateAIAgentAsync(
model: s_config.ChatModelId!,
options: new ChatClientAgentOptions(
instructions: AgentInstructions,
tools: [weatherFunction])),
"CreateWithChatClientAgentOptionsSync" => this._assistantClient.CreateAIAgent(
model: s_config.ChatModelId!,
options: new ChatClientAgentOptions(
instructions: AgentInstructions,
tools: [weatherFunction])),
"CreateWithParamsAsync" => await this._assistantClient.CreateAIAgentAsync(
model: s_config.ChatModelId!,
instructions: AgentInstructions,
tools: [weatherFunction]),
_ => throw new InvalidOperationException($"Unknown create mechanism: {createMechanism}")
};
try
{
// Trigger function call.
var response = await agent.RunAsync("What is the weather like in Amsterdam?");
var text = response.Text;
// Assert
Assert.Contains("Amsterdam", text, StringComparison.OrdinalIgnoreCase);
Assert.Contains("sunny", text, StringComparison.OrdinalIgnoreCase);
Assert.Contains("23", text, StringComparison.OrdinalIgnoreCase);
}
finally
{
await this._assistantClient.DeleteAssistantAsync(agent.Id);
}
}
[Theory]
[InlineData("CreateWithChatClientAgentOptionsAsync")]
[InlineData("CreateWithChatClientAgentOptionsSync")]
[InlineData("CreateWithParamsAsync")]
public async Task CreateAIAgentAsync_WithHostedCodeInterpreter_RunsCodeAsync(string createMechanism)
{
// Arrange
const string Instructions = "Use the Code Interpreter Tool to run the uploaded python file and respond only with the secret number.";
// Create a python file that prints a known value.
var codeFilePath = Path.GetTempFileName() + "openai_secret_number.py";
File.WriteAllText(
path: codeFilePath,
contents: "print(\"OPENAI_SECRET=13579\")" // Deterministic output we will look for.
);
// Upload file to OpenAI Assistants file store for use with the Code Interpreter.
var uploadResult = await this._fileClient.UploadFileAsync(codeFilePath, FileUploadPurpose.Assistants);
string uploadedFileId = uploadResult.Value.Id;
var codeInterpreterTool = new HostedCodeInterpreterTool() { Inputs = [new HostedFileContent(uploadedFileId)] };
var agent = createMechanism switch
{
"CreateWithChatClientAgentOptionsAsync" => await this._assistantClient.CreateAIAgentAsync(
model: s_config.ChatModelId!,
options: new ChatClientAgentOptions(
instructions: Instructions,
tools: [codeInterpreterTool])),
"CreateWithChatClientAgentOptionsSync" => this._assistantClient.CreateAIAgent(
model: s_config.ChatModelId!,
options: new ChatClientAgentOptions(
instructions: Instructions,
tools: [codeInterpreterTool])),
"CreateWithParamsAsync" => await this._assistantClient.CreateAIAgentAsync(
model: s_config.ChatModelId!,
instructions: Instructions,
tools: [codeInterpreterTool]),
_ => throw new InvalidOperationException($"Unknown create mechanism: {createMechanism}")
};
try
{
var response = await agent.RunAsync("What is the OPENAI_SECRET number?");
var text = response.ToString();
Assert.Contains("13579", text);
}
finally
{
await this._assistantClient.DeleteAssistantAsync(agent.Id);
await this._fileClient.DeleteFileAsync(uploadedFileId);
File.Delete(codeFilePath);
}
}
[Theory]
[InlineData("CreateWithChatClientAgentOptionsAsync")]
[InlineData("CreateWithChatClientAgentOptionsSync")]
[InlineData("CreateWithParamsAsync")]
public async Task CreateAIAgentAsync_WithHostedFileSearchTool_SearchesFilesAsync(string createMechanism)
{
// Arrange.
const string Instructions = """
You are a helpful agent that can help fetch data from files you know about.
Use the File Search Tool to look up codes for words.
Do not answer a question unless you can find the answer using the File Search Tool.
""";
// Create a local file with deterministic content and upload it.
var searchFilePath = Path.GetTempFileName() + "wordcodelookup.txt";
File.WriteAllText(
path: searchFilePath,
contents: "The word 'apple' uses the code 442345, while the word 'banana' uses the code 673457.");
var uploadResult = await this._fileClient.UploadFileAsync(searchFilePath, FileUploadPurpose.Assistants);
string uploadedFileId = uploadResult.Value.Id;
// Create a vector store backing the file search (HostedFileSearchTool requires a vector store id).
var vectorStoreClient = new OpenAIClient(s_config.ApiKey).GetVectorStoreClient();
var vectorStoreCreate = await vectorStoreClient.CreateVectorStoreAsync(options: new VectorStoreCreationOptions()
{
Name = "WordCodeLookup_VectorStore",
FileIds = { uploadedFileId }
});
string vectorStoreId = vectorStoreCreate.Value.Id;
var fileSearchTool = new HostedFileSearchTool() { Inputs = [new HostedVectorStoreContent(vectorStoreId)] };
var agent = createMechanism switch
{
"CreateWithChatClientAgentOptionsAsync" => await this._assistantClient.CreateAIAgentAsync(
model: s_config.ChatModelId!,
options: new ChatClientAgentOptions(
instructions: Instructions,
tools: [fileSearchTool])),
"CreateWithChatClientAgentOptionsSync" => this._assistantClient.CreateAIAgent(
model: s_config.ChatModelId!,
options: new ChatClientAgentOptions(
instructions: Instructions,
tools: [fileSearchTool])),
"CreateWithParamsAsync" => await this._assistantClient.CreateAIAgentAsync(
model: s_config.ChatModelId!,
instructions: Instructions,
tools: [fileSearchTool]),
_ => throw new InvalidOperationException($"Unknown create mechanism: {createMechanism}")
};
try
{
// Act - ask about banana code which must be retrieved via file search.
var response = await agent.RunAsync("Can you give me the documented code for 'banana'?");
var text = response.ToString();
Assert.Contains("673457", text);
}
finally
{
await this._assistantClient.DeleteAssistantAsync(agent.Id);
await vectorStoreClient.DeleteVectorStoreAsync(vectorStoreId);
await this._fileClient.DeleteFileAsync(uploadedFileId);
File.Delete(searchFilePath);
}
}
}