Rename ConversationDynamics to AgentConversation; add example test project and README

Co-authored-by: crickman <66376200+crickman@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-03-04 00:05:43 +00:00
Unverified
parent 0888c89a4e
commit 98819e5e94
18 changed files with 418 additions and 13 deletions
+2 -1
View File
@@ -464,7 +464,8 @@
<Folder Name="/Tests/" />
<Folder Name="/Tests/IntegrationTests/">
<Project Path="tests/AgentConformance.IntegrationTests/AgentConformance.IntegrationTests.csproj" />
<Project Path="tests/ConversationDynamics.IntegrationTests/ConversationDynamics.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" />
@@ -3,7 +3,7 @@
using System.Collections.Generic;
using Microsoft.Extensions.AI;
namespace ConversationDynamics.IntegrationTests;
namespace AgentConversation.IntegrationTests;
/// <summary>
/// Defines an agent participating in a <see cref="IConversationTestCase"/>.
@@ -7,7 +7,7 @@ using System.Text.Json;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
namespace ConversationDynamics.IntegrationTests;
namespace AgentConversation.IntegrationTests;
/// <summary>
/// Provides helpers for serializing and deserializing conversation contexts (lists of <see cref="ChatMessage"/>)
@@ -8,7 +8,7 @@ using System.Threading.Tasks;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
namespace ConversationDynamics.IntegrationTests;
namespace AgentConversation.IntegrationTests;
/// <summary>
/// Orchestrates the execution of a <see cref="IConversationTestCase"/> against a given
@@ -6,10 +6,10 @@ using System.Diagnostics;
using System.Threading.Tasks;
using Xunit.Abstractions;
namespace ConversationDynamics.IntegrationTests;
namespace AgentConversation.IntegrationTests;
/// <summary>
/// Abstract xunit base class for conversation dynamics integration tests.
/// Abstract xunit base class for agent conversation integration tests.
/// </summary>
/// <remarks>
/// <para>
@@ -1,6 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
namespace ConversationDynamics.IntegrationTests;
namespace AgentConversation.IntegrationTests;
/// <summary>
/// Captures the size characteristics of a conversation context at a specific point in time.
@@ -1,6 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
namespace ConversationDynamics.IntegrationTests;
namespace AgentConversation.IntegrationTests;
/// <summary>
/// Captures the before-and-after <see cref="ConversationMetrics"/> for a single test case run,
@@ -4,7 +4,7 @@ using System;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
namespace ConversationDynamics.IntegrationTests;
namespace AgentConversation.IntegrationTests;
/// <summary>
/// Represents a single step within a <see cref="IConversationTestCase"/>, combining the agent to invoke,
@@ -6,10 +6,10 @@ using System.Threading.Tasks;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
namespace ConversationDynamics.IntegrationTests;
namespace AgentConversation.IntegrationTests;
/// <summary>
/// Defines a single conversation dynamics test case.
/// 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),
@@ -6,10 +6,10 @@ using System.Threading.Tasks;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
namespace ConversationDynamics.IntegrationTests;
namespace AgentConversation.IntegrationTests;
/// <summary>
/// Abstracts the system-specific concerns of a conversation dynamics test run: how agents are created
/// Abstracts the system-specific concerns of an agent conversation test run: how agents are created
/// and how context compaction is performed.
/// </summary>
/// <remarks>
@@ -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.
@@ -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(),
];
}
@@ -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);
}
}
@@ -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";
}
@@ -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>