From 54c3eb726a519626a2b08216cb10cdd73433cd6b Mon Sep 17 00:00:00 2001
From: westey <164392973+westey-m@users.noreply.github.com>
Date: Thu, 16 Oct 2025 16:39:48 +0100
Subject: [PATCH] .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.
---
dotnet/.vscode/settings.json | 3 +-
.../PersistentAgentsClientExtensions.cs | 330 +++++++++++++
.../OpenAIAssistantClientExtensions.cs | 308 ++++++++++---
.../ChatClient/ChatClientAgentOptions.cs | 2 +-
...AIAgentsPersistent.IntegrationTests.csproj | 1 +
.../AzureAIAgentsPersistentCreateTests.cs | 262 +++++++++++
.../PersistentAgentsClientExtensionsTests.cs | 432 ++++++++++++++++++
.../OpenAIAssistantClientExtensionsTests.cs | 259 +++++++++++
.../OpenAIAssistant.IntegrationTests.csproj | 4 +-
.../OpenAIAssistantClientExtensionsTests.cs | 192 ++++++++
10 files changed, 1730 insertions(+), 63 deletions(-)
create mode 100644 dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentCreateTests.cs
create mode 100644 dotnet/tests/OpenAIAssistant.IntegrationTests/OpenAIAssistantClientExtensionsTests.cs
diff --git a/dotnet/.vscode/settings.json b/dotnet/.vscode/settings.json
index 2a6a38bc59..4fa848ae28 100644
--- a/dotnet/.vscode/settings.json
+++ b/dotnet/.vscode/settings.json
@@ -1,4 +1,5 @@
{
"dotnet.defaultSolution": "agent-framework-dotnet.slnx",
- "git.openRepositoryInParentFolders": "always"
+ "git.openRepositoryInParentFolders": "always",
+ "chat.agent.enabled": true
}
diff --git a/dotnet/src/Microsoft.Agents.AI.AzureAI/PersistentAgentsClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.AzureAI/PersistentAgentsClientExtensions.cs
index 72c77a94a4..1d5f228fcc 100644
--- a/dotnet/src/Microsoft.Agents.AI.AzureAI/PersistentAgentsClientExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.AzureAI/PersistentAgentsClientExtensions.cs
@@ -127,6 +127,147 @@ public static class PersistentAgentsClientExtensions
return persistentAgentsClient.GetAIAgent(persistentAgentResponse, chatOptions, clientFactory);
}
+ ///
+ /// Gets a runnable agent instance from the provided response containing persistent agent metadata.
+ ///
+ /// The client used to interact with persistent agents. Cannot be .
+ /// The response containing the persistent agent to be converted. Cannot be .
+ /// Full set of options to configure the agent.
+ /// Provides a way to customize the creation of the underlying used by the agent.
+ /// A instance that can be used to perform operations on the persistent agent.
+ /// Thrown when or is .
+ public static ChatClientAgent GetAIAgent(this PersistentAgentsClient persistentAgentsClient, Response persistentAgentResponse, ChatClientAgentOptions options, Func? clientFactory = null)
+ {
+ if (persistentAgentResponse is null)
+ {
+ throw new ArgumentNullException(nameof(persistentAgentResponse));
+ }
+
+ return GetAIAgent(persistentAgentsClient, persistentAgentResponse.Value, options, clientFactory);
+ }
+
+ ///
+ /// Gets a runnable agent instance from a containing metadata about a persistent agent.
+ ///
+ /// The client used to interact with persistent agents. Cannot be .
+ /// The persistent agent metadata to be converted. Cannot be .
+ /// Full set of options to configure the agent.
+ /// Provides a way to customize the creation of the underlying used by the agent.
+ /// A instance that can be used to perform operations on the persistent agent.
+ /// Thrown when or is .
+ public static ChatClientAgent GetAIAgent(this PersistentAgentsClient persistentAgentsClient, PersistentAgent persistentAgentMetadata, ChatClientAgentOptions options, Func? clientFactory = null)
+ {
+ if (persistentAgentMetadata is null)
+ {
+ throw new ArgumentNullException(nameof(persistentAgentMetadata));
+ }
+
+ if (persistentAgentsClient is null)
+ {
+ throw new ArgumentNullException(nameof(persistentAgentsClient));
+ }
+
+ if (options is null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ var chatClient = persistentAgentsClient.AsIChatClient(persistentAgentMetadata.Id);
+
+ if (clientFactory is not null)
+ {
+ chatClient = clientFactory(chatClient);
+ }
+
+ var agentOptions = new ChatClientAgentOptions()
+ {
+ Id = persistentAgentMetadata.Id,
+ Name = options.Name ?? persistentAgentMetadata.Name,
+ Description = options.Description ?? persistentAgentMetadata.Description,
+ Instructions = options.Instructions ?? persistentAgentMetadata.Instructions,
+ ChatOptions = options.ChatOptions,
+ AIContextProviderFactory = options.AIContextProviderFactory,
+ ChatMessageStoreFactory = options.ChatMessageStoreFactory,
+ UseProvidedChatClientAsIs = options.UseProvidedChatClientAsIs
+ };
+
+ return new ChatClientAgent(chatClient, agentOptions);
+ }
+
+ ///
+ /// Retrieves an existing server side agent, wrapped as a using the provided .
+ ///
+ /// The to create the with.
+ /// The ID of the server side agent to create a for.
+ /// Full set of options to configure the agent.
+ /// Provides a way to customize the creation of the underlying used by the agent.
+ /// The to monitor for cancellation requests. The default is .
+ /// A instance that can be used to perform operations on the persistent agent.
+ /// Thrown when or is .
+ /// Thrown when is empty or whitespace.
+ public static ChatClientAgent GetAIAgent(
+ this PersistentAgentsClient persistentAgentsClient,
+ string agentId,
+ ChatClientAgentOptions options,
+ Func? clientFactory = null,
+ CancellationToken cancellationToken = default)
+ {
+ if (persistentAgentsClient is null)
+ {
+ throw new ArgumentNullException(nameof(persistentAgentsClient));
+ }
+
+ if (string.IsNullOrWhiteSpace(agentId))
+ {
+ throw new ArgumentException($"{nameof(agentId)} should not be null or whitespace.", nameof(agentId));
+ }
+
+ if (options is null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ var persistentAgentResponse = persistentAgentsClient.Administration.GetAgent(agentId, cancellationToken);
+ return persistentAgentsClient.GetAIAgent(persistentAgentResponse, options, clientFactory);
+ }
+
+ ///
+ /// Retrieves an existing server side agent, wrapped as a using the provided .
+ ///
+ /// The to create the with.
+ /// The ID of the server side agent to create a for.
+ /// Full set of options to configure the agent.
+ /// Provides a way to customize the creation of the underlying used by the agent.
+ /// The to monitor for cancellation requests. The default is .
+ /// A instance that can be used to perform operations on the persistent agent.
+ /// Thrown when or is .
+ /// Thrown when is empty or whitespace.
+ public static async Task GetAIAgentAsync(
+ this PersistentAgentsClient persistentAgentsClient,
+ string agentId,
+ ChatClientAgentOptions options,
+ Func? clientFactory = null,
+ CancellationToken cancellationToken = default)
+ {
+ if (persistentAgentsClient is null)
+ {
+ throw new ArgumentNullException(nameof(persistentAgentsClient));
+ }
+
+ if (string.IsNullOrWhiteSpace(agentId))
+ {
+ throw new ArgumentException($"{nameof(agentId)} should not be null or whitespace.", nameof(agentId));
+ }
+
+ if (options is null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ var persistentAgentResponse = await persistentAgentsClient.Administration.GetAgentAsync(agentId, cancellationToken).ConfigureAwait(false);
+ return persistentAgentsClient.GetAIAgent(persistentAgentResponse, options, clientFactory);
+ }
+
///
/// Creates a new server side agent using the provided .
///
@@ -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);
}
+
+ ///
+ /// Creates a new server side agent using the provided .
+ ///
+ /// The to create the agent with.
+ /// The model to be used by the agent.
+ /// Full set of options to configure the agent.
+ /// Provides a way to customize the creation of the underlying used by the agent.
+ /// The to monitor for cancellation requests. The default is .
+ /// A instance that can be used to perform operations on the newly created agent.
+ /// Thrown when or or is .
+ /// Thrown when is empty or whitespace.
+ public static ChatClientAgent CreateAIAgent(
+ this PersistentAgentsClient persistentAgentsClient,
+ string model,
+ ChatClientAgentOptions options,
+ Func? clientFactory = null,
+ CancellationToken cancellationToken = default)
+ {
+ if (persistentAgentsClient is null)
+ {
+ throw new ArgumentNullException(nameof(persistentAgentsClient));
+ }
+
+ if (string.IsNullOrWhiteSpace(model))
+ {
+ throw new ArgumentException($"{nameof(model)} should not be null or whitespace.", nameof(model));
+ }
+
+ if (options is null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ var toolDefinitionsAndResources = ConvertAIToolsToToolDefinitions(options.ChatOptions?.Tools);
+
+ var createPersistentAgentResponse = persistentAgentsClient.Administration.CreateAgent(
+ model: model,
+ name: options.Name,
+ description: options.Description,
+ instructions: options.Instructions,
+ tools: toolDefinitionsAndResources.ToolDefinitions,
+ toolResources: toolDefinitionsAndResources.ToolResources,
+ temperature: null,
+ topP: null,
+ responseFormat: null,
+ metadata: null,
+ cancellationToken: cancellationToken);
+
+ if (options.ChatOptions?.Tools is { Count: > 0 } && (toolDefinitionsAndResources.FunctionToolsAndOtherTools is null || options.ChatOptions.Tools.Count != toolDefinitionsAndResources.FunctionToolsAndOtherTools.Count))
+ {
+ options = options.Clone();
+ options.ChatOptions!.Tools = toolDefinitionsAndResources.FunctionToolsAndOtherTools;
+ }
+
+ // Get a local proxy for the agent to work with.
+ return persistentAgentsClient.GetAIAgent(createPersistentAgentResponse.Value.Id, options, clientFactory: clientFactory, cancellationToken: cancellationToken);
+ }
+
+ ///
+ /// Creates a new server side agent using the provided .
+ ///
+ /// The to create the agent with.
+ /// The model to be used by the agent.
+ /// Full set of options to configure the agent.
+ /// Provides a way to customize the creation of the underlying used by the agent.
+ /// The to monitor for cancellation requests. The default is .
+ /// A instance that can be used to perform operations on the newly created agent.
+ /// Thrown when or or is .
+ /// Thrown when is empty or whitespace.
+ public static async Task CreateAIAgentAsync(
+ this PersistentAgentsClient persistentAgentsClient,
+ string model,
+ ChatClientAgentOptions options,
+ Func? clientFactory = null,
+ CancellationToken cancellationToken = default)
+ {
+ if (persistentAgentsClient is null)
+ {
+ throw new ArgumentNullException(nameof(persistentAgentsClient));
+ }
+
+ if (string.IsNullOrWhiteSpace(model))
+ {
+ throw new ArgumentException($"{nameof(model)} should not be null or whitespace.", nameof(model));
+ }
+
+ if (options is null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ var toolDefinitionsAndResources = ConvertAIToolsToToolDefinitions(options.ChatOptions?.Tools);
+
+ var createPersistentAgentResponse = await persistentAgentsClient.Administration.CreateAgentAsync(
+ model: model,
+ name: options.Name,
+ description: options.Description,
+ instructions: options.Instructions,
+ tools: toolDefinitionsAndResources.ToolDefinitions,
+ toolResources: toolDefinitionsAndResources.ToolResources,
+ temperature: null,
+ topP: null,
+ responseFormat: null,
+ metadata: null,
+ cancellationToken: cancellationToken).ConfigureAwait(false);
+
+ if (options.ChatOptions?.Tools is { Count: > 0 } && (toolDefinitionsAndResources.FunctionToolsAndOtherTools is null || options.ChatOptions.Tools.Count != toolDefinitionsAndResources.FunctionToolsAndOtherTools.Count))
+ {
+ options = options.Clone();
+ options.ChatOptions!.Tools = toolDefinitionsAndResources.FunctionToolsAndOtherTools;
+ }
+
+ // Get a local proxy for the agent to work with.
+ return await persistentAgentsClient.GetAIAgentAsync(createPersistentAgentResponse.Value.Id, options, clientFactory: clientFactory, cancellationToken: cancellationToken).ConfigureAwait(false);
+ }
+
+ private static (List? ToolDefinitions, ToolResources? ToolResources, List? FunctionToolsAndOtherTools) ConvertAIToolsToToolDefinitions(IList? tools)
+ {
+ List? toolDefinitions = null;
+ ToolResources? toolResources = null;
+ List? functionToolsAndOtherTools = null;
+
+ if (tools is not null)
+ {
+ foreach (AITool tool in tools)
+ {
+ switch (tool)
+ {
+ case HostedCodeInterpreterTool codeTool:
+
+ toolDefinitions ??= new();
+ toolDefinitions.Add(new CodeInterpreterToolDefinition());
+
+ if (codeTool.Inputs is { Count: > 0 })
+ {
+ foreach (var input in codeTool.Inputs)
+ {
+ switch (input)
+ {
+ case HostedFileContent hostedFile:
+ // If the input is a HostedFileContent, we can use its ID directly.
+ toolResources ??= new();
+ toolResources.CodeInterpreter ??= new();
+ toolResources.CodeInterpreter.FileIds.Add(hostedFile.FileId);
+ break;
+ }
+ }
+ }
+ break;
+
+ case HostedFileSearchTool fileSearchTool:
+ toolDefinitions ??= new();
+ toolDefinitions.Add(new FileSearchToolDefinition
+ {
+ FileSearch = new() { MaxNumResults = fileSearchTool.MaximumResultCount }
+ });
+
+ if (fileSearchTool.Inputs is { Count: > 0 })
+ {
+ foreach (var input in fileSearchTool.Inputs)
+ {
+ switch (input)
+ {
+ case HostedVectorStoreContent hostedVectorStore:
+ toolResources ??= new();
+ toolResources.FileSearch ??= new();
+ toolResources.FileSearch.VectorStoreIds.Add(hostedVectorStore.VectorStoreId);
+ break;
+ }
+ }
+ }
+ break;
+
+ case HostedWebSearchTool webSearch when webSearch.AdditionalProperties?.TryGetValue("connectionId", out object? connectionId) is true:
+ toolDefinitions ??= new();
+ toolDefinitions.Add(new BingGroundingToolDefinition(new BingGroundingSearchToolParameters([new BingGroundingSearchConfiguration(connectionId!.ToString())])));
+ break;
+
+ default:
+ functionToolsAndOtherTools ??= new();
+ functionToolsAndOtherTools.Add(tool);
+ break;
+ }
+ }
+ }
+
+ return (toolDefinitions, toolResources, functionToolsAndOtherTools);
+ }
}
diff --git a/dotnet/src/Microsoft.Agents.AI.OpenAI/Extensions/OpenAIAssistantClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.OpenAI/Extensions/OpenAIAssistantClientExtensions.cs
index 94410bb7d0..71f9b5436b 100644
--- a/dotnet/src/Microsoft.Agents.AI.OpenAI/Extensions/OpenAIAssistantClientExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.OpenAI/Extensions/OpenAIAssistantClientExtensions.cs
@@ -143,6 +143,155 @@ public static class OpenAIAssistantClientExtensions
return assistantClient.GetAIAgent(assistantResponse, chatOptions, clientFactory);
}
+ ///
+ /// Gets a from a .
+ ///
+ /// The assistant client.
+ /// The client result containing the assistant.
+ /// Full set of options to configure the agent.
+ /// Provides a way to customize the creation of the underlying used by the agent.
+ /// A instance that can be used to perform operations on the assistant.
+ /// or is .
+ public static ChatClientAgent GetAIAgent(
+ this AssistantClient assistantClient,
+ ClientResult assistantClientResult,
+ ChatClientAgentOptions options,
+ Func? clientFactory = null)
+ {
+ if (assistantClientResult is null)
+ {
+ throw new ArgumentNullException(nameof(assistantClientResult));
+ }
+
+ return assistantClient.GetAIAgent(assistantClientResult.Value, options, clientFactory);
+ }
+
+ ///
+ /// Gets a from an .
+ ///
+ /// The assistant client.
+ /// The assistant metadata.
+ /// Full set of options to configure the agent.
+ /// Provides a way to customize the creation of the underlying used by the agent.
+ /// A instance that can be used to perform operations on the assistant.
+ /// or is .
+ public static ChatClientAgent GetAIAgent(
+ this AssistantClient assistantClient,
+ Assistant assistantMetadata,
+ ChatClientAgentOptions options,
+ Func? 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);
+ }
+
+ ///
+ /// Retrieves an existing server side agent, wrapped as a using the provided .
+ ///
+ /// The to create the with.
+ /// The ID of the server side agent to create a for.
+ /// Full set of options to configure the agent.
+ /// Provides a way to customize the creation of the underlying used by the agent.
+ /// The to monitor for cancellation requests. The default is .
+ /// A instance that can be used to perform operations on the assistant agent.
+ /// or is .
+ /// is empty or whitespace.
+ public static ChatClientAgent GetAIAgent(
+ this AssistantClient assistantClient,
+ string agentId,
+ ChatClientAgentOptions options,
+ Func? 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);
+ }
+
+ ///
+ /// Retrieves an existing server side agent, wrapped as a using the provided .
+ ///
+ /// The to create the with.
+ /// The ID of the server side agent to create a for.
+ /// Full set of options to configure the agent.
+ /// Provides a way to customize the creation of the underlying used by the agent.
+ /// The to monitor for cancellation requests. The default is .
+ /// A instance that can be used to perform operations on the assistant agent.
+ /// or is .
+ /// is empty or whitespace.
+ public static async Task GetAIAgentAsync(
+ this AssistantClient assistantClient,
+ string agentId,
+ ChatClientAgentOptions options,
+ Func? 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);
+ }
+
///
/// Creates an AI agent from an using the OpenAI Assistant API.
///
@@ -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? ToolDefinitions, ToolResources? ToolResources, List? FunctionToolsAndOtherTools) ConvertAIToolsToToolDefinitions(IList? tools)
+ {
+ List? toolDefinitions = null;
+ ToolResources? toolResources = null;
+ List? functionToolsAndOtherTools = null;
+
+ if (tools is not null)
+ {
+ foreach (AITool tool in tools)
+ {
+ switch (tool)
+ {
+ case HostedCodeInterpreterTool codeTool:
+
+ toolDefinitions ??= new();
+ toolDefinitions.Add(new CodeInterpreterToolDefinition());
+
+ if (codeTool.Inputs is { Count: > 0 })
+ {
+ foreach (var input in codeTool.Inputs)
+ {
+ switch (input)
+ {
+ case HostedFileContent hostedFile:
+ // If the input is a HostedFileContent, we can use its ID directly.
+ toolResources ??= new();
+ toolResources.CodeInterpreter ??= new();
+ toolResources.CodeInterpreter.FileIds.Add(hostedFile.FileId);
+ break;
+ }
+ }
+ }
+ break;
+
+ case HostedFileSearchTool fileSearchTool:
+ toolDefinitions ??= new();
+ toolDefinitions.Add(new FileSearchToolDefinition
+ {
+ 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);
+ }
}
diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentOptions.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentOptions.cs
index aede5ef3cb..f83e6912d5 100644
--- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentOptions.cs
+++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentOptions.cs
@@ -101,7 +101,7 @@ public class ChatClientAgentOptions
///
/// Creates a new instance of with the same values as this instance.
///
- internal ChatClientAgentOptions Clone()
+ public ChatClientAgentOptions Clone()
=> new()
{
Id = this.Id,
diff --git a/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj
index c53ca9de0f..f5cafd2975 100644
--- a/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj
+++ b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj
@@ -7,6 +7,7 @@
+
diff --git a/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentCreateTests.cs b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentCreateTests.cs
new file mode 100644
index 0000000000..32b51b4196
--- /dev/null
+++ b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentCreateTests.cs
@@ -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();
+ 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);
+ }
+ }
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/Extensions/PersistentAgentsClientExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/Extensions/PersistentAgentsClientExtensionsTests.cs
index 705136ba0e..2405cd3347 100644
--- a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/Extensions/PersistentAgentsClientExtensionsTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/Extensions/PersistentAgentsClientExtensionsTests.cs
@@ -294,6 +294,438 @@ public sealed class PersistentAgentsClientExtensionsTests
Assert.Null(retrievedTestClient);
}
+ ///
+ /// Verify that GetAIAgent with Response and options works correctly.
+ ///
+ [Fact]
+ public void GetAIAgent_WithResponseAndOptions_WorksCorrectly()
+ {
+ // Arrange
+ var client = CreateFakePersistentAgentsClient();
+ var persistentAgent = ModelReaderWriter.Read(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);
+ }
+
+ ///
+ /// Verify that GetAIAgent with PersistentAgent and options works correctly.
+ ///
+ [Fact]
+ public void GetAIAgent_WithPersistentAgentAndOptions_WorksCorrectly()
+ {
+ // Arrange
+ var client = CreateFakePersistentAgentsClient();
+ var persistentAgent = ModelReaderWriter.Read(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);
+ }
+
+ ///
+ /// Verify that GetAIAgent with PersistentAgent and options falls back to agent metadata when options are null.
+ ///
+ [Fact]
+ public void GetAIAgent_WithPersistentAgentAndOptionsWithNullFields_FallsBackToAgentMetadata()
+ {
+ // Arrange
+ var client = CreateFakePersistentAgentsClient();
+ var persistentAgent = ModelReaderWriter.Read(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);
+ }
+
+ ///
+ /// Verify that GetAIAgent with agentId and options works correctly.
+ ///
+ [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);
+ }
+
+ ///
+ /// Verify that GetAIAgentAsync with agentId and options works correctly.
+ ///
+ [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);
+ }
+
+ ///
+ /// Verify that GetAIAgent with clientFactory parameter correctly applies the factory.
+ ///
+ [Fact]
+ public void GetAIAgent_WithOptionsAndClientFactory_AppliesFactoryCorrectly()
+ {
+ // Arrange
+ var client = CreateFakePersistentAgentsClient();
+ var persistentAgent = ModelReaderWriter.Read(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();
+ Assert.NotNull(retrievedTestClient);
+ Assert.Same(testChatClient, retrievedTestClient);
+ }
+
+ ///
+ /// Verify that GetAIAgent throws ArgumentNullException when response is null.
+ ///
+ [Fact]
+ public void GetAIAgent_WithNullResponse_ThrowsArgumentNullException()
+ {
+ // Arrange
+ var client = CreateFakePersistentAgentsClient();
+ var options = new ChatClientAgentOptions();
+
+ // Act & Assert
+ var exception = Assert.Throws(() =>
+ client.GetAIAgent((Response)null!, options));
+
+ Assert.Equal("persistentAgentResponse", exception.ParamName);
+ }
+
+ ///
+ /// Verify that GetAIAgent throws ArgumentNullException when persistentAgent is null.
+ ///
+ [Fact]
+ public void GetAIAgent_WithNullPersistentAgent_ThrowsArgumentNullException()
+ {
+ // Arrange
+ var client = CreateFakePersistentAgentsClient();
+ var options = new ChatClientAgentOptions();
+
+ // Act & Assert
+ var exception = Assert.Throws(() =>
+ client.GetAIAgent((PersistentAgent)null!, options));
+
+ Assert.Equal("persistentAgentMetadata", exception.ParamName);
+ }
+
+ ///
+ /// Verify that GetAIAgent throws ArgumentNullException when options is null.
+ ///
+ [Fact]
+ public void GetAIAgent_WithNullOptions_ThrowsArgumentNullException()
+ {
+ // Arrange
+ var client = CreateFakePersistentAgentsClient();
+ var persistentAgent = ModelReaderWriter.Read(BinaryData.FromString("""{"id": "agent_abc123"}"""))!;
+
+ // Act & Assert
+ var exception = Assert.Throws(() =>
+ client.GetAIAgent(persistentAgent, (ChatClientAgentOptions)null!));
+
+ Assert.Equal("options", exception.ParamName);
+ }
+
+ ///
+ /// Verify that GetAIAgent throws ArgumentException when agentId is empty.
+ ///
+ [Fact]
+ public void GetAIAgent_WithOptionsAndEmptyAgentId_ThrowsArgumentException()
+ {
+ // Arrange
+ var client = CreateFakePersistentAgentsClient();
+ var options = new ChatClientAgentOptions();
+
+ // Act & Assert
+ var exception = Assert.Throws(() =>
+ client.GetAIAgent(string.Empty, options));
+
+ Assert.Equal("agentId", exception.ParamName);
+ }
+
+ ///
+ /// Verify that GetAIAgentAsync throws ArgumentException when agentId is empty.
+ ///
+ [Fact]
+ public async Task GetAIAgentAsync_WithOptionsAndEmptyAgentId_ThrowsArgumentExceptionAsync()
+ {
+ // Arrange
+ var client = CreateFakePersistentAgentsClient();
+ var options = new ChatClientAgentOptions();
+
+ // Act & Assert
+ var exception = await Assert.ThrowsAsync(() =>
+ client.GetAIAgentAsync(string.Empty, options));
+
+ Assert.Equal("agentId", exception.ParamName);
+ }
+
+ ///
+ /// Verify that CreateAIAgent with options works correctly.
+ ///
+ [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);
+ }
+
+ ///
+ /// Verify that CreateAIAgentAsync with options works correctly.
+ ///
+ [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);
+ }
+
+ ///
+ /// Verify that CreateAIAgent with options and clientFactory applies the factory correctly.
+ ///
+ [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();
+ Assert.NotNull(retrievedTestClient);
+ Assert.Same(testChatClient, retrievedTestClient);
+ }
+
+ ///
+ /// Verify that CreateAIAgentAsync with options and clientFactory applies the factory correctly.
+ ///
+ [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();
+ Assert.NotNull(retrievedTestClient);
+ Assert.Same(testChatClient, retrievedTestClient);
+ }
+
+ ///
+ /// Verify that CreateAIAgent throws ArgumentNullException when options is null.
+ ///
+ [Fact]
+ public void CreateAIAgent_WithNullOptions_ThrowsArgumentNullException()
+ {
+ // Arrange
+ var client = CreateFakePersistentAgentsClient();
+
+ // Act & Assert
+ var exception = Assert.Throws(() =>
+ client.CreateAIAgent("test-model", (ChatClientAgentOptions)null!));
+
+ Assert.Equal("options", exception.ParamName);
+ }
+
+ ///
+ /// Verify that CreateAIAgentAsync throws ArgumentNullException when options is null.
+ ///
+ [Fact]
+ public async Task CreateAIAgentAsync_WithNullOptions_ThrowsArgumentNullExceptionAsync()
+ {
+ // Arrange
+ var client = CreateFakePersistentAgentsClient();
+
+ // Act & Assert
+ var exception = await Assert.ThrowsAsync(() =>
+ client.CreateAIAgentAsync("test-model", (ChatClientAgentOptions)null!));
+
+ Assert.Equal("options", exception.ParamName);
+ }
+
+ ///
+ /// Verify that CreateAIAgent throws ArgumentException when model is empty.
+ ///
+ [Fact]
+ public void CreateAIAgent_WithEmptyModel_ThrowsArgumentException()
+ {
+ // Arrange
+ var client = CreateFakePersistentAgentsClient();
+ var options = new ChatClientAgentOptions();
+
+ // Act & Assert
+ var exception = Assert.Throws(() =>
+ client.CreateAIAgent(string.Empty, options));
+
+ Assert.Equal("model", exception.ParamName);
+ }
+
+ ///
+ /// Verify that CreateAIAgentAsync throws ArgumentException when model is empty.
+ ///
+ [Fact]
+ public async Task CreateAIAgentAsync_WithEmptyModel_ThrowsArgumentExceptionAsync()
+ {
+ // Arrange
+ var client = CreateFakePersistentAgentsClient();
+ var options = new ChatClientAgentOptions();
+
+ // Act & Assert
+ var exception = await Assert.ThrowsAsync(() =>
+ client.CreateAIAgentAsync(string.Empty, options));
+
+ Assert.Equal("model", exception.ParamName);
+ }
+
///
/// Test custom chat client that can be used to verify clientFactory functionality.
///
diff --git a/dotnet/tests/Microsoft.Agents.AI.OpenAI.UnitTests/Extensions/OpenAIAssistantClientExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.OpenAI.UnitTests/Extensions/OpenAIAssistantClientExtensionsTests.cs
index 743cadabc4..61e3f5ef57 100644
--- a/dotnet/tests/Microsoft.Agents.AI.OpenAI.UnitTests/Extensions/OpenAIAssistantClientExtensionsTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.OpenAI.UnitTests/Extensions/OpenAIAssistantClientExtensionsTests.cs
@@ -207,6 +207,254 @@ public sealed class OpenAIAssistantClientExtensionsTests
Assert.Equal("options", exception.ParamName);
}
+ ///
+ /// Verify that GetAIAgent with ClientResult and options works correctly.
+ ///
+ [Fact]
+ public void GetAIAgent_WithClientResultAndOptions_WorksCorrectly()
+ {
+ // Arrange
+ var assistantClient = new TestAssistantClient();
+ var assistant = ModelReaderWriter.Read(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);
+ }
+
+ ///
+ /// Verify that GetAIAgent with Assistant and options works correctly.
+ ///
+ [Fact]
+ public void GetAIAgent_WithAssistantAndOptions_WorksCorrectly()
+ {
+ // Arrange
+ var assistantClient = new TestAssistantClient();
+ var assistant = ModelReaderWriter.Read(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);
+ }
+
+ ///
+ /// Verify that GetAIAgent with Assistant and options falls back to assistant metadata when options are null.
+ ///
+ [Fact]
+ public void GetAIAgent_WithAssistantAndOptionsWithNullFields_FallsBackToAssistantMetadata()
+ {
+ // Arrange
+ var assistantClient = new TestAssistantClient();
+ var assistant = ModelReaderWriter.Read(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);
+ }
+
+ ///
+ /// Verify that GetAIAgent with agentId and options works correctly.
+ ///
+ [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);
+ }
+
+ ///
+ /// Verify that GetAIAgentAsync with agentId and options works correctly.
+ ///
+ [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);
+ }
+
+ ///
+ /// Verify that GetAIAgent with clientFactory parameter correctly applies the factory.
+ ///
+ [Fact]
+ public void GetAIAgent_WithClientFactory_AppliesFactoryCorrectly()
+ {
+ // Arrange
+ var assistantClient = new TestAssistantClient();
+ var assistant = ModelReaderWriter.Read(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();
+ Assert.NotNull(retrievedTestClient);
+ Assert.Same(testChatClient, retrievedTestClient);
+ }
+
+ ///
+ /// Verify that GetAIAgent throws ArgumentNullException when assistantClientResult is null.
+ ///
+ [Fact]
+ public void GetAIAgent_WithNullClientResult_ThrowsArgumentNullException()
+ {
+ // Arrange
+ var assistantClient = new TestAssistantClient();
+ var options = new ChatClientAgentOptions();
+
+ // Act & Assert
+ var exception = Assert.Throws(() =>
+ assistantClient.GetAIAgent((ClientResult)null!, options));
+
+ Assert.Equal("assistantClientResult", exception.ParamName);
+ }
+
+ ///
+ /// Verify that GetAIAgent throws ArgumentNullException when assistant is null.
+ ///
+ [Fact]
+ public void GetAIAgent_WithNullAssistant_ThrowsArgumentNullException()
+ {
+ // Arrange
+ var assistantClient = new TestAssistantClient();
+ var options = new ChatClientAgentOptions();
+
+ // Act & Assert
+ var exception = Assert.Throws(() =>
+ assistantClient.GetAIAgent((Assistant)null!, options));
+
+ Assert.Equal("assistantMetadata", exception.ParamName);
+ }
+
+ ///
+ /// Verify that GetAIAgent throws ArgumentNullException when options is null.
+ ///
+ [Fact]
+ public void GetAIAgent_WithNullOptions_ThrowsArgumentNullException()
+ {
+ // Arrange
+ var assistantClient = new TestAssistantClient();
+ var assistant = ModelReaderWriter.Read(BinaryData.FromString("""{"id": "asst_abc123"}"""))!;
+
+ // Act & Assert
+ var exception = Assert.Throws(() =>
+ assistantClient.GetAIAgent(assistant, (ChatClientAgentOptions)null!));
+
+ Assert.Equal("options", exception.ParamName);
+ }
+
+ ///
+ /// Verify that GetAIAgent throws ArgumentException when agentId is empty.
+ ///
+ [Fact]
+ public void GetAIAgent_WithEmptyAgentId_ThrowsArgumentException()
+ {
+ // Arrange
+ var assistantClient = new TestAssistantClient();
+ var options = new ChatClientAgentOptions();
+
+ // Act & Assert
+ var exception = Assert.Throws(() =>
+ assistantClient.GetAIAgent(string.Empty, options));
+
+ Assert.Equal("agentId", exception.ParamName);
+ }
+
+ ///
+ /// Verify that GetAIAgentAsync throws ArgumentException when agentId is empty.
+ ///
+ [Fact]
+ public async Task GetAIAgentAsync_WithEmptyAgentId_ThrowsArgumentExceptionAsync()
+ {
+ // Arrange
+ var assistantClient = new TestAssistantClient();
+ var options = new ChatClientAgentOptions();
+
+ // Act & Assert
+ var exception = await Assert.ThrowsAsync(() =>
+ assistantClient.GetAIAgentAsync(string.Empty, options));
+
+ Assert.Equal("agentId", exception.ParamName);
+ }
+
///
/// Creates a test AssistantClient implementation for testing.
///
@@ -220,6 +468,17 @@ public sealed class OpenAIAssistantClientExtensionsTests
{
return ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString("""{"id": "asst_abc123"}""")), new FakePipelineResponse())!;
}
+
+ public override ClientResult GetAssistant(string assistantId, CancellationToken cancellationToken = default)
+ {
+ return ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString("""{"id": "asst_abc123", "name": "Original Name", "description": "Original Description", "instructions": "Original Instructions"}""")), new FakePipelineResponse())!;
+ }
+
+ public override async Task> GetAssistantAsync(string assistantId, CancellationToken cancellationToken = default)
+ {
+ await Task.Delay(1, cancellationToken); // Simulate async operation
+ return ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString("""{"id": "asst_abc123", "name": "Original Name", "description": "Original Description", "instructions": "Original Instructions"}""")), new FakePipelineResponse())!;
+ }
}
private sealed class TestChatClient : DelegatingChatClient
diff --git a/dotnet/tests/OpenAIAssistant.IntegrationTests/OpenAIAssistant.IntegrationTests.csproj b/dotnet/tests/OpenAIAssistant.IntegrationTests/OpenAIAssistant.IntegrationTests.csproj
index df161719c9..17ca46e4af 100644
--- a/dotnet/tests/OpenAIAssistant.IntegrationTests/OpenAIAssistant.IntegrationTests.csproj
+++ b/dotnet/tests/OpenAIAssistant.IntegrationTests/OpenAIAssistant.IntegrationTests.csproj
@@ -1,7 +1,8 @@
-
+
$(ProjectsTargetFrameworks)
+ $(ProjectsDebugTargetFrameworks)
True
$(NoWarn);OPENAI001;
@@ -12,6 +13,7 @@
+
diff --git a/dotnet/tests/OpenAIAssistant.IntegrationTests/OpenAIAssistantClientExtensionsTests.cs b/dotnet/tests/OpenAIAssistant.IntegrationTests/OpenAIAssistantClientExtensionsTests.cs
new file mode 100644
index 0000000000..c5a683ec2d
--- /dev/null
+++ b/dotnet/tests/OpenAIAssistant.IntegrationTests/OpenAIAssistantClientExtensionsTests.cs
@@ -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();
+ 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);
+ }
+ }
+}