chore: import upstream snapshot with attribution
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);CS8793</NoWarn>
|
||||
<InjectSharedIntegrationTestCode>True</InjectSharedIntegrationTestCode>
|
||||
<InjectSharedIntegrationTestAzureCredentialsCode>True</InjectSharedIntegrationTestAzureCredentialsCode>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
|
||||
<ProjectReference Include="..\AgentConformance.IntegrationTests\AgentConformance.IntegrationTests.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,229 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using AgentConformance.IntegrationTests.Support;
|
||||
using Azure.AI.Projects;
|
||||
using Azure.AI.Projects.Agents;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Foundry;
|
||||
using OpenAI.Files;
|
||||
using OpenAI.Responses;
|
||||
using OpenAI.VectorStores;
|
||||
using Shared.IntegrationTests;
|
||||
|
||||
namespace Foundry.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for the file and vector-store forwarder extensions on
|
||||
/// <see cref="FoundryAgent"/> declared in <see cref="FoundryAgentExtensions"/>. End-to-end
|
||||
/// counterparts of the unit tests in
|
||||
/// <c>FoundryAgentExtensionsTests</c> that exercise the live Foundry project pipeline.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Mirrors <see cref="FoundryVersionedAgentCreateTests.CreateAgent_CreatesAgentWithVectorStoresAsync(string)"/>
|
||||
/// in shape (file upload → vector store creation → FileSearchTool answer → cleanup), but routes
|
||||
/// every helper call through the new <see cref="FoundryAgent"/> extensions instead of the raw
|
||||
/// <c>projectOpenAIClient.GetProjectFilesClient()</c> / <c>GetProjectVectorStoresClient()</c>
|
||||
/// path. Skipped by default for the same reasons as the existing vector-store IT (cost and
|
||||
/// runtime); flip Skip to run manually after seeding the right Foundry project.
|
||||
/// </remarks>
|
||||
public class FoundryAgentExtensionsTests
|
||||
{
|
||||
private readonly AIProjectClient _client = new(
|
||||
new Uri(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint)),
|
||||
TestAzureCliCredentials.CreateAzureCliCredential());
|
||||
|
||||
[Fact(Skip = "For manual testing only")]
|
||||
public async Task UploadFileAsync_ViaAgentExtension_UploadsToProjectAsync()
|
||||
{
|
||||
// Arrange — non-versioned Responses Agent (Mode 1) so we do not have to provision a server-side agent.
|
||||
var agent = this._client.AsAIAgent(
|
||||
model: TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName),
|
||||
instructions: "Be helpful.");
|
||||
var foundryAgent = this.WrapAsFoundryAgent(agent);
|
||||
|
||||
var filePath = Path.GetTempFileName() + ".txt";
|
||||
File.WriteAllText(filePath, "agent-extensions integration test payload");
|
||||
|
||||
OpenAIFile? uploaded = null;
|
||||
try
|
||||
{
|
||||
// Act.
|
||||
uploaded = await foundryAgent.UploadFileAsync(filePath, FileUploadPurpose.Assistants);
|
||||
|
||||
// Assert.
|
||||
Assert.NotNull(uploaded);
|
||||
Assert.False(string.IsNullOrEmpty(uploaded.Id));
|
||||
Assert.Equal(Path.GetFileName(filePath), uploaded.Filename);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (uploaded is not null)
|
||||
{
|
||||
await foundryAgent.DeleteFileAsync(uploaded.Id);
|
||||
}
|
||||
|
||||
File.Delete(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Skip = "For manual testing only")]
|
||||
public async Task DeleteFileAsync_ViaAgentExtension_RemovesUploadedFileAsync()
|
||||
{
|
||||
var agent = this._client.AsAIAgent(
|
||||
model: TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName),
|
||||
instructions: "Be helpful.");
|
||||
var foundryAgent = this.WrapAsFoundryAgent(agent);
|
||||
|
||||
var filePath = Path.GetTempFileName() + ".txt";
|
||||
File.WriteAllText(filePath, "delete-me payload");
|
||||
|
||||
try
|
||||
{
|
||||
var uploaded = await foundryAgent.UploadFileAsync(filePath, FileUploadPurpose.Assistants);
|
||||
|
||||
// Act.
|
||||
var result = await foundryAgent.DeleteFileAsync(uploaded.Id);
|
||||
|
||||
// Assert.
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(uploaded.Id, result.FileId);
|
||||
Assert.True(result.Deleted);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Skip = "For manual testing only")]
|
||||
public async Task CreateVectorStoreAsync_ViaAgentExtension_BuildsStoreAndAnswersFileSearchQuestionAsync()
|
||||
{
|
||||
// Mirrors CreateAgent_CreatesAgentWithVectorStoresAsync but the upload-then-create-store
|
||||
// sequence routes through the FoundryAgent.CreateVectorStoreAsync extension (single call
|
||||
// that uploads, creates the store, and polls until ready). The resulting vector store id
|
||||
// is then wired to a versioned agent's FileSearch tool and queried for a known value.
|
||||
string AgentName = FoundryVersionedAgentFixture.GenerateUniqueAgentName("VectorStoreExtAgent");
|
||||
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.
|
||||
""";
|
||||
|
||||
// Non-versioned helper agent that owns the upload pipeline.
|
||||
var helperAgent = this._client.AsAIAgent(
|
||||
model: TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName),
|
||||
instructions: "Be helpful.");
|
||||
var helperFoundryAgent = this.WrapAsFoundryAgent(helperAgent);
|
||||
|
||||
var searchFilePath = Path.GetTempFileName() + "wordcodelookup.txt";
|
||||
File.WriteAllText(searchFilePath, "The word 'apple' uses the code 442345, while the word 'banana' uses the code 673457.");
|
||||
|
||||
VectorStore? vectorStore = null;
|
||||
FoundryAgent? versionedAgent = null;
|
||||
try
|
||||
{
|
||||
// Act — single agent-level helper call uploads, creates, and waits until ready.
|
||||
vectorStore = await helperFoundryAgent.CreateVectorStoreAsync(
|
||||
"WordCodeLookup_ExtensionVectorStore",
|
||||
new[] { searchFilePath });
|
||||
|
||||
Assert.NotNull(vectorStore);
|
||||
Assert.False(string.IsNullOrEmpty(vectorStore.Id));
|
||||
Assert.NotEqual(VectorStoreStatus.InProgress, vectorStore.Status);
|
||||
|
||||
// Wire the store id into a versioned agent's FileSearch tool to prove it is actually usable.
|
||||
var definition = new DeclarativeAgentDefinition(TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName))
|
||||
{
|
||||
Instructions = AgentInstructions,
|
||||
Tools = { ResponseTool.CreateFileSearchTool(vectorStoreIds: [vectorStore.Id]) },
|
||||
};
|
||||
|
||||
var agentVersion = await this._client.AgentAdministrationClient.CreateAgentVersionAsync(
|
||||
AgentName,
|
||||
new ProjectsAgentVersionCreationOptions(definition));
|
||||
|
||||
versionedAgent = this._client.AsAIAgent(agentVersion);
|
||||
|
||||
// Assert.
|
||||
var result = await versionedAgent.RunAsync("Can you give me the documented code for 'banana'?");
|
||||
Assert.Contains("673457", result.ToString());
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (versionedAgent is not null)
|
||||
{
|
||||
await this._client.AgentAdministrationClient.DeleteAgentAsync(versionedAgent.Name);
|
||||
}
|
||||
|
||||
// Cleanup the vector store via the new extension too.
|
||||
if (vectorStore is not null)
|
||||
{
|
||||
await helperFoundryAgent.DeleteVectorStoreAsync(vectorStore.Id);
|
||||
}
|
||||
|
||||
File.Delete(searchFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Skip = "For manual testing only")]
|
||||
public async Task DeleteVectorStoreAsync_ViaAgentExtension_RemovesStoreAsync()
|
||||
{
|
||||
var agent = this._client.AsAIAgent(
|
||||
model: TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName),
|
||||
instructions: "Be helpful.");
|
||||
var foundryAgent = this.WrapAsFoundryAgent(agent);
|
||||
|
||||
var filePath = Path.GetTempFileName() + ".txt";
|
||||
File.WriteAllText(filePath, "delete-store payload");
|
||||
|
||||
VectorStore? vectorStore = null;
|
||||
try
|
||||
{
|
||||
vectorStore = await foundryAgent.CreateVectorStoreAsync(
|
||||
"DeleteVectorStore_ExtensionTest",
|
||||
new[] { filePath });
|
||||
|
||||
// Act.
|
||||
var result = await foundryAgent.DeleteVectorStoreAsync(vectorStore.Id);
|
||||
|
||||
// Assert.
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(vectorStore.Id, result.VectorStoreId);
|
||||
Assert.True(result.Deleted);
|
||||
vectorStore = null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (vectorStore is not null)
|
||||
{
|
||||
await foundryAgent.DeleteVectorStoreAsync(vectorStore.Id);
|
||||
}
|
||||
|
||||
File.Delete(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the underlying <see cref="FoundryAgent"/> from an <see cref="AIAgent"/> handle
|
||||
/// returned by <c>AIProjectClient.AsAIAgent(model, instructions)</c>. The Mode 1 overload
|
||||
/// returns a <see cref="ChatClientAgent"/>; the extension forwarders we test live on
|
||||
/// <see cref="FoundryAgent"/>, so callers wanting them through this entry point need to
|
||||
/// reach for the FoundryAgent constructor instead. This helper makes the test setup
|
||||
/// consistent across the four IT scenarios.
|
||||
/// </summary>
|
||||
private FoundryAgent WrapAsFoundryAgent(AIAgent agent)
|
||||
{
|
||||
// The Mode 1 AsAIAgent overload returns ChatClientAgent rather than FoundryAgent; use
|
||||
// the FoundryAgent projectEndpoint+model+instructions ctor to get the same underlying
|
||||
// FoundryChatClient surfaced through a FoundryAgent typed handle.
|
||||
_ = agent;
|
||||
return new FoundryAgent(
|
||||
projectEndpoint: new Uri(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint)),
|
||||
credential: TestAzureCliCredentials.CreateAzureCliCredential(),
|
||||
model: TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName),
|
||||
instructions: "Be helpful.");
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using AgentConformance.IntegrationTests;
|
||||
|
||||
namespace Foundry.IntegrationTests;
|
||||
|
||||
public class FoundryVersionedAgentChatClientRunStreamingTests() : ChatClientAgentRunStreamingTests<FoundryVersionedAgentFixture>(() => new())
|
||||
{
|
||||
public override Task RunWithInstructionsAndNoMessageReturnsExpectedResultAsync()
|
||||
{
|
||||
Assert.Skip("No messages is not supported");
|
||||
return base.RunWithInstructionsAndNoMessageReturnsExpectedResultAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using AgentConformance.IntegrationTests;
|
||||
|
||||
namespace Foundry.IntegrationTests;
|
||||
|
||||
public class FoundryVersionedAgentChatClientRunTests() : ChatClientAgentRunTests<FoundryVersionedAgentFixture>(() => new())
|
||||
{
|
||||
public override Task RunWithInstructionsAndNoMessageReturnsExpectedResultAsync()
|
||||
{
|
||||
Assert.Skip("No messages is not supported");
|
||||
return base.RunWithInstructionsAndNoMessageReturnsExpectedResultAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using AgentConformance.IntegrationTests.Support;
|
||||
using Azure.AI.Projects;
|
||||
using Azure.AI.Projects.Agents;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Foundry;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI.Files;
|
||||
using OpenAI.Responses;
|
||||
using Shared.IntegrationTests;
|
||||
|
||||
namespace Foundry.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for versioned <see cref="FoundryAgent"/> creation via
|
||||
/// <c>AIProjectClient.AgentAdministrationClient.CreateAgentVersionAsync</c> and <c>AIProjectClient.AsAIAgent(ProjectsAgentVersion)</c>.
|
||||
/// </summary>
|
||||
public class FoundryVersionedAgentCreateTests
|
||||
{
|
||||
private readonly AIProjectClient _client = new(new Uri(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint)), TestAzureCliCredentials.CreateAzureCliCredential());
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAgent_CreatesAgentWithCorrectMetadataAsync()
|
||||
{
|
||||
// Arrange.
|
||||
string AgentName = FoundryVersionedAgentFixture.GenerateUniqueAgentName("IntegrationTestAgent");
|
||||
const string AgentDescription = "An agent created during integration tests";
|
||||
const string AgentInstructions = "You are an integration test agent";
|
||||
|
||||
// Act.
|
||||
var agentVersion = await this._client.AgentAdministrationClient.CreateAgentVersionAsync(
|
||||
AgentName,
|
||||
new ProjectsAgentVersionCreationOptions(
|
||||
new DeclarativeAgentDefinition(TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName))
|
||||
{
|
||||
Instructions = AgentInstructions
|
||||
})
|
||||
{
|
||||
Description = AgentDescription
|
||||
});
|
||||
|
||||
var agent = this._client.AsAIAgent(agentVersion);
|
||||
|
||||
try
|
||||
{
|
||||
// Assert.
|
||||
Assert.NotNull(agent);
|
||||
Assert.Equal(AgentName, agent.Name);
|
||||
Assert.Equal(AgentDescription, agent.Description);
|
||||
Assert.Equal(AgentInstructions, agent.GetService<ChatClientAgent>()!.Instructions);
|
||||
|
||||
var agentRecord = await this._client.AgentAdministrationClient.GetAgentAsync(agent.Name);
|
||||
Assert.NotNull(agentRecord);
|
||||
Assert.Equal(AgentName, agentRecord.Value.Name);
|
||||
var definition = Assert.IsType<DeclarativeAgentDefinition>(agentRecord.Value.GetLatestVersion().Definition);
|
||||
Assert.Equal(AgentDescription, agentRecord.Value.GetLatestVersion().Description);
|
||||
Assert.Equal(AgentInstructions, definition.Instructions);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Cleanup.
|
||||
await this._client.AgentAdministrationClient.DeleteAgentAsync(agent.Name);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory(Skip = "For manual testing only")]
|
||||
[InlineData("FileSearchTool")]
|
||||
public async Task CreateAgent_CreatesAgentWithVectorStoresAsync(string _)
|
||||
{
|
||||
// Arrange.
|
||||
string AgentName = FoundryVersionedAgentFixture.GenerateUniqueAgentName("VectorStoreAgent");
|
||||
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.
|
||||
""";
|
||||
|
||||
// Get the project OpenAI client.
|
||||
var projectOpenAIClient = this._client.GetProjectOpenAIClient();
|
||||
|
||||
// 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."
|
||||
);
|
||||
OpenAIFile uploadedAgentFile = projectOpenAIClient.GetProjectFilesClient().UploadFile(
|
||||
filePath: searchFilePath,
|
||||
purpose: FileUploadPurpose.Assistants
|
||||
);
|
||||
var vectorStoreMetadata = await projectOpenAIClient.GetProjectVectorStoresClient().CreateVectorStoreAsync(options: new() { FileIds = { uploadedAgentFile.Id }, Name = "WordCodeLookup_VectorStore" });
|
||||
|
||||
// Act — create agent version with FileSearch tool via native SDK, then wrap with AsAIAgent.
|
||||
var definition = new DeclarativeAgentDefinition(TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName))
|
||||
{
|
||||
Instructions = AgentInstructions,
|
||||
Tools = { ResponseTool.CreateFileSearchTool(vectorStoreIds: [vectorStoreMetadata.Value.Id]) }
|
||||
};
|
||||
|
||||
var agentVersion = await this._client.AgentAdministrationClient.CreateAgentVersionAsync(
|
||||
AgentName,
|
||||
new ProjectsAgentVersionCreationOptions(definition));
|
||||
|
||||
var agent = this._client.AsAIAgent(agentVersion);
|
||||
|
||||
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._client.AgentAdministrationClient.DeleteAgentAsync(agent.Name);
|
||||
await projectOpenAIClient.GetProjectVectorStoresClient().DeleteVectorStoreAsync(vectorStoreMetadata.Value.Id);
|
||||
await projectOpenAIClient.GetProjectFilesClient().DeleteFileAsync(uploadedAgentFile.Id);
|
||||
File.Delete(searchFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAgent_CreatesAgentWithCodeInterpreterAsync()
|
||||
{
|
||||
// Arrange.
|
||||
string AgentName = FoundryVersionedAgentFixture.GenerateUniqueAgentName("CodeInterpreterAgent");
|
||||
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.
|
||||
""";
|
||||
|
||||
// Get the project OpenAI client.
|
||||
var projectOpenAIClient = this._client.GetProjectOpenAIClient();
|
||||
|
||||
// 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.
|
||||
);
|
||||
OpenAIFile uploadedCodeFile = projectOpenAIClient.GetProjectFilesClient().UploadFile(
|
||||
filePath: codeFilePath,
|
||||
purpose: FileUploadPurpose.Assistants
|
||||
);
|
||||
|
||||
// Act — create agent version with CodeInterpreter tool via native SDK, then wrap with AsAIAgent.
|
||||
var definition = new DeclarativeAgentDefinition(TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName))
|
||||
{
|
||||
Instructions = AgentInstructions,
|
||||
Tools = { ResponseTool.CreateCodeInterpreterTool(new CodeInterpreterToolContainer(CodeInterpreterToolContainerConfiguration.CreateAutomaticContainerConfiguration([uploadedCodeFile.Id]))) }
|
||||
};
|
||||
|
||||
var agentVersion = await this._client.AgentAdministrationClient.CreateAgentVersionAsync(
|
||||
AgentName,
|
||||
new ProjectsAgentVersionCreationOptions(definition));
|
||||
|
||||
var agent = this._client.AsAIAgent(agentVersion);
|
||||
|
||||
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._client.AgentAdministrationClient.DeleteAgentAsync(agent.Name);
|
||||
await projectOpenAIClient.GetProjectFilesClient().DeleteFileAsync(uploadedCodeFile.Id);
|
||||
File.Delete(codeFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that an agent version created with an OpenAPI tool definition via the native
|
||||
/// Azure.AI.Projects SDK and then wrapped with <c>AsAIAgent(agentVersion)</c> correctly
|
||||
/// invokes the server-side OpenAPI function through <c>RunAsync</c>.
|
||||
/// Regression test for https://github.com/microsoft/agent-framework/issues/4883.
|
||||
/// </summary>
|
||||
[RetryFact(Constants.RetryCount, Constants.RetryDelay)]
|
||||
public async Task AsAIAgent_WithOpenAPITool_NativeSDKCreation_InvokesServerSideToolAsync()
|
||||
{
|
||||
// Arrange — create agent version with OpenAPI tool using native Azure.AI.Projects SDK types.
|
||||
string AgentName = FoundryVersionedAgentFixture.GenerateUniqueAgentName("OpenAPITestAgent");
|
||||
const string AgentInstructions = "You are a helpful assistant that can use the countries API to retrieve information about countries by their currency code.";
|
||||
|
||||
const string CountriesOpenApiSpec = """
|
||||
{
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "REST Countries API",
|
||||
"description": "Retrieve information about countries by currency code",
|
||||
"version": "v3.1"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "https://restcountries.com/v3.1"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/currency/{currency}": {
|
||||
"get": {
|
||||
"description": "Get countries that use a specific currency code (e.g., USD, EUR, GBP)",
|
||||
"operationId": "GetCountriesByCurrency",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "currency",
|
||||
"in": "path",
|
||||
"description": "Currency code (e.g., USD, EUR, GBP)",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response with list of countries",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "No countries found for the currency"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
// Step 1: Create the OpenAPI function definition and agent version using native SDK types.
|
||||
var openApiFunction = new OpenApiFunctionDefinition(
|
||||
"get_countries",
|
||||
BinaryData.FromString(CountriesOpenApiSpec),
|
||||
new OpenAPIAnonymousAuthenticationDetails())
|
||||
{
|
||||
Description = "Retrieve information about countries by currency code"
|
||||
};
|
||||
|
||||
var definition = new DeclarativeAgentDefinition(model: TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName))
|
||||
{
|
||||
Instructions = AgentInstructions,
|
||||
Tools = { (ResponseTool)ProjectsAgentTool.CreateOpenApiTool(openApiFunction) }
|
||||
};
|
||||
|
||||
ProjectsAgentVersionCreationOptions creationOptions = new(definition);
|
||||
ProjectsAgentVersion agentVersion = await this._client.AgentAdministrationClient.CreateAgentVersionAsync(AgentName, creationOptions);
|
||||
|
||||
try
|
||||
{
|
||||
// Step 2: Wrap the agent version using AsAIAgent extension.
|
||||
FoundryAgent agent = this._client.AsAIAgent(agentVersion);
|
||||
|
||||
// Assert the agent was created correctly and retains version metadata.
|
||||
Assert.NotNull(agent);
|
||||
Assert.Equal(AgentName, agent.Name);
|
||||
var retrievedVersion = agent.GetService<ProjectsAgentVersion>();
|
||||
Assert.NotNull(retrievedVersion);
|
||||
|
||||
// Step 3: Call RunAsync to trigger the server-side OpenAPI function.
|
||||
var result = await agent.RunAsync("What countries use the Euro (EUR) as their currency? Please list them.");
|
||||
|
||||
// Step 4: Validate the OpenAPI tool was invoked server-side.
|
||||
// Note: Server-side OpenAPI tools (executed within the Responses API via AgentReference)
|
||||
// do not surface as FunctionCallContent in the MEAI abstraction — the API handles the full
|
||||
// tool loop internally. We validate tool invocation by asserting the response contains
|
||||
// multiple specific country names that the model would need API data to enumerate accurately.
|
||||
var text = result.ToString();
|
||||
Assert.NotEmpty(text);
|
||||
|
||||
// The response must mention multiple well-known Eurozone countries — requiring several
|
||||
// correct entries makes it highly unlikely the model answered purely from parametric knowledge.
|
||||
int matchCount = 0;
|
||||
foreach (var country in new[] { "Germany", "France", "Italy", "Spain", "Portugal", "Netherlands", "Belgium", "Austria", "Ireland", "Finland" })
|
||||
{
|
||||
if (text.Contains(country, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
matchCount++;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(
|
||||
matchCount >= 3,
|
||||
$"Expected response to list at least 3 Eurozone countries from the OpenAPI tool, but found {matchCount}. Response: {text}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Cleanup.
|
||||
await this._client.AgentAdministrationClient.DeleteAgentAsync(AgentName);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAgent_CreatesAgentWithAIFunctionToolsAsync()
|
||||
{
|
||||
// Arrange.
|
||||
string AgentName = FoundryVersionedAgentFixture.GenerateUniqueAgentName("WeatherAgent");
|
||||
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);
|
||||
|
||||
// Create agent version with the function tool registered in the server-side definition,
|
||||
// then wrap with AsAIAgent passing the local AIFunction implementation.
|
||||
var definition = new DeclarativeAgentDefinition(TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName))
|
||||
{
|
||||
Instructions = AgentInstructions,
|
||||
};
|
||||
definition.Tools.Add(weatherFunction.AsOpenAIResponseTool());
|
||||
|
||||
var agentVersion = await this._client.AgentAdministrationClient.CreateAgentVersionAsync(
|
||||
AgentName,
|
||||
new ProjectsAgentVersionCreationOptions(definition));
|
||||
|
||||
FoundryAgent agent = this._client.AsAIAgent(agentVersion, tools: [weatherFunction]);
|
||||
|
||||
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._client.AgentAdministrationClient.DeleteAgentAsync(agent.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AgentConformance.IntegrationTests;
|
||||
using AgentConformance.IntegrationTests.Support;
|
||||
using Azure.AI.Extensions.OpenAI;
|
||||
using Azure.AI.Projects;
|
||||
using Azure.AI.Projects.Agents;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Foundry;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI.Responses;
|
||||
using Shared.IntegrationTests;
|
||||
|
||||
namespace Foundry.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Integration test fixture that creates versioned Foundry agents via
|
||||
/// <c>AIProjectClient.AgentAdministrationClient.CreateAgentVersionAsync</c> and wraps them
|
||||
/// with <c>AIProjectClient.AsAIAgent(ProjectsAgentVersion)</c>.
|
||||
/// </summary>
|
||||
public class FoundryVersionedAgentFixture : IChatClientAgentFixture
|
||||
{
|
||||
private FoundryAgent _agent = null!;
|
||||
private AIProjectClient _client = null!;
|
||||
|
||||
public IChatClient ChatClient => this._agent.GetService<ChatClientAgent>()!.ChatClient;
|
||||
|
||||
public AIAgent Agent => this._agent;
|
||||
|
||||
public async Task<string> CreateConversationAsync()
|
||||
{
|
||||
var response = await this._client.GetProjectOpenAIClient().GetProjectConversationsClient().CreateProjectConversationAsync();
|
||||
return response.Value.Id;
|
||||
}
|
||||
|
||||
public async Task<List<ChatMessage>> GetChatHistoryAsync(AIAgent agent, AgentSession session)
|
||||
{
|
||||
var chatClientSession = (ChatClientAgentSession)session;
|
||||
|
||||
if (chatClientSession.ConversationId?.StartsWith("conv_", StringComparison.OrdinalIgnoreCase) == true)
|
||||
{
|
||||
return await this.GetChatHistoryFromConversationAsync(chatClientSession.ConversationId);
|
||||
}
|
||||
|
||||
if (chatClientSession.ConversationId?.StartsWith("resp_", StringComparison.OrdinalIgnoreCase) == true)
|
||||
{
|
||||
return await this.GetChatHistoryFromResponsesChainAsync(chatClientSession.ConversationId);
|
||||
}
|
||||
|
||||
var chatHistoryProvider = agent.GetService<ChatHistoryProvider>();
|
||||
|
||||
if (chatHistoryProvider is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return (await chatHistoryProvider.InvokingAsync(new(agent, session, []))).ToList();
|
||||
}
|
||||
|
||||
private async Task<List<ChatMessage>> GetChatHistoryFromResponsesChainAsync(string conversationId)
|
||||
{
|
||||
var openAIResponseClient = this._client.GetProjectOpenAIClient().GetProjectResponsesClient();
|
||||
var inputItems = await openAIResponseClient.GetResponseInputItemsAsync(conversationId).ToListAsync();
|
||||
var response = await openAIResponseClient.GetResponseAsync(conversationId);
|
||||
var responseItem = response.Value.OutputItems.FirstOrDefault()!;
|
||||
|
||||
// Take the messages that were the chat history leading up to the current response
|
||||
// remove the instruction messages, and reverse the order so that the most recent message is last.
|
||||
var previousMessages = inputItems
|
||||
.Select(ConvertToChatMessage)
|
||||
.Where(x => x.Text != "You are a helpful assistant.")
|
||||
.Reverse();
|
||||
|
||||
// Convert the response item to a chat message.
|
||||
var responseMessage = ConvertToChatMessage(responseItem);
|
||||
|
||||
// Concatenate the previous messages with the response message to get a full chat history
|
||||
// that includes the current response.
|
||||
return [.. previousMessages, responseMessage];
|
||||
}
|
||||
|
||||
private static ChatMessage ConvertToChatMessage(ResponseItem item)
|
||||
{
|
||||
if (item is MessageResponseItem messageResponseItem)
|
||||
{
|
||||
var role = messageResponseItem.Role == MessageRole.User ? ChatRole.User : ChatRole.Assistant;
|
||||
return new ChatMessage(role, messageResponseItem.Content.FirstOrDefault()?.Text);
|
||||
}
|
||||
|
||||
throw new NotSupportedException("This test currently only supports text messages");
|
||||
}
|
||||
|
||||
private async Task<List<ChatMessage>> GetChatHistoryFromConversationAsync(string conversationId)
|
||||
{
|
||||
List<ChatMessage> messages = [];
|
||||
await foreach (AgentResponseItem item in this._client.GetProjectOpenAIClient().GetProjectConversationsClient().GetProjectConversationItemsAsync(conversationId, order: "asc"))
|
||||
{
|
||||
var openAIItem = item.AsResponseResultItem();
|
||||
if (openAIItem is MessageResponseItem messageItem)
|
||||
{
|
||||
messages.Add(new ChatMessage
|
||||
{
|
||||
Role = new ChatRole(messageItem.Role.ToString()),
|
||||
Contents = messageItem.Content
|
||||
.Where(c => c.Kind is ResponseContentPartKind.OutputText or ResponseContentPartKind.InputText)
|
||||
.Select(c => new TextContent(c.Text))
|
||||
.ToList<AIContent>()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
public async Task<ChatClientAgent> CreateChatClientAgentAsync(
|
||||
string name = "HelpfulAssistant",
|
||||
string instructions = "You are a helpful assistant.",
|
||||
IList<AITool>? aiTools = null)
|
||||
{
|
||||
var definition = new DeclarativeAgentDefinition(TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName))
|
||||
{
|
||||
Instructions = instructions
|
||||
};
|
||||
|
||||
// Register AIFunction tool definitions in the server-side agent definition so the model
|
||||
// can invoke them. The local AIFunction implementations are matched by name via AsAIAgent.
|
||||
if (aiTools is not null)
|
||||
{
|
||||
foreach (var tool in aiTools)
|
||||
{
|
||||
if (tool.AsOpenAIResponseTool() is ResponseTool responseTool)
|
||||
{
|
||||
definition.Tools.Add(responseTool);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var agentVersion = await this._client.AgentAdministrationClient.CreateAgentVersionAsync(
|
||||
GenerateUniqueAgentName(name),
|
||||
new ProjectsAgentVersionCreationOptions(definition));
|
||||
|
||||
return this._client.AsAIAgent(agentVersion, tools: aiTools).GetService<ChatClientAgent>()!;
|
||||
}
|
||||
|
||||
public async Task<ChatClientAgent> CreateChatClientAgentAsync(ChatClientAgentOptions options)
|
||||
{
|
||||
options.Name ??= GenerateUniqueAgentName("HelpfulAssistant");
|
||||
|
||||
var definition = new DeclarativeAgentDefinition(
|
||||
options.ChatOptions?.ModelId ?? TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName))
|
||||
{
|
||||
Instructions = options.ChatOptions?.Instructions
|
||||
};
|
||||
|
||||
var agentVersion = await this._client.AgentAdministrationClient.CreateAgentVersionAsync(
|
||||
options.Name,
|
||||
new ProjectsAgentVersionCreationOptions(definition) { Description = options.Description });
|
||||
|
||||
var agent = this._client.AsAIAgent(agentVersion, tools: options.ChatOptions?.Tools);
|
||||
|
||||
return agent.GetService<ChatClientAgent>()!;
|
||||
}
|
||||
|
||||
public static string GenerateUniqueAgentName(string baseName) =>
|
||||
$"{baseName}-{Guid.NewGuid().ToString("N").Substring(0, 8)}";
|
||||
|
||||
public Task DeleteAgentAsync(ChatClientAgent agent) =>
|
||||
this._client.AgentAdministrationClient.DeleteAgentAsync(agent.Name);
|
||||
|
||||
public async Task DeleteSessionAsync(AgentSession session)
|
||||
{
|
||||
var typedSession = (ChatClientAgentSession)session;
|
||||
if (typedSession.ConversationId?.StartsWith("conv_", StringComparison.OrdinalIgnoreCase) == true)
|
||||
{
|
||||
await this._client.GetProjectOpenAIClient().GetProjectConversationsClient().DeleteConversationAsync(typedSession.ConversationId);
|
||||
}
|
||||
else if (typedSession.ConversationId?.StartsWith("resp_", StringComparison.OrdinalIgnoreCase) == true)
|
||||
{
|
||||
await this.DeleteResponseChainAsync(typedSession.ConversationId!);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteResponseChainAsync(string lastResponseId)
|
||||
{
|
||||
var response = await this._client.GetProjectOpenAIClient().GetProjectResponsesClient().GetResponseAsync(lastResponseId);
|
||||
await this._client.GetProjectOpenAIClient().GetProjectResponsesClient().DeleteResponseAsync(lastResponseId);
|
||||
|
||||
if (response.Value.PreviousResponseId is not null)
|
||||
{
|
||||
await this.DeleteResponseChainAsync(response.Value.PreviousResponseId);
|
||||
}
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
|
||||
if (this._client is not null && this._agent is not null)
|
||||
{
|
||||
return new ValueTask(this._client.AgentAdministrationClient.DeleteAgentAsync(this._agent.Name));
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public virtual async ValueTask InitializeAsync()
|
||||
{
|
||||
this._client = new(new Uri(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint)), TestAzureCliCredentials.CreateAzureCliCredential());
|
||||
|
||||
var agentVersion = await this._client.AgentAdministrationClient.CreateAgentVersionAsync(
|
||||
GenerateUniqueAgentName("HelpfulAssistant"),
|
||||
new ProjectsAgentVersionCreationOptions(
|
||||
new DeclarativeAgentDefinition(TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName))
|
||||
{
|
||||
Instructions = "You are a helpful assistant."
|
||||
}));
|
||||
|
||||
this._agent = this._client.AsAIAgent(agentVersion);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync(ChatClientAgentOptions options)
|
||||
{
|
||||
this._client = new(new Uri(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint)), TestAzureCliCredentials.CreateAzureCliCredential());
|
||||
options.Name ??= GenerateUniqueAgentName("HelpfulAssistant");
|
||||
|
||||
var definition = new DeclarativeAgentDefinition(
|
||||
options.ChatOptions?.ModelId ?? TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName))
|
||||
{
|
||||
Instructions = options.ChatOptions?.Instructions
|
||||
};
|
||||
|
||||
var agentVersion = await this._client.AgentAdministrationClient.CreateAgentVersionAsync(
|
||||
options.Name,
|
||||
new ProjectsAgentVersionCreationOptions(definition) { Description = options.Description });
|
||||
|
||||
this._agent = this._client.AsAIAgent(agentVersion, tools: options.ChatOptions?.Tools);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using AgentConformance.IntegrationTests;
|
||||
using Microsoft.Agents.AI;
|
||||
|
||||
namespace Foundry.IntegrationTests;
|
||||
|
||||
public class FoundryVersionedAgentRunStreamingPreviousResponseTests() : RunStreamingTests<FoundryVersionedAgentFixture>(() => new())
|
||||
{
|
||||
public override Task RunWithNoMessageDoesNotFailAsync()
|
||||
{
|
||||
Assert.Skip("No messages is not supported");
|
||||
return base.RunWithNoMessageDoesNotFailAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public class FoundryVersionedAgentRunStreamingConversationTests() : RunStreamingTests<FoundryVersionedAgentFixture>(() => new())
|
||||
{
|
||||
public override Func<Task<AgentRunOptions?>> AgentRunOptionsFactory => async () =>
|
||||
{
|
||||
var conversationId = await this.Fixture.CreateConversationAsync();
|
||||
return new ChatClientAgentRunOptions(new() { ConversationId = conversationId });
|
||||
};
|
||||
|
||||
public override Task RunWithNoMessageDoesNotFailAsync()
|
||||
{
|
||||
Assert.Skip("No messages is not supported");
|
||||
return base.RunWithNoMessageDoesNotFailAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using AgentConformance.IntegrationTests;
|
||||
using Microsoft.Agents.AI;
|
||||
|
||||
namespace Foundry.IntegrationTests;
|
||||
|
||||
public class FoundryVersionedAgentRunPreviousResponseTests() : RunTests<FoundryVersionedAgentFixture>(() => new())
|
||||
{
|
||||
public override Task RunWithNoMessageDoesNotFailAsync()
|
||||
{
|
||||
Assert.Skip("No messages is not supported");
|
||||
return base.RunWithNoMessageDoesNotFailAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public class FoundryVersionedAgentRunConversationTests() : RunTests<FoundryVersionedAgentFixture>(() => new())
|
||||
{
|
||||
public override Func<Task<AgentRunOptions?>> AgentRunOptionsFactory => async () =>
|
||||
{
|
||||
var conversationId = await this.Fixture.CreateConversationAsync();
|
||||
return new ChatClientAgentRunOptions(new() { ConversationId = conversationId });
|
||||
};
|
||||
|
||||
public override Task RunWithNoMessageDoesNotFailAsync()
|
||||
{
|
||||
Assert.Skip("No messages is not supported");
|
||||
return base.RunWithNoMessageDoesNotFailAsync();
|
||||
}
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using AgentConformance.IntegrationTests;
|
||||
using AgentConformance.IntegrationTests.Support;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Foundry.IntegrationTests;
|
||||
|
||||
public class FoundryVersionedAgentStructuredOutputRunTests() : StructuredOutputRunTests<FoundryVersionedAgentStructuredOutputFixture<CityInfo>>(() => new FoundryVersionedAgentStructuredOutputFixture<CityInfo>())
|
||||
{
|
||||
private const string NotSupported = "Versioned Foundry agents do not support specifying structured output type at invocation time.";
|
||||
private const string ResponseFormatNotSupported = "FoundryChatClient clears ResponseFormat for versioned agents; structured output must be defined in the server-side agent definition.";
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that response format provided at agent initialization is used when invoking RunAsync.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[RetryFact(Constants.RetryCount, Constants.RetryDelay, Skip = ResponseFormatNotSupported)]
|
||||
public async Task RunWithResponseFormatAtAgentInitializationReturnsExpectedResultAsync()
|
||||
{
|
||||
// Arrange
|
||||
var agent = this.Fixture.Agent;
|
||||
var session = await agent.CreateSessionAsync();
|
||||
await using var cleanup = new SessionCleanup(session, this.Fixture);
|
||||
|
||||
// Act
|
||||
var response = await agent.RunAsync(new ChatMessage(ChatRole.User, "Provide information about the capital of France."), session);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(response);
|
||||
Assert.Single(response.Messages);
|
||||
Assert.Contains("Paris", response.Text);
|
||||
Assert.True(TryDeserialize(response.Text, AgentAbstractionsJsonUtilities.DefaultOptions, out CityInfo cityInfo));
|
||||
Assert.Equal("Paris", cityInfo.Name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that generic RunAsync works with versioned Foundry agents when structured output is configured at agent initialization.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Versioned Foundry agents do not support specifying the structured output type at invocation time yet.
|
||||
/// The type T provided to RunAsync<T> is ignored by FoundryChatClient and is only used
|
||||
/// for deserializing the agent response by AgentResponse<T>.Result.
|
||||
/// </remarks>
|
||||
[RetryFact(Constants.RetryCount, Constants.RetryDelay, Skip = ResponseFormatNotSupported)]
|
||||
public async Task RunGenericWithResponseFormatAtAgentInitializationReturnsExpectedResultAsync()
|
||||
{
|
||||
// Arrange
|
||||
var agent = this.Fixture.Agent;
|
||||
var session = await agent.CreateSessionAsync();
|
||||
await using var cleanup = new SessionCleanup(session, this.Fixture);
|
||||
|
||||
// Act
|
||||
AgentResponse<CityInfo> response = await agent.RunAsync<CityInfo>(
|
||||
new ChatMessage(ChatRole.User, "Provide information about the capital of France."),
|
||||
session);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(response);
|
||||
Assert.Single(response.Messages);
|
||||
Assert.Contains("Paris", response.Text);
|
||||
|
||||
Assert.NotNull(response.Result);
|
||||
Assert.Equal("Paris", response.Result.Name);
|
||||
}
|
||||
|
||||
public override Task RunWithGenericTypeReturnsExpectedResultAsync()
|
||||
{
|
||||
Assert.Skip(NotSupported);
|
||||
return base.RunWithGenericTypeReturnsExpectedResultAsync();
|
||||
}
|
||||
|
||||
public override Task RunWithResponseFormatReturnsExpectedResultAsync()
|
||||
{
|
||||
Assert.Skip(NotSupported);
|
||||
return base.RunWithResponseFormatReturnsExpectedResultAsync();
|
||||
}
|
||||
|
||||
public override Task RunWithPrimitiveTypeReturnsExpectedResultAsync()
|
||||
{
|
||||
Assert.Skip(NotSupported);
|
||||
return base.RunWithPrimitiveTypeReturnsExpectedResultAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a fixture for testing versioned Foundry agents with structured output of type <typeparamref name="T"/> provided at agent initialization.
|
||||
/// </summary>
|
||||
public class FoundryVersionedAgentStructuredOutputFixture<T> : FoundryVersionedAgentFixture
|
||||
{
|
||||
public override async ValueTask InitializeAsync()
|
||||
{
|
||||
var agentOptions = new ChatClientAgentOptions
|
||||
{
|
||||
ChatOptions = new ChatOptions()
|
||||
{
|
||||
ResponseFormat = ChatResponseFormat.ForJsonSchema<T>(AgentAbstractionsJsonUtilities.DefaultOptions)
|
||||
},
|
||||
};
|
||||
|
||||
await this.InitializeAsync(agentOptions);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Azure.AI.Projects;
|
||||
using Azure.AI.Projects.Memory;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Foundry;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using OpenAI.Responses;
|
||||
using Shared.IntegrationTests;
|
||||
|
||||
namespace Foundry.IntegrationTests.Memory;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for <see cref="FoundryMemoryProvider"/> against a configured Azure AI Foundry Memory service.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// These integration tests are skipped by default and require a live Azure AI Foundry Memory service.
|
||||
/// The tests need to be updated to use the new AIAgent-based API pattern.
|
||||
/// </remarks>
|
||||
public sealed class FoundryMemoryProviderTests : IDisposable
|
||||
{
|
||||
private const string SkipReason = "Requires an Azure AI Foundry Memory service configured"; // Set to null to enable.
|
||||
|
||||
private readonly AIProjectClient? _client;
|
||||
private readonly string? _memoryStoreName;
|
||||
private readonly string? _deploymentName;
|
||||
private readonly string? _embeddingDeploymentName;
|
||||
private bool _disposed;
|
||||
|
||||
public FoundryMemoryProviderTests()
|
||||
{
|
||||
IConfigurationRoot configuration = new ConfigurationBuilder()
|
||||
.AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true)
|
||||
.AddEnvironmentVariables()
|
||||
.AddUserSecrets<FoundryMemoryProviderTests>(optional: true)
|
||||
.Build();
|
||||
|
||||
var endpoint = configuration[TestSettings.AzureAIProjectEndpoint];
|
||||
var memoryStoreName = configuration[TestSettings.AzureAIMemoryStoreId];
|
||||
var deploymentName = configuration[TestSettings.AzureAIModelDeploymentName];
|
||||
var embeddingDeploymentName = configuration[TestSettings.AzureAIEmbeddingDeploymentName];
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(endpoint) &&
|
||||
!string.IsNullOrWhiteSpace(memoryStoreName))
|
||||
{
|
||||
this._client = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential());
|
||||
this._memoryStoreName = memoryStoreName;
|
||||
this._deploymentName = deploymentName ?? "gpt-4.1-mini";
|
||||
this._embeddingDeploymentName = embeddingDeploymentName ?? "text-embedding-ada-002";
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Skip = SkipReason)]
|
||||
public async Task CanAddAndRetrieveUserMemoriesAsync()
|
||||
{
|
||||
// Arrange
|
||||
FoundryMemoryProvider memoryProvider = new(
|
||||
this._client!,
|
||||
this._memoryStoreName!,
|
||||
stateInitializer: _ => new(new FoundryMemoryProviderScope("it-user-1")));
|
||||
|
||||
await memoryProvider.EnsureMemoryStoreCreatedAsync(this._deploymentName!, this._embeddingDeploymentName!);
|
||||
|
||||
AIAgent agent = this._client!.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
ChatOptions = new ChatOptions
|
||||
{
|
||||
ModelId = this._deploymentName!,
|
||||
Instructions = "You are a helpful assistant. Use known memories about the user when responding, and do not invent details."
|
||||
},
|
||||
AIContextProviders = [memoryProvider]
|
||||
});
|
||||
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
|
||||
await memoryProvider.EnsureStoredMemoriesDeletedAsync(session);
|
||||
|
||||
// Act
|
||||
AgentResponse resultBefore = await agent.RunAsync("What is my name?", session);
|
||||
Assert.DoesNotContain("Caoimhe", resultBefore.Text);
|
||||
|
||||
await agent.RunAsync("Hello, my name is Caoimhe.", session);
|
||||
await memoryProvider.WhenUpdatesCompletedAsync();
|
||||
await Task.Delay(2000);
|
||||
|
||||
// Assert - verify memories were actually created in the store before querying via agent
|
||||
var searchResult = await this._client!.MemoryStores.SearchMemoriesAsync(
|
||||
this._memoryStoreName!,
|
||||
new MemorySearchOptions("it-user-1")
|
||||
{
|
||||
Items = { ResponseItem.CreateUserMessageItem("Caoimhe") }
|
||||
});
|
||||
Assert.NotEmpty(searchResult.Value.Memories);
|
||||
|
||||
AgentResponse resultAfter = await agent.RunAsync("What is my name?", session);
|
||||
|
||||
// Cleanup
|
||||
await memoryProvider.EnsureStoredMemoriesDeletedAsync(session);
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Caoimhe", resultAfter.Text);
|
||||
}
|
||||
|
||||
[Fact(Skip = SkipReason)]
|
||||
public async Task DoesNotLeakMemoriesAcrossScopesAsync()
|
||||
{
|
||||
// Arrange
|
||||
FoundryMemoryProvider memoryProvider1 = new(
|
||||
this._client!,
|
||||
this._memoryStoreName!,
|
||||
stateInitializer: _ => new(new FoundryMemoryProviderScope("it-scope-a")));
|
||||
|
||||
FoundryMemoryProvider memoryProvider2 = new(
|
||||
this._client!,
|
||||
this._memoryStoreName!,
|
||||
stateInitializer: _ => new(new FoundryMemoryProviderScope("it-scope-b")));
|
||||
|
||||
await memoryProvider1.EnsureMemoryStoreCreatedAsync(this._deploymentName!, this._embeddingDeploymentName!);
|
||||
|
||||
AIAgent agent1 = this._client!.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
ChatOptions = new ChatOptions
|
||||
{
|
||||
ModelId = this._deploymentName!,
|
||||
Instructions = "You are a helpful assistant. Use known memories about the user when responding, and do not invent details."
|
||||
},
|
||||
AIContextProviders = [memoryProvider1]
|
||||
});
|
||||
|
||||
AIAgent agent2 = this._client!.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
ChatOptions = new ChatOptions
|
||||
{
|
||||
ModelId = this._deploymentName!,
|
||||
Instructions = "You are a helpful assistant. Use known memories about the user when responding, and do not invent details."
|
||||
},
|
||||
AIContextProviders = [memoryProvider2]
|
||||
});
|
||||
|
||||
AgentSession session1 = await agent1.CreateSessionAsync();
|
||||
AgentSession session2 = await agent2.CreateSessionAsync();
|
||||
|
||||
await memoryProvider1.EnsureStoredMemoriesDeletedAsync(session1);
|
||||
await memoryProvider2.EnsureStoredMemoriesDeletedAsync(session2);
|
||||
|
||||
// Act - add memory only to scope A
|
||||
await agent1.RunAsync("Hello, I'm an AI tutor and my name is Caoimhe.", session1);
|
||||
await memoryProvider1.WhenUpdatesCompletedAsync();
|
||||
await Task.Delay(2000);
|
||||
|
||||
// Assert - verify memories were created in scope A but not in scope B
|
||||
var searchResultA = await this._client!.MemoryStores.SearchMemoriesAsync(
|
||||
this._memoryStoreName!,
|
||||
new MemorySearchOptions("it-scope-a")
|
||||
{
|
||||
Items = { ResponseItem.CreateUserMessageItem("Caoimhe") }
|
||||
});
|
||||
Assert.NotEmpty(searchResultA.Value.Memories);
|
||||
|
||||
var searchResultB = await this._client.MemoryStores.SearchMemoriesAsync(
|
||||
this._memoryStoreName!,
|
||||
new MemorySearchOptions("it-scope-b")
|
||||
{
|
||||
Items = { ResponseItem.CreateUserMessageItem("Caoimhe") }
|
||||
});
|
||||
Assert.Empty(searchResultB.Value.Memories);
|
||||
|
||||
AgentResponse result1 = await agent1.RunAsync("What is my name?", session1);
|
||||
AgentResponse result2 = await agent2.RunAsync("What is my name?", session2);
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Caoimhe", result1.Text);
|
||||
Assert.DoesNotContain("Caoimhe", result2.Text);
|
||||
|
||||
// Cleanup
|
||||
await memoryProvider1.EnsureStoredMemoriesDeletedAsync(session1);
|
||||
await memoryProvider2.EnsureStoredMemoriesDeletedAsync(session2);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!this._disposed)
|
||||
{
|
||||
this._disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using AgentConformance.IntegrationTests;
|
||||
|
||||
namespace Foundry.IntegrationTests;
|
||||
|
||||
public class ResponsesAgentChatClientRunStreamingTests() : ChatClientAgentRunStreamingTests<ResponsesAgentFixture>(() => new())
|
||||
{
|
||||
public override Task RunWithInstructionsAndNoMessageReturnsExpectedResultAsync()
|
||||
{
|
||||
Assert.Skip("No messages is not supported");
|
||||
return base.RunWithInstructionsAndNoMessageReturnsExpectedResultAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using AgentConformance.IntegrationTests;
|
||||
|
||||
namespace Foundry.IntegrationTests;
|
||||
|
||||
public class ResponsesAgentChatClientRunTests() : ChatClientAgentRunTests<ResponsesAgentFixture>(() => new())
|
||||
{
|
||||
public override Task RunWithInstructionsAndNoMessageReturnsExpectedResultAsync()
|
||||
{
|
||||
Assert.Skip("No messages is not supported");
|
||||
return base.RunWithInstructionsAndNoMessageReturnsExpectedResultAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using AgentConformance.IntegrationTests.Support;
|
||||
using Azure.AI.Extensions.OpenAI;
|
||||
using Azure.AI.Projects;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Shared.IntegrationTests;
|
||||
|
||||
namespace Foundry.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for non-versioned <see cref="ChatClientAgent"/> creation via <see cref="AIProjectClient"/> extension methods.
|
||||
/// </summary>
|
||||
public class ResponsesAgentExtensionCreateTests
|
||||
{
|
||||
private static Uri Endpoint => new(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint));
|
||||
|
||||
private static string Model => TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName);
|
||||
|
||||
private readonly AIProjectClient _client = new(Endpoint, TestAzureCliCredentials.CreateAzureCliCredential());
|
||||
|
||||
[Fact]
|
||||
public async Task AsAIAgent_WithModelAndInstructions_CreatesChatClientAgentAndRunsAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string AgentName = "ResponsesAgentExtensionSimple";
|
||||
const string AgentDescription = "Integration test agent created from AIProjectClient.AsAIAgent(model, instructions).";
|
||||
const string VerificationToken = "integration-extension-ok";
|
||||
|
||||
ChatClientAgent agent = this._client.AsAIAgent(
|
||||
model: Model,
|
||||
instructions: $"You are a helpful assistant. When asked for verification, reply with exactly '{VerificationToken}'.",
|
||||
name: AgentName,
|
||||
description: AgentDescription);
|
||||
|
||||
AgentSession? session = null;
|
||||
|
||||
try
|
||||
{
|
||||
var conversation = await CreateConversationAsync(this._client);
|
||||
session = await agent.CreateSessionAsync(conversation.Id);
|
||||
|
||||
// Act
|
||||
AgentResponse response = await agent.RunAsync("Return the verification token.", session);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(agent);
|
||||
Assert.Equal(AgentName, agent.Name);
|
||||
Assert.Equal(AgentDescription, agent.Description);
|
||||
Assert.NotNull(agent.GetService<IChatClient>());
|
||||
Assert.Contains(VerificationToken, response.Text, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await DeleteSessionAsync(this._client, session);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsAIAgent_WithOptions_CreatesChatClientAgentAndRunsAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string VerificationToken = "integration-options-ok";
|
||||
ChatClientAgentOptions options = new()
|
||||
{
|
||||
Name = "ResponsesAgentExtensionOptions",
|
||||
Description = "Integration test agent created from AIProjectClient.AsAIAgent(options).",
|
||||
ChatOptions = new ChatOptions
|
||||
{
|
||||
ModelId = Model,
|
||||
Instructions = $"You are a helpful assistant. When asked for verification, reply with exactly '{VerificationToken}'.",
|
||||
},
|
||||
};
|
||||
|
||||
ChatClientAgent agent = this._client.AsAIAgent(options);
|
||||
|
||||
ChatClientAgentSession? session = null;
|
||||
|
||||
try
|
||||
{
|
||||
var conversation = await CreateConversationAsync(this._client);
|
||||
session = ((await agent.CreateSessionAsync(conversation.Id)) as ChatClientAgentSession)!;
|
||||
|
||||
// Act
|
||||
AgentResponse response = await agent.RunAsync("Return the verification token.", session);
|
||||
|
||||
// Assert
|
||||
Assert.StartsWith("conv_", session!.ConversationId, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Equal(options.Name, agent.Name);
|
||||
Assert.Equal(options.Description, agent.Description);
|
||||
Assert.Contains(VerificationToken, response.Text, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await DeleteSessionAsync(this._client, session);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task DeleteSessionAsync(AIProjectClient client, AgentSession? session)
|
||||
{
|
||||
if (session is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ChatClientAgentSession typedSession = (ChatClientAgentSession)session;
|
||||
|
||||
if (typedSession.ConversationId?.StartsWith("conv_", StringComparison.OrdinalIgnoreCase) == true)
|
||||
{
|
||||
await client.GetProjectOpenAIClient().GetProjectConversationsClient().DeleteConversationAsync(typedSession.ConversationId);
|
||||
}
|
||||
else if (typedSession.ConversationId?.StartsWith("resp_", StringComparison.OrdinalIgnoreCase) == true)
|
||||
{
|
||||
await DeleteResponseChainAsync(client, typedSession.ConversationId);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task DeleteResponseChainAsync(AIProjectClient client, string lastResponseId)
|
||||
{
|
||||
var responsesClient = client.GetProjectOpenAIClient().GetProjectResponsesClient();
|
||||
var response = await responsesClient.GetResponseAsync(lastResponseId);
|
||||
await responsesClient.DeleteResponseAsync(lastResponseId);
|
||||
|
||||
if (response.Value.PreviousResponseId is not null)
|
||||
{
|
||||
await DeleteResponseChainAsync(client, response.Value.PreviousResponseId);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<ProjectConversation> CreateConversationAsync(AIProjectClient client)
|
||||
{
|
||||
ProjectConversationsClient conversationsClient = client.GetProjectOpenAIClient().GetProjectConversationsClient();
|
||||
return (await conversationsClient.CreateProjectConversationAsync()).Value!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AgentConformance.IntegrationTests;
|
||||
using AgentConformance.IntegrationTests.Support;
|
||||
using Azure.AI.Extensions.OpenAI;
|
||||
using Azure.AI.Projects;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using OpenAI.Responses;
|
||||
using Shared.IntegrationTests;
|
||||
|
||||
namespace Foundry.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Integration test fixture that creates non-versioned Responses agents via the direct <c>AIProjectClient.AsAIAgent(...)</c> path.
|
||||
/// </summary>
|
||||
public class ResponsesAgentFixture : IChatClientAgentFixture
|
||||
{
|
||||
private ChatClientAgent _agent = null!;
|
||||
private AIProjectClient _client = null!;
|
||||
|
||||
public IChatClient ChatClient => this._agent.GetService<ChatClientAgent>()!.ChatClient;
|
||||
|
||||
public AIAgent Agent => this._agent;
|
||||
|
||||
public async Task<string> CreateConversationAsync()
|
||||
{
|
||||
var response = await this._client.GetProjectOpenAIClient().GetProjectConversationsClient().CreateProjectConversationAsync();
|
||||
return response.Value.Id;
|
||||
}
|
||||
|
||||
public async Task<List<ChatMessage>> GetChatHistoryAsync(AIAgent agent, AgentSession session)
|
||||
{
|
||||
ChatClientAgentSession chatClientSession = (ChatClientAgentSession)session;
|
||||
|
||||
if (chatClientSession.ConversationId?.StartsWith("conv_", StringComparison.OrdinalIgnoreCase) == true)
|
||||
{
|
||||
return await this.GetChatHistoryFromConversationAsync(chatClientSession.ConversationId);
|
||||
}
|
||||
|
||||
if (chatClientSession.ConversationId?.StartsWith("resp_", StringComparison.OrdinalIgnoreCase) == true)
|
||||
{
|
||||
return await this.GetChatHistoryFromResponsesChainAsync(chatClientSession.ConversationId);
|
||||
}
|
||||
|
||||
ChatHistoryProvider? chatHistoryProvider = agent.GetService<ChatHistoryProvider>();
|
||||
|
||||
if (chatHistoryProvider is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return (await chatHistoryProvider.InvokingAsync(new(agent, session, []))).ToList();
|
||||
}
|
||||
|
||||
private async Task<List<ChatMessage>> GetChatHistoryFromResponsesChainAsync(string conversationId)
|
||||
{
|
||||
var openAIResponseClient = this._client.GetProjectOpenAIClient().GetProjectResponsesClient();
|
||||
var inputItems = await openAIResponseClient.GetResponseInputItemsAsync(conversationId).ToListAsync();
|
||||
var response = await openAIResponseClient.GetResponseAsync(conversationId);
|
||||
ResponseItem responseItem = response.Value.OutputItems.FirstOrDefault()!;
|
||||
|
||||
var previousMessages = inputItems
|
||||
.Select(ConvertToChatMessage)
|
||||
.Where(x => x.Text != "You are a helpful assistant.")
|
||||
.Reverse();
|
||||
|
||||
ChatMessage responseMessage = ConvertToChatMessage(responseItem);
|
||||
|
||||
return [.. previousMessages, responseMessage];
|
||||
}
|
||||
|
||||
private static ChatMessage ConvertToChatMessage(ResponseItem item)
|
||||
{
|
||||
if (item is MessageResponseItem messageResponseItem)
|
||||
{
|
||||
ChatRole role = messageResponseItem.Role == MessageRole.User ? ChatRole.User : ChatRole.Assistant;
|
||||
return new ChatMessage(role, messageResponseItem.Content.FirstOrDefault()?.Text);
|
||||
}
|
||||
|
||||
throw new NotSupportedException("This test currently only supports text messages");
|
||||
}
|
||||
|
||||
private async Task<List<ChatMessage>> GetChatHistoryFromConversationAsync(string conversationId)
|
||||
{
|
||||
List<ChatMessage> messages = [];
|
||||
await foreach (AgentResponseItem item in this._client.GetProjectOpenAIClient().GetProjectConversationsClient().GetProjectConversationItemsAsync(conversationId, order: "asc"))
|
||||
{
|
||||
var openAIItem = item.AsResponseResultItem();
|
||||
if (openAIItem is MessageResponseItem messageItem)
|
||||
{
|
||||
messages.Add(new ChatMessage
|
||||
{
|
||||
Role = new ChatRole(messageItem.Role.ToString()),
|
||||
Contents = messageItem.Content
|
||||
.Where(c => c.Kind is ResponseContentPartKind.OutputText or ResponseContentPartKind.InputText)
|
||||
.Select(c => new TextContent(c.Text))
|
||||
.ToList<AIContent>()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
public Task<ChatClientAgent> CreateChatClientAgentAsync(
|
||||
string name = "HelpfulAssistant",
|
||||
string instructions = "You are a helpful assistant.",
|
||||
IList<AITool>? aiTools = null)
|
||||
{
|
||||
return Task.FromResult(this._client.AsAIAgent(
|
||||
model: TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName),
|
||||
instructions: instructions,
|
||||
name: name,
|
||||
tools: aiTools).GetService<ChatClientAgent>()!);
|
||||
}
|
||||
|
||||
public Task<ChatClientAgent> CreateChatClientAgentAsync(ChatClientAgentOptions options)
|
||||
{
|
||||
return Task.FromResult(this._client.AsAIAgent(options).GetService<ChatClientAgent>()!);
|
||||
}
|
||||
|
||||
// Non-versioned Responses agents have no server-side agent to delete.
|
||||
public Task DeleteAgentAsync(ChatClientAgent agent) => Task.CompletedTask;
|
||||
|
||||
public async Task DeleteSessionAsync(AgentSession session)
|
||||
{
|
||||
ChatClientAgentSession typedSession = (ChatClientAgentSession)session;
|
||||
|
||||
if (typedSession.ConversationId?.StartsWith("conv_", StringComparison.OrdinalIgnoreCase) == true)
|
||||
{
|
||||
await this._client.GetProjectOpenAIClient().GetProjectConversationsClient().DeleteConversationAsync(typedSession.ConversationId);
|
||||
}
|
||||
else if (typedSession.ConversationId?.StartsWith("resp_", StringComparison.OrdinalIgnoreCase) == true)
|
||||
{
|
||||
await this.DeleteResponseChainAsync(typedSession.ConversationId!);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteResponseChainAsync(string lastResponseId)
|
||||
{
|
||||
var response = await this._client.GetProjectOpenAIClient().GetProjectResponsesClient().GetResponseAsync(lastResponseId);
|
||||
await this._client.GetProjectOpenAIClient().GetProjectResponsesClient().DeleteResponseAsync(lastResponseId);
|
||||
|
||||
if (response.Value.PreviousResponseId is not null)
|
||||
{
|
||||
await this.DeleteResponseChainAsync(response.Value.PreviousResponseId);
|
||||
}
|
||||
}
|
||||
|
||||
// Non-versioned Responses agents have no server-side agent to clean up on dispose.
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
return default;
|
||||
}
|
||||
|
||||
public virtual ValueTask InitializeAsync()
|
||||
{
|
||||
this._client = new AIProjectClient(
|
||||
new Uri(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint)),
|
||||
TestAzureCliCredentials.CreateAzureCliCredential());
|
||||
|
||||
this._agent = this._client.AsAIAgent(
|
||||
model: TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName),
|
||||
instructions: "You are a helpful assistant.",
|
||||
name: "HelpfulAssistant");
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public ValueTask InitializeAsync(ChatClientAgentOptions options)
|
||||
{
|
||||
this._client = new AIProjectClient(
|
||||
new Uri(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint)),
|
||||
TestAzureCliCredentials.CreateAzureCliCredential());
|
||||
|
||||
this._agent = this._client.AsAIAgent(options);
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using AgentConformance.IntegrationTests;
|
||||
using Microsoft.Agents.AI;
|
||||
|
||||
namespace Foundry.IntegrationTests;
|
||||
|
||||
public class ResponsesAgentRunStreamingPreviousResponseTests() : RunStreamingTests<ResponsesAgentFixture>(() => new())
|
||||
{
|
||||
public override Task RunWithNoMessageDoesNotFailAsync()
|
||||
{
|
||||
Assert.Skip("No messages is not supported");
|
||||
return base.RunWithNoMessageDoesNotFailAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public class ResponsesAgentRunStreamingConversationTests() : RunStreamingTests<ResponsesAgentFixture>(() => new())
|
||||
{
|
||||
public override Func<Task<AgentRunOptions?>> AgentRunOptionsFactory => async () =>
|
||||
{
|
||||
var conversationId = await this.Fixture.CreateConversationAsync();
|
||||
return new ChatClientAgentRunOptions(new() { ConversationId = conversationId });
|
||||
};
|
||||
|
||||
public override Task RunWithNoMessageDoesNotFailAsync()
|
||||
{
|
||||
Assert.Skip("No messages is not supported");
|
||||
return base.RunWithNoMessageDoesNotFailAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using AgentConformance.IntegrationTests;
|
||||
using Microsoft.Agents.AI;
|
||||
|
||||
namespace Foundry.IntegrationTests;
|
||||
|
||||
public class ResponsesAgentRunPreviousResponseTests() : RunTests<ResponsesAgentFixture>(() => new())
|
||||
{
|
||||
public override Task RunWithNoMessageDoesNotFailAsync()
|
||||
{
|
||||
Assert.Skip("No messages is not supported");
|
||||
return base.RunWithNoMessageDoesNotFailAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public class ResponsesAgentRunConversationTests() : RunTests<ResponsesAgentFixture>(() => new())
|
||||
{
|
||||
public override Func<Task<AgentRunOptions?>> AgentRunOptionsFactory => async () =>
|
||||
{
|
||||
var conversationId = await this.Fixture.CreateConversationAsync();
|
||||
return new ChatClientAgentRunOptions(new() { ConversationId = conversationId });
|
||||
};
|
||||
|
||||
public override Task RunWithNoMessageDoesNotFailAsync()
|
||||
{
|
||||
Assert.Skip("No messages is not supported");
|
||||
return base.RunWithNoMessageDoesNotFailAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using AgentConformance.IntegrationTests.Support;
|
||||
using Azure.AI.Projects;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Shared.IntegrationTests;
|
||||
|
||||
namespace Foundry.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests validating that the <c>x-ms-served-model</c> response header
|
||||
/// returned by the Azure OpenAI Responses API is surfaced on <see cref="ChatResponse.ModelId"/>.
|
||||
/// </summary>
|
||||
public class ResponsesAgentServedModelTests
|
||||
{
|
||||
// Matches a dated served-model snapshot, e.g. "gpt-5-nano-2025-08-07".
|
||||
private static readonly Regex s_snapshotRegex = new(@"-\d{4}-\d{2}-\d{2}$", RegexOptions.Compiled);
|
||||
|
||||
private static Uri Endpoint => new(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint));
|
||||
|
||||
private static string DeploymentName => TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName);
|
||||
|
||||
private readonly AIProjectClient _client = new(Endpoint, TestAzureCliCredentials.CreateAzureCliCredential());
|
||||
|
||||
[Fact]
|
||||
public async Task GetResponseAsync_ReturnsServedModelSnapshotOnModelIdAsync()
|
||||
{
|
||||
// Arrange
|
||||
ChatClientAgent agent = this._client.AsAIAgent(
|
||||
model: DeploymentName,
|
||||
instructions: "You are a helpful assistant. Reply with a single short word.",
|
||||
name: "ServedModelTest");
|
||||
|
||||
IChatClient chatClient = agent.ChatClient;
|
||||
|
||||
// Act
|
||||
ChatResponse response = await chatClient.GetResponseAsync(
|
||||
[new ChatMessage(ChatRole.User, "Say hi.")],
|
||||
new ChatOptions { ModelId = DeploymentName });
|
||||
|
||||
// Assert
|
||||
AssertServedModel(response.ModelId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_AgentResponseRawRepresentationCarriesServedModelAsync()
|
||||
{
|
||||
// Arrange
|
||||
ChatClientAgent agent = this._client.AsAIAgent(
|
||||
model: DeploymentName,
|
||||
instructions: "You are a helpful assistant. Reply with a single short word.",
|
||||
name: "ServedModelTestRun");
|
||||
|
||||
// Act
|
||||
AgentResponse agentResponse = await agent.RunAsync("Say hi.");
|
||||
|
||||
// Assert
|
||||
ChatResponse? chatResponse = agentResponse.RawRepresentation as ChatResponse;
|
||||
Assert.NotNull(chatResponse);
|
||||
AssertServedModel(chatResponse!.ModelId);
|
||||
}
|
||||
|
||||
private static void AssertServedModel(string? modelId)
|
||||
{
|
||||
Assert.False(string.IsNullOrWhiteSpace(modelId), "ChatResponse.ModelId must be populated.");
|
||||
|
||||
// Primary invariant: the served-model value must look like a dated snapshot
|
||||
// (e.g. "gpt-5-nano-2025-08-07"). This is what the x-ms-served-model header carries.
|
||||
// Only when the configured deployment name itself already matches the snapshot pattern
|
||||
// do we fall back to permitting equality with the deployment alias.
|
||||
bool aliasIsSnapshot = s_snapshotRegex.IsMatch(DeploymentName);
|
||||
|
||||
if (aliasIsSnapshot)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Assert.Matches(s_snapshotRegex, modelId!);
|
||||
Assert.NotEqual(DeploymentName, modelId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using AgentConformance.IntegrationTests;
|
||||
using AgentConformance.IntegrationTests.Support;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Shared.IntegrationTests;
|
||||
|
||||
namespace Foundry.IntegrationTests;
|
||||
|
||||
public class ResponsesAgentStructuredOutputRunTests() : StructuredOutputRunTests<ResponsesAgentStructuredOutputFixture<CityInfo>>(() => new())
|
||||
{
|
||||
private const string NotSupported = "The direct Responses AsAIAgent path does not support specifying structured output type at invocation time.";
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that response format provided at agent initialization is used when invoking RunAsync.
|
||||
/// </summary>
|
||||
[RetryFact(Constants.RetryCount, Constants.RetryDelay)]
|
||||
public async Task RunWithResponseFormatAtAgentInitializationReturnsExpectedResultAsync()
|
||||
{
|
||||
// Arrange
|
||||
AIAgent agent = this.Fixture.Agent;
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
await using var cleanup = new SessionCleanup(session, this.Fixture);
|
||||
|
||||
// Act
|
||||
AgentResponse response = await agent.RunAsync(new ChatMessage(ChatRole.User, "Provide information about the capital of France."), session);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(response);
|
||||
Assert.Single(response.Messages);
|
||||
Assert.Contains("Paris", response.Text);
|
||||
Assert.True(TryDeserialize(response.Text, AgentAbstractionsJsonUtilities.DefaultOptions, out CityInfo cityInfo));
|
||||
Assert.Equal("Paris", cityInfo.Name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that generic RunAsync works when structured output is configured at agent initialization.
|
||||
/// </summary>
|
||||
[RetryFact(Constants.RetryCount, Constants.RetryDelay)]
|
||||
public async Task RunGenericWithResponseFormatAtAgentInitializationReturnsExpectedResultAsync()
|
||||
{
|
||||
// Arrange
|
||||
AIAgent agent = this.Fixture.Agent;
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
await using var cleanup = new SessionCleanup(session, this.Fixture);
|
||||
|
||||
// Act
|
||||
AgentResponse<CityInfo> response = await agent.RunAsync<CityInfo>(
|
||||
new ChatMessage(ChatRole.User, "Provide information about the capital of France."),
|
||||
session);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(response);
|
||||
Assert.Single(response.Messages);
|
||||
Assert.Contains("Paris", response.Text);
|
||||
|
||||
Assert.NotNull(response.Result);
|
||||
Assert.Equal("Paris", response.Result.Name);
|
||||
}
|
||||
|
||||
public override Task RunWithGenericTypeReturnsExpectedResultAsync()
|
||||
{
|
||||
Assert.Skip(NotSupported);
|
||||
return base.RunWithGenericTypeReturnsExpectedResultAsync();
|
||||
}
|
||||
|
||||
public override Task RunWithResponseFormatReturnsExpectedResultAsync()
|
||||
{
|
||||
Assert.Skip(NotSupported);
|
||||
return base.RunWithResponseFormatReturnsExpectedResultAsync();
|
||||
}
|
||||
|
||||
public override Task RunWithPrimitiveTypeReturnsExpectedResultAsync()
|
||||
{
|
||||
Assert.Skip(NotSupported);
|
||||
return base.RunWithPrimitiveTypeReturnsExpectedResultAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fixture for testing the direct Responses <see cref="ChatClientAgent"/> path with structured output of type <typeparamref name="T"/> provided at agent initialization.
|
||||
/// </summary>
|
||||
public class ResponsesAgentStructuredOutputFixture<T> : ResponsesAgentFixture
|
||||
{
|
||||
public override ValueTask InitializeAsync()
|
||||
{
|
||||
ChatClientAgentOptions agentOptions = new()
|
||||
{
|
||||
ChatOptions = new ChatOptions()
|
||||
{
|
||||
ModelId = TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName),
|
||||
ResponseFormat = ChatResponseFormat.ForJsonSchema<T>(AgentAbstractionsJsonUtilities.DefaultOptions)
|
||||
},
|
||||
};
|
||||
|
||||
return this.InitializeAsync(agentOptions);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user