mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
d3f0c33180
* Checkpoint * Checkpoint * Stable * Strategies * Updated * Encoding * Formatting * Cleanup * Formatting * Tests * Tuning * Update tests * Test update * Remove working solution * Add sample to solution * Sample readyme * Experimental * Format * Formatting * Encoding * Support IChatReducer * Sample output formatting * Initial plan * Replace CompactingChatClient with MessageCompactionContextProvider Co-authored-by: crickman <66376200+crickman@users.noreply.github.com> * Boundary condition * Fix encoding * Fix cast * Test coverage * Namespace * Improvements * Efficiency * Cleanup * Detect service managed conversation * Fix namespace * Fix merge * Fix test expectation * Update dotnet/src/Microsoft.Agents.AI.Abstractions/InMemoryChatHistoryProvider.cs Co-authored-by: westey <164392973+westey-m@users.noreply.github.com> * Address PR comments (x1) * Update comment * Update comments * Clean-up * Format output * Sync sample comment * Fix condition * Adjust data-flow * Address comments (x2) * Direct compaction * Fix summarization content * Argument check / fix count calculation * Minor follow-up * Diagnostics * Minor updates * Fix state test * Fix sliding window perf * Stable state keys * Increase size computation * Formatting * Add README.md for Agent_Step18_CompactionPipeline sample (#4574) * Sample comments * Updated * Update dotnet/src/Microsoft.Agents.AI/Compaction/MessageIndex.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/CompactionProviderTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dotnet/src/Microsoft.Agents.AI/Compaction/MessageIndex.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Address copilot comments * Fix namespace * Comments / convensions * Prefix `MessageGroup` and `MessageIndex` * Fix sliding window * Update dotnet/src/Microsoft.Agents.AI/Compaction/SummarizationCompactionStrategy.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dotnet/src/Microsoft.Agents.AI.Abstractions/InMemoryChatHistoryProvider.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Python alignment * Fix merge * Fix equality, readme, and sample * Readme update and ToolResult fix * Update dotnet/src/Microsoft.Agents.AI/Compaction/SummarizationCompactionStrategy.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dotnet/samples/02-agents/Agents/Agent_Step18_CompactionPipeline/README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Simplify readme * Update dotnet/samples/02-agents/Agents/Agent_Step18_CompactionPipeline/README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Remove example * Remove unused --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: westey <164392973+westey-m@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
367 lines
12 KiB
C#
367 lines
12 KiB
C#
// Copyright (c) Microsoft. All rights reserved.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Agents.AI.Compaction;
|
|
using Microsoft.Extensions.AI;
|
|
using Moq;
|
|
|
|
namespace Microsoft.Agents.AI.UnitTests.Compaction;
|
|
|
|
/// <summary>
|
|
/// Contains tests for the <see cref="CompactionProvider"/> class.
|
|
/// </summary>
|
|
public sealed class CompactionProviderTests
|
|
{
|
|
[Fact]
|
|
public void ConstructorThrowsOnNullStrategy()
|
|
{
|
|
Assert.Throws<ArgumentNullException>(() => new CompactionProvider(null!));
|
|
}
|
|
|
|
[Fact]
|
|
public void StateKeysReturnsExpectedKey()
|
|
{
|
|
// Arrange
|
|
TruncationCompactionStrategy strategy = new(CompactionTriggers.TokensExceed(100000));
|
|
CompactionProvider provider = new(strategy);
|
|
|
|
// Act & Assert — default state key is the strategy type name
|
|
Assert.Single(provider.StateKeys);
|
|
Assert.Equal(nameof(TruncationCompactionStrategy), provider.StateKeys[0]);
|
|
}
|
|
|
|
[Fact]
|
|
public void StateKeysAreStableAcrossEquivalentInstances()
|
|
{
|
|
// Arrange — two providers with equivalent (but distinct) strategies
|
|
CompactionProvider provider1 = new(new TruncationCompactionStrategy(CompactionTriggers.TokensExceed(100000)));
|
|
CompactionProvider provider2 = new(new TruncationCompactionStrategy(CompactionTriggers.TokensExceed(100000)));
|
|
|
|
// Act & Assert — default keys must be identical for session state stability
|
|
Assert.Equal(provider1.StateKeys[0], provider2.StateKeys[0]);
|
|
}
|
|
|
|
[Fact]
|
|
public void StateKeysReturnsCustomKeyWhenProvided()
|
|
{
|
|
// Arrange
|
|
TruncationCompactionStrategy strategy = new(CompactionTriggers.TokensExceed(100000));
|
|
CompactionProvider provider = new(strategy, stateKey: "my-custom-key");
|
|
|
|
// Act & Assert
|
|
Assert.Single(provider.StateKeys);
|
|
Assert.Equal("my-custom-key", provider.StateKeys[0]);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InvokingAsyncNoSessionPassesThroughAsync()
|
|
{
|
|
// Arrange — no session → passthrough
|
|
TruncationCompactionStrategy strategy = new(CompactionTriggers.TokensExceed(100000));
|
|
CompactionProvider provider = new(strategy);
|
|
|
|
Mock<AIAgent> mockAgent = new() { CallBase = true };
|
|
List<ChatMessage> messages =
|
|
[
|
|
new ChatMessage(ChatRole.User, "Hello"),
|
|
];
|
|
|
|
AIContextProvider.InvokingContext context = new(
|
|
mockAgent.Object,
|
|
session: null,
|
|
new AIContext { Messages = messages });
|
|
|
|
// Act
|
|
AIContext result = await provider.InvokingAsync(context);
|
|
|
|
// Assert — original context returned unchanged
|
|
Assert.Same(messages, result.Messages);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InvokingAsyncNullMessagesPassesThroughAsync()
|
|
{
|
|
// Arrange — messages is null → passthrough
|
|
TruncationCompactionStrategy strategy = new(CompactionTriggers.TokensExceed(100000));
|
|
CompactionProvider provider = new(strategy);
|
|
|
|
Mock<AIAgent> mockAgent = new() { CallBase = true };
|
|
TestAgentSession session = new();
|
|
AIContextProvider.InvokingContext context = new(
|
|
mockAgent.Object,
|
|
session,
|
|
new AIContext { Messages = null });
|
|
|
|
// Act
|
|
AIContext result = await provider.InvokingAsync(context);
|
|
|
|
// Assert — original context returned unchanged
|
|
Assert.Null(result.Messages);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InvokingAsyncAppliesCompactionWhenTriggeredAsync()
|
|
{
|
|
// Arrange — strategy that always triggers and keeps only 1 group
|
|
TruncationCompactionStrategy strategy = new(_ => true, minimumPreservedGroups: 1);
|
|
CompactionProvider provider = new(strategy);
|
|
|
|
Mock<AIAgent> mockAgent = new() { CallBase = true };
|
|
TestAgentSession session = new();
|
|
List<ChatMessage> messages =
|
|
[
|
|
new ChatMessage(ChatRole.User, "Q1"),
|
|
new ChatMessage(ChatRole.Assistant, "A1"),
|
|
new ChatMessage(ChatRole.User, "Q2"),
|
|
];
|
|
|
|
AIContextProvider.InvokingContext context = new(
|
|
mockAgent.Object,
|
|
session,
|
|
new AIContext { Messages = messages });
|
|
|
|
// Act
|
|
AIContext result = await provider.InvokingAsync(context);
|
|
|
|
// Assert — compaction should have reduced the message count
|
|
Assert.NotNull(result.Messages);
|
|
List<ChatMessage> resultList = [.. result.Messages!];
|
|
Assert.True(resultList.Count < messages.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InvokingAsyncNoCompactionNeededReturnsOriginalMessagesAsync()
|
|
{
|
|
// Arrange — trigger never fires → no compaction
|
|
TruncationCompactionStrategy strategy = new(CompactionTriggers.TokensExceed(100000));
|
|
CompactionProvider provider = new(strategy);
|
|
|
|
Mock<AIAgent> mockAgent = new() { CallBase = true };
|
|
TestAgentSession session = new();
|
|
List<ChatMessage> messages =
|
|
[
|
|
new ChatMessage(ChatRole.User, "Hello"),
|
|
];
|
|
|
|
AIContextProvider.InvokingContext context = new(
|
|
mockAgent.Object,
|
|
session,
|
|
new AIContext { Messages = messages });
|
|
|
|
// Act
|
|
AIContext result = await provider.InvokingAsync(context);
|
|
|
|
// Assert — original messages passed through
|
|
Assert.NotNull(result.Messages);
|
|
List<ChatMessage> resultList = [.. result.Messages!];
|
|
Assert.Single(resultList);
|
|
Assert.Equal("Hello", resultList[0].Text);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InvokingAsyncPreservesInstructionsAndToolsAsync()
|
|
{
|
|
// Arrange
|
|
TruncationCompactionStrategy strategy = new(CompactionTriggers.TokensExceed(100000));
|
|
CompactionProvider provider = new(strategy);
|
|
|
|
Mock<AIAgent> mockAgent = new() { CallBase = true };
|
|
TestAgentSession session = new();
|
|
List<ChatMessage> messages = [new ChatMessage(ChatRole.User, "Hello")];
|
|
AITool[] tools = [AIFunctionFactory.Create(() => "tool", "MyTool")];
|
|
|
|
AIContextProvider.InvokingContext context = new(
|
|
mockAgent.Object,
|
|
session,
|
|
new AIContext
|
|
{
|
|
Instructions = "Be helpful",
|
|
Messages = messages,
|
|
Tools = tools
|
|
});
|
|
|
|
// Act
|
|
AIContext result = await provider.InvokingAsync(context);
|
|
|
|
// Assert — instructions and tools are preserved
|
|
Assert.Equal("Be helpful", result.Instructions);
|
|
Assert.Same(tools, result.Tools);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InvokingAsyncWithExistingIndexUpdatesAsync()
|
|
{
|
|
// Arrange — call twice to exercise the "existing index" path
|
|
TruncationCompactionStrategy strategy = new(_ => true, minimumPreservedGroups: 1);
|
|
CompactionProvider provider = new(strategy);
|
|
|
|
Mock<AIAgent> mockAgent = new() { CallBase = true };
|
|
TestAgentSession session = new();
|
|
|
|
List<ChatMessage> messages1 =
|
|
[
|
|
new ChatMessage(ChatRole.User, "Q1"),
|
|
new ChatMessage(ChatRole.Assistant, "A1"),
|
|
new ChatMessage(ChatRole.User, "Q2"),
|
|
];
|
|
|
|
AIContextProvider.InvokingContext context1 = new(
|
|
mockAgent.Object,
|
|
session,
|
|
new AIContext { Messages = messages1 });
|
|
|
|
// First call — initializes state
|
|
await provider.InvokingAsync(context1);
|
|
|
|
List<ChatMessage> messages2 =
|
|
[
|
|
new ChatMessage(ChatRole.User, "Q1"),
|
|
new ChatMessage(ChatRole.Assistant, "A1"),
|
|
new ChatMessage(ChatRole.User, "Q2"),
|
|
new ChatMessage(ChatRole.Assistant, "A2"),
|
|
new ChatMessage(ChatRole.User, "Q3"),
|
|
];
|
|
|
|
AIContextProvider.InvokingContext context2 = new(
|
|
mockAgent.Object,
|
|
session,
|
|
new AIContext { Messages = messages2 });
|
|
|
|
// Act — second call exercises the update path
|
|
AIContext result = await provider.InvokingAsync(context2);
|
|
|
|
// Assert
|
|
Assert.NotNull(result.Messages);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InvokingAsyncWithNonListEnumerableCreatesListCopyAsync()
|
|
{
|
|
// Arrange — pass IEnumerable (not List<ChatMessage>) to exercise the list copy branch
|
|
TruncationCompactionStrategy strategy = new(CompactionTriggers.TokensExceed(100000));
|
|
CompactionProvider provider = new(strategy);
|
|
|
|
Mock<AIAgent> mockAgent = new() { CallBase = true };
|
|
TestAgentSession session = new();
|
|
|
|
// Use an IEnumerable (not a List) to trigger the copy path
|
|
IEnumerable<ChatMessage> messages = [new ChatMessage(ChatRole.User, "Hello")];
|
|
|
|
AIContextProvider.InvokingContext context = new(
|
|
mockAgent.Object,
|
|
session,
|
|
new AIContext { Messages = messages });
|
|
|
|
// Act
|
|
AIContext result = await provider.InvokingAsync(context);
|
|
|
|
// Assert
|
|
Assert.NotNull(result.Messages);
|
|
List<ChatMessage> resultList = [.. result.Messages!];
|
|
Assert.Single(resultList);
|
|
Assert.Equal("Hello", resultList[0].Text);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CompactAsyncThrowsOnNullStrategyAsync()
|
|
{
|
|
List<ChatMessage> messages = [new ChatMessage(ChatRole.User, "Hello")];
|
|
|
|
await Assert.ThrowsAsync<ArgumentNullException>(() => CompactionProvider.CompactAsync(null!, messages));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CompactAsyncReturnsAllMessagesWhenTriggerDoesNotFireAsync()
|
|
{
|
|
// Arrange — trigger never fires → no compaction
|
|
TruncationCompactionStrategy strategy = new(CompactionTriggers.TokensExceed(100000));
|
|
List<ChatMessage> messages =
|
|
[
|
|
new ChatMessage(ChatRole.User, "Q1"),
|
|
new ChatMessage(ChatRole.Assistant, "A1"),
|
|
new ChatMessage(ChatRole.User, "Q2"),
|
|
];
|
|
|
|
// Act
|
|
IEnumerable<ChatMessage> result = await CompactionProvider.CompactAsync(strategy, messages);
|
|
|
|
// Assert — all messages preserved
|
|
List<ChatMessage> resultList = [.. result];
|
|
Assert.Equal(messages.Count, resultList.Count);
|
|
Assert.Equal("Q1", resultList[0].Text);
|
|
Assert.Equal("A1", resultList[1].Text);
|
|
Assert.Equal("Q2", resultList[2].Text);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CompactAsyncReducesMessagesWhenTriggeredAsync()
|
|
{
|
|
// Arrange — strategy that always triggers and keeps only 1 group
|
|
TruncationCompactionStrategy strategy = new(CompactionTriggers.Always, minimumPreservedGroups: 1);
|
|
List<ChatMessage> messages =
|
|
[
|
|
new ChatMessage(ChatRole.User, "Q1"),
|
|
new ChatMessage(ChatRole.Assistant, "A1"),
|
|
new ChatMessage(ChatRole.User, "Q2"),
|
|
];
|
|
|
|
// Act
|
|
IEnumerable<ChatMessage> result = await CompactionProvider.CompactAsync(strategy, messages);
|
|
|
|
// Assert — compaction should have reduced the message count
|
|
List<ChatMessage> resultList = [.. result];
|
|
Assert.True(resultList.Count < messages.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CompactAsyncHandlesEmptyMessageListAsync()
|
|
{
|
|
// Arrange
|
|
TruncationCompactionStrategy strategy = new(CompactionTriggers.Always, minimumPreservedGroups: 1);
|
|
List<ChatMessage> messages = [];
|
|
|
|
// Act
|
|
IEnumerable<ChatMessage> result = await CompactionProvider.CompactAsync(strategy, messages);
|
|
|
|
// Assert
|
|
Assert.Empty(result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CompactAsyncWorksWithNonListEnumerableAsync()
|
|
{
|
|
// Arrange — IEnumerable (not a List<ChatMessage>) to exercise the list copy branch
|
|
TruncationCompactionStrategy strategy = new(CompactionTriggers.TokensExceed(100000));
|
|
IEnumerable<ChatMessage> messages = [new ChatMessage(ChatRole.User, "Hello")];
|
|
|
|
// Act
|
|
IEnumerable<ChatMessage> result = await CompactionProvider.CompactAsync(strategy, messages);
|
|
|
|
// Assert
|
|
List<ChatMessage> resultList = [.. result];
|
|
Assert.Single(resultList);
|
|
Assert.Equal("Hello", resultList[0].Text);
|
|
}
|
|
|
|
[Fact]
|
|
public void CompactionStateAssignment()
|
|
{
|
|
// Arrange
|
|
CompactionProvider.State state = new();
|
|
|
|
// Assert
|
|
Assert.NotNull(state.MessageGroups);
|
|
Assert.Empty(state.MessageGroups);
|
|
|
|
// Act
|
|
state.MessageGroups = [new CompactionMessageGroup(CompactionGroupKind.User, [], 0, 0, 0)];
|
|
|
|
// Assert
|
|
Assert.Single(state.MessageGroups);
|
|
}
|
|
|
|
private sealed class TestAgentSession : AgentSession;
|
|
}
|