.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 15:39:48 +00:00
committed by GitHub
parent 3b9193c15e
commit 54c3eb726a
10 changed files with 1730 additions and 63 deletions
@@ -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,