Files
agent-framework/dotnet/tests/OpenAIAssistant.IntegrationTests/OpenAIAssistantClientExtensionsTests.cs
Chris 904a5b843e Python / .NET Samples - Restructure and Improve Samples (Feature Branc… (#4092)
* Python: .NET Samples - Restructure and Improve Samples (Feature Branch) (#4091)

* Moved by agent (#4094)

* Fix readme links

* .NET Samples - Create `04-hosting` learning path step (#4098)

* Agent move

* Agent reorderd

* Remove A2A section from README 

Removed A2A section from the Getting Started README.

* Agent fixed links

* Fix broken sample links in durable-agents README (#4101)

* Initial plan

* Fix broken internal links in documentation

Co-authored-by: crickman <66376200+crickman@users.noreply.github.com>

* Revert template link changes; keep only durable-agents README fix

Co-authored-by: crickman <66376200+crickman@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: crickman <66376200+crickman@users.noreply.github.com>

* .NET Samples - Create `03-workflows` learning path step (#4102)

* Fix solution project path

* Python: Fix broken markdown links to repo resources (outside /docs) (#4105)

* Initial plan

* Fix broken markdown links to repo resources

Co-authored-by: crickman <66376200+crickman@users.noreply.github.com>

* Update README to rename .NET Workflows Samples section

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: crickman <66376200+crickman@users.noreply.github.com>

* .NET Samples - Create `02-agents` learning path step (#4107)

* .NET: Fix broken relative link in GroupChatToolApproval README (#4108)

* Initial plan

* Fix broken link in GroupChatToolApproval README

Co-authored-by: crickman <66376200+crickman@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: crickman <66376200+crickman@users.noreply.github.com>

* Update labeler configuration for workflow samples

* .NET - Reorder Agents samples to start from Step01 instead of Step04 (#4110)

* Fix solution

* Resolve new sample paths

* Move new AgentSkills and AgentWithMemory_Step04 samples

* Fix link

* Fix readme path

* fix: update stale dotnet/samples/Durable path reference in AGENTS.md

Co-authored-by: crickman <66376200+crickman@users.noreply.github.com>

* Moved new sample

* Update solution

* Resolve merge (new sample)

* Sync to new sample - FoundryAgents_Step21_BingCustomSearch

* Updated README

* .NET Samples - Configuration Naming Update (#4149)

* .NET: Restore AzureFunctions index parity with ConsoleApps under DurableAgents samples (#4221)

* Clean-up `05_host_your_agent`

* Config setting consistency

* Refine samples

* AGENTS.md

* Move new samples

* Re-order samples

* Move new project and fixup solution

* Fixup model config

* Fix up new UT project

---------

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
2026-02-26 00:56:10 +00:00

267 lines
12 KiB
C#

// Copyright (c) Microsoft. All rights reserved.
#pragma warning disable CS0618 // Type or member is obsolete - Testing deprecated OpenAI Assistants API extension methods
using System;
using System.Diagnostics;
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 readonly AssistantClient _assistantClient = new OpenAIClient(TestConfiguration.GetRequiredValue(TestSettings.OpenAIApiKey)).GetAssistantClient();
private readonly OpenAIFileClient _fileClient = new OpenAIClient(TestConfiguration.GetRequiredValue(TestSettings.OpenAIApiKey)).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: TestConfiguration.GetRequiredValue(TestSettings.OpenAIChatModelName),
options: new ChatClientAgentOptions()
{
ChatOptions = new()
{
Instructions = AgentInstructions,
Tools = [weatherFunction]
}
}),
"CreateWithChatClientAgentOptionsSync" => await this._assistantClient.CreateAIAgentAsync(
model: TestConfiguration.GetRequiredValue(TestSettings.OpenAIChatModelName),
options: new ChatClientAgentOptions()
{
ChatOptions = new()
{
Instructions = AgentInstructions,
Tools = [weatherFunction]
}
}),
"CreateWithParamsAsync" => await this._assistantClient.CreateAIAgentAsync(
model: TestConfiguration.GetRequiredValue(TestSettings.OpenAIChatModelName),
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: TestConfiguration.GetRequiredValue(TestSettings.OpenAIChatModelName),
options: new ChatClientAgentOptions()
{
ChatOptions = new()
{
Instructions = Instructions,
Tools = [codeInterpreterTool]
}
}),
"CreateWithChatClientAgentOptionsSync" => await this._assistantClient.CreateAIAgentAsync(
model: TestConfiguration.GetRequiredValue(TestSettings.OpenAIChatModelName),
options: new ChatClientAgentOptions()
{
ChatOptions = new()
{
Instructions = Instructions,
Tools = [codeInterpreterTool]
}
}),
"CreateWithParamsAsync" => await this._assistantClient.CreateAIAgentAsync(
model: TestConfiguration.GetRequiredValue(TestSettings.OpenAIChatModelName),
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(Skip = "For manual testing only")]
[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(TestConfiguration.GetRequiredValue(TestSettings.OpenAIApiKey)).GetVectorStoreClient();
var vectorStoreCreate = await vectorStoreClient.CreateVectorStoreAsync(options: new VectorStoreCreationOptions()
{
Name = "WordCodeLookup_VectorStore",
FileIds = { uploadedFileId }
});
string vectorStoreId = vectorStoreCreate.Value.Id;
// Wait for vector store indexing to complete before using it
await WaitForVectorStoreReadyAsync(vectorStoreClient, vectorStoreId);
var fileSearchTool = new HostedFileSearchTool() { Inputs = [new HostedVectorStoreContent(vectorStoreId)] };
var agent = createMechanism switch
{
"CreateWithChatClientAgentOptionsAsync" => await this._assistantClient.CreateAIAgentAsync(
model: TestConfiguration.GetRequiredValue(TestSettings.OpenAIChatModelName),
options: new ChatClientAgentOptions()
{
ChatOptions = new()
{
Instructions = Instructions,
Tools = [fileSearchTool]
}
}),
"CreateWithChatClientAgentOptionsSync" => await this._assistantClient.CreateAIAgentAsync(
model: TestConfiguration.GetRequiredValue(TestSettings.OpenAIChatModelName),
options: new ChatClientAgentOptions()
{
ChatOptions = new()
{
Instructions = Instructions,
Tools = [fileSearchTool]
}
}),
"CreateWithParamsAsync" => await this._assistantClient.CreateAIAgentAsync(
model: TestConfiguration.GetRequiredValue(TestSettings.OpenAIChatModelName),
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);
}
}
/// <summary>
/// Waits for a vector store to complete indexing by polling its status.
/// </summary>
/// <param name="client">The vector store client.</param>
/// <param name="vectorStoreId">The ID of the vector store.</param>
/// <param name="maxWaitSeconds">Maximum time to wait in seconds (default: 30).</param>
/// <returns>A task that completes when the vector store is ready or throws on timeout/failure.</returns>
private static async Task WaitForVectorStoreReadyAsync(
VectorStoreClient client,
string vectorStoreId,
int maxWaitSeconds = 30)
{
Stopwatch sw = Stopwatch.StartNew();
while (sw.Elapsed.TotalSeconds < maxWaitSeconds)
{
VectorStore vectorStore = await client.GetVectorStoreAsync(vectorStoreId);
VectorStoreStatus status = vectorStore.Status;
if (status == VectorStoreStatus.Completed)
{
if (vectorStore.FileCounts.Failed > 0)
{
throw new InvalidOperationException("Vector store indexing failed for some files");
}
return;
}
if (status == VectorStoreStatus.Expired)
{
throw new InvalidOperationException("Vector store has expired");
}
await Task.Delay(1000);
}
throw new TimeoutException($"Vector store did not complete indexing within {maxWaitSeconds}s");
}
}