mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98819e5e94 | ||
|
|
0888c89a4e | ||
|
|
1b4c59ea38 | ||
|
|
d0c097941a |
@@ -464,6 +464,8 @@
|
||||
<Folder Name="/Tests/" />
|
||||
<Folder Name="/Tests/IntegrationTests/">
|
||||
<Project Path="tests/AgentConformance.IntegrationTests/AgentConformance.IntegrationTests.csproj" />
|
||||
<Project Path="tests/AgentConversation.IntegrationTests/AgentConversation.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Microsoft.Agents.AI.Abstractions.IntegrationTests/Microsoft.Agents.AI.Abstractions.IntegrationTests.csproj" />
|
||||
<Project Path="tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletion.IntegrationTests.csproj" />
|
||||
<Project Path="tests/AzureAI.IntegrationTests/AzureAI.IntegrationTests.csproj" />
|
||||
<Project Path="tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj" />
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
bin/
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<IsTestProject>false</IsTestProject>
|
||||
<InjectIsExternalInitOnLegacy>true</InjectIsExternalInitOnLegacy>
|
||||
<InjectRequiredMemberOnLegacy>true</InjectRequiredMemberOnLegacy>
|
||||
<InjectCompilerFeatureRequiredOnLegacy>true</InjectCompilerFeatureRequiredOnLegacy>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace AgentConversation.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Defines an agent participating in a <see cref="IConversationTestCase"/>.
|
||||
/// </summary>
|
||||
public sealed class ConversationAgentDefinition
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the unique name identifying this agent within the test case.
|
||||
/// </summary>
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the system instructions for the agent.
|
||||
/// </summary>
|
||||
public string Instructions { get; init; } = "You are a helpful assistant.";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the optional list of tools available to the agent.
|
||||
/// </summary>
|
||||
public IList<AITool>? Tools { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace AgentConversation.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Provides helpers for serializing and deserializing conversation contexts (lists of <see cref="ChatMessage"/>)
|
||||
/// to and from JSON, enabling the initial context of a test case to be captured once and reused across runs.
|
||||
/// </summary>
|
||||
public static class ConversationContextSerializer
|
||||
{
|
||||
private static readonly JsonSerializerOptions s_serializerOptions = AgentAbstractionsJsonUtilities.DefaultOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a list of <see cref="ChatMessage"/> instances to a JSON string.
|
||||
/// </summary>
|
||||
/// <param name="messages">The messages to serialize.</param>
|
||||
/// <returns>A JSON string representation of the messages.</returns>
|
||||
public static string Serialize(IList<ChatMessage> messages) =>
|
||||
JsonSerializer.Serialize(messages, s_serializerOptions);
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes a JSON string into a list of <see cref="ChatMessage"/> instances.
|
||||
/// </summary>
|
||||
/// <param name="json">The JSON string to deserialize.</param>
|
||||
/// <returns>The deserialized list of messages.</returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown when the JSON cannot be deserialized into a list of <see cref="ChatMessage"/> instances.
|
||||
/// </exception>
|
||||
public static IList<ChatMessage> Deserialize(string json)
|
||||
{
|
||||
var messages = JsonSerializer.Deserialize<List<ChatMessage>>(json, s_serializerOptions);
|
||||
return messages ?? throw new InvalidOperationException("Failed to deserialize chat messages from the provided JSON.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves a list of <see cref="ChatMessage"/> instances to a JSON file.
|
||||
/// </summary>
|
||||
/// <param name="filePath">The path of the file to write.</param>
|
||||
/// <param name="messages">The messages to save.</param>
|
||||
public static void SaveToFile(string filePath, IList<ChatMessage> messages)
|
||||
{
|
||||
var json = Serialize(messages);
|
||||
File.WriteAllText(filePath, json);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads a list of <see cref="ChatMessage"/> instances from a JSON file.
|
||||
/// </summary>
|
||||
/// <param name="filePath">The path of the file to read.</param>
|
||||
/// <returns>The deserialized list of messages.</returns>
|
||||
/// <exception cref="FileNotFoundException">Thrown when <paramref name="filePath"/> does not exist.</exception>
|
||||
public static IList<ChatMessage> LoadFromFile(string filePath)
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
throw new FileNotFoundException($"Conversation context file not found: '{filePath}'. " +
|
||||
"Run the context creation step first to generate this file.", filePath);
|
||||
}
|
||||
|
||||
var json = File.ReadAllText(filePath);
|
||||
return Deserialize(json);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace AgentConversation.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Orchestrates the execution of a <see cref="IConversationTestCase"/> against a given
|
||||
/// <see cref="IConversationTestSystem"/>: restores the conversation context, optionally runs compaction,
|
||||
/// executes each step, captures before/after metrics, and runs per-step validations.
|
||||
/// </summary>
|
||||
public sealed class ConversationHarness
|
||||
{
|
||||
private readonly IConversationTestSystem _system;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="ConversationHarness"/>.
|
||||
/// </summary>
|
||||
/// <param name="system">The system under test that provides agent creation and compaction.</param>
|
||||
public ConversationHarness(IConversationTestSystem system)
|
||||
{
|
||||
if (system is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(system));
|
||||
}
|
||||
|
||||
this._system = system;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the supplied <paramref name="testCase"/> from its serialized initial context, executing
|
||||
/// every <see cref="ConversationStep"/> in order and returning the combined metrics report.
|
||||
/// </summary>
|
||||
/// <param name="testCase">The test case to execute.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
/// <returns>
|
||||
/// A <see cref="ConversationMetricsReport"/> describing the before-and-after state of the
|
||||
/// conversation context across all steps.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="testCase"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown when a step references an agent name that is not present in <see cref="IConversationTestCase.AgentDefinitions"/>.
|
||||
/// </exception>
|
||||
public async Task<ConversationMetricsReport> RunAsync(
|
||||
IConversationTestCase testCase,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (testCase is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(testCase));
|
||||
}
|
||||
|
||||
// 1. Restore the initial context.
|
||||
var initialMessages = testCase.GetInitialMessages();
|
||||
|
||||
// 2. Capture "before" metrics.
|
||||
var beforeMetrics = MeasureMetrics(initialMessages);
|
||||
|
||||
// 3. Create the agents defined for this test case.
|
||||
var agents = new Dictionary<string, AIAgent>(StringComparer.Ordinal);
|
||||
foreach (var entry in testCase.AgentDefinitions)
|
||||
{
|
||||
agents[entry.Key] = await this._system.CreateAgentAsync(entry.Value, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// 4. Create sessions and restore the initial messages for each agent.
|
||||
var sessions = new Dictionary<string, AgentSession>(StringComparer.Ordinal);
|
||||
foreach (var entry in agents)
|
||||
{
|
||||
var session = await entry.Value.CreateSessionAsync(cancellationToken).ConfigureAwait(false);
|
||||
RestoreMessages(entry.Value, session, initialMessages);
|
||||
sessions[entry.Key] = session;
|
||||
}
|
||||
|
||||
// 5. Optionally compact the messages.
|
||||
var compacted = await this._system.CompactAsync(initialMessages, cancellationToken).ConfigureAwait(false);
|
||||
if (compacted is not null)
|
||||
{
|
||||
// Apply the compacted history to all agent sessions.
|
||||
foreach (var entry in agents)
|
||||
{
|
||||
RestoreMessages(entry.Value, sessions[entry.Key], compacted);
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Execute each step.
|
||||
foreach (var step in testCase.Steps)
|
||||
{
|
||||
if (!agents.TryGetValue(step.AgentName, out var agent))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Step references agent '{step.AgentName}' which is not defined in the test case. " +
|
||||
$"Defined agents: {string.Join(", ", agents.Keys)}");
|
||||
}
|
||||
|
||||
var session = sessions[step.AgentName];
|
||||
AgentResponse response;
|
||||
|
||||
if (step.Input is not null)
|
||||
{
|
||||
response = await agent.RunAsync(step.Input, session, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
response = await agent.RunAsync(session, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// 7. Capture "after" metrics for this step and run the step's validation.
|
||||
var currentMessages = GetCurrentMessages(agent, sessions[step.AgentName], initialMessages, compacted);
|
||||
var afterMetrics = MeasureMetrics(currentMessages);
|
||||
var metricsReport = new ConversationMetricsReport
|
||||
{
|
||||
Before = beforeMetrics,
|
||||
After = afterMetrics,
|
||||
};
|
||||
|
||||
step.Validate?.Invoke(response, metricsReport);
|
||||
}
|
||||
|
||||
// 8. Capture the final "after" metrics from the first agent's session.
|
||||
var firstAgent = agents.Values.First();
|
||||
var firstSession = sessions[agents.Keys.First()];
|
||||
var finalMessages = GetCurrentMessages(firstAgent, firstSession, initialMessages, compacted);
|
||||
var finalAfterMetrics = MeasureMetrics(finalMessages);
|
||||
|
||||
return new ConversationMetricsReport
|
||||
{
|
||||
Before = beforeMetrics,
|
||||
After = finalAfterMetrics,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drives a conversation with the agents defined in <paramref name="testCase"/> to produce the initial
|
||||
/// context, then serializes that context to <paramref name="outputFilePath"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method should be called once (outside of normal test execution) to generate the fixture
|
||||
/// data that tests will subsequently restore via <see cref="IConversationTestCase.GetInitialMessages"/>.
|
||||
/// </remarks>
|
||||
/// <param name="testCase">The test case whose initial context should be created.</param>
|
||||
/// <param name="outputFilePath">The file path to write the serialized context to.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
public async Task SerializeInitialContextAsync(
|
||||
IConversationTestCase testCase,
|
||||
string outputFilePath,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (testCase is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(testCase));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(outputFilePath))
|
||||
{
|
||||
throw new ArgumentException("Output file path must not be null or empty.", nameof(outputFilePath));
|
||||
}
|
||||
|
||||
// Create agents for context generation.
|
||||
var agents = new Dictionary<string, AIAgent>(StringComparer.Ordinal);
|
||||
foreach (var entry in testCase.AgentDefinitions)
|
||||
{
|
||||
agents[entry.Key] = await this._system.CreateAgentAsync(entry.Value, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var messages = await testCase.CreateInitialContextAsync(agents, cancellationToken).ConfigureAwait(false);
|
||||
ConversationContextSerializer.SaveToFile(outputFilePath, messages);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Private helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private static ConversationMetrics MeasureMetrics(IList<ChatMessage> messages)
|
||||
{
|
||||
var serialized = ConversationContextSerializer.Serialize(messages);
|
||||
return new ConversationMetrics
|
||||
{
|
||||
MessageCount = messages.Count,
|
||||
SerializedSizeBytes = System.Text.Encoding.UTF8.GetByteCount(serialized),
|
||||
};
|
||||
}
|
||||
|
||||
private static void RestoreMessages(AIAgent agent, AgentSession session, IList<ChatMessage> messages)
|
||||
{
|
||||
// InMemoryChatHistoryProvider is the standard history provider for ChatClientAgent.
|
||||
// When found, load the messages directly into the provider's state for this session.
|
||||
if (agent.GetService<ChatHistoryProvider>() is InMemoryChatHistoryProvider memProvider)
|
||||
{
|
||||
memProvider.SetMessages(session, messages.ToList());
|
||||
}
|
||||
}
|
||||
|
||||
private static IList<ChatMessage> GetCurrentMessages(
|
||||
AIAgent agent,
|
||||
AgentSession session,
|
||||
IList<ChatMessage> fallbackInitial,
|
||||
IList<ChatMessage>? compacted)
|
||||
{
|
||||
if (agent.GetService<ChatHistoryProvider>() is InMemoryChatHistoryProvider memProvider)
|
||||
{
|
||||
return memProvider.GetMessages(session);
|
||||
}
|
||||
|
||||
// Fall back to the compacted (or original) initial messages when the provider is unavailable.
|
||||
return compacted ?? fallbackInitial;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace AgentConversation.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Abstract xunit base class for agent conversation integration tests.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Subclasses must implement <see cref="CreateTestSystem"/> and <see cref="GetTestCases"/> to provide
|
||||
/// the AI backend and the set of test cases to run. Each subclass will automatically inherit the
|
||||
/// <see cref="RunAllTestCasesAsync"/> test method, which runs every case returned by
|
||||
/// <see cref="GetTestCases"/> through the <see cref="ConversationHarness"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// To generate (and serialize) the initial context for a test case, the same subclass inherits
|
||||
/// <see cref="SerializeAllInitialContextsAsync"/>, which should be run once outside of normal CI.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <typeparam name="TSystem">
|
||||
/// The concrete <see cref="IConversationTestSystem"/> implementation that provides agent creation
|
||||
/// and compaction for the system under test.
|
||||
/// </typeparam>
|
||||
public abstract class ConversationHarnessTests<TSystem>
|
||||
where TSystem : IConversationTestSystem
|
||||
{
|
||||
private readonly ITestOutputHelper? _output;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="ConversationHarnessTests{TSystem}"/>.
|
||||
/// </summary>
|
||||
protected ConversationHarnessTests()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="ConversationHarnessTests{TSystem}"/> with xunit test output.
|
||||
/// </summary>
|
||||
/// <param name="output">The xunit test output helper used to log metrics and step results.</param>
|
||||
protected ConversationHarnessTests(ITestOutputHelper output)
|
||||
{
|
||||
this._output = output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the <see cref="IConversationTestSystem"/> to use for agent creation and compaction.
|
||||
/// </summary>
|
||||
protected abstract TSystem CreateTestSystem();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the set of <see cref="IConversationTestCase"/> instances to exercise.
|
||||
/// </summary>
|
||||
protected abstract IEnumerable<IConversationTestCase> GetTestCases();
|
||||
|
||||
/// <summary>
|
||||
/// Runs all test cases returned by <see cref="GetTestCases"/> and logs the metrics report for each.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public virtual async Task RunAllTestCasesAsync()
|
||||
{
|
||||
var system = this.CreateTestSystem();
|
||||
var harness = new ConversationHarness(system);
|
||||
|
||||
foreach (var testCase in this.GetTestCases())
|
||||
{
|
||||
this.Log($"[{testCase.Name}] Running...");
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
|
||||
var report = await harness.RunAsync(testCase);
|
||||
|
||||
stopwatch.Stop();
|
||||
this.Log($"[{testCase.Name}] Completed in {stopwatch.ElapsedMilliseconds}ms. Metrics: {report}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates and serializes the initial context for each test case returned by <see cref="GetTestCases"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This test is skipped during normal test runs because generating contexts requires live AI calls and
|
||||
/// can be expensive. Run it explicitly (e.g., with <c>dotnet test --filter "FullyQualifiedName~Serialize"</c>)
|
||||
/// to regenerate the fixture files. After running, commit the generated files alongside the test code.
|
||||
/// </remarks>
|
||||
[Fact(Skip = "Run explicitly to regenerate initial context fixture files.")]
|
||||
public virtual async Task SerializeAllInitialContextsAsync()
|
||||
{
|
||||
var system = this.CreateTestSystem();
|
||||
var harness = new ConversationHarness(system);
|
||||
|
||||
foreach (var testCase in this.GetTestCases())
|
||||
{
|
||||
var outputPath = GetDefaultContextFilePath(testCase);
|
||||
this.Log($"[{testCase.Name}] Serializing initial context to '{outputPath}'...");
|
||||
|
||||
await harness.SerializeInitialContextAsync(testCase, outputPath);
|
||||
|
||||
this.Log($"[{testCase.Name}] Context serialized successfully.");
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Protected helpers for subclasses
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Computes the default file path used by <see cref="SerializeAllInitialContextsAsync"/> when
|
||||
/// writing the initial context for <paramref name="testCase"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Override this method to change the output location. The default path is
|
||||
/// <c>{TestCase.Name}.context.json</c> relative to the current working directory.
|
||||
/// </remarks>
|
||||
/// <param name="testCase">The test case whose default output path is required.</param>
|
||||
/// <returns>The absolute or relative file path to write the serialized context to.</returns>
|
||||
protected virtual string GetDefaultContextFilePath(IConversationTestCase testCase) =>
|
||||
$"{testCase.Name}.context.json";
|
||||
|
||||
/// <summary>
|
||||
/// Writes a message to the xunit test output, if available, otherwise to the console.
|
||||
/// </summary>
|
||||
protected void Log(string message)
|
||||
{
|
||||
if (this._output is not null)
|
||||
{
|
||||
this._output.WriteLine(message);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace AgentConversation.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Captures the size characteristics of a conversation context at a specific point in time.
|
||||
/// </summary>
|
||||
public sealed class ConversationMetrics
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the number of messages in the conversation context.
|
||||
/// </summary>
|
||||
public required int MessageCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the approximate serialized size of the conversation context in bytes.
|
||||
/// This serves as a proxy for context window consumption.
|
||||
/// </summary>
|
||||
public required long SerializedSizeBytes { get; init; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() =>
|
||||
$"Messages={MessageCount}, Size={SerializedSizeBytes}B";
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace AgentConversation.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Captures the before-and-after <see cref="ConversationMetrics"/> for a single test case run,
|
||||
/// enabling comparison and reporting of context size changes.
|
||||
/// </summary>
|
||||
public sealed class ConversationMetricsReport
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the metrics captured before the agent steps were executed.
|
||||
/// </summary>
|
||||
public required ConversationMetrics Before { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the metrics captured after the agent steps were executed.
|
||||
/// </summary>
|
||||
public required ConversationMetrics After { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the change in message count between <see cref="Before"/> and <see cref="After"/>.
|
||||
/// A positive value means messages were added; a negative value means compaction removed messages.
|
||||
/// </summary>
|
||||
public int MessageCountDelta => After.MessageCount - Before.MessageCount;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the change in serialized size in bytes between <see cref="Before"/> and <see cref="After"/>.
|
||||
/// </summary>
|
||||
public long SizeDeltaBytes => After.SerializedSizeBytes - Before.SerializedSizeBytes;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() =>
|
||||
$"Before=[{Before}] After=[{After}] Delta=[Messages={MessageCountDelta:+#;-#;0}, Size={SizeDeltaBytes:+#;-#;0}B]";
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace AgentConversation.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single step within a <see cref="IConversationTestCase"/>, combining the agent to invoke,
|
||||
/// an optional input message, and an optional validation delegate.
|
||||
/// </summary>
|
||||
public sealed class ConversationStep
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the agent to invoke for this step.
|
||||
/// Must match a key in <see cref="IConversationTestCase.AgentDefinitions"/>.
|
||||
/// </summary>
|
||||
public required string AgentName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the optional input message to send to the agent.
|
||||
/// When <see langword="null"/>, the agent is invoked with no new user input (useful for
|
||||
/// eliciting a response from the existing conversation context).
|
||||
/// </summary>
|
||||
public ChatMessage? Input { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an optional delegate that validates the agent response and metrics for this step.
|
||||
/// When <see langword="null"/>, no validation is performed.
|
||||
/// </summary>
|
||||
public Action<AgentResponse, ConversationMetricsReport>? Validate { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace AgentConversation.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a single agent conversation test case.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Each test case describes the initial conversation context (as a list of <see cref="ChatMessage"/> instances),
|
||||
/// the agents that participate in the conversation, the steps to execute, and the expected outcomes.
|
||||
/// The initial context can either be loaded from a previously serialized file or generated on-demand
|
||||
/// via <see cref="CreateInitialContextAsync"/>.
|
||||
/// </remarks>
|
||||
public interface IConversationTestCase
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the human-readable name that uniquely identifies this test case.
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the agents involved in this test case, keyed by their names.
|
||||
/// Each entry is a <see cref="ConversationAgentDefinition"/> that describes how to create the agent.
|
||||
/// </summary>
|
||||
IReadOnlyDictionary<string, ConversationAgentDefinition> AgentDefinitions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns the initial list of <see cref="ChatMessage"/> instances to restore into the conversation
|
||||
/// context before any steps are executed.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// The initial chat messages. These are typically loaded from a previously serialized JSON file
|
||||
/// produced by <see cref="CreateInitialContextAsync"/>.
|
||||
/// </returns>
|
||||
IList<ChatMessage> GetInitialMessages();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ordered list of steps to execute against the restored conversation context.
|
||||
/// </summary>
|
||||
IReadOnlyList<ConversationStep> Steps { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates the initial conversation context by actually driving a conversation with the provided agents,
|
||||
/// then returns the resulting list of messages.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is intended to be called once (e.g., during a setup phase) to produce the serialized
|
||||
/// context that subsequent test runs will deserialize. Implementations should build up a long or
|
||||
/// complex conversation that is representative of the long-running operation scenario being validated.
|
||||
/// </remarks>
|
||||
/// <param name="agents">
|
||||
/// The agents to use when building the initial context, keyed by their names as defined in
|
||||
/// <see cref="AgentDefinitions"/>.
|
||||
/// </param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
/// <returns>
|
||||
/// The ordered list of <see cref="ChatMessage"/> instances that form the initial context.
|
||||
/// </returns>
|
||||
Task<IList<ChatMessage>> CreateInitialContextAsync(
|
||||
IReadOnlyDictionary<string, AIAgent> agents,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace AgentConversation.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Abstracts the system-specific concerns of an agent conversation test run: how agents are created
|
||||
/// and how context compaction is performed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Implement this interface to adapt the <see cref="ConversationHarness"/> to a particular AI backend
|
||||
/// (e.g., OpenAI Chat Completion, Azure AI, OpenAI Responses API). Each implementation controls:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>
|
||||
/// How <see cref="ConversationAgentDefinition"/> instances are turned into live <see cref="AIAgent"/> objects.
|
||||
/// </description></item>
|
||||
/// <item><description>
|
||||
/// How context compaction is applied to a list of messages. Compaction is optional; returning
|
||||
/// <see langword="null"/> means no compaction is performed.
|
||||
/// </description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public interface IConversationTestSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a live <see cref="AIAgent"/> from the supplied <paramref name="definition"/>.
|
||||
/// </summary>
|
||||
/// <param name="definition">The definition describing the agent to create.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
/// <returns>A fully-initialised <see cref="AIAgent"/> ready to participate in the conversation.</returns>
|
||||
Task<AIAgent> CreateAgentAsync(
|
||||
ConversationAgentDefinition definition,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Optionally compacts (reduces) the supplied <paramref name="messages"/>.
|
||||
/// </summary>
|
||||
/// <param name="messages">The current list of messages to compact.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
/// <returns>
|
||||
/// The compacted list of messages, or <see langword="null"/> if no compaction was performed.
|
||||
/// When <see langword="null"/> is returned the original <paramref name="messages"/> list is used unchanged.
|
||||
/// </returns>
|
||||
Task<IList<ChatMessage>?> CompactAsync(
|
||||
IList<ChatMessage> messages,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
# AgentConversation Integration Test Harness
|
||||
|
||||
The `AgentConversation.IntegrationTests` project provides a **reusable test harness** for validating conversation dynamics in long-running, multi-agent scenarios that involve tool use. Instead of rebuilding a large conversation context in every test run, the harness allows you to:
|
||||
|
||||
1. **Capture** a representative conversation context once (by driving a real conversation with your AI agents).
|
||||
2. **Serialize** that context to a JSON file and commit it alongside your tests.
|
||||
3. **Restore** the saved context in each test run.
|
||||
4. **Solicit** one or more agent responses from the restored context.
|
||||
5. **Validate** each response and compare before/after metrics.
|
||||
|
||||
---
|
||||
|
||||
## Key Abstractions
|
||||
|
||||
| Type | Role |
|
||||
|------|------|
|
||||
| `IConversationTestCase` | Defines a test case: agents, initial messages, ordered steps, and a method to automate context creation. |
|
||||
| `IConversationTestSystem` | System-specific plugin: how to **create agents** and how to apply **context compaction** (both vary per AI backend). |
|
||||
| `ConversationAgentDefinition` | Describes a participating agent — name, instructions, and optional tools. |
|
||||
| `ConversationStep` | One step in the conversation: which agent to invoke, an optional input message, and an optional validation delegate. |
|
||||
| `ConversationMetrics` | A snapshot of conversation context size — message count and serialized byte size. |
|
||||
| `ConversationMetricsReport` | A before/after `ConversationMetrics` pair with delta helpers for reporting. |
|
||||
| `ConversationContextSerializer` | Serializes and deserializes `IList<ChatMessage>` to/from JSON strings or files. |
|
||||
| `ConversationHarness` | The core runner that ties everything together. |
|
||||
| `ConversationHarnessTests<TSystem>` | Abstract xunit base class that subclasses inherit to get the `RunAllTestCasesAsync` test. |
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
### 1. Implement `IConversationTestSystem`
|
||||
|
||||
Provide an implementation that knows how to create agents for your target AI backend and optionally compact messages:
|
||||
|
||||
```csharp
|
||||
public sealed class OpenAIConversationTestSystem : IConversationTestSystem
|
||||
{
|
||||
public Task<AIAgent> CreateAgentAsync(ConversationAgentDefinition definition, CancellationToken ct = default)
|
||||
{
|
||||
var chatClient = new OpenAIClient(apiKey)
|
||||
.GetChatClient("gpt-4o")
|
||||
.AsIChatClient();
|
||||
|
||||
AIAgent agent = new ChatClientAgent(chatClient, options: new()
|
||||
{
|
||||
Name = definition.Name,
|
||||
ChatOptions = new() { Instructions = definition.Instructions, Tools = definition.Tools }
|
||||
});
|
||||
|
||||
return Task.FromResult(agent);
|
||||
}
|
||||
|
||||
public Task<IList<ChatMessage>?> CompactAsync(IList<ChatMessage> messages, CancellationToken ct = default)
|
||||
{
|
||||
// Return null for no compaction, or apply an IChatReducer here.
|
||||
return Task.FromResult<IList<ChatMessage>?>(null);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Implement `IConversationTestCase`
|
||||
|
||||
Define the agents involved, the initial context to restore, and the steps to execute:
|
||||
|
||||
```csharp
|
||||
public sealed class MyConversationTestCase : IConversationTestCase
|
||||
{
|
||||
public string Name => "MyConversation";
|
||||
|
||||
public IReadOnlyDictionary<string, ConversationAgentDefinition> AgentDefinitions { get; } =
|
||||
new Dictionary<string, ConversationAgentDefinition>
|
||||
{
|
||||
["Assistant"] = new() { Name = "Assistant", Instructions = "You are a helpful assistant." }
|
||||
};
|
||||
|
||||
// Load the saved context from a JSON fixture file.
|
||||
public IList<ChatMessage> GetInitialMessages() =>
|
||||
ConversationContextSerializer.LoadFromFile("fixtures/my-conversation.context.json");
|
||||
|
||||
public IReadOnlyList<ConversationStep> Steps { get; } =
|
||||
[
|
||||
new ConversationStep
|
||||
{
|
||||
AgentName = "Assistant",
|
||||
Input = new ChatMessage(ChatRole.User, "Summarize our conversation so far."),
|
||||
Validate = (response, metrics) =>
|
||||
{
|
||||
Assert.NotEmpty(response.Text);
|
||||
Assert.True(metrics.After.MessageCount > metrics.Before.MessageCount);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// Called once to generate the fixture file — not during normal CI runs.
|
||||
public async Task<IList<ChatMessage>> CreateInitialContextAsync(
|
||||
IReadOnlyDictionary<string, AIAgent> agents, CancellationToken ct = default)
|
||||
{
|
||||
var agent = agents["Assistant"];
|
||||
var session = await agent.CreateSessionAsync(ct);
|
||||
|
||||
// Drive a rich, representative conversation to build up the context.
|
||||
await agent.RunAsync(new ChatMessage(ChatRole.User, "Tell me about the weather."), session, cancellationToken: ct);
|
||||
await agent.RunAsync(new ChatMessage(ChatRole.User, "What about tomorrow?"), session, cancellationToken: ct);
|
||||
// ... more turns ...
|
||||
|
||||
var provider = agent.GetService<InMemoryChatHistoryProvider>()!;
|
||||
return provider.GetMessages(session);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Derive from `ConversationHarnessTests<TSystem>`
|
||||
|
||||
Wire the system and test cases into a concrete test class:
|
||||
|
||||
```csharp
|
||||
public class MyConversationTests(ITestOutputHelper output)
|
||||
: ConversationHarnessTests<OpenAIConversationTestSystem>(output)
|
||||
{
|
||||
protected override OpenAIConversationTestSystem CreateTestSystem() => new();
|
||||
|
||||
protected override IEnumerable<IConversationTestCase> GetTestCases() =>
|
||||
[
|
||||
new MyConversationTestCase(),
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
The inherited `RunAllTestCasesAsync` test method will automatically run all cases and log the before/after metrics to the xunit test output.
|
||||
|
||||
---
|
||||
|
||||
## Generating Initial Context Fixtures
|
||||
|
||||
The context fixture files need to be generated once and committed to the repository. Run the inherited `SerializeAllInitialContextsAsync` test to produce them:
|
||||
|
||||
```bash
|
||||
dotnet test --filter "FullyQualifiedName~SerializeAllInitialContexts"
|
||||
```
|
||||
|
||||
This test is **skipped during normal CI runs** to avoid expensive AI calls. After generating the files, commit them alongside your test code so that all subsequent runs can restore the context without calling the AI service again.
|
||||
|
||||
---
|
||||
|
||||
## Metrics Reporting
|
||||
|
||||
After each test case runs, a `ConversationMetricsReport` is logged. It captures:
|
||||
|
||||
- **`Before`** — message count and serialized byte size of the initial context.
|
||||
- **`After`** — message count and byte size after all steps have executed.
|
||||
- **`MessageCountDelta`** / **`SizeDeltaBytes`** — the change between before and after.
|
||||
|
||||
Example output:
|
||||
|
||||
```
|
||||
[MyConversation] Before=[Messages=12, Size=4096B] After=[Messages=14, Size=4712B] Delta=[Messages=+2, Size=+616B]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example
|
||||
|
||||
See [`Microsoft.Agents.AI.Abstractions.IntegrationTests`](../Microsoft.Agents.AI.Abstractions.IntegrationTests) for a self-contained working example that uses an in-memory mock `IChatClient` so it runs without live AI credentials.
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using AgentConversation.IntegrationTests;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.Agents.AI.Abstractions.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Example integration tests that exercise the <see cref="ConversationHarness"/> using an
|
||||
/// in-memory <see cref="InMemoryConversationTestSystem"/> that does not require live AI credentials.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This class derives from <see cref="ConversationHarnessTests{TSystem}"/> and provides the system
|
||||
/// and test cases. The <see cref="ConversationHarnessTests{TSystem}.RunAllTestCasesAsync"/> test
|
||||
/// method is inherited automatically and will run all cases returned by <see cref="GetTestCases"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// To adapt these tests to a real AI backend, replace <see cref="InMemoryConversationTestSystem"/>
|
||||
/// with an implementation that constructs agents backed by your AI service.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public class AgentConversationHarnessTests(ITestOutputHelper output)
|
||||
: ConversationHarnessTests<InMemoryConversationTestSystem>(output)
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override InMemoryConversationTestSystem CreateTestSystem() =>
|
||||
new InMemoryConversationTestSystem();
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override IEnumerable<IConversationTestCase> GetTestCases() =>
|
||||
[
|
||||
new MenuConversationTestCase(),
|
||||
];
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AgentConversation.IntegrationTests;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Moq;
|
||||
|
||||
namespace Microsoft.Agents.AI.Abstractions.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// An example <see cref="IConversationTestSystem"/> that uses an in-memory mock <see cref="IChatClient"/>
|
||||
/// so the harness can be exercised without live AI service credentials.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// In a real integration test against a live AI service (e.g., OpenAI Chat Completion), this class
|
||||
/// would be replaced with an implementation that constructs <see cref="ChatClientAgent"/> instances
|
||||
/// backed by the real <see cref="IChatClient"/>. The compaction contract can similarly be wired up
|
||||
/// to an <see cref="Microsoft.Extensions.AI.IChatReducer"/> of your choice.
|
||||
/// </remarks>
|
||||
public sealed class InMemoryConversationTestSystem : IConversationTestSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// A deterministic response suffix appended by the mock chat client to every assistant reply.
|
||||
/// Test validations can assert on this value to confirm the mock was invoked.
|
||||
/// </summary>
|
||||
public const string MockResponseSuffix = "[mock-response]";
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<AIAgent> CreateAgentAsync(
|
||||
ConversationAgentDefinition definition,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Create a mock IChatClient that returns a deterministic response.
|
||||
var mockClient = new Mock<IChatClient>();
|
||||
mockClient
|
||||
.Setup(c => c.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions?>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(() => new ChatResponse(
|
||||
new ChatMessage(ChatRole.Assistant,
|
||||
$"Here are today's specials: Clam Chowder, Cobb Salad, Chai Tea. {MockResponseSuffix}")));
|
||||
|
||||
// GetService is called internally by the harness for metadata; return null for unknown types.
|
||||
mockClient
|
||||
.Setup(c => c.GetService(It.IsAny<System.Type>(), It.IsAny<object?>()))
|
||||
.Returns((System.Type _, object? _) => null);
|
||||
|
||||
AIAgent agent = new ChatClientAgent(
|
||||
mockClient.Object,
|
||||
options: new ChatClientAgentOptions
|
||||
{
|
||||
Name = definition.Name,
|
||||
ChatOptions = new ChatOptions
|
||||
{
|
||||
Instructions = definition.Instructions,
|
||||
Tools = definition.Tools is not null ? new System.Collections.Generic.List<AITool>(definition.Tools) : null,
|
||||
}
|
||||
});
|
||||
|
||||
return Task.FromResult(agent);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IList<ChatMessage>?> CompactAsync(
|
||||
IList<ChatMessage> messages,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// No compaction in this example system.
|
||||
return Task.FromResult<IList<ChatMessage>?>(null);
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AgentConversation.IntegrationTests;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Abstractions.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// An example <see cref="IConversationTestCase"/> that validates the harness can restore a
|
||||
/// pre-built conversation context and solicit a response from an agent.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This test case uses a fixed, in-memory conversation representing a menu-ordering interaction.
|
||||
/// The messages are defined inline (no JSON fixture file is required), which makes this a
|
||||
/// self-contained example that runs without live AI credentials.
|
||||
/// </remarks>
|
||||
public sealed class MenuConversationTestCase : IConversationTestCase
|
||||
{
|
||||
private const string AgentKey = "MenuAgent";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "MenuConversation";
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyDictionary<string, ConversationAgentDefinition> AgentDefinitions { get; } =
|
||||
new Dictionary<string, ConversationAgentDefinition>
|
||||
{
|
||||
[AgentKey] = new ConversationAgentDefinition
|
||||
{
|
||||
Name = AgentKey,
|
||||
Instructions = "You are a helpful restaurant assistant. Answer questions about the menu.",
|
||||
Tools =
|
||||
[
|
||||
AIFunctionFactory.Create(MenuTools.GetSpecials),
|
||||
AIFunctionFactory.Create(MenuTools.GetItemPrice),
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<ConversationStep> Steps { get; } =
|
||||
[
|
||||
new ConversationStep
|
||||
{
|
||||
AgentName = AgentKey,
|
||||
Input = new ChatMessage(ChatRole.User, "What are the specials today?"),
|
||||
Validate = (response, metrics) =>
|
||||
{
|
||||
Assert.NotNull(response);
|
||||
Assert.NotEmpty(response.Text);
|
||||
Assert.True(metrics.After.MessageCount > metrics.Before.MessageCount,
|
||||
"Message count should grow after the step.");
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
/// <inheritdoc />
|
||||
public IList<ChatMessage> GetInitialMessages() =>
|
||||
// A short, representative conversation context that is already in memory.
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Hello, I'd like to see the menu."),
|
||||
new ChatMessage(ChatRole.Assistant, "Welcome! I'm happy to help you with our menu. Feel free to ask about today's specials or the price of any item."),
|
||||
];
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IList<ChatMessage>> CreateInitialContextAsync(
|
||||
IReadOnlyDictionary<string, AIAgent> agents,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Build the initial context by running a short greeting exchange.
|
||||
var agent = agents[AgentKey];
|
||||
var session = await agent.CreateSessionAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await agent.RunAsync(
|
||||
new ChatMessage(ChatRole.User, "Hello, I'd like to see the menu."),
|
||||
session,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var historyProvider = agent.GetService<ChatHistoryProvider>() as InMemoryChatHistoryProvider;
|
||||
if (historyProvider is not null)
|
||||
{
|
||||
return historyProvider.GetMessages(session);
|
||||
}
|
||||
|
||||
return GetInitialMessages();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace Microsoft.Agents.AI.Abstractions.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Example tools used by the <see cref="MenuConversationTestCase"/> to simulate a restaurant menu service.
|
||||
/// </summary>
|
||||
internal static class MenuTools
|
||||
{
|
||||
[Description("Provides a list of today's specials from the restaurant 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";
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<InjectSharedIntegrationTestCode>True</InjectSharedIntegrationTestCode>
|
||||
<InjectIsExternalInitOnLegacy>true</InjectIsExternalInitOnLegacy>
|
||||
<InjectRequiredMemberOnLegacy>true</InjectRequiredMemberOnLegacy>
|
||||
<InjectCompilerFeatureRequiredOnLegacy>true</InjectCompilerFeatureRequiredOnLegacy>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.Abstractions\Microsoft.Agents.AI.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
|
||||
<ProjectReference Include="..\AgentConversation.IntegrationTests\AgentConversation.IntegrationTests.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user