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:
+23
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<IsTestProject>false</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net10.0'))">
|
||||
<PackageReference Include="System.Linq.AsyncEnumerable" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AgentConformance.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all test classes used for testing agents.
|
||||
/// </summary>
|
||||
/// <typeparam name="TAgentFixture">The type of the agent fixture used in these tests.</typeparam>
|
||||
/// <param name="createAgentFixture">Used to create a new fixture for this test suite.</param>
|
||||
public abstract class AgentTests<TAgentFixture>(Func<TAgentFixture> createAgentFixture) : IAsyncLifetime
|
||||
where TAgentFixture : IAgentFixture
|
||||
{
|
||||
protected TAgentFixture Fixture { get; private set; } = default!;
|
||||
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
this.Fixture = createAgentFixture();
|
||||
await this.Fixture.InitializeAsync();
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
await this.Fixture.DisposeAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AgentConformance.IntegrationTests.Support;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace AgentConformance.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Conformance tests that are specific to the <see cref="ChatClientAgent"/> in addition to those in <see cref="RunStreamingTests{TAgentFixture}"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TAgentFixture">The type of test fixture used by the concrete test implementation.</typeparam>
|
||||
/// <param name="createAgentFixture">Function to create the test fixture with.</param>
|
||||
public abstract class ChatClientAgentRunStreamingTests<TAgentFixture>(Func<TAgentFixture> createAgentFixture) : AgentTests<TAgentFixture>(createAgentFixture)
|
||||
where TAgentFixture : IChatClientAgentFixture
|
||||
{
|
||||
[RetryFact(Constants.RetryCount, Constants.RetryDelay)]
|
||||
public virtual async Task RunWithInstructionsAndNoMessageReturnsExpectedResultAsync()
|
||||
{
|
||||
// Arrange
|
||||
var agent = await this.Fixture.CreateChatClientAgentAsync(instructions: "Always respond with 'Computer says no', even if there was no user input.");
|
||||
var session = await agent.CreateSessionAsync();
|
||||
await using var agentCleanup = new AgentCleanup(agent, this.Fixture);
|
||||
await using var sessionCleanup = new SessionCleanup(session, this.Fixture);
|
||||
|
||||
// Act
|
||||
var responseUpdates = await agent.RunStreamingAsync(session).ToListAsync();
|
||||
|
||||
// Assert
|
||||
var chatResponseText = string.Concat(responseUpdates.Select(x => x.Text));
|
||||
Assert.Contains("Computer says no", chatResponseText, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[RetryFact(Constants.RetryCount, Constants.RetryDelay)]
|
||||
public virtual async Task RunWithFunctionsInvokesFunctionsAndReturnsExpectedResultsAsync()
|
||||
{
|
||||
// Arrange
|
||||
var questionsAndAnswers = new[]
|
||||
{
|
||||
(Question: "Hello", ExpectedAnswer: string.Empty),
|
||||
(Question: "What is the special soup?", ExpectedAnswer: "Clam Chowder"),
|
||||
(Question: "What is the special drink?", ExpectedAnswer: "Chai Tea"),
|
||||
(Question: "What is the special salad?", ExpectedAnswer: "Cobb Salad"),
|
||||
(Question: "Thank you", ExpectedAnswer: string.Empty)
|
||||
};
|
||||
|
||||
var agent = await this.Fixture.CreateChatClientAgentAsync(
|
||||
aiTools:
|
||||
[
|
||||
AIFunctionFactory.Create(MenuPlugin.GetSpecials),
|
||||
AIFunctionFactory.Create(MenuPlugin.GetItemPrice)
|
||||
]);
|
||||
var session = await agent.CreateSessionAsync();
|
||||
|
||||
foreach (var questionAndAnswer in questionsAndAnswers)
|
||||
{
|
||||
// Act
|
||||
var responseUpdates = await agent.RunStreamingAsync(
|
||||
new ChatMessage(ChatRole.User, questionAndAnswer.Question),
|
||||
session).ToListAsync();
|
||||
|
||||
// Assert
|
||||
var chatResponseText = string.Concat(responseUpdates.Select(x => x.Text));
|
||||
Assert.Contains(questionAndAnswer.ExpectedAnswer, chatResponseText, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using AgentConformance.IntegrationTests.Support;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace AgentConformance.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Conformance tests that are specific to the <see cref="ChatClientAgent"/> in addition to those in <see cref="RunTests{TAgentFixture}"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TAgentFixture">The type of test fixture used by the concrete test implementation.</typeparam>
|
||||
/// <param name="createAgentFixture">Function to create the test fixture with.</param>
|
||||
public abstract class ChatClientAgentRunTests<TAgentFixture>(Func<TAgentFixture> createAgentFixture) : AgentTests<TAgentFixture>(createAgentFixture)
|
||||
where TAgentFixture : IChatClientAgentFixture
|
||||
{
|
||||
[RetryFact(Constants.RetryCount, Constants.RetryDelay)]
|
||||
public virtual async Task RunWithInstructionsAndNoMessageReturnsExpectedResultAsync()
|
||||
{
|
||||
// Arrange
|
||||
var agent = await this.Fixture.CreateChatClientAgentAsync(instructions: "ALWAYS RESPOND WITH 'Computer says no', even if there was no user input.");
|
||||
var session = await agent.CreateSessionAsync();
|
||||
await using var agentCleanup = new AgentCleanup(agent, this.Fixture);
|
||||
await using var sessionCleanup = new SessionCleanup(session, this.Fixture);
|
||||
|
||||
// Act
|
||||
var response = await agent.RunAsync(session);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(response);
|
||||
Assert.Single(response.Messages);
|
||||
Assert.False(string.IsNullOrWhiteSpace(response.Text), "Agent should return non-empty response even without user input");
|
||||
}
|
||||
|
||||
[RetryFact(Constants.RetryCount, Constants.RetryDelay)]
|
||||
public virtual async Task RunWithFunctionsInvokesFunctionsAndReturnsExpectedResultsAsync()
|
||||
{
|
||||
// Arrange
|
||||
var questionsAndAnswers = new[]
|
||||
{
|
||||
(Question: "Hello", ExpectedAnswer: string.Empty),
|
||||
(Question: "What is the special soup?", ExpectedAnswer: "Clam Chowder"),
|
||||
(Question: "What is the special drink?", ExpectedAnswer: "Chai Tea"),
|
||||
(Question: "What is the special salad?", ExpectedAnswer: "Cobb Salad"),
|
||||
(Question: "Thank you", ExpectedAnswer: string.Empty)
|
||||
};
|
||||
|
||||
var agent = await this.Fixture.CreateChatClientAgentAsync(
|
||||
aiTools:
|
||||
[
|
||||
AIFunctionFactory.Create(MenuPlugin.GetSpecials),
|
||||
AIFunctionFactory.Create(MenuPlugin.GetItemPrice)
|
||||
]);
|
||||
var session = await agent.CreateSessionAsync();
|
||||
|
||||
foreach (var questionAndAnswer in questionsAndAnswers)
|
||||
{
|
||||
// Act
|
||||
var result = await agent.RunAsync(
|
||||
new ChatMessage(ChatRole.User, questionAndAnswer.Question),
|
||||
session);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Contains(questionAndAnswer.ExpectedAnswer, result.Text);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace AgentConformance.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for setting up and tearing down agents, to be used in tests.
|
||||
/// Each agent type should have its own derived class.
|
||||
/// </summary>
|
||||
public interface IAgentFixture : IAsyncLifetime
|
||||
{
|
||||
AIAgent Agent { get; }
|
||||
|
||||
Task<List<ChatMessage>> GetChatHistoryAsync(AIAgent agent, AgentSession session);
|
||||
|
||||
Task DeleteSessionAsync(AgentSession session);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace AgentConformance.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for setting up and tearing down <see cref="IChatClient"/> based agents, to be used in tests.
|
||||
/// Each agent type should have its own derived class.
|
||||
/// </summary>
|
||||
public interface IChatClientAgentFixture : IAgentFixture
|
||||
{
|
||||
IChatClient ChatClient { get; }
|
||||
|
||||
Task<ChatClientAgent> CreateChatClientAgentAsync(
|
||||
string name = "HelpfulAssistant",
|
||||
string instructions = "You are a helpful assistant.",
|
||||
IList<AITool>? aiTools = null);
|
||||
|
||||
Task DeleteAgentAsync(ChatClientAgent agent);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace AgentConformance.IntegrationTests;
|
||||
|
||||
#pragma warning disable CA1812 // Avoid uninstantiated internal classes
|
||||
|
||||
/// <summary>
|
||||
/// A test plugin used to verify function invocation.
|
||||
/// </summary>
|
||||
internal static class MenuPlugin
|
||||
{
|
||||
[Description("Provides a list of specials from the menu.")]
|
||||
public static string GetSpecials() => """
|
||||
Special Soup: Clam Chowder
|
||||
Special Salad: Cobb Salad
|
||||
Special Drink: Chai Tea
|
||||
""";
|
||||
|
||||
[Description("Provides the price of the requested menu item.")]
|
||||
public static string GetItemPrice(
|
||||
[Description("The name of the menu item.")]
|
||||
string menuItem) => "$9.99";
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AgentConformance.IntegrationTests.Support;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace AgentConformance.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Conformance tests for run methods on agents.
|
||||
/// </summary>
|
||||
/// <typeparam name="TAgentFixture">The type of test fixture used by the concrete test implementation.</typeparam>
|
||||
/// <param name="createAgentFixture">Function to create the test fixture with.</param>
|
||||
public abstract class RunStreamingTests<TAgentFixture>(Func<TAgentFixture> createAgentFixture) : AgentTests<TAgentFixture>(createAgentFixture)
|
||||
where TAgentFixture : IAgentFixture
|
||||
{
|
||||
public virtual Func<Task<AgentRunOptions?>> AgentRunOptionsFactory { get; set; } = () => Task.FromResult(default(AgentRunOptions));
|
||||
|
||||
[RetryFact(Constants.RetryCount, Constants.RetryDelay)]
|
||||
public virtual async Task RunWithNoMessageDoesNotFailAsync()
|
||||
{
|
||||
// Arrange
|
||||
var agent = this.Fixture.Agent;
|
||||
var session = await agent.CreateSessionAsync();
|
||||
await using var cleanup = new SessionCleanup(session, this.Fixture);
|
||||
|
||||
// Act
|
||||
var chatResponses = await agent.RunStreamingAsync(session, await this.AgentRunOptionsFactory.Invoke()).ToListAsync();
|
||||
}
|
||||
|
||||
[RetryFact(Constants.RetryCount, Constants.RetryDelay)]
|
||||
public virtual async Task RunWithStringReturnsExpectedResultAsync()
|
||||
{
|
||||
// Arrange
|
||||
var agent = this.Fixture.Agent;
|
||||
var session = await agent.CreateSessionAsync();
|
||||
await using var cleanup = new SessionCleanup(session, this.Fixture);
|
||||
|
||||
// Act
|
||||
var responseUpdates = await agent.RunStreamingAsync("What is the capital of France.", session, await this.AgentRunOptionsFactory.Invoke()).ToListAsync();
|
||||
|
||||
// Assert
|
||||
var chatResponseText = string.Concat(responseUpdates.Select(x => x.Text));
|
||||
Assert.Contains("Paris", chatResponseText);
|
||||
}
|
||||
|
||||
[RetryFact(Constants.RetryCount, Constants.RetryDelay)]
|
||||
public virtual async Task RunWithChatMessageReturnsExpectedResultAsync()
|
||||
{
|
||||
// Arrange
|
||||
var agent = this.Fixture.Agent;
|
||||
var session = await agent.CreateSessionAsync();
|
||||
await using var cleanup = new SessionCleanup(session, this.Fixture);
|
||||
|
||||
// Act
|
||||
var responseUpdates = await agent.RunStreamingAsync(new ChatMessage(ChatRole.User, "What is the capital of France."), session, await this.AgentRunOptionsFactory.Invoke()).ToListAsync();
|
||||
|
||||
// Assert
|
||||
var chatResponseText = string.Concat(responseUpdates.Select(x => x.Text));
|
||||
Assert.Contains("Paris", chatResponseText);
|
||||
}
|
||||
|
||||
[RetryFact(Constants.RetryCount, Constants.RetryDelay)]
|
||||
public virtual async Task RunWithChatMessagesReturnsExpectedResultAsync()
|
||||
{
|
||||
// Arrange
|
||||
var agent = this.Fixture.Agent;
|
||||
var session = await agent.CreateSessionAsync();
|
||||
await using var cleanup = new SessionCleanup(session, this.Fixture);
|
||||
|
||||
// Act
|
||||
var responseUpdates = await agent.RunStreamingAsync(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Hello."),
|
||||
new ChatMessage(ChatRole.User, "What is the capital of France.")
|
||||
],
|
||||
session,
|
||||
await this.AgentRunOptionsFactory.Invoke()).ToListAsync();
|
||||
|
||||
// Assert
|
||||
var chatResponseText = string.Concat(responseUpdates.Select(x => x.Text));
|
||||
Assert.Contains("Paris", chatResponseText);
|
||||
}
|
||||
|
||||
[RetryFact(Constants.RetryCount, Constants.RetryDelay)]
|
||||
public virtual async Task SessionMaintainsHistoryAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string Q1 = "What is the capital of France.";
|
||||
const string Q2 = "And Austria?";
|
||||
var agent = this.Fixture.Agent;
|
||||
var session = await agent.CreateSessionAsync();
|
||||
await using var cleanup = new SessionCleanup(session, this.Fixture);
|
||||
|
||||
// Act
|
||||
var options = await this.AgentRunOptionsFactory.Invoke();
|
||||
var responseUpdates1 = await agent.RunStreamingAsync(Q1, session, options).ToListAsync();
|
||||
var responseUpdates2 = await agent.RunStreamingAsync(Q2, session, options).ToListAsync();
|
||||
|
||||
// Assert
|
||||
var response1Text = string.Concat(responseUpdates1.Select(x => x.Text));
|
||||
var response2Text = string.Concat(responseUpdates2.Select(x => x.Text));
|
||||
Assert.Contains("Paris", response1Text);
|
||||
Assert.Contains("Vienna", response2Text);
|
||||
|
||||
var chatHistory = await this.Fixture.GetChatHistoryAsync(agent, session);
|
||||
Assert.Equal(4, chatHistory.Count);
|
||||
Assert.Equal(2, chatHistory.Count(x => x.Role == ChatRole.User));
|
||||
Assert.Equal(2, chatHistory.Count(x => x.Role == ChatRole.Assistant));
|
||||
Assert.Equal(Q1, chatHistory[0].Text);
|
||||
Assert.Equal(Q2, chatHistory[2].Text);
|
||||
Assert.Contains("Paris", chatHistory[1].Text);
|
||||
Assert.Contains("Vienna", chatHistory[3].Text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AgentConformance.IntegrationTests.Support;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace AgentConformance.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Conformance tests for run methods on agents.
|
||||
/// </summary>
|
||||
/// <typeparam name="TAgentFixture">The type of test fixture used by the concrete test implementation.</typeparam>
|
||||
/// <param name="createAgentFixture">Function to create the test fixture with.</param>
|
||||
public abstract class RunTests<TAgentFixture>(Func<TAgentFixture> createAgentFixture) : AgentTests<TAgentFixture>(createAgentFixture)
|
||||
where TAgentFixture : IAgentFixture
|
||||
{
|
||||
public virtual Func<Task<AgentRunOptions?>> AgentRunOptionsFactory { get; set; } = () => Task.FromResult(default(AgentRunOptions));
|
||||
|
||||
[RetryFact(Constants.RetryCount, Constants.RetryDelay)]
|
||||
public virtual async Task RunWithNoMessageDoesNotFailAsync()
|
||||
{
|
||||
// Arrange
|
||||
var agent = this.Fixture.Agent;
|
||||
var session = await agent.CreateSessionAsync();
|
||||
await using var cleanup = new SessionCleanup(session, this.Fixture);
|
||||
|
||||
// Act
|
||||
var chatResponse = await agent.RunAsync(session);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(chatResponse);
|
||||
}
|
||||
|
||||
[RetryFact(Constants.RetryCount, Constants.RetryDelay)]
|
||||
public virtual async Task RunWithStringReturnsExpectedResultAsync()
|
||||
{
|
||||
// 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("What is the capital of France.", session, await this.AgentRunOptionsFactory.Invoke());
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(response);
|
||||
Assert.Single(response.Messages);
|
||||
Assert.Contains("Paris", response.Text);
|
||||
Assert.Equal(agent.Id, response.AgentId);
|
||||
}
|
||||
|
||||
[RetryFact(Constants.RetryCount, Constants.RetryDelay)]
|
||||
public virtual async Task RunWithChatMessageReturnsExpectedResultAsync()
|
||||
{
|
||||
// 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, "What is the capital of France."), session, await this.AgentRunOptionsFactory.Invoke());
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(response);
|
||||
Assert.Single(response.Messages);
|
||||
Assert.Contains("Paris", response.Text);
|
||||
}
|
||||
|
||||
[RetryFact(Constants.RetryCount, Constants.RetryDelay)]
|
||||
public virtual async Task RunWithChatMessagesReturnsExpectedResultAsync()
|
||||
{
|
||||
// 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, "Hello."),
|
||||
new ChatMessage(ChatRole.User, "What is the capital of France.")
|
||||
],
|
||||
session,
|
||||
await this.AgentRunOptionsFactory.Invoke());
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(response);
|
||||
Assert.Single(response.Messages);
|
||||
Assert.Contains("Paris", response.Text);
|
||||
}
|
||||
|
||||
[RetryFact(Constants.RetryCount, Constants.RetryDelay)]
|
||||
public virtual async Task SessionMaintainsHistoryAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string Q1 = "What is the capital of France.";
|
||||
const string Q2 = "And Austria?";
|
||||
var agent = this.Fixture.Agent;
|
||||
var session = await agent.CreateSessionAsync();
|
||||
await using var cleanup = new SessionCleanup(session, this.Fixture);
|
||||
|
||||
// Act
|
||||
var options = await this.AgentRunOptionsFactory.Invoke();
|
||||
var result1 = await agent.RunAsync(Q1, session, options);
|
||||
var result2 = await agent.RunAsync(Q2, session, options);
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Paris", result1.Text);
|
||||
Assert.Contains("Vienna", result2.Text);
|
||||
|
||||
var chatHistory = await this.Fixture.GetChatHistoryAsync(agent, session);
|
||||
Assert.Equal(4, chatHistory.Count);
|
||||
Assert.Equal(2, chatHistory.Count(x => x.Role == ChatRole.User));
|
||||
Assert.Equal(2, chatHistory.Count(x => x.Role == ChatRole.Assistant));
|
||||
Assert.Equal(Q1, chatHistory[0].Text);
|
||||
Assert.Contains("Paris", chatHistory[1].Text);
|
||||
Assert.Equal(Q2, chatHistory[2].Text);
|
||||
Assert.Contains("Vienna", chatHistory[3].Text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using AgentConformance.IntegrationTests.Support;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace AgentConformance.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Conformance tests for structured output handling for run methods on agents.
|
||||
/// </summary>
|
||||
/// <typeparam name="TAgentFixture">The type of test fixture used by the concrete test implementation.</typeparam>
|
||||
/// <param name="createAgentFixture">Function to create the test fixture with.</param>
|
||||
public abstract class StructuredOutputRunTests<TAgentFixture>(Func<TAgentFixture> createAgentFixture) : AgentTests<TAgentFixture>(createAgentFixture)
|
||||
where TAgentFixture : IAgentFixture
|
||||
{
|
||||
[RetryFact(Constants.RetryCount, Constants.RetryDelay)]
|
||||
public virtual async Task RunWithResponseFormatReturnsExpectedResultAsync()
|
||||
{
|
||||
// Arrange
|
||||
var agent = this.Fixture.Agent;
|
||||
var session = await agent.CreateSessionAsync();
|
||||
await using var cleanup = new SessionCleanup(session, this.Fixture);
|
||||
|
||||
var options = new AgentRunOptions
|
||||
{
|
||||
ResponseFormat = ChatResponseFormat.ForJsonSchema<CityInfo>(AgentAbstractionsJsonUtilities.DefaultOptions)
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await agent.RunAsync(new ChatMessage(ChatRole.User, "Provide information about the capital of France."), session, options);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
[RetryFact(Constants.RetryCount, Constants.RetryDelay)]
|
||||
public virtual async Task RunWithGenericTypeReturnsExpectedResultAsync()
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
|
||||
[RetryFact(Constants.RetryCount, Constants.RetryDelay)]
|
||||
public virtual async Task RunWithPrimitiveTypeReturnsExpectedResultAsync()
|
||||
{
|
||||
// Arrange
|
||||
var agent = this.Fixture.Agent;
|
||||
var session = await agent.CreateSessionAsync();
|
||||
await using var cleanup = new SessionCleanup(session, this.Fixture);
|
||||
|
||||
// Act - Request a primitive type, which requires wrapping in an object schema
|
||||
AgentResponse<int> response = await agent.RunAsync<int>(
|
||||
new ChatMessage(ChatRole.User, "What is the sum of 15 and 27? Respond with just the number."),
|
||||
session);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(response);
|
||||
Assert.Single(response.Messages);
|
||||
Assert.Equal(42, response.Result);
|
||||
}
|
||||
|
||||
protected static bool TryDeserialize<T>(string json, JsonSerializerOptions jsonSerializerOptions, out T structuredOutput)
|
||||
{
|
||||
try
|
||||
{
|
||||
T? deserialized = JsonSerializer.Deserialize<T>(json, jsonSerializerOptions);
|
||||
if (deserialized is null)
|
||||
{
|
||||
structuredOutput = default!;
|
||||
return false;
|
||||
}
|
||||
|
||||
structuredOutput = deserialized;
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
structuredOutput = default!;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class CityInfo
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI;
|
||||
|
||||
namespace AgentConformance.IntegrationTests.Support;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class to delete agents after tests.
|
||||
/// </summary>
|
||||
/// <param name="agent">The agent to delete.</param>
|
||||
/// <param name="fixture">The fixture that provides agent specific capabilities.</param>
|
||||
internal sealed class AgentCleanup(ChatClientAgent agent, IChatClientAgentFixture fixture) : IAsyncDisposable
|
||||
{
|
||||
public async ValueTask DisposeAsync() =>
|
||||
await fixture.DeleteAgentAsync(agent);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace AgentConformance.IntegrationTests.Support;
|
||||
|
||||
public static class Constants
|
||||
{
|
||||
public const int RetryCount = 3;
|
||||
public const int RetryDelay = 5000;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI;
|
||||
|
||||
namespace AgentConformance.IntegrationTests.Support;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class to delete sessions after tests.
|
||||
/// </summary>
|
||||
/// <param name="session">The session to delete.</param>
|
||||
/// <param name="fixture">The fixture that provides agent specific capabilities.</param>
|
||||
public sealed class SessionCleanup(AgentSession session, IAgentFixture fixture) : IAsyncDisposable
|
||||
{
|
||||
public async ValueTask DisposeAsync() =>
|
||||
await fixture.DeleteSessionAsync(session);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace AgentConformance.IntegrationTests.Support;
|
||||
|
||||
/// <summary>
|
||||
/// Helper for loading test configuration settings.
|
||||
/// </summary>
|
||||
public sealed class TestConfiguration
|
||||
{
|
||||
private static readonly IConfiguration s_configuration = new ConfigurationBuilder()
|
||||
.AddJsonFile(path: "testsettings.development.json", optional: true)
|
||||
.AddEnvironmentVariables()
|
||||
.AddUserSecrets<TestConfiguration>()
|
||||
.Build();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a configuration value by its flat key name.
|
||||
/// </summary>
|
||||
/// <param name="key">The configuration key.</param>
|
||||
/// <returns>The configuration value, or <see langword="null"/> if not found.</returns>
|
||||
public static string? GetValue(string key) => s_configuration[key];
|
||||
|
||||
/// <summary>
|
||||
/// Gets a required configuration value by its flat key name.
|
||||
/// </summary>
|
||||
/// <param name="key">The configuration key.</param>
|
||||
/// <returns>The configuration value.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown if the configuration value is not found.</exception>
|
||||
public static string GetRequiredValue(string key) =>
|
||||
s_configuration[key] ?? throw new InvalidOperationException($"Configuration key '{key}' is required but was not found.");
|
||||
}
|
||||
Reference in New Issue
Block a user