mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Checkpoint
This commit is contained in:
@@ -13,6 +13,7 @@ using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Compaction;
|
||||
using Microsoft.Extensions.AI;
|
||||
using static Microsoft.Agents.AI.Compaction.ChatHistoryCompactionPipeline;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||
@@ -39,33 +40,45 @@ static string LookupPrice([Description("The product name to look up.")] string p
|
||||
};
|
||||
|
||||
// Configure the compaction pipeline with one of each strategy, ordered least to most aggressive.
|
||||
const int MaxTokens = 512;
|
||||
const int MaxTurns = 4;
|
||||
//const int MaxTokens = 512;
|
||||
//const int MaxTurns = 4;
|
||||
|
||||
ChatHistoryCompactionPipeline compactionPipeline =
|
||||
new(// 1. Gentle: collapse old tool-call groups into short summaries like "[Tool calls: LookupPrice]"
|
||||
new ToolResultCompactionStrategy(MaxTokens, preserveRecentGroups: 2),
|
||||
//new(// 1. Gentle: collapse old tool-call groups into short summaries like "[Tool calls: LookupPrice]"
|
||||
// new ToolResultCompactionStrategy(MaxTokens, preserveRecentGroups: 2),
|
||||
|
||||
// 2. Moderate: use an LLM to summarize older conversation spans into a concise message
|
||||
new SummarizationCompactionStrategy(summarizerChatClient, MaxTokens, preserveRecentGroups: 2),
|
||||
// // 2. Moderate: use an LLM to summarize older conversation spans into a concise message
|
||||
// new SummarizationCompactionStrategy(summarizerChatClient, MaxTokens, preserveRecentGroups: 2),
|
||||
|
||||
// 3. Aggressive: keep only the last N user turns and their responses
|
||||
new SlidingWindowCompactionStrategy(MaxTurns),
|
||||
// // 3. Aggressive: keep only the last N user turns and their responses
|
||||
// new SlidingWindowCompactionStrategy(MaxTurns),
|
||||
|
||||
// 4. Emergency: drop oldest groups until under the token budget
|
||||
new TruncationCompactionStrategy(MaxTokens, preserveRecentGroups: 1));
|
||||
// // 4. Emergency: drop oldest groups until under the token budget
|
||||
// new TruncationCompactionStrategy(MaxTokens, preserveRecentGroups: 1));
|
||||
Create(
|
||||
Approach.Balanced,
|
||||
Size.Compact,
|
||||
summarizerChatClient);
|
||||
|
||||
// Create the agent with an in-memory chat history provider whose reducer is the compaction pipeline.
|
||||
AIAgent agent = agentChatClient.AsAIAgent(new ChatClientAgentOptions
|
||||
{
|
||||
Name = "ShoppingAssistant",
|
||||
ChatOptions = new()
|
||||
{
|
||||
Instructions = "You are a helpful shopping assistant. Help the user look up prices and compare products.",
|
||||
Tools = [AIFunctionFactory.Create(LookupPrice)],
|
||||
},
|
||||
ChatHistoryProvider = new InMemoryChatHistoryProvider(new() { ChatReducer = compactionPipeline }),
|
||||
});
|
||||
AIAgent agent =
|
||||
agentChatClient.AsAIAgent(
|
||||
new ChatClientAgentOptions
|
||||
{
|
||||
Name = "ShoppingAssistant",
|
||||
ChatOptions = new()
|
||||
{
|
||||
Instructions =
|
||||
"""
|
||||
You are a helpful, but long winded, shopping assistant.
|
||||
Help the user look up prices and compare products.
|
||||
When responding, Be sure to be extra descriptive and use as
|
||||
many words as possible without sounding ridiculous.
|
||||
""",
|
||||
Tools = [AIFunctionFactory.Create(LookupPrice)],
|
||||
},
|
||||
ChatHistoryProvider = new InMemoryChatHistoryProvider(new() { ChatReducer = compactionPipeline }),
|
||||
});
|
||||
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
|
||||
@@ -87,6 +100,7 @@ string[] prompts =
|
||||
"Which product is the cheapest?",
|
||||
"Can you compare the laptop and the keyboard for me?",
|
||||
"What was the first product I asked about?",
|
||||
"Thank you!",
|
||||
];
|
||||
|
||||
foreach (string prompt in prompts)
|
||||
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Compaction;
|
||||
|
||||
public partial class ChatHistoryCompactionPipeline
|
||||
{
|
||||
/// <summary>
|
||||
/// %%% COMMENT
|
||||
/// </summary>
|
||||
public enum Size
|
||||
{
|
||||
/// <summary>
|
||||
/// %%% COMMENT
|
||||
/// </summary>
|
||||
Compact,
|
||||
/// <summary>
|
||||
/// %%% COMMENT
|
||||
/// </summary>
|
||||
Adequate,
|
||||
/// <summary>
|
||||
/// %%% COMMENT
|
||||
/// </summary>
|
||||
Accomodating,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// %%% COMMENT
|
||||
/// </summary>
|
||||
public enum Approach
|
||||
{
|
||||
/// <summary>
|
||||
/// %%% COMMENT
|
||||
/// </summary>
|
||||
Aggressive,
|
||||
/// <summary>
|
||||
/// %%% COMMENT
|
||||
/// </summary>
|
||||
Balanced,
|
||||
/// <summary>
|
||||
/// %%% COMMENT
|
||||
/// </summary>
|
||||
Gentle,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// %%% COMMENT
|
||||
/// </summary>
|
||||
/// <param name="approach"></param>
|
||||
/// <param name="size"></param>
|
||||
/// <param name="chatClient"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="NotImplementedException"></exception>
|
||||
public static ChatHistoryCompactionPipeline Create(Approach approach, Size size, IChatClient chatClient) =>
|
||||
approach switch
|
||||
{
|
||||
Approach.Aggressive => CreateAgressive(size, chatClient),
|
||||
Approach.Balanced => CreateBalanced(size),
|
||||
Approach.Gentle => CreateGentle(size),
|
||||
_ => throw new NotImplementedException(), // %%% EXCEPTION
|
||||
};
|
||||
|
||||
private static ChatHistoryCompactionPipeline CreateAgressive(Size size, IChatClient chatClient) =>
|
||||
new(// 1. Gentle: collapse old tool-call groups into short summaries like "[Tool calls: LookupPrice]"
|
||||
new ToolResultCompactionStrategy(MaxTokens(size), preserveRecentGroups: 2),
|
||||
// 2. Moderate: use an LLM to summarize older conversation spans into a concise message
|
||||
new SummarizationCompactionStrategy(chatClient, MaxTokens(size), preserveRecentGroups: 2),
|
||||
// 3. Aggressive: keep only the last N user turns and their responses
|
||||
new SlidingWindowCompactionStrategy(MaxTurns(size)),
|
||||
// 4. Emergency: drop oldest groups until under the token budget
|
||||
new TruncationCompactionStrategy(MaxTokens(size), preserveRecentGroups: 1));
|
||||
|
||||
private static ChatHistoryCompactionPipeline CreateBalanced(Size size) =>
|
||||
new(// 1. Gentle: collapse old tool-call groups into short summaries like "[Tool calls: LookupPrice]"
|
||||
new ToolResultCompactionStrategy(MaxTokens(size), preserveRecentGroups: 2),
|
||||
// 2. Aggressive: keep only the last N user turns and their responses
|
||||
new SlidingWindowCompactionStrategy(MaxTurns(size)));
|
||||
|
||||
private static ChatHistoryCompactionPipeline CreateGentle(Size size) =>
|
||||
new(// 1. Gentle: collapse old tool-call groups into short summaries like "[Tool calls: LookupPrice]"
|
||||
new ToolResultCompactionStrategy(MaxTokens(size), preserveRecentGroups: 2));
|
||||
|
||||
private static int MaxTokens(Size size) =>
|
||||
size switch
|
||||
{
|
||||
Size.Compact => 500,
|
||||
Size.Adequate => 1000,
|
||||
Size.Accomodating => 2000,
|
||||
_ => throw new NotImplementedException(), // %%% EXCEPTION
|
||||
};
|
||||
|
||||
private static int MaxTurns(Size size) =>
|
||||
size switch
|
||||
{
|
||||
Size.Compact => 10,
|
||||
Size.Adequate => 50,
|
||||
Size.Accomodating => 100,
|
||||
_ => throw new NotImplementedException(), // %%% EXCEPTION
|
||||
};
|
||||
}
|
||||
+28
-16
@@ -1,7 +1,8 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
@@ -24,7 +25,7 @@ namespace Microsoft.Agents.AI.Compaction;
|
||||
/// accepted (e.g., <see cref="InMemoryChatHistoryProviderOptions.ChatReducer"/>).
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public class ChatHistoryCompactionPipeline : IChatReducer
|
||||
public partial class ChatHistoryCompactionPipeline : IChatReducer
|
||||
{
|
||||
private readonly ChatHistoryCompactionStrategy[] _strategies;
|
||||
private readonly IChatHistoryMetricsCalculator _metricsCalculator;
|
||||
@@ -52,7 +53,7 @@ public class ChatHistoryCompactionPipeline : IChatReducer
|
||||
IChatHistoryMetricsCalculator? metricsCalculator,
|
||||
params IEnumerable<ChatHistoryCompactionStrategy> strategies)
|
||||
{
|
||||
this._strategies = Throw.IfNull(strategies).ToArray();
|
||||
this._strategies = [.. Throw.IfNull(strategies)];
|
||||
this._metricsCalculator = metricsCalculator ?? DefaultChatHistoryMetricsCalculator.Instance;
|
||||
}
|
||||
|
||||
@@ -66,9 +67,9 @@ public class ChatHistoryCompactionPipeline : IChatReducer
|
||||
IEnumerable<ChatMessage> messages,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
List<ChatMessage> messageList = messages.ToList(); // %%% HAXX
|
||||
await this.CompactAsync(messageList, cancellationToken).ConfigureAwait(false);
|
||||
return messageList;
|
||||
List<ChatMessage> messageBuffer = messages is List<ChatMessage> messageList ? messageList : [.. messages];
|
||||
await this.CompactAsync(messageBuffer, cancellationToken).ConfigureAwait(false);
|
||||
return messageBuffer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -77,26 +78,37 @@ public class ChatHistoryCompactionPipeline : IChatReducer
|
||||
/// <param name="messages">The mutable message list to compact.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
|
||||
/// <returns>A <see cref="CompactionPipelineResult"/> with aggregate and per-strategy metrics.</returns>
|
||||
public async ValueTask<CompactionPipelineResult> CompactAsync( // %%% SCOPE
|
||||
IList<ChatMessage> messages,
|
||||
public async ValueTask<CompactionPipelineResult> CompactAsync(
|
||||
List<ChatMessage> messages,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(messages);
|
||||
|
||||
IReadOnlyList<ChatMessage> readOnlyMessages = messages as IReadOnlyList<ChatMessage> ?? [.. messages]; // %%% TYPE CONSISTENCY
|
||||
CompactionMetric overallBefore = this._metricsCalculator.Calculate(readOnlyMessages);
|
||||
ChatHistoryMetric overallBefore = this._metricsCalculator.Calculate(messages);
|
||||
|
||||
List<CompactionResult> results = new(this._strategies.Length);
|
||||
Debug.WriteLine($"COMPACTION: BEGIN x{overallBefore.MessageCount}/#{overallBefore.UserTurnCount} ({overallBefore.TokenCount} tokens)");
|
||||
|
||||
List<CompactionResult> compactionResults = new(this._strategies.Length);
|
||||
|
||||
Stopwatch timer = new();
|
||||
TimeSpan startTime = TimeSpan.Zero;
|
||||
ChatHistoryMetric overallAfter = overallBefore;
|
||||
ChatHistoryMetric currentBefore = overallBefore;
|
||||
foreach (ChatHistoryCompactionStrategy strategy in this._strategies)
|
||||
{
|
||||
CompactionResult result = await strategy.CompactAsync(messages, this._metricsCalculator, cancellationToken).ConfigureAwait(false);
|
||||
results.Add(result);
|
||||
// %%% VERBOSE - Debug.WriteLine($"COMPACTION: {strategy.Name} START");
|
||||
timer.Start();
|
||||
ChatHistoryCompactionStrategy.s_currentMetrics.Value = currentBefore;
|
||||
CompactionResult strategyResult = await strategy.CompactAsync(messages, this._metricsCalculator, cancellationToken).ConfigureAwait(false);
|
||||
timer.Stop();
|
||||
TimeSpan elapsedTime = timer.Elapsed - startTime;
|
||||
// %%% VERBOSE - Debug.WriteLine($"COMPACTION: {strategy.Name} FINISH [{elapsedTime}]");
|
||||
compactionResults.Add(strategyResult);
|
||||
overallAfter = currentBefore = strategyResult.After;
|
||||
}
|
||||
|
||||
readOnlyMessages = messages as IReadOnlyList<ChatMessage> ?? [.. messages];
|
||||
CompactionMetric overallAfter = this._metricsCalculator.Calculate(readOnlyMessages);
|
||||
Debug.WriteLineIf(overallBefore.TokenCount != overallAfter.TokenCount, $"COMPACTION: TOTAL [{timer.Elapsed}] {overallBefore.TokenCount} => {overallAfter.TokenCount} tokens");
|
||||
|
||||
return new(overallBefore, overallAfter, results);
|
||||
return new(overallBefore, overallAfter, compactionResults);
|
||||
}
|
||||
}
|
||||
|
||||
+29
-28
@@ -2,8 +2,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
@@ -21,7 +20,7 @@ namespace Microsoft.Agents.AI.Compaction;
|
||||
/// while the strategy adds:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>A conditional trigger via <see cref="ShouldCompact"/> that decides whether compaction runs.</description></item>
|
||||
/// <item><description>Before/after <see cref="CompactionMetric"/> reporting via <see cref="CompactionResult"/>.</description></item>
|
||||
/// <item><description>Before/after <see cref="ChatHistoryMetric"/> reporting via <see cref="CompactionResult"/>.</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
@@ -36,7 +35,7 @@ namespace Microsoft.Agents.AI.Compaction;
|
||||
/// </remarks>
|
||||
public abstract class ChatHistoryCompactionStrategy
|
||||
{
|
||||
private static readonly AsyncLocal<CompactionMetric> s_currentMetrics = new();
|
||||
internal static readonly AsyncLocal<ChatHistoryMetric> s_currentMetrics = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChatHistoryCompactionStrategy"/> class.
|
||||
@@ -48,9 +47,9 @@ public abstract class ChatHistoryCompactionStrategy
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exposes the current <see cref="CompactionMetric"/> for the executing strategy, allowing <see cref="Reducer"/> to make informed decisions.
|
||||
/// Exposes the current <see cref="ChatHistoryMetric"/> for the executing strategy, allowing <see cref="Reducer"/> to make informed decisions.
|
||||
/// </summary>
|
||||
protected static CompactionMetric CurrentMetrics => s_currentMetrics.Value ?? throw new InvalidOperationException($"No active {nameof(ChatHistoryCompactionStrategy)}.");
|
||||
protected static ChatHistoryMetric CurrentMetrics => s_currentMetrics.Value ?? throw new InvalidOperationException($"No active {nameof(ChatHistoryCompactionStrategy)}.");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="IChatReducer"/> that performs the actual message compaction.
|
||||
@@ -72,48 +71,50 @@ public abstract class ChatHistoryCompactionStrategy
|
||||
/// <returns>
|
||||
/// <see langword="true"/> to proceed with compaction; <see langword="false"/> to skip.
|
||||
/// </returns>
|
||||
public abstract bool ShouldCompact(CompactionMetric metrics);
|
||||
protected abstract bool ShouldCompact(ChatHistoryMetric metrics);
|
||||
|
||||
/// <summary>
|
||||
/// Execute this strategy: check the trigger, delegate to the <see cref="IChatReducer"/>, and report metrics.
|
||||
/// </summary>
|
||||
/// <param name="messages">The mutable message list to compact.</param>
|
||||
/// <param name="history">The mutable message list to compact.</param>
|
||||
/// <param name="metricsCalculator">The calculator to use for metric snapshots.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
|
||||
/// <returns>A <see cref="CompactionResult"/> reporting the outcome.</returns>
|
||||
public async ValueTask<CompactionResult> CompactAsync(
|
||||
IList<ChatMessage> messages,
|
||||
internal async ValueTask<CompactionResult> CompactAsync(
|
||||
List<ChatMessage> history,
|
||||
IChatHistoryMetricsCalculator metricsCalculator,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
messages = Throw.IfNull(messages);
|
||||
Throw.IfNull(metricsCalculator);
|
||||
Throw.IfNull(history);
|
||||
|
||||
List<ChatMessage>? messageList = messages as List<ChatMessage>;
|
||||
ReadOnlyCollection<ChatMessage> snapshot = messageList is not null ? messageList.AsReadOnly() : new(messages);
|
||||
CompactionMetric before = metricsCalculator.Calculate(snapshot);
|
||||
s_currentMetrics.Value = before;
|
||||
if (!this.ShouldCompact(before))
|
||||
ChatHistoryMetric beforeMetrics = CurrentMetrics;
|
||||
if (!this.ShouldCompact(beforeMetrics))
|
||||
{
|
||||
return CompactionResult.Skipped(this.Name, before);
|
||||
// %%% VERBOSE - Debug.WriteLine($"COMPACTION: {this.Name} - Skipped");
|
||||
return CompactionResult.Skipped(this.Name, beforeMetrics);
|
||||
}
|
||||
|
||||
ChatMessage[] reduced = (await this.Reducer.ReduceAsync(snapshot, cancellationToken).ConfigureAwait(false)).ToArray();
|
||||
Debug.WriteLine($"COMPACTION: {this.Name} - Reducing");
|
||||
|
||||
bool modified = reduced.Length != snapshot.Count;
|
||||
IEnumerable<ChatMessage> reducerResult = await this.Reducer.ReduceAsync(history, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Ensure we have a concrete collection to avoid multiple enumerations of the reducer result, which could be costly if it's an iterator.
|
||||
ChatMessage[] reducedCopy = [.. reducerResult];
|
||||
|
||||
bool modified = reducedCopy.Length != history.Count;
|
||||
if (modified)
|
||||
{
|
||||
messages.Clear();
|
||||
foreach (ChatMessage message in reduced)
|
||||
{
|
||||
messages.Add(message);
|
||||
}
|
||||
history.Clear();
|
||||
history.AddRange(reducedCopy);
|
||||
}
|
||||
|
||||
CompactionMetric after = modified
|
||||
? metricsCalculator.Calculate(reduced)
|
||||
: before;
|
||||
ChatHistoryMetric afterMetrics = modified
|
||||
? metricsCalculator.Calculate(reducedCopy)
|
||||
: beforeMetrics;
|
||||
|
||||
return new(this.Name, applied: modified, before, after);
|
||||
Debug.WriteLine($"COMPACTION: {this.Name} - Tokens {beforeMetrics.TokenCount} => {afterMetrics.TokenCount}");
|
||||
|
||||
return new(this.Name, applied: modified, beforeMetrics, afterMetrics);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ namespace Microsoft.Agents.AI.Compaction;
|
||||
/// <summary>
|
||||
/// Immutable snapshot of conversation metrics used for compaction trigger evaluation and reporting.
|
||||
/// </summary>
|
||||
public sealed class CompactionMetric
|
||||
public sealed class ChatHistoryMetric
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the estimated token count across all messages.
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI.Compaction;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a chat history compaction strategy that uses a condition function to determine when compaction should
|
||||
/// occur.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This strategy evaluates a user-provided condition against compaction metrics to decide whether to
|
||||
/// compact the chat history. It is useful for scenarios where compaction should be triggered based on custom thresholds
|
||||
/// or criteria. Inherits from ChatHistoryCompactionStrategy.
|
||||
/// </remarks>
|
||||
public class ChatReducerCompactionStrategy : ChatHistoryCompactionStrategy
|
||||
{
|
||||
private readonly Func<ChatHistoryMetric, bool> _condition;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChatReducerCompactionStrategy"/> class.
|
||||
/// </summary>
|
||||
public ChatReducerCompactionStrategy(
|
||||
IChatReducer reducer,
|
||||
Func<ChatHistoryMetric, bool> condition)
|
||||
: base(reducer)
|
||||
{
|
||||
this._condition = Throw.IfNull(condition);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool ShouldCompact(ChatHistoryMetric metrics) => this._condition(metrics);
|
||||
}
|
||||
@@ -18,8 +18,8 @@ public sealed class CompactionPipelineResult
|
||||
/// <param name="after">Metrics of the conversation after all strategies ran.</param>
|
||||
/// <param name="strategyResults">Per-strategy results in execution order.</param>
|
||||
internal CompactionPipelineResult(
|
||||
CompactionMetric before,
|
||||
CompactionMetric after,
|
||||
ChatHistoryMetric before,
|
||||
ChatHistoryMetric after,
|
||||
IReadOnlyList<CompactionResult> strategyResults)
|
||||
{
|
||||
this.Before = Throw.IfNull(before);
|
||||
@@ -30,12 +30,12 @@ public sealed class CompactionPipelineResult
|
||||
/// <summary>
|
||||
/// Gets the conversation metrics before any compaction strategy ran.
|
||||
/// </summary>
|
||||
public CompactionMetric Before { get; }
|
||||
public ChatHistoryMetric Before { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the conversation metrics after all compaction strategies ran.
|
||||
/// </summary>
|
||||
public CompactionMetric After { get; }
|
||||
public ChatHistoryMetric After { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the per-strategy results in execution order.
|
||||
|
||||
@@ -16,7 +16,7 @@ public sealed class CompactionResult
|
||||
/// <param name="applied">Whether the strategy modified the message list.</param>
|
||||
/// <param name="before">Metrics before the strategy ran.</param>
|
||||
/// <param name="after">Metrics after the strategy ran.</param>
|
||||
public CompactionResult(string strategyName, bool applied, CompactionMetric before, CompactionMetric after)
|
||||
public CompactionResult(string strategyName, bool applied, ChatHistoryMetric before, ChatHistoryMetric after)
|
||||
{
|
||||
this.StrategyName = Throw.IfNullOrWhitespace(strategyName);
|
||||
this.Applied = applied;
|
||||
@@ -37,12 +37,12 @@ public sealed class CompactionResult
|
||||
/// <summary>
|
||||
/// Gets the conversation metrics before the strategy executed.
|
||||
/// </summary>
|
||||
public CompactionMetric Before { get; }
|
||||
public ChatHistoryMetric Before { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the conversation metrics after the strategy executed.
|
||||
/// </summary>
|
||||
public CompactionMetric After { get; }
|
||||
public ChatHistoryMetric After { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="CompactionResult"/> representing a skipped strategy.
|
||||
@@ -50,6 +50,6 @@ public sealed class CompactionResult
|
||||
/// <param name="strategyName">The name of the skipped strategy.</param>
|
||||
/// <param name="metrics">The current conversation metrics.</param>
|
||||
/// <returns>A result indicating no compaction was applied.</returns>
|
||||
internal static CompactionResult Skipped(string strategyName, CompactionMetric metrics)
|
||||
internal static CompactionResult Skipped(string strategyName, ChatHistoryMetric metrics)
|
||||
=> new(strategyName, applied: false, metrics, metrics);
|
||||
}
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ public sealed class DefaultChatHistoryMetricsCalculator : IChatHistoryMetricsCal
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public CompactionMetric Calculate(IReadOnlyList<ChatMessage> messages)
|
||||
public ChatHistoryMetric Calculate(IReadOnlyList<ChatMessage> messages)
|
||||
{
|
||||
if (messages is null || messages.Count == 0)
|
||||
{
|
||||
|
||||
+4
-4
@@ -9,18 +9,18 @@ namespace Microsoft.Agents.AI.Compaction;
|
||||
// and whether custom metrics calculators are a realistic extension point.
|
||||
|
||||
/// <summary>
|
||||
/// Computes <see cref="CompactionMetric"/> for a list of messages.
|
||||
/// Computes <see cref="ChatHistoryMetric"/> for a list of messages.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Token counting is model-specific. Implementations can provide precise tokenization
|
||||
/// (e.g., using tiktoken or a model-specific tokenizer) or use estimation heuristics.
|
||||
/// </remarks>
|
||||
public interface IChatHistoryMetricsCalculator // %%% NEEDED ???
|
||||
public interface IChatHistoryMetricsCalculator
|
||||
{
|
||||
/// <summary>
|
||||
/// Compute metrics for the given messages.
|
||||
/// </summary>
|
||||
/// <param name="messages">The messages to analyze.</param>
|
||||
/// <returns>A <see cref="CompactionMetric"/> snapshot.</returns>
|
||||
CompactionMetric Calculate(IReadOnlyList<ChatMessage> messages);
|
||||
/// <returns>A <see cref="ChatHistoryMetric"/> snapshot.</returns>
|
||||
ChatHistoryMetric Calculate(IReadOnlyList<ChatMessage> messages);
|
||||
}
|
||||
|
||||
+9
-14
@@ -1,6 +1,5 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
@@ -44,7 +43,7 @@ public class SlidingWindowCompactionStrategy : ChatHistoryCompactionStrategy
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool ShouldCompact(CompactionMetric metrics) =>
|
||||
protected override bool ShouldCompact(ChatHistoryMetric metrics) =>
|
||||
metrics.UserTurnCount > this._maxTurns;
|
||||
|
||||
/// <summary>
|
||||
@@ -57,25 +56,21 @@ public class SlidingWindowCompactionStrategy : ChatHistoryCompactionStrategy
|
||||
IEnumerable<ChatMessage> messages,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
IReadOnlyList<ChatMessage> messageList = [.. messages];
|
||||
IReadOnlyList<ChatMessage> messageList = [.. messages]; // %%% PERFORMANCE
|
||||
IReadOnlyList<ChatMessageGroup> groups = CurrentMetrics.Groups;
|
||||
|
||||
// Find the group-list indices where each user turn starts
|
||||
//int[] turnGroupIndices = groups.Where(group => group.Kind == ChatMessageGroupKind.UserTurn).Select(group => group.StartIndex).ToArray(); // %%% TODO
|
||||
List<int> turnGroupIndices = [];
|
||||
for (int i = 0; i < groups.Count; i++)
|
||||
{
|
||||
if (groups[i].Kind == ChatMessageGroupKind.UserTurn)
|
||||
{
|
||||
turnGroupIndices.Add(i);
|
||||
}
|
||||
}
|
||||
int[] turnGroupIndices =
|
||||
[.. CurrentMetrics.Groups
|
||||
.Select((group, index) => (group, index))
|
||||
.Where(t => t.group.Kind == ChatMessageGroupKind.UserTurn)
|
||||
.Select(t => t.index)];
|
||||
|
||||
// Keep the last maxTurns user turns and everything after the first kept turn
|
||||
int firstKeptTurnIndex = turnGroupIndices.Count - maxTurns;
|
||||
int firstKeptTurnIndex = turnGroupIndices.Length - maxTurns;
|
||||
int firstKeptGroupIndex = turnGroupIndices[firstKeptTurnIndex];
|
||||
|
||||
List<ChatMessage> result = new(messageList.Count);
|
||||
List<ChatMessage> result = new(messageList.Count); // %%% PERFORMANCE
|
||||
for (int gi = 0; gi < groups.Count; gi++)
|
||||
{
|
||||
ChatMessageGroup group = groups[gi];
|
||||
|
||||
+2
-2
@@ -69,7 +69,7 @@ public class SummarizationCompactionStrategy : ChatHistoryCompactionStrategy
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool ShouldCompact(CompactionMetric metrics) =>
|
||||
protected override bool ShouldCompact(ChatHistoryMetric metrics) =>
|
||||
metrics.TokenCount > this._maxTokens;
|
||||
|
||||
/// <summary>
|
||||
@@ -96,7 +96,7 @@ public class SummarizationCompactionStrategy : ChatHistoryCompactionStrategy
|
||||
IReadOnlyList<ChatMessage> messageList = [.. messages];
|
||||
IReadOnlyList<ChatMessageGroup> groups = CurrentMetrics.Groups;
|
||||
|
||||
List<ChatMessageGroup> nonSystemGroups = groups.Where(g => g.Kind != ChatMessageGroupKind.System).ToList();
|
||||
List<ChatMessageGroup> nonSystemGroups = [.. groups.Where(g => g.Kind != ChatMessageGroupKind.System)];
|
||||
int protectedFromIndex = Math.Max(0, nonSystemGroups.Count - this._preserveRecentGroups);
|
||||
|
||||
if (protectedFromIndex == 0)
|
||||
|
||||
+8
-3
@@ -29,6 +29,11 @@ namespace Microsoft.Agents.AI.Compaction;
|
||||
/// </remarks>
|
||||
public class ToolResultCompactionStrategy : ChatHistoryCompactionStrategy
|
||||
{
|
||||
/// <summary>
|
||||
/// The default value for `preserveRecentGroups` used when constructing <see cref="ToolResultCompactionStrategy"/>.
|
||||
/// </summary>
|
||||
public const int DefaultPreserveRecentGroups = 2;
|
||||
|
||||
private readonly int _maxTokens;
|
||||
|
||||
/// <summary>
|
||||
@@ -39,14 +44,14 @@ public class ToolResultCompactionStrategy : ChatHistoryCompactionStrategy
|
||||
/// The number of most-recent non-system message groups to protect from collapsing.
|
||||
/// Defaults to 2, ensuring the current turn's tool interactions remain visible.
|
||||
/// </param>
|
||||
public ToolResultCompactionStrategy(int maxTokens, int preserveRecentGroups = 2)
|
||||
public ToolResultCompactionStrategy(int maxTokens, int preserveRecentGroups = DefaultPreserveRecentGroups)
|
||||
: base(new ToolResultClearingReducer(preserveRecentGroups))
|
||||
{
|
||||
this._maxTokens = maxTokens;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool ShouldCompact(CompactionMetric metrics) =>
|
||||
protected override bool ShouldCompact(ChatHistoryMetric metrics) =>
|
||||
metrics.TokenCount > this._maxTokens && metrics.ToolCallCount > 0;
|
||||
|
||||
/// <summary>
|
||||
@@ -62,7 +67,7 @@ public class ToolResultCompactionStrategy : ChatHistoryCompactionStrategy
|
||||
IReadOnlyList<ChatMessage> messageList = [.. messages];
|
||||
IReadOnlyList<ChatMessageGroup> groups = CurrentMetrics.Groups;
|
||||
|
||||
List<ChatMessageGroup> nonSystemGroups = groups.Where(g => g.Kind != ChatMessageGroupKind.System).ToList();
|
||||
List<ChatMessageGroup> nonSystemGroups = [.. groups.Where(g => g.Kind != ChatMessageGroupKind.System)];
|
||||
int protectedFromIndex = Math.Max(0, nonSystemGroups.Count - preserveRecentGroups);
|
||||
HashSet<int> protectedGroupStarts = [];
|
||||
for (int i = protectedFromIndex; i < nonSystemGroups.Count; i++)
|
||||
|
||||
+4
-4
@@ -41,7 +41,7 @@ public class TruncationCompactionStrategy : ChatHistoryCompactionStrategy
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool ShouldCompact(CompactionMetric metrics) =>
|
||||
protected override bool ShouldCompact(ChatHistoryMetric metrics) =>
|
||||
metrics.TokenCount > this._maxTokens;
|
||||
|
||||
/// <summary>
|
||||
@@ -56,15 +56,15 @@ public class TruncationCompactionStrategy : ChatHistoryCompactionStrategy
|
||||
{
|
||||
IReadOnlyList<ChatMessage> messageList = [.. messages];
|
||||
|
||||
List<ChatMessageGroup> removableGroups = CurrentMetrics.Groups.Where(g => g.Kind != ChatMessageGroupKind.System).ToList();
|
||||
ChatMessageGroup[] removableGroups = [.. CurrentMetrics.Groups.Where(g => g.Kind != ChatMessageGroupKind.System)];
|
||||
|
||||
if (removableGroups.Count == 0)
|
||||
if (removableGroups.Length == 0)
|
||||
{
|
||||
return Task.FromResult<IEnumerable<ChatMessage>>(messageList);
|
||||
}
|
||||
|
||||
// Remove oldest non-system groups, keeping at least preserveRecentGroups.
|
||||
int maxRemovable = removableGroups.Count - preserveRecentGroups;
|
||||
int maxRemovable = removableGroups.Length - preserveRecentGroups;
|
||||
|
||||
if (maxRemovable <= 0)
|
||||
{
|
||||
|
||||
+23
-24
@@ -14,11 +14,14 @@ public class ChatHistoryCompactionPipelineTests
|
||||
[Fact]
|
||||
public async Task EmptyStrategies_ReturnsUnmodifiedAsync()
|
||||
{
|
||||
// Arrange
|
||||
ChatHistoryCompactionPipeline pipeline = new([]);
|
||||
List<ChatMessage> messages = [new(ChatRole.User, "Hello")];
|
||||
|
||||
// Act
|
||||
CompactionPipelineResult result = await pipeline.CompactAsync(messages);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.AnyApplied);
|
||||
Assert.Equal(1, result.Before.MessageCount);
|
||||
Assert.Equal(1, result.After.MessageCount);
|
||||
@@ -28,6 +31,7 @@ public class ChatHistoryCompactionPipelineTests
|
||||
[Fact]
|
||||
public async Task ChainsStrategies_InOrderAsync()
|
||||
{
|
||||
// Arrange
|
||||
ChatHistoryCompactionStrategy[] strategies =
|
||||
[
|
||||
new NeverCompactStrategy(),
|
||||
@@ -40,8 +44,10 @@ public class ChatHistoryCompactionPipelineTests
|
||||
new(ChatRole.User, "Second"),
|
||||
];
|
||||
|
||||
// Act
|
||||
CompactionPipelineResult result = await pipeline.CompactAsync(messages);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.AnyApplied);
|
||||
Assert.Equal(2, result.StrategyResults.Count);
|
||||
Assert.False(result.StrategyResults[0].Applied);
|
||||
@@ -52,6 +58,7 @@ public class ChatHistoryCompactionPipelineTests
|
||||
[Fact]
|
||||
public async Task ReportsOverallMetricsAsync()
|
||||
{
|
||||
// Arrange
|
||||
ChatHistoryCompactionPipeline pipeline = new([new RemoveFirstMessageStrategy()]);
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
@@ -60,8 +67,10 @@ public class ChatHistoryCompactionPipelineTests
|
||||
new(ChatRole.User, "Third"),
|
||||
];
|
||||
|
||||
// Act
|
||||
CompactionPipelineResult result = await pipeline.CompactAsync(messages);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(3, result.Before.MessageCount);
|
||||
Assert.Equal(2, result.After.MessageCount);
|
||||
}
|
||||
@@ -69,51 +78,39 @@ public class ChatHistoryCompactionPipelineTests
|
||||
[Fact]
|
||||
public async Task CustomMetricsCalculator_IsUsedAsync()
|
||||
{
|
||||
// Arrange
|
||||
Moq.Mock<IChatHistoryMetricsCalculator> calcMock = new();
|
||||
calcMock
|
||||
.Setup(c => c.Calculate(Moq.It.IsAny<IReadOnlyList<ChatMessage>>()))
|
||||
.Returns(new CompactionMetric { MessageCount = 42 });
|
||||
|
||||
.Returns(new ChatHistoryMetric { MessageCount = 42 });
|
||||
ChatHistoryCompactionPipeline pipeline = new(calcMock.Object, []);
|
||||
List<ChatMessage> messages = [new(ChatRole.User, "Hello")];
|
||||
|
||||
// Act
|
||||
CompactionPipelineResult result = await pipeline.CompactAsync(messages);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(42, result.Before.MessageCount);
|
||||
calcMock.Verify(c => c.Calculate(Moq.It.IsAny<IReadOnlyList<ChatMessage>>()), Moq.Times.AtLeast(2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsync_NonReadOnlyListMessages_WorksAsync()
|
||||
{
|
||||
ChatHistoryCompactionPipeline pipeline = new([new RemoveFirstMessageStrategy()]);
|
||||
NonReadOnlyList<ChatMessage> messages = new(
|
||||
[
|
||||
new(ChatRole.User, "First"),
|
||||
new(ChatRole.User, "Second"),
|
||||
]);
|
||||
|
||||
CompactionPipelineResult result = await pipeline.CompactAsync(messages);
|
||||
|
||||
Assert.True(result.AnyApplied);
|
||||
Assert.Single(messages);
|
||||
calcMock.Verify(c => c.Calculate(Moq.It.IsAny<IReadOnlyList<ChatMessage>>()), Moq.Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReduceAsync_DelegatesCompactionAsync()
|
||||
{
|
||||
// Arrange
|
||||
ChatHistoryCompactionPipeline pipeline = new([new RemoveFirstMessageStrategy()]);
|
||||
|
||||
ChatMessage[] messages =
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(ChatRole.User, "First"),
|
||||
new(ChatRole.User, "Second"),
|
||||
new(ChatRole.User, "Third"),
|
||||
];
|
||||
|
||||
IEnumerable<ChatMessage> result = await ((IChatReducer)pipeline).ReduceAsync(messages, default);
|
||||
// Act
|
||||
IEnumerable<ChatMessage> result = await pipeline.ReduceAsync(messages, default);
|
||||
List<ChatMessage> resultList = result.ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, resultList.Count);
|
||||
Assert.Equal("Second", resultList[0].Text);
|
||||
Assert.Equal("Third", resultList[1].Text);
|
||||
@@ -122,16 +119,18 @@ public class ChatHistoryCompactionPipelineTests
|
||||
[Fact]
|
||||
public async Task ReduceAsync_EmptyStrategies_ReturnsAllMessagesAsync()
|
||||
{
|
||||
// Arrange
|
||||
ChatHistoryCompactionPipeline pipeline = new([]);
|
||||
|
||||
ChatMessage[] messages =
|
||||
[
|
||||
new(ChatRole.User, "Hello"),
|
||||
new(ChatRole.User, "World"),
|
||||
];
|
||||
|
||||
IEnumerable<ChatMessage> result = await ((IChatReducer)pipeline).ReduceAsync(messages, default);
|
||||
// Act
|
||||
IEnumerable<ChatMessage> result = await pipeline.ReduceAsync(messages, default);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, result.Count());
|
||||
}
|
||||
}
|
||||
|
||||
+45
-73
@@ -17,28 +17,32 @@ public class ChatHistoryCompactionStrategyTests
|
||||
[Fact]
|
||||
public async Task ShouldCompactReturnsFalse_SkipsAsync()
|
||||
{
|
||||
NeverCompactStrategy strategy = new();
|
||||
// Arrange
|
||||
List<ChatMessage> messages = [new(ChatRole.User, "Hello")];
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
NeverCompactStrategy strategy = new();
|
||||
|
||||
CompactionResult result = await strategy.CompactAsync(messages, calculator);
|
||||
// Act
|
||||
CompactionResult result = await RunCompactionStrategyAsync(strategy, messages);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Applied);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ShouldCompactReturnsTrue_RunsCompactionAsync()
|
||||
{
|
||||
RemoveFirstMessageStrategy strategy = new();
|
||||
// Arrange
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(ChatRole.User, "First"),
|
||||
new(ChatRole.User, "Second"),
|
||||
];
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
RemoveFirstMessageStrategy strategy = new();
|
||||
|
||||
CompactionResult result = await strategy.CompactAsync(messages, calculator);
|
||||
// Act
|
||||
CompactionResult result = await RunCompactionStrategyAsync(strategy, messages);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Applied);
|
||||
Assert.Single(messages);
|
||||
Assert.Equal("Second", messages[0].Text);
|
||||
@@ -47,23 +51,24 @@ public class ChatHistoryCompactionStrategyTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DelegatesToIChatReducerAsync()
|
||||
public async Task DelegatesToReducerAsync()
|
||||
{
|
||||
Mock<IChatReducer> reducerMock = new();
|
||||
reducerMock
|
||||
.Setup(r => r.ReduceAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((IEnumerable<ChatMessage> msgs, CancellationToken _) => msgs.Skip(1));
|
||||
|
||||
TestCompactionStrategy strategy = new(reducerMock.Object);
|
||||
// Arrange
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(ChatRole.User, "First"),
|
||||
new(ChatRole.User, "Second"),
|
||||
];
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
Mock<IChatReducer> reducerMock = new();
|
||||
reducerMock
|
||||
.Setup(r => r.ReduceAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((IEnumerable<ChatMessage> messages, CancellationToken _) => messages.Skip(1));
|
||||
TestCompactionStrategy strategy = new(reducerMock.Object);
|
||||
|
||||
CompactionResult result = await strategy.CompactAsync(messages, calculator);
|
||||
// Act
|
||||
CompactionResult result = await RunCompactionStrategyAsync(strategy, messages);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Applied);
|
||||
Assert.Single(messages);
|
||||
Assert.Equal("Second", messages[0].Text);
|
||||
@@ -73,88 +78,55 @@ public class ChatHistoryCompactionStrategyTests
|
||||
[Fact]
|
||||
public async Task ReducerNoChange_ReturnsFalseAsync()
|
||||
{
|
||||
Mock<IChatReducer> reducerMock = new();
|
||||
reducerMock
|
||||
.Setup(r => r.ReduceAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((IEnumerable<ChatMessage> msgs, CancellationToken _) => msgs);
|
||||
|
||||
TestCompactionStrategy strategy = new(reducerMock.Object);
|
||||
// Arrange
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(ChatRole.User, "Hello"),
|
||||
];
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
Mock<IChatReducer> reducerMock = new();
|
||||
reducerMock
|
||||
.Setup(r => r.ReduceAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((IEnumerable<ChatMessage> msgs, CancellationToken _) => msgs);
|
||||
TestCompactionStrategy strategy = new(reducerMock.Object, shouldCompact: false);
|
||||
|
||||
CompactionResult result = await strategy.CompactAsync(messages, calculator);
|
||||
// Act
|
||||
CompactionResult result = await RunCompactionStrategyAsync(strategy, messages);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Applied);
|
||||
Assert.Single(messages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExposesReducer()
|
||||
public void ReducerLifecycle()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IChatReducer> reducerMock = new();
|
||||
|
||||
// Act
|
||||
TestCompactionStrategy strategy = new(reducerMock.Object);
|
||||
|
||||
// Assert
|
||||
Assert.Same(reducerMock.Object, strategy.Reducer);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultName_IsReducerTypeName()
|
||||
{
|
||||
Mock<IChatReducer> reducerMock = new();
|
||||
TestCompactionStrategy strategy = new(reducerMock.Object);
|
||||
|
||||
// Moq proxy type name is used since we're using a mock
|
||||
Assert.NotNull(strategy.Name);
|
||||
Assert.NotEmpty(strategy.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConditionDelegate_ReturnsTrue_ShouldCompactReturnsTrue()
|
||||
{
|
||||
Mock<IChatReducer> reducerMock = new();
|
||||
TestCompactionStrategy strategy = new(reducerMock.Object);
|
||||
|
||||
CompactionMetric metrics = new() { TokenCount = 100 };
|
||||
Assert.True(strategy.ShouldCompact(metrics));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConditionDelegate_ReturnsFalse_ShouldCompactReturnsFalse()
|
||||
{
|
||||
Mock<IChatReducer> reducerMock = new();
|
||||
TestCompactionStrategy strategy = new(reducerMock.Object, shouldCompact: false);
|
||||
|
||||
CompactionMetric metrics = new() { TokenCount = 100 };
|
||||
Assert.False(strategy.ShouldCompact(metrics));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsync_NonReadOnlyListMessages_WorksAsync()
|
||||
{
|
||||
RemoveFirstMessageStrategy strategy = new();
|
||||
NonReadOnlyList<ChatMessage> messages = new(
|
||||
[
|
||||
new(ChatRole.User, "First"),
|
||||
new(ChatRole.User, "Second"),
|
||||
]);
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
|
||||
CompactionResult result = await strategy.CompactAsync(messages, calculator);
|
||||
|
||||
Assert.True(result.Applied);
|
||||
Assert.Single(messages);
|
||||
Assert.Equal("Second", messages[0].Text);
|
||||
Assert.Equal(reducerMock.Object.GetType().Name, strategy.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CurrentMetrics_OutsideStrategy_Throws()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.Throws<InvalidOperationException>(() => TestCompactionStrategy.GetCurrentMetrics());
|
||||
}
|
||||
|
||||
public static async ValueTask<CompactionResult> RunCompactionStrategyAsync(ChatHistoryCompactionStrategy strategy, List<ChatMessage> messages)
|
||||
{
|
||||
// Act
|
||||
ChatHistoryCompactionStrategy.s_currentMetrics.Value = DefaultChatHistoryMetricsCalculator.Instance.Calculate(messages);
|
||||
return await strategy.CompactAsync(messages, DefaultChatHistoryMetricsCalculator.Instance);
|
||||
}
|
||||
|
||||
private sealed class TestCompactionStrategy : ChatHistoryCompactionStrategy
|
||||
{
|
||||
private readonly bool _shouldCompact;
|
||||
@@ -165,8 +137,8 @@ public class ChatHistoryCompactionStrategyTests
|
||||
this._shouldCompact = shouldCompact;
|
||||
}
|
||||
|
||||
public override bool ShouldCompact(CompactionMetric metrics) => this._shouldCompact;
|
||||
protected override bool ShouldCompact(ChatHistoryMetric metrics) => this._shouldCompact;
|
||||
|
||||
public static CompactionMetric GetCurrentMetrics() => CurrentMetrics;
|
||||
public static ChatHistoryMetric GetCurrentMetrics() => CurrentMetrics;
|
||||
}
|
||||
}
|
||||
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Compaction;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Moq;
|
||||
|
||||
namespace Microsoft.Agents.AI.Abstractions.UnitTests.Compaction;
|
||||
|
||||
public class ChatReducerCompactionStrategyTests : CompactionStrategyTestBase
|
||||
{
|
||||
[Fact]
|
||||
public async Task ConditionFalse_SkipsAsync()
|
||||
{
|
||||
// Arrange
|
||||
List<ChatMessage> messages = [new(ChatRole.User, "Hello")];
|
||||
Mock<IChatReducer> reducerMock = new();
|
||||
ChatReducerCompactionStrategy strategy = new(reducerMock.Object, _ => false);
|
||||
|
||||
// Act & Assert
|
||||
await RunCompactionStrategySkippedAsync(strategy, messages);
|
||||
|
||||
// Assert
|
||||
reducerMock.Verify(
|
||||
r => r.ReduceAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<CancellationToken>()),
|
||||
Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConditionTrue_RunsReducerAsync()
|
||||
{
|
||||
// Arrange
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(ChatRole.User, "First"),
|
||||
new(ChatRole.User, "Second"),
|
||||
];
|
||||
Mock<IChatReducer> reducerMock = new();
|
||||
reducerMock
|
||||
.Setup(r => r.ReduceAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((IEnumerable<ChatMessage> msgs, CancellationToken _) => msgs.Skip(1));
|
||||
ChatReducerCompactionStrategy strategy = new(reducerMock.Object, _ => true);
|
||||
|
||||
// Act & Assert
|
||||
await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 1);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Second", messages[0].Text);
|
||||
reducerMock.Verify(
|
||||
r => r.ReduceAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<CancellationToken>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConditionReceivesMetricsAsync()
|
||||
{
|
||||
// Arrange
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(ChatRole.User, "Hello"),
|
||||
new(ChatRole.Assistant, "Hi"),
|
||||
];
|
||||
ChatHistoryMetric? capturedMetrics = null;
|
||||
Mock<IChatReducer> reducerMock = new();
|
||||
reducerMock
|
||||
.Setup(r => r.ReduceAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((IEnumerable<ChatMessage> msgs, CancellationToken _) => msgs);
|
||||
ChatReducerCompactionStrategy strategy = new(
|
||||
reducerMock.Object,
|
||||
metrics =>
|
||||
{
|
||||
capturedMetrics = metrics;
|
||||
return false;
|
||||
});
|
||||
|
||||
// Act & Assert
|
||||
await RunCompactionStrategySkippedAsync(strategy, messages);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(capturedMetrics);
|
||||
Assert.Equal(2, capturedMetrics!.MessageCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReducerNoChange_AppliedFalseAsync()
|
||||
{
|
||||
// Arrange
|
||||
List<ChatMessage> messages = [new(ChatRole.User, "Hello")];
|
||||
Mock<IChatReducer> reducerMock = new();
|
||||
reducerMock
|
||||
.Setup(r => r.ReduceAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((IEnumerable<ChatMessage> msgs, CancellationToken _) => msgs);
|
||||
ChatReducerCompactionStrategy strategy = new(reducerMock.Object, _ => true);
|
||||
|
||||
// Act & Assert
|
||||
await RunCompactionStrategySkippedAsync(strategy, messages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Name_ReturnsReducerTypeName()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IChatReducer> reducerMock = new();
|
||||
|
||||
// Act
|
||||
ChatReducerCompactionStrategy strategy = new(reducerMock.Object, _ => true);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(reducerMock.Object.GetType().Name, strategy.Name);
|
||||
}
|
||||
}
|
||||
+7
-2
@@ -9,7 +9,10 @@ public class CompactionMetricTests
|
||||
[Fact]
|
||||
public void DefaultValues_AreZero()
|
||||
{
|
||||
CompactionMetric metrics = new();
|
||||
// Arrange & Act
|
||||
ChatHistoryMetric metrics = new();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(0, metrics.TokenCount);
|
||||
Assert.Equal(0L, metrics.ByteCount);
|
||||
Assert.Equal(0, metrics.MessageCount);
|
||||
@@ -21,7 +24,8 @@ public class CompactionMetricTests
|
||||
[Fact]
|
||||
public void InitProperties_SetCorrectly()
|
||||
{
|
||||
CompactionMetric metrics = new()
|
||||
// Arrange & Act
|
||||
ChatHistoryMetric metrics = new()
|
||||
{
|
||||
TokenCount = 100,
|
||||
ByteCount = 500,
|
||||
@@ -30,6 +34,7 @@ public class CompactionMetricTests
|
||||
UserTurnCount = 3
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.Equal(100, metrics.TokenCount);
|
||||
Assert.Equal(500L, metrics.ByteCount);
|
||||
Assert.Equal(5, metrics.MessageCount);
|
||||
|
||||
+16
-5
@@ -11,13 +11,16 @@ public class CompactionPipelineResultTests
|
||||
[Fact]
|
||||
public void Properties_AreReadable()
|
||||
{
|
||||
CompactionMetric before = new() { MessageCount = 10 };
|
||||
CompactionMetric after = new() { MessageCount = 5 };
|
||||
// Arrange
|
||||
ChatHistoryMetric before = new() { MessageCount = 10 };
|
||||
ChatHistoryMetric after = new() { MessageCount = 5 };
|
||||
CompactionResult strategyResult = new("Test", applied: true, before, after);
|
||||
List<CompactionResult> results = [strategyResult];
|
||||
|
||||
// Act
|
||||
CompactionPipelineResult pipelineResult = new(before, after, results);
|
||||
|
||||
// Assert
|
||||
Assert.Same(before, pipelineResult.Before);
|
||||
Assert.Same(after, pipelineResult.After);
|
||||
Assert.Single(pipelineResult.StrategyResults);
|
||||
@@ -26,21 +29,29 @@ public class CompactionPipelineResultTests
|
||||
[Fact]
|
||||
public void AnyApplied_AllFalse_ReturnsFalse()
|
||||
{
|
||||
CompactionMetric metrics = new() { MessageCount = 5 };
|
||||
// Arrange
|
||||
ChatHistoryMetric metrics = new() { MessageCount = 5 };
|
||||
CompactionResult skipped = CompactionResult.Skipped("Skip", metrics);
|
||||
|
||||
// Act
|
||||
CompactionPipelineResult result = new(metrics, metrics, [skipped]);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.AnyApplied);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnyApplied_SomeTrue_ReturnsTrue()
|
||||
{
|
||||
CompactionMetric before = new() { MessageCount = 10 };
|
||||
CompactionMetric after = new() { MessageCount = 5 };
|
||||
// Arrange
|
||||
ChatHistoryMetric before = new() { MessageCount = 10 };
|
||||
ChatHistoryMetric after = new() { MessageCount = 5 };
|
||||
CompactionResult applied = new("Applied", applied: true, before, after);
|
||||
|
||||
// Act
|
||||
CompactionPipelineResult result = new(before, after, [applied]);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.AnyApplied);
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -9,9 +9,13 @@ public class CompactionResultTests
|
||||
[Fact]
|
||||
public void Skipped_HasSameBeforeAndAfter()
|
||||
{
|
||||
CompactionMetric metrics = new() { MessageCount = 5, TokenCount = 100 };
|
||||
// Arrange
|
||||
ChatHistoryMetric metrics = new() { MessageCount = 5, TokenCount = 100 };
|
||||
|
||||
// Act
|
||||
CompactionResult result = CompactionResult.Skipped("Test", metrics);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Test", result.StrategyName);
|
||||
Assert.False(result.Applied);
|
||||
Assert.Same(metrics, result.Before);
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Compaction;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Abstractions.UnitTests.Compaction;
|
||||
|
||||
public abstract class CompactionStrategyTestBase
|
||||
{
|
||||
public static async ValueTask<CompactionResult> RunCompactionStrategyReducedAsync(ChatHistoryCompactionStrategy strategy, List<ChatMessage> messages, int expectedCount)
|
||||
{
|
||||
// Act
|
||||
ChatHistoryCompactionStrategy.s_currentMetrics.Value = DefaultChatHistoryMetricsCalculator.Instance.Calculate(messages);
|
||||
CompactionResult result = await strategy.CompactAsync(messages, DefaultChatHistoryMetricsCalculator.Instance);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Applied);
|
||||
Assert.NotEqual(result.Before, result.After);
|
||||
Assert.Equal(expectedCount, messages.Count);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static async ValueTask<CompactionResult> RunCompactionStrategySkippedAsync(ChatHistoryCompactionStrategy strategy, List<ChatMessage> messages)
|
||||
{
|
||||
// Act
|
||||
int initialCount = messages.Count;
|
||||
ChatHistoryCompactionStrategy.s_currentMetrics.Value = DefaultChatHistoryMetricsCalculator.Instance.Calculate(messages);
|
||||
CompactionResult result = await strategy.CompactAsync(messages, DefaultChatHistoryMetricsCalculator.Instance);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Applied);
|
||||
Assert.Equal(result.Before, result.After);
|
||||
Assert.Equal(initialCount, messages.Count);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
+79
-21
@@ -11,9 +11,13 @@ public class DefaultChatHistoryMetricsCalculatorTests
|
||||
[Fact]
|
||||
public void EmptyList_ReturnsZeroMetrics()
|
||||
{
|
||||
// Arrange
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
CompactionMetric metrics = calculator.Calculate([]);
|
||||
|
||||
// Act
|
||||
ChatHistoryMetric metrics = calculator.Calculate([]);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(0, metrics.TokenCount);
|
||||
Assert.Equal(0L, metrics.ByteCount);
|
||||
Assert.Equal(0, metrics.MessageCount);
|
||||
@@ -24,6 +28,7 @@ public class DefaultChatHistoryMetricsCalculatorTests
|
||||
[Fact]
|
||||
public void CountsMessages()
|
||||
{
|
||||
// Arrange
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
@@ -31,14 +36,17 @@ public class DefaultChatHistoryMetricsCalculatorTests
|
||||
new(ChatRole.Assistant, "Hi there"),
|
||||
];
|
||||
|
||||
CompactionMetric metrics = calculator.Calculate(messages);
|
||||
// Act
|
||||
ChatHistoryMetric metrics = calculator.Calculate(messages);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, metrics.MessageCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CountsUserTurns()
|
||||
{
|
||||
// Arrange
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
@@ -48,14 +56,17 @@ public class DefaultChatHistoryMetricsCalculatorTests
|
||||
new(ChatRole.Assistant, "Good"),
|
||||
];
|
||||
|
||||
CompactionMetric metrics = calculator.Calculate(messages);
|
||||
// Act
|
||||
ChatHistoryMetric metrics = calculator.Calculate(messages);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, metrics.UserTurnCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CountsToolCalls()
|
||||
{
|
||||
// Arrange
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
ChatMessage assistantMsg = new(ChatRole.Assistant, [
|
||||
new FunctionCallContent("call1", "get_weather", new Dictionary<string, object?> { ["city"] = "NYC" }),
|
||||
@@ -67,14 +78,17 @@ public class DefaultChatHistoryMetricsCalculatorTests
|
||||
assistantMsg,
|
||||
];
|
||||
|
||||
CompactionMetric metrics = calculator.Calculate(messages);
|
||||
// Act
|
||||
ChatHistoryMetric metrics = calculator.Calculate(messages);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, metrics.ToolCallCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConsecutiveUserMessages_CountAsOneTurn()
|
||||
{
|
||||
// Arrange
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
@@ -83,22 +97,27 @@ public class DefaultChatHistoryMetricsCalculatorTests
|
||||
new(ChatRole.Assistant, "Reply"),
|
||||
];
|
||||
|
||||
CompactionMetric metrics = calculator.Calculate(messages);
|
||||
// Act
|
||||
ChatHistoryMetric metrics = calculator.Calculate(messages);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(1, metrics.UserTurnCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TokenCount_IsPositive()
|
||||
{
|
||||
// Arrange
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(ChatRole.User, "Hello world"),
|
||||
];
|
||||
|
||||
CompactionMetric metrics = calculator.Calculate(messages);
|
||||
// Act
|
||||
ChatHistoryMetric metrics = calculator.Calculate(messages);
|
||||
|
||||
// Assert
|
||||
Assert.True(metrics.TokenCount > 0);
|
||||
Assert.True(metrics.ByteCount > 0);
|
||||
}
|
||||
@@ -106,9 +125,13 @@ public class DefaultChatHistoryMetricsCalculatorTests
|
||||
[Fact]
|
||||
public void NullInput_ReturnsZeroMetrics()
|
||||
{
|
||||
// Arrange
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
CompactionMetric metrics = calculator.Calculate(null!);
|
||||
|
||||
// Act
|
||||
ChatHistoryMetric metrics = calculator.Calculate(null!);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(0, metrics.TokenCount);
|
||||
Assert.Equal(0L, metrics.ByteCount);
|
||||
Assert.Equal(0, metrics.MessageCount);
|
||||
@@ -118,45 +141,50 @@ public class DefaultChatHistoryMetricsCalculatorTests
|
||||
[Fact]
|
||||
public void InvalidCharsPerToken_UsesDefault()
|
||||
{
|
||||
// A non-positive charsPerToken should fall back to the default (4)
|
||||
// Arrange
|
||||
DefaultChatHistoryMetricsCalculator calculator = new(charsPerToken: 0);
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(ChatRole.User, "Hello world"),
|
||||
];
|
||||
|
||||
CompactionMetric metrics = calculator.Calculate(messages);
|
||||
// Act
|
||||
ChatHistoryMetric metrics = calculator.Calculate(messages);
|
||||
|
||||
// With default 4 chars/token: "Hello world" = 11 chars → 11/4=2 + 4 overhead = 6 tokens
|
||||
// Assert
|
||||
Assert.True(metrics.TokenCount > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NullMessageText_HandledGracefully()
|
||||
{
|
||||
// Arrange
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
// Message with no text content — Text returns null
|
||||
ChatMessage msg = new() { Role = ChatRole.User };
|
||||
List<ChatMessage> messages = [msg];
|
||||
|
||||
CompactionMetric metrics = calculator.Calculate(messages);
|
||||
// Act
|
||||
ChatHistoryMetric metrics = calculator.Calculate(messages);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(1, metrics.MessageCount);
|
||||
// Null text → empty string → 0 bytes, only overhead tokens
|
||||
Assert.True(metrics.TokenCount > 0); // per-message overhead
|
||||
Assert.True(metrics.TokenCount > 0);
|
||||
Assert.Equal(0L, metrics.ByteCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NullContents_SkipsToolCounting()
|
||||
{
|
||||
// Arrange
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
ChatMessage msg = new(ChatRole.User, "text");
|
||||
msg.Contents = null!;
|
||||
List<ChatMessage> messages = [msg];
|
||||
|
||||
CompactionMetric metrics = calculator.Calculate(messages);
|
||||
// Act
|
||||
ChatHistoryMetric metrics = calculator.Calculate(messages);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(1, metrics.MessageCount);
|
||||
Assert.Equal(0, metrics.ToolCallCount);
|
||||
}
|
||||
@@ -164,16 +192,18 @@ public class DefaultChatHistoryMetricsCalculatorTests
|
||||
[Fact]
|
||||
public void MessageWithOnlyNonTextContent_NullTextHandled()
|
||||
{
|
||||
// Arrange
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
// FunctionCallContent-only message has null Text
|
||||
ChatMessage msg = new(ChatRole.Assistant,
|
||||
[
|
||||
new FunctionCallContent("c1", "func"),
|
||||
]);
|
||||
List<ChatMessage> messages = [msg];
|
||||
|
||||
CompactionMetric metrics = calculator.Calculate(messages);
|
||||
// Act
|
||||
ChatHistoryMetric metrics = calculator.Calculate(messages);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(1, metrics.MessageCount);
|
||||
Assert.Equal(1, metrics.ToolCallCount);
|
||||
}
|
||||
@@ -181,6 +211,7 @@ public class DefaultChatHistoryMetricsCalculatorTests
|
||||
[Fact]
|
||||
public void Calculate_PopulatesGroupIndex()
|
||||
{
|
||||
// Arrange
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
@@ -189,8 +220,10 @@ public class DefaultChatHistoryMetricsCalculatorTests
|
||||
new(ChatRole.Assistant, "Hi there"),
|
||||
];
|
||||
|
||||
CompactionMetric metrics = calculator.Calculate(messages);
|
||||
// Act
|
||||
ChatHistoryMetric metrics = calculator.Calculate(messages);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(3, metrics.Groups.Count);
|
||||
Assert.Equal(ChatMessageGroupKind.System, metrics.Groups[0].Kind);
|
||||
Assert.Equal(ChatMessageGroupKind.UserTurn, metrics.Groups[1].Kind);
|
||||
@@ -200,23 +233,30 @@ public class DefaultChatHistoryMetricsCalculatorTests
|
||||
[Fact]
|
||||
public void EmptyList_GroupIndexIsEmpty()
|
||||
{
|
||||
// Arrange
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
CompactionMetric metrics = calculator.Calculate([]);
|
||||
|
||||
// Act
|
||||
ChatHistoryMetric metrics = calculator.Calculate([]);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(metrics.Groups);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GroupIndex_SystemMessage_IdentifiedCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(ChatRole.System, "You are a helpful assistant"),
|
||||
];
|
||||
|
||||
// Act
|
||||
IReadOnlyList<ChatMessageGroup> groups = calculator.Calculate(messages).Groups;
|
||||
|
||||
// Assert
|
||||
Assert.Single(groups);
|
||||
Assert.Equal(ChatMessageGroupKind.System, groups[0].Kind);
|
||||
Assert.Equal(0, groups[0].StartIndex);
|
||||
@@ -226,6 +266,7 @@ public class DefaultChatHistoryMetricsCalculatorTests
|
||||
[Fact]
|
||||
public void GroupIndex_AssistantWithToolCalls_GroupedWithResults()
|
||||
{
|
||||
// Arrange
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
ChatMessage assistantMsg = new(ChatRole.Assistant, [
|
||||
new FunctionCallContent("call1", "get_weather", new Dictionary<string, object?> { ["city"] = "NYC" }),
|
||||
@@ -240,18 +281,21 @@ public class DefaultChatHistoryMetricsCalculatorTests
|
||||
toolResult,
|
||||
];
|
||||
|
||||
// Act
|
||||
IReadOnlyList<ChatMessageGroup> groups = calculator.Calculate(messages).Groups;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, groups.Count);
|
||||
Assert.Equal(ChatMessageGroupKind.UserTurn, groups[0].Kind);
|
||||
Assert.Equal(ChatMessageGroupKind.AssistantToolGroup, groups[1].Kind);
|
||||
Assert.Equal(1, groups[1].StartIndex);
|
||||
Assert.Equal(2, groups[1].Count); // assistant + tool result
|
||||
Assert.Equal(2, groups[1].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GroupIndex_MultipleToolResults_GroupedTogether()
|
||||
{
|
||||
// Arrange
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
ChatMessage assistantMsg = new(ChatRole.Assistant, [
|
||||
new FunctionCallContent("c1", "func1"),
|
||||
@@ -261,8 +305,10 @@ public class DefaultChatHistoryMetricsCalculatorTests
|
||||
ChatMessage tool2 = new(ChatRole.Tool, [new FunctionResultContent("c2", "result2")]);
|
||||
List<ChatMessage> messages = [assistantMsg, tool1, tool2];
|
||||
|
||||
// Act
|
||||
IReadOnlyList<ChatMessageGroup> groups = calculator.Calculate(messages).Groups;
|
||||
|
||||
// Assert
|
||||
Assert.Single(groups);
|
||||
Assert.Equal(ChatMessageGroupKind.AssistantToolGroup, groups[0].Kind);
|
||||
Assert.Equal(3, groups[0].Count);
|
||||
@@ -271,6 +317,7 @@ public class DefaultChatHistoryMetricsCalculatorTests
|
||||
[Fact]
|
||||
public void GroupIndex_ComplexConversation_CorrectGrouping()
|
||||
{
|
||||
// Arrange
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
@@ -283,29 +330,34 @@ public class DefaultChatHistoryMetricsCalculatorTests
|
||||
new(ChatRole.Assistant, "It's sunny!"),
|
||||
];
|
||||
|
||||
// Act
|
||||
IReadOnlyList<ChatMessageGroup> groups = calculator.Calculate(messages).Groups;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(6, groups.Count);
|
||||
Assert.Equal(ChatMessageGroupKind.System, groups[0].Kind);
|
||||
Assert.Equal(ChatMessageGroupKind.UserTurn, groups[1].Kind);
|
||||
Assert.Equal(ChatMessageGroupKind.AssistantPlain, groups[2].Kind);
|
||||
Assert.Equal(ChatMessageGroupKind.UserTurn, groups[3].Kind);
|
||||
Assert.Equal(ChatMessageGroupKind.AssistantToolGroup, groups[4].Kind);
|
||||
Assert.Equal(2, groups[4].Count); // assistant + tool
|
||||
Assert.Equal(2, groups[4].Count);
|
||||
Assert.Equal(ChatMessageGroupKind.AssistantPlain, groups[5].Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GroupIndex_OrphanToolResult_IdentifiedCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new ChatMessage(ChatRole.Tool, [new FunctionResultContent("c1", "orphan result")]),
|
||||
];
|
||||
|
||||
// Act
|
||||
IReadOnlyList<ChatMessageGroup> groups = calculator.Calculate(messages).Groups;
|
||||
|
||||
// Assert
|
||||
Assert.Single(groups);
|
||||
Assert.Equal(ChatMessageGroupKind.ToolResult, groups[0].Kind);
|
||||
}
|
||||
@@ -313,14 +365,17 @@ public class DefaultChatHistoryMetricsCalculatorTests
|
||||
[Fact]
|
||||
public void GroupIndex_UnknownRole_IdentifiedAsOther()
|
||||
{
|
||||
// Arrange
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(new ChatRole("custom"), "custom message"),
|
||||
];
|
||||
|
||||
// Act
|
||||
IReadOnlyList<ChatMessageGroup> groups = calculator.Calculate(messages).Groups;
|
||||
|
||||
// Assert
|
||||
Assert.Single(groups);
|
||||
Assert.Equal(ChatMessageGroupKind.Other, groups[0].Kind);
|
||||
}
|
||||
@@ -328,13 +383,16 @@ public class DefaultChatHistoryMetricsCalculatorTests
|
||||
[Fact]
|
||||
public void GroupIndex_AssistantWithNullContents_ClassifiedAsPlain()
|
||||
{
|
||||
// Arrange
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
ChatMessage msg = new(ChatRole.Assistant, "reply");
|
||||
msg.Contents = null!;
|
||||
List<ChatMessage> messages = [msg];
|
||||
|
||||
// Act
|
||||
IReadOnlyList<ChatMessageGroup> groups = calculator.Calculate(messages).Groups;
|
||||
|
||||
// Assert
|
||||
Assert.Single(groups);
|
||||
Assert.Equal(ChatMessageGroupKind.AssistantPlain, groups[0].Kind);
|
||||
}
|
||||
|
||||
+2
-1
@@ -16,7 +16,8 @@ internal sealed class NeverCompactStrategy : ChatHistoryCompactionStrategy
|
||||
}
|
||||
|
||||
public override string Name => "NeverCompact";
|
||||
public override bool ShouldCompact(CompactionMetric metrics) => false;
|
||||
|
||||
protected override bool ShouldCompact(ChatHistoryMetric metrics) => false;
|
||||
|
||||
private sealed class NoOpReducer : IChatReducer
|
||||
{
|
||||
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.Agents.AI.Abstractions.UnitTests.Compaction.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// An IList<T> that does NOT implement IReadOnlyList<T>,
|
||||
/// used to test the defensive <c>as IReadOnlyList<T> ?? fallback</c> patterns.
|
||||
/// </summary>
|
||||
internal sealed class NonReadOnlyList<T> : IList<T>
|
||||
{
|
||||
private readonly List<T> _inner;
|
||||
|
||||
public NonReadOnlyList(IEnumerable<T> items)
|
||||
{
|
||||
this._inner = items.ToList();
|
||||
}
|
||||
|
||||
public T this[int index]
|
||||
{
|
||||
get => this._inner[index];
|
||||
set => this._inner[index] = value;
|
||||
}
|
||||
|
||||
public int Count => this._inner.Count;
|
||||
public bool IsReadOnly => false;
|
||||
public void Add(T item) => this._inner.Add(item);
|
||||
public void Clear() => this._inner.Clear();
|
||||
public bool Contains(T item) => this._inner.Contains(item);
|
||||
public void CopyTo(T[] array, int arrayIndex) => this._inner.CopyTo(array, arrayIndex);
|
||||
public IEnumerator<T> GetEnumerator() => this._inner.GetEnumerator();
|
||||
public int IndexOf(T item) => this._inner.IndexOf(item);
|
||||
public void Insert(int index, T item) => this._inner.Insert(index, item);
|
||||
public bool Remove(T item) => this._inner.Remove(item);
|
||||
public void RemoveAt(int index) => this._inner.RemoveAt(index);
|
||||
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
|
||||
}
|
||||
+1
-1
@@ -18,7 +18,7 @@ internal sealed class RemoveFirstMessageStrategy : ChatHistoryCompactionStrategy
|
||||
|
||||
public override string Name => "RemoveFirst";
|
||||
|
||||
public override bool ShouldCompact(CompactionMetric metrics) => metrics.MessageCount > 0;
|
||||
protected override bool ShouldCompact(ChatHistoryMetric metrics) => metrics.MessageCount > 0;
|
||||
|
||||
private sealed class RemoveFirstReducer : IChatReducer
|
||||
{
|
||||
|
||||
+10
@@ -9,10 +9,12 @@ public class MessageGroupTests
|
||||
[Fact]
|
||||
public void Equality_Works()
|
||||
{
|
||||
// Arrange
|
||||
ChatMessageGroup a = new(0, 2, ChatMessageGroupKind.AssistantToolGroup);
|
||||
ChatMessageGroup b = new(0, 2, ChatMessageGroupKind.AssistantToolGroup);
|
||||
ChatMessageGroup c = new(1, 2, ChatMessageGroupKind.AssistantToolGroup);
|
||||
|
||||
// Act & Assert
|
||||
Assert.Equal(a, b);
|
||||
Assert.True(a == b);
|
||||
Assert.NotEqual(a, c);
|
||||
@@ -22,34 +24,42 @@ public class MessageGroupTests
|
||||
[Fact]
|
||||
public void Equals_Object_NullReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
ChatMessageGroup group = new(0, 1, ChatMessageGroupKind.System);
|
||||
|
||||
// Act & Assert
|
||||
Assert.False(group.Equals(null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_Object_BoxedMessageGroupReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
ChatMessageGroup group = new(0, 2, ChatMessageGroupKind.AssistantToolGroup);
|
||||
object boxed = new ChatMessageGroup(0, 2, ChatMessageGroupKind.AssistantToolGroup);
|
||||
|
||||
// Act & Assert
|
||||
Assert.True(group.Equals(boxed));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_Object_WrongTypeReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
ChatMessageGroup group = new(0, 1, ChatMessageGroupKind.System);
|
||||
|
||||
// Act & Assert
|
||||
Assert.False(group.Equals("not a MessageGroup"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHashCode_ConsistentForEqualInstances()
|
||||
{
|
||||
// Arrange
|
||||
ChatMessageGroup a = new(0, 2, ChatMessageGroupKind.AssistantToolGroup);
|
||||
ChatMessageGroup b = new(0, 2, ChatMessageGroupKind.AssistantToolGroup);
|
||||
|
||||
// Act & Assert
|
||||
Assert.Equal(a.GetHashCode(), b.GetHashCode());
|
||||
}
|
||||
}
|
||||
|
||||
+30
-53
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
@@ -7,47 +7,27 @@ using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Abstractions.UnitTests.Compaction;
|
||||
|
||||
public class SlidingWindowCompactionStrategyTests
|
||||
public class SlidingWindowCompactionStrategyTests : CompactionStrategyTestBase
|
||||
{
|
||||
[Fact]
|
||||
public void ShouldCompact_UnderLimit_ReturnsFalse()
|
||||
{
|
||||
SlidingWindowCompactionStrategy strategy = new(maxTurns: 10);
|
||||
CompactionMetric metrics = new() { UserTurnCount = 3 };
|
||||
|
||||
Assert.False(strategy.ShouldCompact(metrics));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldCompact_OverLimit_ReturnsTrue()
|
||||
{
|
||||
SlidingWindowCompactionStrategy strategy = new(maxTurns: 2);
|
||||
CompactionMetric metrics = new() { UserTurnCount = 5 };
|
||||
|
||||
Assert.True(strategy.ShouldCompact(metrics));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UnderLimit_NoChangeAsync()
|
||||
{
|
||||
SlidingWindowCompactionStrategy strategy = new(maxTurns: 10);
|
||||
// Arrange
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(ChatRole.User, "Hello"),
|
||||
new(ChatRole.Assistant, "Hi"),
|
||||
];
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
SlidingWindowCompactionStrategy strategy = new(maxTurns: 10);
|
||||
|
||||
CompactionResult result = await strategy.CompactAsync(messages, calculator);
|
||||
|
||||
Assert.False(result.Applied);
|
||||
Assert.Equal(2, messages.Count);
|
||||
// Act & Assert
|
||||
await RunCompactionStrategySkippedAsync(strategy, messages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task KeepsLastNTurnsAsync()
|
||||
{
|
||||
SlidingWindowCompactionStrategy strategy = new(maxTurns: 2);
|
||||
// Arrange
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(ChatRole.User, "Turn 1"),
|
||||
@@ -57,12 +37,12 @@ public class SlidingWindowCompactionStrategyTests
|
||||
new(ChatRole.User, "Turn 3"),
|
||||
new(ChatRole.Assistant, "Reply 3"),
|
||||
];
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
SlidingWindowCompactionStrategy strategy = new(maxTurns: 2);
|
||||
|
||||
CompactionResult result = await strategy.CompactAsync(messages, calculator);
|
||||
// Act & Assert
|
||||
await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 4);
|
||||
|
||||
Assert.True(result.Applied);
|
||||
Assert.Equal(4, messages.Count);
|
||||
// Assert
|
||||
Assert.Equal("Turn 2", messages[0].Text);
|
||||
Assert.Equal("Reply 2", messages[1].Text);
|
||||
Assert.Equal("Turn 3", messages[2].Text);
|
||||
@@ -72,7 +52,7 @@ public class SlidingWindowCompactionStrategyTests
|
||||
[Fact]
|
||||
public async Task PreservesSystemMessagesAsync()
|
||||
{
|
||||
SlidingWindowCompactionStrategy strategy = new(maxTurns: 1);
|
||||
// Arrange
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(ChatRole.System, "You are a helper"),
|
||||
@@ -81,12 +61,12 @@ public class SlidingWindowCompactionStrategyTests
|
||||
new(ChatRole.User, "Turn 2"),
|
||||
new(ChatRole.Assistant, "Reply 2"),
|
||||
];
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
SlidingWindowCompactionStrategy strategy = new(maxTurns: 1);
|
||||
|
||||
CompactionResult result = await strategy.CompactAsync(messages, calculator);
|
||||
// Act & Assert
|
||||
await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 3);
|
||||
|
||||
Assert.True(result.Applied);
|
||||
Assert.Equal(3, messages.Count);
|
||||
// Assert
|
||||
Assert.Equal(ChatRole.System, messages[0].Role);
|
||||
Assert.Equal("You are a helper", messages[0].Text);
|
||||
Assert.Equal("Turn 2", messages[1].Text);
|
||||
@@ -96,7 +76,7 @@ public class SlidingWindowCompactionStrategyTests
|
||||
[Fact]
|
||||
public async Task PreservesToolGroupsWithinKeptTurnsAsync()
|
||||
{
|
||||
SlidingWindowCompactionStrategy strategy = new(maxTurns: 1);
|
||||
// Arrange
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(ChatRole.User, "Turn 1"),
|
||||
@@ -106,37 +86,34 @@ public class SlidingWindowCompactionStrategyTests
|
||||
new ChatMessage(ChatRole.Tool, [new FunctionResultContent("c1", "Sunny")]),
|
||||
new(ChatRole.Assistant, "It's sunny!"),
|
||||
];
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
SlidingWindowCompactionStrategy strategy = new(maxTurns: 1);
|
||||
|
||||
CompactionResult result = await strategy.CompactAsync(messages, calculator);
|
||||
// Act & Assert
|
||||
await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 4);
|
||||
|
||||
Assert.True(result.Applied);
|
||||
// Turn 1 dropped, Turn 2 kept (user + assistant-tool-group + plain assistant)
|
||||
Assert.Equal(4, messages.Count);
|
||||
// Assert
|
||||
Assert.Equal("Get weather", messages[0].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SingleTurn_AtLimit_NoChangeAsync()
|
||||
{
|
||||
SlidingWindowCompactionStrategy strategy = new(maxTurns: 1);
|
||||
// Arrange
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(ChatRole.User, "Hello"),
|
||||
new(ChatRole.Assistant, "Hi"),
|
||||
];
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
SlidingWindowCompactionStrategy strategy = new(maxTurns: 1);
|
||||
|
||||
CompactionResult result = await strategy.CompactAsync(messages, calculator);
|
||||
|
||||
Assert.False(result.Applied);
|
||||
Assert.Equal(2, messages.Count);
|
||||
// Act & Assert
|
||||
await RunCompactionStrategySkippedAsync(strategy, messages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DropsResponseGroupsFromOldTurnsAsync()
|
||||
{
|
||||
SlidingWindowCompactionStrategy strategy = new(maxTurns: 1);
|
||||
// Arrange
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(ChatRole.User, "Turn 1"),
|
||||
@@ -146,12 +123,12 @@ public class SlidingWindowCompactionStrategyTests
|
||||
new(ChatRole.User, "Turn 2"),
|
||||
new(ChatRole.Assistant, "Reply 2"),
|
||||
];
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
SlidingWindowCompactionStrategy strategy = new(maxTurns: 1);
|
||||
|
||||
CompactionResult result = await strategy.CompactAsync(messages, calculator);
|
||||
// Act & Assert
|
||||
await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 2);
|
||||
|
||||
Assert.True(result.Applied);
|
||||
Assert.Equal(2, messages.Count);
|
||||
// Assert
|
||||
Assert.Equal("Turn 2", messages[0].Text);
|
||||
Assert.Equal("Reply 2", messages[1].Text);
|
||||
}
|
||||
|
||||
+51
-77
@@ -9,44 +9,24 @@ using Moq;
|
||||
|
||||
namespace Microsoft.Agents.AI.Abstractions.UnitTests.Compaction;
|
||||
|
||||
public class SummarizationCompactionStrategyTests
|
||||
public class SummarizationCompactionStrategyTests : CompactionStrategyTestBase
|
||||
{
|
||||
[Fact]
|
||||
public void ShouldCompact_UnderLimit_ReturnsFalse()
|
||||
{
|
||||
Mock<IChatClient> chatClientMock = new();
|
||||
SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 100000);
|
||||
CompactionMetric metrics = new() { TokenCount = 500 };
|
||||
|
||||
Assert.False(strategy.ShouldCompact(metrics));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldCompact_OverLimit_ReturnsTrue()
|
||||
{
|
||||
Mock<IChatClient> chatClientMock = new();
|
||||
SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 100);
|
||||
CompactionMetric metrics = new() { TokenCount = 500 };
|
||||
|
||||
Assert.True(strategy.ShouldCompact(metrics));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UnderLimit_NoChangeAsync()
|
||||
{
|
||||
Mock<IChatClient> chatClientMock = new();
|
||||
SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 100000);
|
||||
// Arrange
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(ChatRole.User, "Hello"),
|
||||
new(ChatRole.Assistant, "Hi"),
|
||||
];
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
Mock<IChatClient> chatClientMock = new();
|
||||
SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 100000);
|
||||
|
||||
CompactionResult result = await strategy.CompactAsync(messages, calculator);
|
||||
// Act & Assert
|
||||
await RunCompactionStrategySkippedAsync(strategy, messages);
|
||||
|
||||
Assert.False(result.Applied);
|
||||
Assert.Equal(2, messages.Count);
|
||||
// Assert
|
||||
chatClientMock.Verify(
|
||||
c => c.GetResponseAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<ChatOptions>(), It.IsAny<CancellationToken>()),
|
||||
Times.Never);
|
||||
@@ -55,13 +35,7 @@ public class SummarizationCompactionStrategyTests
|
||||
[Fact]
|
||||
public async Task SummarizesOldGroupsAsync()
|
||||
{
|
||||
Mock<IChatClient> chatClientMock = new();
|
||||
chatClientMock
|
||||
.Setup(c => c.GetResponseAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<ChatOptions>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "User asked about weather. It was sunny.")));
|
||||
|
||||
// preserveRecentGroups=2 means keep last 2 non-system groups
|
||||
SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 1, preserveRecentGroups: 2);
|
||||
// Arrange
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(ChatRole.User, "What's the weather?"),
|
||||
@@ -71,14 +45,16 @@ public class SummarizationCompactionStrategyTests
|
||||
new(ChatRole.User, "Thanks!"),
|
||||
new(ChatRole.Assistant, "You're welcome!"),
|
||||
];
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
Mock<IChatClient> chatClientMock = new();
|
||||
chatClientMock
|
||||
.Setup(c => c.GetResponseAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<ChatOptions>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "User asked about weather. It was sunny.")));
|
||||
SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 1, preserveRecentGroups: 2);
|
||||
|
||||
CompactionResult result = await strategy.CompactAsync(messages, calculator);
|
||||
// Act & Assert
|
||||
await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 3);
|
||||
|
||||
Assert.True(result.Applied);
|
||||
// 6 groups (3 user + 3 assistant), protect last 2 → summarize first 4 groups
|
||||
// Result: summary + 2 protected groups = 3 messages
|
||||
Assert.Equal(3, messages.Count);
|
||||
// Assert
|
||||
Assert.Contains("[Summary]", messages[0].Text);
|
||||
Assert.Contains("sunny", messages[0].Text);
|
||||
Assert.Equal("Thanks!", messages[1].Text);
|
||||
@@ -88,12 +64,7 @@ public class SummarizationCompactionStrategyTests
|
||||
[Fact]
|
||||
public async Task PreservesSystemMessagesAsync()
|
||||
{
|
||||
Mock<IChatClient> chatClientMock = new();
|
||||
chatClientMock
|
||||
.Setup(c => c.GetResponseAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<ChatOptions>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Summary of earlier discussion.")));
|
||||
|
||||
SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 1, preserveRecentGroups: 1);
|
||||
// Arrange
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(ChatRole.System, "You are a helper"),
|
||||
@@ -102,11 +73,16 @@ public class SummarizationCompactionStrategyTests
|
||||
new(ChatRole.User, "Turn 2"),
|
||||
new(ChatRole.Assistant, "Reply 2"),
|
||||
];
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
Mock<IChatClient> chatClientMock = new();
|
||||
chatClientMock
|
||||
.Setup(c => c.GetResponseAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<ChatOptions>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Summary of earlier discussion.")));
|
||||
SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 1, preserveRecentGroups: 1);
|
||||
|
||||
CompactionResult result = await strategy.CompactAsync(messages, calculator);
|
||||
// Act & Assert
|
||||
await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 3);
|
||||
|
||||
Assert.True(result.Applied);
|
||||
// Assert
|
||||
Assert.Equal(ChatRole.System, messages[0].Role);
|
||||
Assert.Equal("You are a helper", messages[0].Text);
|
||||
Assert.Contains("[Summary]", messages[1].Text);
|
||||
@@ -115,20 +91,19 @@ public class SummarizationCompactionStrategyTests
|
||||
[Fact]
|
||||
public async Task AllGroupsProtected_NoChangeAsync()
|
||||
{
|
||||
Mock<IChatClient> chatClientMock = new();
|
||||
SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 1, preserveRecentGroups: 10);
|
||||
// Arrange
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(ChatRole.User, "Hello"),
|
||||
new(ChatRole.Assistant, "Hi"),
|
||||
];
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
Mock<IChatClient> chatClientMock = new();
|
||||
SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 1, preserveRecentGroups: 10);
|
||||
|
||||
CompactionResult result = await strategy.CompactAsync(messages, calculator);
|
||||
// Act & Assert
|
||||
await RunCompactionStrategySkippedAsync(strategy, messages);
|
||||
|
||||
// All groups protected → nothing to summarize → no change
|
||||
Assert.False(result.Applied);
|
||||
Assert.Equal(2, messages.Count);
|
||||
// Assert
|
||||
chatClientMock.Verify(
|
||||
c => c.GetResponseAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<ChatOptions>(), It.IsAny<CancellationToken>()),
|
||||
Times.Never);
|
||||
@@ -137,16 +112,8 @@ public class SummarizationCompactionStrategyTests
|
||||
[Fact]
|
||||
public async Task CustomPrompt_UsedInRequestAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string CustomPrompt = "Summarize briefly.";
|
||||
List<ChatMessage>? capturedMessages = null;
|
||||
|
||||
Mock<IChatClient> chatClientMock = new();
|
||||
chatClientMock
|
||||
.Setup(c => c.GetResponseAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<ChatOptions>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((msgs, _, _) => capturedMessages = [.. msgs])
|
||||
.ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Brief summary.")));
|
||||
|
||||
SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 1, preserveRecentGroups: 1, summarizationPrompt: CustomPrompt);
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(ChatRole.User, "First"),
|
||||
@@ -154,12 +121,19 @@ public class SummarizationCompactionStrategyTests
|
||||
new(ChatRole.User, "Second"),
|
||||
new(ChatRole.Assistant, "Reply 2"),
|
||||
];
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
List<ChatMessage>? capturedMessages = null;
|
||||
Mock<IChatClient> chatClientMock = new();
|
||||
chatClientMock
|
||||
.Setup(c => c.GetResponseAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<ChatOptions>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((msgs, _, _) => capturedMessages = [.. msgs])
|
||||
.ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Brief summary.")));
|
||||
SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 1, preserveRecentGroups: 1, summarizationPrompt: CustomPrompt);
|
||||
|
||||
await strategy.CompactAsync(messages, calculator);
|
||||
// Act & Assert
|
||||
await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 2);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(capturedMessages);
|
||||
// First message in request should be the custom system prompt
|
||||
Assert.Equal(ChatRole.System, capturedMessages![0].Role);
|
||||
Assert.Equal(CustomPrompt, capturedMessages[0].Text);
|
||||
}
|
||||
@@ -167,12 +141,7 @@ public class SummarizationCompactionStrategyTests
|
||||
[Fact]
|
||||
public async Task NullResponseText_UsesFallbackAsync()
|
||||
{
|
||||
Mock<IChatClient> chatClientMock = new();
|
||||
chatClientMock
|
||||
.Setup(c => c.GetResponseAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<ChatOptions>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, (string?)null)));
|
||||
|
||||
SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 1, preserveRecentGroups: 1);
|
||||
// Arrange
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(ChatRole.User, "First"),
|
||||
@@ -180,11 +149,16 @@ public class SummarizationCompactionStrategyTests
|
||||
new(ChatRole.User, "Second"),
|
||||
new(ChatRole.Assistant, "Reply 2"),
|
||||
];
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
Mock<IChatClient> chatClientMock = new();
|
||||
chatClientMock
|
||||
.Setup(c => c.GetResponseAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<ChatOptions>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, (string?)null)));
|
||||
SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 1, preserveRecentGroups: 1);
|
||||
|
||||
CompactionResult result = await strategy.CompactAsync(messages, calculator);
|
||||
// Act & Assert
|
||||
await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 2);
|
||||
|
||||
Assert.True(result.Applied);
|
||||
// Assert
|
||||
Assert.Contains("[Summary unavailable]", messages[0].Text);
|
||||
}
|
||||
}
|
||||
|
||||
+29
-66
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
@@ -7,57 +7,28 @@ using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Abstractions.UnitTests.Compaction;
|
||||
|
||||
public class ToolResultCompactionStrategyTests
|
||||
public class ToolResultCompactionStrategyTests : CompactionStrategyTestBase
|
||||
{
|
||||
[Fact]
|
||||
public void ShouldCompact_UnderLimit_ReturnsFalse()
|
||||
{
|
||||
ToolResultCompactionStrategy strategy = new(maxTokens: 100000);
|
||||
CompactionMetric metrics = new() { TokenCount = 500, ToolCallCount = 3 };
|
||||
|
||||
Assert.False(strategy.ShouldCompact(metrics));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldCompact_OverLimitNoToolCalls_ReturnsFalse()
|
||||
{
|
||||
ToolResultCompactionStrategy strategy = new(maxTokens: 100);
|
||||
CompactionMetric metrics = new() { TokenCount = 500, ToolCallCount = 0 };
|
||||
|
||||
Assert.False(strategy.ShouldCompact(metrics));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldCompact_OverLimitWithToolCalls_ReturnsTrue()
|
||||
{
|
||||
ToolResultCompactionStrategy strategy = new(maxTokens: 100);
|
||||
CompactionMetric metrics = new() { TokenCount = 500, ToolCallCount = 2 };
|
||||
|
||||
Assert.True(strategy.ShouldCompact(metrics));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UnderLimit_NoChangeAsync()
|
||||
{
|
||||
ToolResultCompactionStrategy strategy = new(maxTokens: 100000);
|
||||
// Arrange
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(ChatRole.User, "Hello"),
|
||||
new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("c1", "get_weather")]),
|
||||
new ChatMessage(ChatRole.Tool, [new FunctionResultContent("c1", "Sunny")]),
|
||||
];
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
ToolResultCompactionStrategy strategy = new(maxTokens: 100000);
|
||||
|
||||
CompactionResult result = await strategy.CompactAsync(messages, calculator);
|
||||
|
||||
Assert.False(result.Applied);
|
||||
Assert.Equal(3, messages.Count);
|
||||
// Act & Assert
|
||||
await RunCompactionStrategySkippedAsync(strategy, messages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CollapsesOldToolGroupAsync()
|
||||
{
|
||||
ToolResultCompactionStrategy strategy = new(maxTokens: 1, preserveRecentGroups: 1);
|
||||
// Arrange
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(ChatRole.User, "Check weather"),
|
||||
@@ -66,13 +37,12 @@ public class ToolResultCompactionStrategyTests
|
||||
new(ChatRole.User, "Thanks"),
|
||||
new(ChatRole.Assistant, "You're welcome!"),
|
||||
];
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
ToolResultCompactionStrategy strategy = new(maxTokens: 1, preserveRecentGroups: 1);
|
||||
|
||||
CompactionResult result = await strategy.CompactAsync(messages, calculator);
|
||||
// Act & Assert
|
||||
await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 4);
|
||||
|
||||
Assert.True(result.Applied);
|
||||
// The old tool group (assistant+tool = 2 messages) should be collapsed to 1
|
||||
Assert.Equal(4, messages.Count); // user + [collapsed] + user + assistant
|
||||
// Assert
|
||||
Assert.Contains("[Tool calls: get_weather]", messages[1].Text);
|
||||
Assert.Equal(ChatRole.Assistant, messages[1].Role);
|
||||
}
|
||||
@@ -80,8 +50,7 @@ public class ToolResultCompactionStrategyTests
|
||||
[Fact]
|
||||
public async Task ProtectsRecentGroupsAsync()
|
||||
{
|
||||
// With preserveRecentGroups=4, all groups are protected (5 groups, protect 4 non-system)
|
||||
ToolResultCompactionStrategy strategy = new(maxTokens: 1, preserveRecentGroups: 10);
|
||||
// Arrange
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(ChatRole.User, "Check weather"),
|
||||
@@ -90,19 +59,16 @@ public class ToolResultCompactionStrategyTests
|
||||
new(ChatRole.User, "Thanks"),
|
||||
new(ChatRole.Assistant, "You're welcome!"),
|
||||
];
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
ToolResultCompactionStrategy strategy = new(maxTokens: 1, preserveRecentGroups: 10);
|
||||
|
||||
CompactionResult result = await strategy.CompactAsync(messages, calculator);
|
||||
|
||||
// All groups protected, so no collapse
|
||||
Assert.False(result.Applied);
|
||||
Assert.Equal(5, messages.Count);
|
||||
// Act & Assert
|
||||
await RunCompactionStrategySkippedAsync(strategy, messages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PreservesSystemMessagesAsync()
|
||||
{
|
||||
ToolResultCompactionStrategy strategy = new(maxTokens: 1, preserveRecentGroups: 1);
|
||||
// Arrange
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(ChatRole.System, "You are a helper"),
|
||||
@@ -112,11 +78,12 @@ public class ToolResultCompactionStrategyTests
|
||||
new(ChatRole.User, "Thanks"),
|
||||
new(ChatRole.Assistant, "You're welcome!"),
|
||||
];
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
ToolResultCompactionStrategy strategy = new(maxTokens: 1, preserveRecentGroups: 1);
|
||||
|
||||
CompactionResult result = await strategy.CompactAsync(messages, calculator);
|
||||
// Act & Assert
|
||||
await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 5);
|
||||
|
||||
Assert.True(result.Applied);
|
||||
// Assert
|
||||
Assert.Equal(ChatRole.System, messages[0].Role);
|
||||
Assert.Equal("You are a helper", messages[0].Text);
|
||||
}
|
||||
@@ -124,7 +91,7 @@ public class ToolResultCompactionStrategyTests
|
||||
[Fact]
|
||||
public async Task MultipleToolCalls_ListedInSummaryAsync()
|
||||
{
|
||||
ToolResultCompactionStrategy strategy = new(maxTokens: 1, preserveRecentGroups: 1);
|
||||
// Arrange
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(ChatRole.User, "Do research"),
|
||||
@@ -137,13 +104,12 @@ public class ToolResultCompactionStrategyTests
|
||||
new(ChatRole.User, "Summarize"),
|
||||
new(ChatRole.Assistant, "Here's the summary."),
|
||||
];
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
ToolResultCompactionStrategy strategy = new(maxTokens: 1, preserveRecentGroups: 1);
|
||||
|
||||
CompactionResult result = await strategy.CompactAsync(messages, calculator);
|
||||
// Act & Assert
|
||||
await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 4);
|
||||
|
||||
Assert.True(result.Applied);
|
||||
// Old tool group (1 assistant + 2 tools = 3 messages) collapsed to 1
|
||||
Assert.Equal(4, messages.Count);
|
||||
// Assert
|
||||
Assert.Contains("search", messages[1].Text);
|
||||
Assert.Contains("fetch_page", messages[1].Text);
|
||||
}
|
||||
@@ -151,18 +117,15 @@ public class ToolResultCompactionStrategyTests
|
||||
[Fact]
|
||||
public async Task NoToolGroups_NoChangeAsync()
|
||||
{
|
||||
ToolResultCompactionStrategy strategy = new(maxTokens: 1);
|
||||
// Arrange
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(ChatRole.User, "Hello"),
|
||||
new(ChatRole.Assistant, "Hi there"),
|
||||
];
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
ToolResultCompactionStrategy strategy = new(maxTokens: 1);
|
||||
|
||||
// ToolCallCount is 0, so ShouldCompact returns false
|
||||
CompactionResult result = await strategy.CompactAsync(messages, calculator);
|
||||
|
||||
Assert.False(result.Applied);
|
||||
Assert.Equal(2, messages.Count);
|
||||
// Act & Assert
|
||||
await RunCompactionStrategySkippedAsync(strategy, messages);
|
||||
}
|
||||
}
|
||||
|
||||
+23
-60
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
@@ -7,29 +7,26 @@ using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Abstractions.UnitTests.Compaction;
|
||||
|
||||
public class TruncationCompactionStrategyTests
|
||||
public class TruncationCompactionStrategyTests : CompactionStrategyTestBase
|
||||
{
|
||||
[Fact]
|
||||
public async Task UnderLimit_NoChangeAsync()
|
||||
{
|
||||
TruncationCompactionStrategy strategy = new(maxTokens: 100000);
|
||||
// Arrange
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(ChatRole.User, "Hello"),
|
||||
];
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
TruncationCompactionStrategy strategy = new(maxTokens: 100000);
|
||||
|
||||
CompactionResult result = await strategy.CompactAsync(messages, calculator);
|
||||
|
||||
Assert.False(result.Applied);
|
||||
Assert.Single(messages);
|
||||
// Act & Assert
|
||||
await RunCompactionStrategySkippedAsync(strategy, messages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OverLimit_RemovesOldestGroupsAsync()
|
||||
{
|
||||
// Use a very low max to trigger compaction
|
||||
TruncationCompactionStrategy strategy = new(maxTokens: 1);
|
||||
// Arrange
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(ChatRole.User, "First message"),
|
||||
@@ -37,77 +34,45 @@ public class TruncationCompactionStrategyTests
|
||||
new(ChatRole.User, "Second message"),
|
||||
new(ChatRole.Assistant, "Second reply"),
|
||||
];
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
TruncationCompactionStrategy strategy = new(maxTokens: 1);
|
||||
|
||||
CompactionResult result = await strategy.CompactAsync(messages, calculator);
|
||||
|
||||
Assert.True(result.Applied);
|
||||
// Should have removed old groups, keeping at least the last one
|
||||
Assert.True(messages.Count < 4);
|
||||
Assert.True(messages.Count > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldCompact_ReturnsFalseWhenUnderLimit()
|
||||
{
|
||||
TruncationCompactionStrategy strategy = new(maxTokens: 10000);
|
||||
CompactionMetric metrics = new() { TokenCount = 500 };
|
||||
|
||||
Assert.False(strategy.ShouldCompact(metrics));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldCompact_ReturnsTrueWhenOverLimit()
|
||||
{
|
||||
TruncationCompactionStrategy strategy = new(maxTokens: 100);
|
||||
CompactionMetric metrics = new() { TokenCount = 500 };
|
||||
|
||||
Assert.True(strategy.ShouldCompact(metrics));
|
||||
// Act & Assert
|
||||
await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SystemOnlyMessages_NoChangeAsync()
|
||||
{
|
||||
// Only system messages → removableGroups.Count == 0 → no change
|
||||
TruncationCompactionStrategy strategy = new(maxTokens: 1);
|
||||
// Arrange
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(ChatRole.System, "You are a helper"),
|
||||
];
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
TruncationCompactionStrategy strategy = new(maxTokens: 1);
|
||||
|
||||
CompactionResult result = await strategy.CompactAsync(messages, calculator);
|
||||
|
||||
// ShouldCompact triggers but reducer finds nothing removable
|
||||
Assert.Single(messages);
|
||||
Assert.Equal(result.After.MessageCount, result.Before.MessageCount);
|
||||
Assert.Equal(result.After.TokenCount, result.Before.TokenCount);
|
||||
// Act & Assert
|
||||
await RunCompactionStrategySkippedAsync(strategy, messages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SingleNonSystemGroup_NoChangeAsync()
|
||||
{
|
||||
// Only one non-system group → maxRemovable <= 0 → no change
|
||||
TruncationCompactionStrategy strategy = new(maxTokens: 1);
|
||||
// Arrange
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(ChatRole.System, "System prompt"),
|
||||
new(ChatRole.User, "Only user message"),
|
||||
];
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
TruncationCompactionStrategy strategy = new(maxTokens: 1);
|
||||
|
||||
CompactionResult result = await strategy.CompactAsync(messages, calculator);
|
||||
|
||||
Assert.Equal(2, messages.Count);
|
||||
Assert.Equal(result.After.MessageCount, result.Before.MessageCount);
|
||||
Assert.Equal(result.After.TokenCount, result.Before.TokenCount);
|
||||
// Act & Assert
|
||||
await RunCompactionStrategySkippedAsync(strategy, messages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PreserveRecentGroups_KeepsMultipleGroupsAsync()
|
||||
{
|
||||
// preserveRecentGroups=2 means keep at least the last 2 non-system groups
|
||||
TruncationCompactionStrategy strategy = new(maxTokens: 1, preserveRecentGroups: 2);
|
||||
// Arrange
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new(ChatRole.User, "Turn 1"),
|
||||
@@ -117,14 +82,12 @@ public class TruncationCompactionStrategyTests
|
||||
new(ChatRole.User, "Turn 3"),
|
||||
new(ChatRole.Assistant, "Reply 3"),
|
||||
];
|
||||
DefaultChatHistoryMetricsCalculator calculator = new();
|
||||
TruncationCompactionStrategy strategy = new(maxTokens: 1, preserveRecentGroups: 2);
|
||||
|
||||
CompactionResult result = await strategy.CompactAsync(messages, calculator);
|
||||
// Act & Assert
|
||||
await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 2);
|
||||
|
||||
Assert.True(result.Applied);
|
||||
// 6 groups (3 user + 3 assistant), protect last 2 → 4 removable
|
||||
// Should keep at least: Turn 3 user + Reply 3 assistant (last 2 groups)
|
||||
Assert.True(messages.Count >= 2);
|
||||
// Assert
|
||||
Assert.Equal("Reply 3", messages[^1].Text);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user