mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.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:
committed by
GitHub
Unverified
parent
3b9193c15e
commit
54c3eb726a
Vendored
+2
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+248
-60
@@ -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,
|
||||
|
||||
+1
@@ -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>
|
||||
|
||||
|
||||
+262
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+432
@@ -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>
|
||||
|
||||
+259
@@ -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
|
||||
|
||||
+3
-1
@@ -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>
|
||||
|
||||
+192
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user