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>
329 lines
12 KiB
C#
329 lines
12 KiB
C#
// Copyright (c) Microsoft. All rights reserved.
|
|
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Agents.AI.Compaction;
|
|
using Microsoft.Extensions.AI;
|
|
|
|
namespace Microsoft.Agents.AI.UnitTests.Compaction;
|
|
|
|
/// <summary>
|
|
/// Contains tests for the <see cref="TruncationCompactionStrategy"/> class.
|
|
/// </summary>
|
|
public class TruncationCompactionStrategyTests
|
|
{
|
|
[Fact]
|
|
public async Task CompactAsyncAlwaysTriggerCompactsToPreserveRecentAsync()
|
|
{
|
|
// Arrange — always-trigger means always compact
|
|
TruncationCompactionStrategy strategy = new(CompactionTriggers.Always, minimumPreservedGroups: 1);
|
|
CompactionMessageIndex groups = CompactionMessageIndex.Create(
|
|
[
|
|
new ChatMessage(ChatRole.User, "First"),
|
|
new ChatMessage(ChatRole.Assistant, "Response 1"),
|
|
new ChatMessage(ChatRole.User, "Second"),
|
|
]);
|
|
|
|
// Act
|
|
bool result = await strategy.CompactAsync(groups);
|
|
|
|
// Assert
|
|
Assert.True(result);
|
|
Assert.Equal(1, groups.Groups.Count(g => !g.IsExcluded));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CompactAsyncTriggerNotMetReturnsFalseAsync()
|
|
{
|
|
// Arrange — trigger requires > 1000 tokens, conversation is tiny
|
|
TruncationCompactionStrategy strategy = new(
|
|
minimumPreservedGroups: 1,
|
|
trigger: CompactionTriggers.TokensExceed(1000));
|
|
|
|
CompactionMessageIndex groups = CompactionMessageIndex.Create(
|
|
[
|
|
new ChatMessage(ChatRole.User, "Hello"),
|
|
new ChatMessage(ChatRole.Assistant, "Hi!"),
|
|
]);
|
|
|
|
// Act
|
|
bool result = await strategy.CompactAsync(groups);
|
|
|
|
// Assert
|
|
Assert.False(result);
|
|
Assert.Equal(2, groups.IncludedGroupCount);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CompactAsyncTriggerMetExcludesOldestGroupsAsync()
|
|
{
|
|
// Arrange — trigger on groups > 2
|
|
TruncationCompactionStrategy strategy = new(
|
|
minimumPreservedGroups: 1,
|
|
trigger: CompactionTriggers.GroupsExceed(2));
|
|
|
|
CompactionMessageIndex groups = CompactionMessageIndex.Create(
|
|
[
|
|
new ChatMessage(ChatRole.User, "First"),
|
|
new ChatMessage(ChatRole.Assistant, "Response 1"),
|
|
new ChatMessage(ChatRole.User, "Second"),
|
|
new ChatMessage(ChatRole.Assistant, "Response 2"),
|
|
]);
|
|
|
|
// Act
|
|
bool result = await strategy.CompactAsync(groups);
|
|
|
|
// Assert — incremental: excludes until GroupsExceed(2) is no longer met → 2 groups remain
|
|
Assert.True(result);
|
|
Assert.Equal(2, groups.IncludedGroupCount);
|
|
// Oldest 2 excluded, newest 2 kept
|
|
Assert.True(groups.Groups[0].IsExcluded);
|
|
Assert.True(groups.Groups[1].IsExcluded);
|
|
Assert.False(groups.Groups[2].IsExcluded);
|
|
Assert.False(groups.Groups[3].IsExcluded);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CompactAsyncPreservesSystemMessagesAsync()
|
|
{
|
|
// Arrange
|
|
TruncationCompactionStrategy strategy = new(CompactionTriggers.Always, minimumPreservedGroups: 1);
|
|
CompactionMessageIndex groups = CompactionMessageIndex.Create(
|
|
[
|
|
new ChatMessage(ChatRole.System, "You are helpful."),
|
|
new ChatMessage(ChatRole.User, "First"),
|
|
new ChatMessage(ChatRole.Assistant, "Response 1"),
|
|
new ChatMessage(ChatRole.User, "Second"),
|
|
]);
|
|
|
|
// Act
|
|
bool result = await strategy.CompactAsync(groups);
|
|
|
|
// Assert
|
|
Assert.True(result);
|
|
// System message should be preserved
|
|
Assert.False(groups.Groups[0].IsExcluded);
|
|
Assert.Equal(CompactionGroupKind.System, groups.Groups[0].Kind);
|
|
// Oldest non-system groups excluded
|
|
Assert.True(groups.Groups[1].IsExcluded);
|
|
Assert.True(groups.Groups[2].IsExcluded);
|
|
// Most recent kept
|
|
Assert.False(groups.Groups[3].IsExcluded);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CompactAsyncPreservesToolCallGroupAtomicityAsync()
|
|
{
|
|
// Arrange
|
|
TruncationCompactionStrategy strategy = new(CompactionTriggers.Always, minimumPreservedGroups: 1);
|
|
|
|
ChatMessage assistantToolCall = new(ChatRole.Assistant, [new FunctionCallContent("call1", "get_weather")]);
|
|
ChatMessage toolResult = new(ChatRole.Tool, "Sunny");
|
|
ChatMessage finalResponse = new(ChatRole.User, "Thanks!");
|
|
|
|
CompactionMessageIndex groups = CompactionMessageIndex.Create([assistantToolCall, toolResult, finalResponse]);
|
|
|
|
// Act
|
|
bool result = await strategy.CompactAsync(groups);
|
|
|
|
// Assert
|
|
Assert.True(result);
|
|
// Tool call group should be excluded as one atomic unit
|
|
Assert.True(groups.Groups[0].IsExcluded);
|
|
Assert.Equal(CompactionGroupKind.ToolCall, groups.Groups[0].Kind);
|
|
Assert.Equal(2, groups.Groups[0].Messages.Count);
|
|
Assert.False(groups.Groups[1].IsExcluded);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CompactAsyncSetsExcludeReasonAsync()
|
|
{
|
|
// Arrange
|
|
TruncationCompactionStrategy strategy = new(CompactionTriggers.Always, minimumPreservedGroups: 1);
|
|
CompactionMessageIndex groups = CompactionMessageIndex.Create(
|
|
[
|
|
new ChatMessage(ChatRole.User, "Old"),
|
|
new ChatMessage(ChatRole.User, "New"),
|
|
]);
|
|
|
|
// Act
|
|
await strategy.CompactAsync(groups);
|
|
|
|
// Assert
|
|
Assert.NotNull(groups.Groups[0].ExcludeReason);
|
|
Assert.Contains("TruncationCompactionStrategy", groups.Groups[0].ExcludeReason);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CompactAsyncSkipsAlreadyExcludedGroupsAsync()
|
|
{
|
|
// Arrange
|
|
TruncationCompactionStrategy strategy = new(CompactionTriggers.Always, minimumPreservedGroups: 1);
|
|
CompactionMessageIndex groups = CompactionMessageIndex.Create(
|
|
[
|
|
new ChatMessage(ChatRole.User, "Already excluded"),
|
|
new ChatMessage(ChatRole.User, "Included 1"),
|
|
new ChatMessage(ChatRole.User, "Included 2"),
|
|
]);
|
|
groups.Groups[0].IsExcluded = true;
|
|
|
|
// Act
|
|
bool result = await strategy.CompactAsync(groups);
|
|
|
|
// Assert
|
|
Assert.True(result);
|
|
Assert.True(groups.Groups[0].IsExcluded); // was already excluded
|
|
Assert.True(groups.Groups[1].IsExcluded); // newly excluded
|
|
Assert.False(groups.Groups[2].IsExcluded); // kept
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CompactAsyncMinimumPreservedKeepsMultipleAsync()
|
|
{
|
|
// Arrange — keep 2 most recent
|
|
TruncationCompactionStrategy strategy = new(CompactionTriggers.Always, minimumPreservedGroups: 2);
|
|
CompactionMessageIndex groups = CompactionMessageIndex.Create(
|
|
[
|
|
new ChatMessage(ChatRole.User, "Q1"),
|
|
new ChatMessage(ChatRole.Assistant, "A1"),
|
|
new ChatMessage(ChatRole.User, "Q2"),
|
|
new ChatMessage(ChatRole.Assistant, "A2"),
|
|
]);
|
|
|
|
// Act
|
|
bool result = await strategy.CompactAsync(groups);
|
|
|
|
// Assert
|
|
Assert.True(result);
|
|
Assert.True(groups.Groups[0].IsExcluded);
|
|
Assert.True(groups.Groups[1].IsExcluded);
|
|
Assert.False(groups.Groups[2].IsExcluded);
|
|
Assert.False(groups.Groups[3].IsExcluded);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CompactAsyncNothingToRemoveReturnsFalseAsync()
|
|
{
|
|
// Arrange — preserve 5 but only 2 groups
|
|
TruncationCompactionStrategy strategy = new(CompactionTriggers.Always, minimumPreservedGroups: 5);
|
|
CompactionMessageIndex groups = CompactionMessageIndex.Create(
|
|
[
|
|
new ChatMessage(ChatRole.User, "Hello"),
|
|
new ChatMessage(ChatRole.Assistant, "Hi!"),
|
|
]);
|
|
|
|
// Act
|
|
bool result = await strategy.CompactAsync(groups);
|
|
|
|
// Assert
|
|
Assert.False(result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CompactAsyncCustomTargetStopsEarlyAsync()
|
|
{
|
|
// Arrange — always trigger, custom target stops after 1 exclusion
|
|
int targetChecks = 0;
|
|
bool TargetAfterOne(CompactionMessageIndex _) => ++targetChecks >= 1;
|
|
|
|
TruncationCompactionStrategy strategy = new(
|
|
CompactionTriggers.Always,
|
|
minimumPreservedGroups: 1,
|
|
target: TargetAfterOne);
|
|
|
|
CompactionMessageIndex groups = CompactionMessageIndex.Create(
|
|
[
|
|
new ChatMessage(ChatRole.User, "Q1"),
|
|
new ChatMessage(ChatRole.Assistant, "A1"),
|
|
new ChatMessage(ChatRole.User, "Q2"),
|
|
new ChatMessage(ChatRole.User, "Q3"),
|
|
]);
|
|
|
|
// Act
|
|
bool result = await strategy.CompactAsync(groups);
|
|
|
|
// Assert — only 1 group excluded (target met after first)
|
|
Assert.True(result);
|
|
Assert.True(groups.Groups[0].IsExcluded);
|
|
Assert.False(groups.Groups[1].IsExcluded);
|
|
Assert.False(groups.Groups[2].IsExcluded);
|
|
Assert.False(groups.Groups[3].IsExcluded);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CompactAsyncIncrementalStopsAtTargetAsync()
|
|
{
|
|
// Arrange — trigger on groups > 2, target is default (inverse of trigger: groups <= 2)
|
|
TruncationCompactionStrategy strategy = new(
|
|
CompactionTriggers.GroupsExceed(2),
|
|
minimumPreservedGroups: 1);
|
|
|
|
CompactionMessageIndex groups = CompactionMessageIndex.Create(
|
|
[
|
|
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"),
|
|
]);
|
|
|
|
// Act — 5 groups, trigger fires (5 > 2), compacts until groups <= 2
|
|
bool result = await strategy.CompactAsync(groups);
|
|
|
|
// Assert — should stop at 2 included groups (not go all the way to 1)
|
|
Assert.True(result);
|
|
Assert.Equal(2, groups.IncludedGroupCount);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CompactAsyncLoopExitsWhenMaxRemovableReachedAsync()
|
|
{
|
|
// Arrange — target never stops (always false), so the loop must exit via removed >= maxRemovable
|
|
TruncationCompactionStrategy strategy = new(CompactionTriggers.Always, minimumPreservedGroups: 2, target: CompactionTriggers.Never);
|
|
CompactionMessageIndex groups = CompactionMessageIndex.Create(
|
|
[
|
|
new ChatMessage(ChatRole.User, "Q1"),
|
|
new ChatMessage(ChatRole.Assistant, "A1"),
|
|
new ChatMessage(ChatRole.User, "Q2"),
|
|
new ChatMessage(ChatRole.Assistant, "A2"),
|
|
]);
|
|
|
|
// Act
|
|
bool result = await strategy.CompactAsync(groups);
|
|
|
|
// Assert — only 2 removed (maxRemovable = 4 - 2 = 2), 2 preserved
|
|
Assert.True(result);
|
|
Assert.Equal(2, groups.IncludedGroupCount);
|
|
Assert.True(groups.Groups[0].IsExcluded);
|
|
Assert.True(groups.Groups[1].IsExcluded);
|
|
Assert.False(groups.Groups[2].IsExcluded);
|
|
Assert.False(groups.Groups[3].IsExcluded);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CompactAsyncSkipsPreExcludedAndSystemGroupsAsync()
|
|
{
|
|
// Arrange — has excluded + system groups that the loop must skip
|
|
TruncationCompactionStrategy strategy = new(CompactionTriggers.Always, minimumPreservedGroups: 1);
|
|
CompactionMessageIndex groups = CompactionMessageIndex.Create(
|
|
[
|
|
new ChatMessage(ChatRole.System, "System"),
|
|
new ChatMessage(ChatRole.User, "Q1"),
|
|
new ChatMessage(ChatRole.Assistant, "A1"),
|
|
new ChatMessage(ChatRole.User, "Q2"),
|
|
]);
|
|
// Pre-exclude one group
|
|
groups.Groups[1].IsExcluded = true;
|
|
|
|
// Act
|
|
bool result = await strategy.CompactAsync(groups);
|
|
|
|
// Assert — system preserved, pre-excluded skipped, A1 removed, Q2 preserved
|
|
Assert.True(result);
|
|
Assert.False(groups.Groups[0].IsExcluded); // System
|
|
Assert.True(groups.Groups[1].IsExcluded); // Pre-excluded Q1
|
|
Assert.True(groups.Groups[2].IsExcluded); // Newly excluded A1
|
|
Assert.False(groups.Groups[3].IsExcluded); // Preserved Q2
|
|
}
|
|
}
|