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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:25 +08:00
commit db620d33df
5151 changed files with 925932 additions and 0 deletions
@@ -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.");
}
}
@@ -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();
}
}
@@ -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&lt;T&gt; is ignored by FoundryChatClient and is only used
/// for deserializing the agent response by AgentResponse&lt;T&gt;.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);
}
}