mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Checkpoint
This commit is contained in:
@@ -56,6 +56,7 @@
|
||||
<Project Path="samples/02-agents/Agents/Agent_Step15_DeepResearch/Agent_Step15_DeepResearch.csproj" />
|
||||
<Project Path="samples/02-agents/Agents/Agent_Step16_Declarative/Agent_Step16_Declarative.csproj" />
|
||||
<Project Path="samples/02-agents/Agents/Agent_Step17_AdditionalAIContext/Agent_Step17_AdditionalAIContext.csproj" />
|
||||
<Project Path="samples/02-agents/Agents/Agent_Step18_CompactionPipeline/Agent_Step18_CompactionPipeline.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/Samples/02-agents/DeclarativeAgents/">
|
||||
<Project Path="samples/02-agents/DeclarativeAgents/ChatClient/DeclarativeChatClientAgents.csproj" />
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,109 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// This sample demonstrates how to use a ChatHistoryCompactionPipeline as the ChatReducer for an agent's
|
||||
// in-memory chat history. The pipeline chains multiple compaction strategies from gentle to aggressive:
|
||||
// 1. ToolResultCompactionStrategy - Collapses old tool-call groups into concise summaries
|
||||
// 2. SummarizationCompactionStrategy - LLM-compresses older conversation spans
|
||||
// 3. SlidingWindowCompactionStrategy - Keeps only the most recent N user turns
|
||||
// 4. TruncationCompactionStrategy - Emergency token-budget backstop
|
||||
|
||||
using System.ComponentModel;
|
||||
using Azure.AI.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Compaction;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
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";
|
||||
|
||||
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
|
||||
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
|
||||
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
|
||||
AzureOpenAIClient openAIClient = new(new Uri(endpoint), new DefaultAzureCredential());
|
||||
|
||||
// Create a chat client for the agent and a separate one for the summarization strategy.
|
||||
// Using the same model for simplicity; in production, use a smaller/cheaper model for summarization.
|
||||
IChatClient agentChatClient = openAIClient.GetChatClient(deploymentName).AsIChatClient();
|
||||
IChatClient summarizerChatClient = openAIClient.GetChatClient(deploymentName).AsIChatClient();
|
||||
|
||||
// Define a tool the agent can use, so we can see tool-result compaction in action.
|
||||
[Description("Look up the current price of a product by name.")]
|
||||
static string LookupPrice([Description("The product name to look up.")] string productName) =>
|
||||
productName.ToUpperInvariant() switch
|
||||
{
|
||||
"LAPTOP" => "The laptop costs $999.99.",
|
||||
"KEYBOARD" => "The keyboard costs $79.99.",
|
||||
"MOUSE" => "The mouse costs $29.99.",
|
||||
_ => $"Sorry, I don't have pricing for '{productName}'."
|
||||
};
|
||||
|
||||
// Configure the compaction pipeline with one of each strategy, ordered least to most aggressive.
|
||||
//const int MaxTokens = 512;
|
||||
//const int MaxTurns = 4;
|
||||
const int MaxGroups = 2;
|
||||
|
||||
PipelineCompactionStrategy compactionPipeline =
|
||||
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, MaxGroups)
|
||||
|
||||
// 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(MaxGroups)
|
||||
);
|
||||
|
||||
// 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, 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)],
|
||||
},
|
||||
CompactionStrategy = compactionPipeline,
|
||||
});
|
||||
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
|
||||
// Helper to print chat history size
|
||||
void PrintChatHistory()
|
||||
{
|
||||
if (session.TryGetInMemoryChatHistory(out var history))
|
||||
{
|
||||
Console.WriteLine($" [Chat history: {history.Count} messages]\n");
|
||||
}
|
||||
}
|
||||
|
||||
// Run a multi-turn conversation with tool calls to exercise the pipeline.
|
||||
string[] prompts =
|
||||
[
|
||||
"What's the price of a laptop?",
|
||||
"How about a keyboard?",
|
||||
"And a mouse?",
|
||||
"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)
|
||||
{
|
||||
Console.WriteLine($"User: {prompt}");
|
||||
Console.WriteLine($"Agent: {await agent.RunAsync(prompt, session)}");
|
||||
|
||||
PrintChatHistory();
|
||||
}
|
||||
@@ -5,7 +5,6 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Compaction;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
@@ -270,35 +269,6 @@ public abstract class ChatHistoryProvider
|
||||
protected virtual ValueTask StoreChatHistoryAsync(InvokedContext context, CancellationToken cancellationToken = default) =>
|
||||
default;
|
||||
|
||||
/// <summary>
|
||||
/// Compacts the messages in place using the specified compaction strategy before they are stored.
|
||||
/// </summary>
|
||||
/// <param name="messages">The messages to compact. This list is mutated in place.</param>
|
||||
/// <param name="compactionStrategy">The compaction strategy to apply.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
|
||||
/// <returns>A task representing the asynchronous operation. The task result is <see langword="true"/> if compaction occurred.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This method organizes the messages into atomic <see cref="MessageGroup"/> units,
|
||||
/// applies the compaction strategy, and replaces the contents of the list with the compacted result.
|
||||
/// Tool call groups (assistant message + tool results) are treated as atomic units.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
protected static async Task<bool> CompactMessagesAsync(List<ChatMessage> messages, ICompactionStrategy compactionStrategy, CancellationToken cancellationToken = default)
|
||||
{
|
||||
MessageGroups groups = MessageGroups.Create(messages);
|
||||
|
||||
bool compacted = await compactionStrategy.CompactAsync(groups, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (compacted)
|
||||
{
|
||||
messages.Clear();
|
||||
messages.AddRange(groups.GetIncludedMessages());
|
||||
}
|
||||
|
||||
return compacted;
|
||||
}
|
||||
|
||||
/// <summary>Asks the <see cref="ChatHistoryProvider"/> for an object of the specified type <paramref name="serviceType"/>.</summary>
|
||||
/// <param name="serviceType">The type of object being requested.</param>
|
||||
/// <param name="serviceKey">An optional key that can be used to help identify the target service.</param>
|
||||
|
||||
@@ -6,11 +6,11 @@ using System.Threading.Tasks;
|
||||
namespace Microsoft.Agents.AI.Compaction;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a strategy for compacting a <see cref="MessageGroups"/> to reduce context size.
|
||||
/// Defines a strategy for compacting a <see cref="MessageIndex"/> to reduce context size.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Compaction strategies operate on <see cref="MessageGroups"/> instances, which organize messages
|
||||
/// Compaction strategies operate on <see cref="MessageIndex"/> instances, which organize messages
|
||||
/// into atomic groups that respect the tool-call/result pairing constraint. Strategies mutate the collection
|
||||
/// in place by marking groups as excluded, removing groups, or replacing message content (e.g., with summaries).
|
||||
/// </para>
|
||||
@@ -23,7 +23,7 @@ namespace Microsoft.Agents.AI.Compaction;
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Multiple strategies can be composed by applying them sequentially to the same <see cref="MessageGroups"/>.
|
||||
/// Multiple strategies can be composed by applying them sequentially to the same <see cref="MessageIndex"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public interface ICompactionStrategy
|
||||
@@ -34,5 +34,5 @@ public interface ICompactionStrategy
|
||||
/// <param name="groups">The message group collection to compact. The strategy mutates this collection in place.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
|
||||
/// <returns>A task representing the asynchronous operation. The task result is <see langword="true"/> if compaction occurred, <see langword="false"/> otherwise.</returns>
|
||||
Task<bool> CompactAsync(MessageGroups groups, CancellationToken cancellationToken = default);
|
||||
Task<bool> CompactAsync(MessageIndex groups, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Compaction;
|
||||
@@ -21,8 +22,8 @@ namespace Microsoft.Agents.AI.Compaction;
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Each group tracks its <see cref="MessageCount"/>, <see cref="ByteCount"/>, and <see cref="TokenCount"/>
|
||||
/// so that <see cref="MessageGroups"/> can efficiently aggregate totals across all or only included groups.
|
||||
/// These values are computed by <see cref="MessageGroups.Create"/> and passed into the constructor.
|
||||
/// so that <see cref="MessageIndex"/> can efficiently aggregate totals across all or only included groups.
|
||||
/// These values are computed by <see cref="MessageIndex.Create"/> and passed into the constructor.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class MessageGroup
|
||||
@@ -32,7 +33,7 @@ public sealed class MessageGroup
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When this key is present with a value of <see langword="true"/>, the message is classified as
|
||||
/// <see cref="MessageGroupKind.Summary"/> by <see cref="MessageGroups.Create"/>.
|
||||
/// <see cref="MessageGroupKind.Summary"/> by <see cref="MessageIndex.Create"/>.
|
||||
/// </remarks>
|
||||
public static readonly string SummaryPropertyKey = "_is_summary";
|
||||
|
||||
@@ -47,6 +48,7 @@ public sealed class MessageGroup
|
||||
/// The zero-based user turn this group belongs to, or <see langword="null"/> for groups that precede
|
||||
/// the first user message (e.g., system messages).
|
||||
/// </param>
|
||||
[JsonConstructor]
|
||||
public MessageGroup(MessageGroupKind kind, IReadOnlyList<ChatMessage> messages, int byteCount, int tokenCount, int? turnIndex = null)
|
||||
{
|
||||
this.Kind = kind;
|
||||
@@ -97,7 +99,7 @@ public sealed class MessageGroup
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Excluded groups are preserved in the collection for diagnostics or storage purposes
|
||||
/// but are not included when calling <see cref="MessageGroups.GetIncludedMessages"/>.
|
||||
/// but are not included when calling <see cref="MessageIndex.GetIncludedMessages"/>.
|
||||
/// </remarks>
|
||||
public bool IsExcluded { get; set; }
|
||||
|
||||
|
||||
+92
-17
@@ -13,7 +13,7 @@ namespace Microsoft.Agents.AI.Compaction;
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="MessageGroups"/> provides structural grouping of messages into logical units that
|
||||
/// <see cref="MessageIndex"/> provides structural grouping of messages into logical units that
|
||||
/// respect the atomic group preservation constraint: tool call assistant messages and their corresponding
|
||||
/// tool result messages are always grouped together.
|
||||
/// </para>
|
||||
@@ -27,9 +27,16 @@ namespace Microsoft.Agents.AI.Compaction;
|
||||
/// and <see cref="MessageGroup.TokenCount"/>. The collection provides aggregate properties for both
|
||||
/// the total (all groups) and included (non-excluded groups only) counts.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Instances created via <see cref="Create"/> track internal state that enables efficient incremental
|
||||
/// updates via <see cref="Update"/>. This allows caching a <see cref="MessageIndex"/> instance and
|
||||
/// appending only new messages without reprocessing the entire history.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class MessageGroups
|
||||
public sealed class MessageIndex
|
||||
{
|
||||
private int _currentTurn;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of message groups in this collection.
|
||||
/// </summary>
|
||||
@@ -41,25 +48,43 @@ public sealed class MessageGroups
|
||||
public Tokenizer? Tokenizer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MessageGroups"/> class with the specified groups.
|
||||
/// Gets the number of raw messages that have been processed into groups.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This value is set by <see cref="Create"/> and updated by <see cref="Update"/>.
|
||||
/// It is used by <see cref="Update"/> to determine which messages are new and need processing.
|
||||
/// </remarks>
|
||||
public int ProcessedMessageCount { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MessageIndex"/> class with the specified groups.
|
||||
/// </summary>
|
||||
/// <param name="groups">The message groups.</param>
|
||||
/// <param name="tokenizer">An optional tokenizer retained for computing token counts when adding new groups.</param>
|
||||
public MessageGroups(IList<MessageGroup> groups, Tokenizer? tokenizer = null)
|
||||
public MessageIndex(IList<MessageGroup> groups, Tokenizer? tokenizer = null)
|
||||
{
|
||||
this.Groups = groups;
|
||||
this.Tokenizer = tokenizer;
|
||||
|
||||
for (int index = groups.Count - 1; index >= 0; --index)
|
||||
{
|
||||
if (this.Groups[0].TurnIndex.HasValue)
|
||||
{
|
||||
this._currentTurn = this.Groups[0].TurnIndex!.Value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="MessageGroups"/> from a flat list of <see cref="ChatMessage"/> instances.
|
||||
/// Creates a <see cref="MessageIndex"/> from a flat list of <see cref="ChatMessage"/> instances.
|
||||
/// </summary>
|
||||
/// <param name="messages">The messages to group.</param>
|
||||
/// <param name="tokenizer">
|
||||
/// An optional <see cref="Tokenizer"/> for computing token counts on each group.
|
||||
/// When <see langword="null"/>, token counts are estimated as <c>ByteCount / 4</c>.
|
||||
/// </param>
|
||||
/// <returns>A new <see cref="MessageGroups"/> with messages organized into logical groups.</returns>
|
||||
/// <returns>A new <see cref="MessageIndex"/> with messages organized into logical groups.</returns>
|
||||
/// <remarks>
|
||||
/// The grouping algorithm:
|
||||
/// <list type="bullet">
|
||||
@@ -70,11 +95,61 @@ public sealed class MessageGroups
|
||||
/// <item><description>Assistant messages without tool calls become <see cref="MessageGroupKind.AssistantText"/> groups.</description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public static MessageGroups Create(IList<ChatMessage> messages, Tokenizer? tokenizer = null)
|
||||
public static MessageIndex Create(IList<ChatMessage> messages, Tokenizer? tokenizer = null)
|
||||
{
|
||||
List<MessageGroup> groups = [];
|
||||
int index = 0;
|
||||
int currentTurn = 0;
|
||||
MessageIndex instance = new(new List<MessageGroup>(), tokenizer);
|
||||
instance.AppendFromMessages(messages, 0);
|
||||
return instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Incrementally updates the groups with new messages from the conversation.
|
||||
/// </summary>
|
||||
/// <param name="allMessages">
|
||||
/// The full list of messages for the conversation. This must be the same list (or a replacement with the same
|
||||
/// prefix) that was used to create or last update this instance.
|
||||
/// </param>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// If the message count exceeds <see cref="ProcessedMessageCount"/>, only the new (delta) messages
|
||||
/// are processed and appended as new groups. Existing groups and their compaction state (exclusions)
|
||||
/// are preserved, allowing compaction strategies to build on previous results.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// If the message count is less than <see cref="ProcessedMessageCount"/> (e.g., after storage compaction
|
||||
/// replaced messages with summaries), all groups are cleared and rebuilt from scratch.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// If the message count equals <see cref="ProcessedMessageCount"/>, no work is performed.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public void Update(IList<ChatMessage> allMessages)
|
||||
{
|
||||
if (allMessages.Count == this.ProcessedMessageCount)
|
||||
{
|
||||
return; // No new messages
|
||||
}
|
||||
|
||||
if (allMessages.Count < this.ProcessedMessageCount)
|
||||
{
|
||||
// Message list shrank (e.g., after storage compaction). Rebuild from scratch.
|
||||
this.ProcessedMessageCount = 0;
|
||||
}
|
||||
|
||||
if (this.ProcessedMessageCount == 0)
|
||||
{
|
||||
// First update on a manually constructed instance — clear any pre-existing groups
|
||||
this.Groups.Clear();
|
||||
this._currentTurn = 0;
|
||||
}
|
||||
|
||||
// Process only the delta messages
|
||||
this.AppendFromMessages(allMessages, this.ProcessedMessageCount);
|
||||
}
|
||||
|
||||
private void AppendFromMessages(IList<ChatMessage> messages, int startIndex)
|
||||
{
|
||||
int index = startIndex;
|
||||
|
||||
while (index < messages.Count)
|
||||
{
|
||||
@@ -83,13 +158,13 @@ public sealed class MessageGroups
|
||||
if (message.Role == ChatRole.System)
|
||||
{
|
||||
// System messages are not part of any turn
|
||||
groups.Add(CreateGroup(MessageGroupKind.System, [message], tokenizer, turnIndex: null));
|
||||
this.Groups.Add(CreateGroup(MessageGroupKind.System, [message], this.Tokenizer, turnIndex: null));
|
||||
index++;
|
||||
}
|
||||
else if (message.Role == ChatRole.User)
|
||||
{
|
||||
currentTurn++;
|
||||
groups.Add(CreateGroup(MessageGroupKind.User, [message], tokenizer, currentTurn));
|
||||
this._currentTurn++;
|
||||
this.Groups.Add(CreateGroup(MessageGroupKind.User, [message], this.Tokenizer, this._currentTurn));
|
||||
index++;
|
||||
}
|
||||
else if (message.Role == ChatRole.Assistant && HasToolCalls(message))
|
||||
@@ -104,21 +179,21 @@ public sealed class MessageGroups
|
||||
index++;
|
||||
}
|
||||
|
||||
groups.Add(CreateGroup(MessageGroupKind.ToolCall, groupMessages, tokenizer, currentTurn));
|
||||
this.Groups.Add(CreateGroup(MessageGroupKind.ToolCall, groupMessages, this.Tokenizer, this._currentTurn));
|
||||
}
|
||||
else if (message.Role == ChatRole.Assistant && IsSummaryMessage(message))
|
||||
{
|
||||
groups.Add(CreateGroup(MessageGroupKind.Summary, [message], tokenizer, currentTurn));
|
||||
this.Groups.Add(CreateGroup(MessageGroupKind.Summary, [message], this.Tokenizer, this._currentTurn));
|
||||
index++;
|
||||
}
|
||||
else
|
||||
{
|
||||
groups.Add(CreateGroup(MessageGroupKind.AssistantText, [message], tokenizer, currentTurn));
|
||||
this.Groups.Add(CreateGroup(MessageGroupKind.AssistantText, [message], this.Tokenizer, this._currentTurn));
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
return new MessageGroups(groups, tokenizer);
|
||||
this.ProcessedMessageCount = messages.Count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
+4
-3
@@ -9,7 +9,7 @@ namespace Microsoft.Agents.AI.Compaction;
|
||||
|
||||
/// <summary>
|
||||
/// A compaction strategy that executes a sequential pipeline of <see cref="ICompactionStrategy"/> instances
|
||||
/// against the same <see cref="MessageGroups"/>.
|
||||
/// against the same <see cref="MessageIndex"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
@@ -28,7 +28,8 @@ public sealed class PipelineCompactionStrategy : ICompactionStrategy
|
||||
/// Initializes a new instance of the <see cref="PipelineCompactionStrategy"/> class.
|
||||
/// </summary>
|
||||
/// <param name="strategies">The ordered sequence of strategies to execute. Must not be empty.</param>
|
||||
public PipelineCompactionStrategy(params IEnumerable<ICompactionStrategy> strategies)
|
||||
///// <param name="cache">An optional cache for <see cref="MessageIndex"/> instances. When <see langword="null"/>, a default <see cref="InMemoryMessageIndexCache"/> is created.</param>
|
||||
public PipelineCompactionStrategy(params IEnumerable<ICompactionStrategy> strategies/*, IMessageIndexCache? cache = null*/)
|
||||
{
|
||||
this.Strategies = [.. Throw.IfNull(strategies)];
|
||||
}
|
||||
@@ -58,7 +59,7 @@ public sealed class PipelineCompactionStrategy : ICompactionStrategy
|
||||
public int? TargetIncludedGroupCount { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<bool> CompactAsync(MessageGroups groups, CancellationToken cancellationToken = default)
|
||||
public async Task<bool> CompactAsync(MessageIndex groups, CancellationToken cancellationToken = default)
|
||||
{
|
||||
bool anyCompacted = false;
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Compaction;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
@@ -47,7 +46,6 @@ public sealed class InMemoryChatHistoryProvider : ChatHistoryProvider
|
||||
options?.JsonSerializerOptions);
|
||||
this.ChatReducer = options?.ChatReducer;
|
||||
this.ReducerTriggerEvent = options?.ReducerTriggerEvent ?? InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent.BeforeMessagesRetrieval;
|
||||
this.CompactionStrategy = options?.CompactionStrategy;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -63,11 +61,6 @@ public sealed class InMemoryChatHistoryProvider : ChatHistoryProvider
|
||||
/// </summary>
|
||||
public InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent ReducerTriggerEvent { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the compaction strategy used to compact stored messages. If <see langword="null"/>, no compaction is applied.
|
||||
/// </summary>
|
||||
public ICompactionStrategy? CompactionStrategy { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the chat messages stored for the specified session.
|
||||
/// </summary>
|
||||
@@ -84,20 +77,21 @@ public sealed class InMemoryChatHistoryProvider : ChatHistoryProvider
|
||||
/// <exception cref="ArgumentNullException"><paramref name="messages"/> is <see langword="null"/>.</exception>
|
||||
public void SetMessages(AgentSession? session, List<ChatMessage> messages)
|
||||
{
|
||||
_ = Throw.IfNull(messages);
|
||||
Throw.IfNull(messages);
|
||||
|
||||
var state = this._sessionState.GetOrInitializeState(session);
|
||||
State state = this._sessionState.GetOrInitializeState(session);
|
||||
state.Messages = messages;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask<IEnumerable<ChatMessage>> ProvideChatHistoryAsync(InvokingContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var state = this._sessionState.GetOrInitializeState(context.Session);
|
||||
State state = this._sessionState.GetOrInitializeState(context.Session);
|
||||
|
||||
if (this.ReducerTriggerEvent is InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent.BeforeMessagesRetrieval && this.ChatReducer is not null)
|
||||
{
|
||||
state.Messages = (await this.ChatReducer.ReduceAsync(state.Messages, cancellationToken).ConfigureAwait(false)).ToList();
|
||||
// Apply pre-invocation compaction strategy if configured
|
||||
await this.CompactMessagesAsync(state, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return state.Messages;
|
||||
@@ -106,46 +100,29 @@ public sealed class InMemoryChatHistoryProvider : ChatHistoryProvider
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask StoreChatHistoryAsync(InvokedContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var state = this._sessionState.GetOrInitializeState(context.Session);
|
||||
State state = this._sessionState.GetOrInitializeState(context.Session);
|
||||
|
||||
// Add request and response messages to the provider
|
||||
var allNewMessages = context.RequestMessages.Concat(context.ResponseMessages ?? []);
|
||||
state.Messages.AddRange(allNewMessages);
|
||||
|
||||
if (this.ReducerTriggerEvent is InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent.AfterMessageAdded && this.ChatReducer is not null)
|
||||
if (this.ReducerTriggerEvent is InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent.AfterMessageAdded)
|
||||
{
|
||||
state.Messages = (await this.ChatReducer.ReduceAsync(state.Messages, cancellationToken).ConfigureAwait(false)).ToList();
|
||||
}
|
||||
|
||||
// Apply compaction strategy if configured (pre-write compaction)
|
||||
if (this.CompactionStrategy is not null)
|
||||
{
|
||||
await CompactMessagesAsync(state.Messages, this.CompactionStrategy, cancellationToken).ConfigureAwait(false);
|
||||
// Apply pre-write compaction strategy if configured
|
||||
await this.CompactMessagesAsync(state, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compacts the stored messages for the specified session using the given or configured compaction strategy.
|
||||
/// </summary>
|
||||
/// <param name="session">The agent session whose stored messages should be compacted.</param>
|
||||
/// <param name="compactionStrategy">
|
||||
/// An optional compaction strategy to use. If <see langword="null"/>, the provider's configured
|
||||
/// <see cref="CompactionStrategy"/> is used. If neither is available, an <see cref="InvalidOperationException"/> is thrown.
|
||||
/// </param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
|
||||
/// <returns>A task representing the asynchronous operation. The task result is <see langword="true"/> if compaction occurred.</returns>
|
||||
/// <exception cref="InvalidOperationException">No compaction strategy is configured or provided.</exception>
|
||||
/// <remarks>
|
||||
/// This method enables on-demand compaction of stored history, for example as a maintenance operation.
|
||||
/// It reads the full stored history, applies the compaction strategy, and writes the compacted result back.
|
||||
/// </remarks>
|
||||
public async Task<bool> CompactStorageAsync(AgentSession? session, ICompactionStrategy? compactionStrategy = null, CancellationToken cancellationToken = default)
|
||||
private async Task CompactMessagesAsync(State state, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ICompactionStrategy strategy = compactionStrategy ?? this.CompactionStrategy
|
||||
?? throw new InvalidOperationException("No compaction strategy is configured or provided.");
|
||||
if (this.ChatReducer is not null)
|
||||
{
|
||||
// ChatReducer takes precedence, if configured
|
||||
state.Messages = [.. await this.ChatReducer.ReduceAsync(state.Messages, cancellationToken).ConfigureAwait(false)];
|
||||
return;
|
||||
}
|
||||
|
||||
var state = this._sessionState.GetOrInitializeState(session);
|
||||
return await CompactMessagesAsync(state.Messages, strategy, cancellationToken).ConfigureAwait(false);
|
||||
// %%% TODO: CONSIDER COMPACTION
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -74,21 +74,6 @@ public sealed class InMemoryChatHistoryProviderOptions
|
||||
/// </value>
|
||||
public Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? ProvideOutputMessageFilter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an optional <see cref="ICompactionStrategy"/> to apply to stored messages after new messages are added.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// When set, this strategy is applied to the full stored message list after new messages have been appended.
|
||||
/// This enables pre-write compaction to limit storage size.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The compaction strategy organizes messages into atomic groups (preserving tool-call/result pairings)
|
||||
/// before applying the strategy logic. See <see cref="ICompactionStrategy"/> for details.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public ICompactionStrategy? CompactionStrategy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Defines the events that can trigger a reducer in the <see cref="InMemoryChatHistoryProvider"/>.
|
||||
/// </summary>
|
||||
|
||||
@@ -59,10 +59,6 @@ public sealed class ChatClientAgentOptions
|
||||
/// The strategy organizes messages into atomic groups (preserving tool-call/result pairings)
|
||||
/// before applying compaction logic. See <see cref="ICompactionStrategy"/> for details.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This is separate from the compaction strategy on <see cref="InMemoryChatHistoryProviderOptions.CompactionStrategy"/>,
|
||||
/// which applies pre-write compaction before storing messages. Both can be used together.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public ICompactionStrategy? CompactionStrategy { get; set; }
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Compaction;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Compaction;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// A delegating <see cref="IChatClient"/> that applies an <see cref="ICompactionStrategy"/> to the message list
|
||||
/// before each call to the inner chat client.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This client is used for in-run compaction during the tool loop. It is inserted into the
|
||||
/// <see cref="IChatClient"/> pipeline before the <see cref="FunctionInvokingChatClient"/> so that
|
||||
/// compaction is applied before every LLM call, including those triggered by tool call iterations.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The compaction strategy organizes messages into atomic groups (preserving tool-call/result pairings)
|
||||
/// before applying compaction logic. Only included messages are forwarded to the inner client.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal sealed class CompactingChatClient : DelegatingChatClient
|
||||
{
|
||||
private readonly ICompactionStrategy _compactionStrategy;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CompactingChatClient"/> class.
|
||||
/// </summary>
|
||||
/// <param name="innerClient">The inner chat client to delegate to.</param>
|
||||
/// <param name="compactionStrategy">The compaction strategy to apply before each call.</param>
|
||||
public CompactingChatClient(IChatClient innerClient, ICompactionStrategy compactionStrategy)
|
||||
: base(innerClient)
|
||||
{
|
||||
this._compactionStrategy = Throw.IfNull(compactionStrategy);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<ChatResponse> GetResponseAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
ChatOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
List<ChatMessage> compactedMessages = await this.ApplyCompactionAsync(messages, cancellationToken).ConfigureAwait(false);
|
||||
return await base.GetResponseAsync(compactedMessages, options, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
ChatOptions? options = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
List<ChatMessage> compactedMessages = await this.ApplyCompactionAsync(messages, cancellationToken).ConfigureAwait(false);
|
||||
await foreach (var update in base.GetStreamingResponseAsync(compactedMessages, options, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
yield return update;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<List<ChatMessage>> ApplyCompactionAsync(IEnumerable<ChatMessage> messages, CancellationToken cancellationToken)
|
||||
{
|
||||
List<ChatMessage> messageList = messages as List<ChatMessage> ?? [.. messages];
|
||||
MessageGroups groups = MessageGroups.Create(messageList);
|
||||
|
||||
bool compacted = await this._compactionStrategy.CompactAsync(groups, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return compacted ? [.. groups.GetIncludedMessages()] : messageList;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI.Compaction;
|
||||
|
||||
/// <summary>
|
||||
/// A delegating <see cref="IChatClient"/> that applies an <see cref="ICompactionStrategy"/> to the message list
|
||||
/// before each call to the inner chat client.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This client is used for in-run compaction during the tool loop. It is inserted into the
|
||||
/// <see cref="IChatClient"/> pipeline before the `FunctionInvokingChatClient` so that
|
||||
/// compaction is applied before every LLM call, including those triggered by tool call iterations.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The compaction strategy organizes messages into atomic groups (preserving tool-call/result pairings)
|
||||
/// before applying compaction logic. Only included messages are forwarded to the inner client.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal sealed class CompactingChatClient : DelegatingChatClient
|
||||
{
|
||||
private readonly ICompactionStrategy _compactionStrategy;
|
||||
private readonly ProviderSessionState<State> _sessionState;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CompactingChatClient"/> class.
|
||||
/// </summary>
|
||||
/// <param name="innerClient">The inner chat client to delegate to.</param>
|
||||
/// <param name="compactionStrategy">The compaction strategy to apply before each call.</param>
|
||||
public CompactingChatClient(IChatClient innerClient, ICompactionStrategy compactionStrategy)
|
||||
: base(innerClient)
|
||||
{
|
||||
this._compactionStrategy = Throw.IfNull(compactionStrategy);
|
||||
this._sessionState = new ProviderSessionState<State>(
|
||||
_ => new State(),
|
||||
Convert.ToBase64String(BitConverter.GetBytes(compactionStrategy.GetHashCode())),
|
||||
AgentJsonUtilities.DefaultOptions);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<ChatResponse> GetResponseAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
ChatOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
IEnumerable<ChatMessage> compactedMessages = await this.ApplyCompactionAsync(messages, cancellationToken).ConfigureAwait(false);
|
||||
return await base.GetResponseAsync(compactedMessages, options, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
ChatOptions? options = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
IEnumerable<ChatMessage> compactedMessages = await this.ApplyCompactionAsync(messages, cancellationToken).ConfigureAwait(false);
|
||||
await foreach (var update in base.GetStreamingResponseAsync(compactedMessages, options, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
yield return update;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? GetService(Type serviceType, object? serviceKey = null)
|
||||
{
|
||||
Throw.IfNull(serviceType);
|
||||
|
||||
return
|
||||
serviceKey is null && serviceType.IsInstanceOfType(typeof(ICompactionStrategy)) ?
|
||||
this._compactionStrategy :
|
||||
base.GetService(serviceType, serviceKey);
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<ChatMessage>> ApplyCompactionAsync(
|
||||
IEnumerable<ChatMessage> messages, CancellationToken cancellationToken)
|
||||
{
|
||||
List<ChatMessage> messageList = messages as List<ChatMessage> ?? [.. messages]; // %%% TODO - LIST COPY
|
||||
|
||||
AgentRunContext? currentAgentContext = AIAgent.CurrentRunContext;
|
||||
if (currentAgentContext is null ||
|
||||
currentAgentContext.Session is null)
|
||||
{
|
||||
// No session available — no reason to compact
|
||||
return messages;
|
||||
}
|
||||
|
||||
State state = this._sessionState.GetOrInitializeState(currentAgentContext.Session);
|
||||
|
||||
MessageIndex messageIndex;
|
||||
if (state.MessageIndex.Count > 0)
|
||||
{
|
||||
// Update existing index
|
||||
messageIndex = new(state.MessageIndex);
|
||||
messageIndex.Update(messageList);
|
||||
}
|
||||
else
|
||||
{
|
||||
// First pass — initialize message index state
|
||||
messageIndex = MessageIndex.Create(messageList);
|
||||
}
|
||||
|
||||
// Apply compaction
|
||||
bool wasCompacted = await this._compactionStrategy.CompactAsync(messageIndex, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (wasCompacted)
|
||||
{
|
||||
state.MessageIndex = [.. messageIndex.Groups]; // %%% TODO - LIST COPY
|
||||
}
|
||||
|
||||
return wasCompacted ? messageIndex.GetIncludedMessages() : messageList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the state of a <see cref="InMemoryChatHistoryProvider"/> stored in the <see cref="AgentSession.StateBag"/>.
|
||||
/// </summary>
|
||||
public sealed class State
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the message index.
|
||||
/// </summary>
|
||||
[JsonPropertyName("messages")]
|
||||
public List<MessageGroup> MessageIndex { get; set; } = [];
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ public sealed class SummarizationCompactionStrategy : ICompactionStrategy
|
||||
public string SummarizationPrompt { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<bool> CompactAsync(MessageGroups groups, CancellationToken cancellationToken = default)
|
||||
public async Task<bool> CompactAsync(MessageIndex groups, CancellationToken cancellationToken = default)
|
||||
{
|
||||
int includedCount = groups.IncludedGroupCount;
|
||||
if (includedCount <= this.MaxGroupsBeforeSummary)
|
||||
|
||||
@@ -44,7 +44,7 @@ public sealed class TruncationCompactionStrategy : ICompactionStrategy
|
||||
public bool PreserveSystemMessages { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<bool> CompactAsync(MessageGroups groups, CancellationToken cancellationToken = default)
|
||||
public Task<bool> CompactAsync(MessageIndex groups, CancellationToken cancellationToken = default)
|
||||
{
|
||||
int includedCount = groups.IncludedGroupCount;
|
||||
if (includedCount <= this.MaxGroups)
|
||||
|
||||
+228
-227
@@ -1,268 +1,269 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Compaction;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Moq;
|
||||
// %%% SAVE - RE-ANALYZE
|
||||
//using System.Collections.Generic;
|
||||
//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;
|
||||
//namespace Microsoft.Agents.AI.Abstractions.UnitTests.Compaction;
|
||||
|
||||
/// <summary>
|
||||
/// Contains tests for the compaction integration with <see cref="InMemoryChatHistoryProvider"/>.
|
||||
/// </summary>
|
||||
public class InMemoryChatHistoryProviderCompactionTests
|
||||
{
|
||||
private static readonly AIAgent s_mockAgent = new Mock<AIAgent>().Object;
|
||||
///// <summary>
|
||||
///// Contains tests for the compaction integration with <see cref="InMemoryChatHistoryProvider"/>.
|
||||
///// </summary>
|
||||
//public class InMemoryChatHistoryProviderCompactionTests
|
||||
//{
|
||||
// private static readonly AIAgent s_mockAgent = new Mock<AIAgent>().Object;
|
||||
|
||||
private static AgentSession CreateMockSession() => new Mock<AgentSession>().Object;
|
||||
// private static AgentSession CreateMockSession() => new Mock<AgentSession>().Object;
|
||||
|
||||
[Fact]
|
||||
public void Constructor_SetsCompactionStrategy_FromOptions()
|
||||
{
|
||||
// Arrange
|
||||
Mock<ICompactionStrategy> strategy = new();
|
||||
// [Fact]
|
||||
// public void Constructor_SetsCompactionStrategy_FromOptions()
|
||||
// {
|
||||
// // Arrange
|
||||
// Mock<ICompactionStrategy> strategy = new();
|
||||
|
||||
// Act
|
||||
InMemoryChatHistoryProvider provider = new(new InMemoryChatHistoryProviderOptions
|
||||
{
|
||||
CompactionStrategy = strategy.Object,
|
||||
});
|
||||
// // Act
|
||||
// InMemoryChatHistoryProvider provider = new(new InMemoryChatHistoryProviderOptions
|
||||
// {
|
||||
// CompactionStrategy = strategy.Object,
|
||||
// });
|
||||
|
||||
// Assert
|
||||
Assert.Same(strategy.Object, provider.CompactionStrategy);
|
||||
}
|
||||
// // Assert
|
||||
// Assert.Same(strategy.Object, provider.CompactionStrategy);
|
||||
// }
|
||||
|
||||
[Fact]
|
||||
public void Constructor_CompactionStrategyIsNull_ByDefault()
|
||||
{
|
||||
// Arrange & Act
|
||||
InMemoryChatHistoryProvider provider = new();
|
||||
// [Fact]
|
||||
// public void Constructor_CompactionStrategyIsNull_ByDefault()
|
||||
// {
|
||||
// // Arrange & Act
|
||||
// InMemoryChatHistoryProvider provider = new();
|
||||
|
||||
// Assert
|
||||
Assert.Null(provider.CompactionStrategy);
|
||||
}
|
||||
// // Assert
|
||||
// Assert.Null(provider.CompactionStrategy);
|
||||
// }
|
||||
|
||||
[Fact]
|
||||
public async Task StoreChatHistoryAsync_AppliesCompaction_WhenStrategyConfiguredAsync()
|
||||
{
|
||||
// Arrange — mock strategy that excludes the first included non-system group
|
||||
Mock<ICompactionStrategy> mockStrategy = new();
|
||||
mockStrategy.Setup(s => s.CompactAsync(It.IsAny<MessageGroups>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<MessageGroups, CancellationToken>((groups, _) =>
|
||||
{
|
||||
foreach (MessageGroup group in groups.Groups)
|
||||
{
|
||||
if (!group.IsExcluded && group.Kind != MessageGroupKind.System)
|
||||
{
|
||||
group.IsExcluded = true;
|
||||
group.ExcludeReason = "Mock compaction";
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
.ReturnsAsync(true);
|
||||
// [Fact]
|
||||
// public async Task StoreChatHistoryAsync_AppliesCompaction_WhenStrategyConfiguredAsync()
|
||||
// {
|
||||
// // Arrange — mock strategy that excludes the first included non-system group
|
||||
// Mock<ICompactionStrategy> mockStrategy = new();
|
||||
// mockStrategy.Setup(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()))
|
||||
// .Callback<MessageIndex, CancellationToken>((groups, _) =>
|
||||
// {
|
||||
// foreach (MessageGroup group in groups.Groups)
|
||||
// {
|
||||
// if (!group.IsExcluded && group.Kind != MessageGroupKind.System)
|
||||
// {
|
||||
// group.IsExcluded = true;
|
||||
// group.ExcludeReason = "Mock compaction";
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// .ReturnsAsync(true);
|
||||
|
||||
InMemoryChatHistoryProvider provider = new(new InMemoryChatHistoryProviderOptions
|
||||
{
|
||||
CompactionStrategy = mockStrategy.Object,
|
||||
});
|
||||
// InMemoryChatHistoryProvider provider = new(new InMemoryChatHistoryProviderOptions
|
||||
// {
|
||||
// CompactionStrategy = mockStrategy.Object,
|
||||
// });
|
||||
|
||||
AgentSession session = CreateMockSession();
|
||||
// AgentSession session = CreateMockSession();
|
||||
|
||||
// Pre-populate with some messages
|
||||
List<ChatMessage> existingMessages =
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "First"),
|
||||
new ChatMessage(ChatRole.Assistant, "Response 1"),
|
||||
];
|
||||
provider.SetMessages(session, existingMessages);
|
||||
// // Pre-populate with some messages
|
||||
// List<ChatMessage> existingMessages =
|
||||
// [
|
||||
// new ChatMessage(ChatRole.User, "First"),
|
||||
// new ChatMessage(ChatRole.Assistant, "Response 1"),
|
||||
// ];
|
||||
// provider.SetMessages(session, existingMessages);
|
||||
|
||||
// Invoke the store flow with additional messages
|
||||
List<ChatMessage> requestMessages =
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Second"),
|
||||
];
|
||||
List<ChatMessage> responseMessages =
|
||||
[
|
||||
new ChatMessage(ChatRole.Assistant, "Response 2"),
|
||||
];
|
||||
// // Invoke the store flow with additional messages
|
||||
// List<ChatMessage> requestMessages =
|
||||
// [
|
||||
// new ChatMessage(ChatRole.User, "Second"),
|
||||
// ];
|
||||
// List<ChatMessage> responseMessages =
|
||||
// [
|
||||
// new ChatMessage(ChatRole.Assistant, "Response 2"),
|
||||
// ];
|
||||
|
||||
ChatHistoryProvider.InvokedContext context = new(s_mockAgent, session, requestMessages, responseMessages);
|
||||
// ChatHistoryProvider.InvokedContext context = new(s_mockAgent, session, requestMessages, responseMessages);
|
||||
|
||||
// Act
|
||||
await provider.InvokedAsync(context);
|
||||
// // Act
|
||||
// await provider.InvokedAsync(context);
|
||||
|
||||
// Assert - compaction should have removed one group
|
||||
List<ChatMessage> storedMessages = provider.GetMessages(session);
|
||||
Assert.Equal(3, storedMessages.Count);
|
||||
mockStrategy.Verify(s => s.CompactAsync(It.IsAny<MessageGroups>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
// // Assert - compaction should have removed one group
|
||||
// List<ChatMessage> storedMessages = provider.GetMessages(session);
|
||||
// Assert.Equal(3, storedMessages.Count);
|
||||
// mockStrategy.Verify(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
// }
|
||||
|
||||
[Fact]
|
||||
public async Task StoreChatHistoryAsync_DoesNotCompact_WhenNoStrategyAsync()
|
||||
{
|
||||
// Arrange
|
||||
InMemoryChatHistoryProvider provider = new();
|
||||
AgentSession session = CreateMockSession();
|
||||
// [Fact]
|
||||
// public async Task StoreChatHistoryAsync_DoesNotCompact_WhenNoStrategyAsync()
|
||||
// {
|
||||
// // Arrange
|
||||
// InMemoryChatHistoryProvider provider = new();
|
||||
// AgentSession session = CreateMockSession();
|
||||
|
||||
List<ChatMessage> requestMessages =
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Hello"),
|
||||
];
|
||||
List<ChatMessage> responseMessages =
|
||||
[
|
||||
new ChatMessage(ChatRole.Assistant, "Hi!"),
|
||||
];
|
||||
// List<ChatMessage> requestMessages =
|
||||
// [
|
||||
// new ChatMessage(ChatRole.User, "Hello"),
|
||||
// ];
|
||||
// List<ChatMessage> responseMessages =
|
||||
// [
|
||||
// new ChatMessage(ChatRole.Assistant, "Hi!"),
|
||||
// ];
|
||||
|
||||
ChatHistoryProvider.InvokedContext context = new(s_mockAgent, session, requestMessages, responseMessages);
|
||||
// ChatHistoryProvider.InvokedContext context = new(s_mockAgent, session, requestMessages, responseMessages);
|
||||
|
||||
// Act
|
||||
await provider.InvokedAsync(context);
|
||||
// // Act
|
||||
// await provider.InvokedAsync(context);
|
||||
|
||||
// Assert - all messages should be stored
|
||||
List<ChatMessage> storedMessages = provider.GetMessages(session);
|
||||
Assert.Equal(2, storedMessages.Count);
|
||||
}
|
||||
// // Assert - all messages should be stored
|
||||
// List<ChatMessage> storedMessages = provider.GetMessages(session);
|
||||
// Assert.Equal(2, storedMessages.Count);
|
||||
// }
|
||||
|
||||
[Fact]
|
||||
public async Task CompactStorageAsync_CompactsStoredMessagesAsync()
|
||||
{
|
||||
// Arrange — mock strategy that excludes the two oldest non-system groups
|
||||
Mock<ICompactionStrategy> mockStrategy = new();
|
||||
mockStrategy.Setup(s => s.CompactAsync(It.IsAny<MessageGroups>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<MessageGroups, CancellationToken>((groups, _) =>
|
||||
{
|
||||
int excluded = 0;
|
||||
foreach (MessageGroup group in groups.Groups)
|
||||
{
|
||||
if (!group.IsExcluded && group.Kind != MessageGroupKind.System && excluded < 2)
|
||||
{
|
||||
group.IsExcluded = true;
|
||||
excluded++;
|
||||
}
|
||||
}
|
||||
})
|
||||
.ReturnsAsync(true);
|
||||
// [Fact]
|
||||
// public async Task CompactStorageAsync_CompactsStoredMessagesAsync()
|
||||
// {
|
||||
// // Arrange — mock strategy that excludes the two oldest non-system groups
|
||||
// Mock<ICompactionStrategy> mockStrategy = new();
|
||||
// mockStrategy.Setup(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()))
|
||||
// .Callback<MessageIndex, CancellationToken>((groups, _) =>
|
||||
// {
|
||||
// int excluded = 0;
|
||||
// foreach (MessageGroup group in groups.Groups)
|
||||
// {
|
||||
// if (!group.IsExcluded && group.Kind != MessageGroupKind.System && excluded < 2)
|
||||
// {
|
||||
// group.IsExcluded = true;
|
||||
// excluded++;
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// .ReturnsAsync(true);
|
||||
|
||||
InMemoryChatHistoryProvider provider = new(new InMemoryChatHistoryProviderOptions
|
||||
{
|
||||
CompactionStrategy = mockStrategy.Object,
|
||||
});
|
||||
// InMemoryChatHistoryProvider provider = new(new InMemoryChatHistoryProviderOptions
|
||||
// {
|
||||
// CompactionStrategy = mockStrategy.Object,
|
||||
// });
|
||||
|
||||
AgentSession session = CreateMockSession();
|
||||
provider.SetMessages(session,
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "First"),
|
||||
new ChatMessage(ChatRole.Assistant, "Response 1"),
|
||||
new ChatMessage(ChatRole.User, "Second"),
|
||||
new ChatMessage(ChatRole.Assistant, "Response 2"),
|
||||
]);
|
||||
// AgentSession session = CreateMockSession();
|
||||
// provider.SetMessages(session,
|
||||
// [
|
||||
// new ChatMessage(ChatRole.User, "First"),
|
||||
// new ChatMessage(ChatRole.Assistant, "Response 1"),
|
||||
// new ChatMessage(ChatRole.User, "Second"),
|
||||
// new ChatMessage(ChatRole.Assistant, "Response 2"),
|
||||
// ]);
|
||||
|
||||
// Act
|
||||
bool result = await provider.CompactStorageAsync(session);
|
||||
// // Act
|
||||
// bool result = await provider.CompactStorageAsync(session);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
List<ChatMessage> messages = provider.GetMessages(session);
|
||||
Assert.Equal(2, messages.Count);
|
||||
mockStrategy.Verify(s => s.CompactAsync(It.IsAny<MessageGroups>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
// // Assert
|
||||
// Assert.True(result);
|
||||
// List<ChatMessage> messages = provider.GetMessages(session);
|
||||
// Assert.Equal(2, messages.Count);
|
||||
// mockStrategy.Verify(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
// }
|
||||
|
||||
[Fact]
|
||||
public async Task CompactStorageAsync_UsesProvidedStrategy_OverDefaultAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<ICompactionStrategy> defaultStrategy = new();
|
||||
Mock<ICompactionStrategy> overrideStrategy = new();
|
||||
// [Fact]
|
||||
// public async Task CompactStorageAsync_UsesProvidedStrategy_OverDefaultAsync()
|
||||
// {
|
||||
// // Arrange
|
||||
// Mock<ICompactionStrategy> defaultStrategy = new();
|
||||
// Mock<ICompactionStrategy> overrideStrategy = new();
|
||||
|
||||
overrideStrategy.Setup(s => s.CompactAsync(It.IsAny<MessageGroups>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<MessageGroups, CancellationToken>((groups, _) =>
|
||||
{
|
||||
// Exclude all but the last group
|
||||
for (int i = 0; i < groups.Groups.Count - 1; i++)
|
||||
{
|
||||
groups.Groups[i].IsExcluded = true;
|
||||
}
|
||||
})
|
||||
.ReturnsAsync(true);
|
||||
// overrideStrategy.Setup(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()))
|
||||
// .Callback<MessageIndex, CancellationToken>((groups, _) =>
|
||||
// {
|
||||
// // Exclude all but the last group
|
||||
// for (int i = 0; i < groups.Groups.Count - 1; i++)
|
||||
// {
|
||||
// groups.Groups[i].IsExcluded = true;
|
||||
// }
|
||||
// })
|
||||
// .ReturnsAsync(true);
|
||||
|
||||
InMemoryChatHistoryProvider provider = new(new InMemoryChatHistoryProviderOptions
|
||||
{
|
||||
CompactionStrategy = defaultStrategy.Object,
|
||||
});
|
||||
// InMemoryChatHistoryProvider provider = new(new InMemoryChatHistoryProviderOptions
|
||||
// {
|
||||
// CompactionStrategy = defaultStrategy.Object,
|
||||
// });
|
||||
|
||||
AgentSession session = CreateMockSession();
|
||||
provider.SetMessages(session,
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "First"),
|
||||
new ChatMessage(ChatRole.User, "Second"),
|
||||
new ChatMessage(ChatRole.User, "Third"),
|
||||
]);
|
||||
// AgentSession session = CreateMockSession();
|
||||
// provider.SetMessages(session,
|
||||
// [
|
||||
// new ChatMessage(ChatRole.User, "First"),
|
||||
// new ChatMessage(ChatRole.User, "Second"),
|
||||
// new ChatMessage(ChatRole.User, "Third"),
|
||||
// ]);
|
||||
|
||||
// Act
|
||||
bool result = await provider.CompactStorageAsync(session, overrideStrategy.Object);
|
||||
// // Act
|
||||
// bool result = await provider.CompactStorageAsync(session, overrideStrategy.Object);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
List<ChatMessage> messages = provider.GetMessages(session);
|
||||
Assert.Single(messages);
|
||||
Assert.Equal("Third", messages[0].Text);
|
||||
// // Assert
|
||||
// Assert.True(result);
|
||||
// List<ChatMessage> messages = provider.GetMessages(session);
|
||||
// Assert.Single(messages);
|
||||
// Assert.Equal("Third", messages[0].Text);
|
||||
|
||||
// Verify the override was used, not the default
|
||||
overrideStrategy.Verify(s => s.CompactAsync(It.IsAny<MessageGroups>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
defaultStrategy.Verify(s => s.CompactAsync(It.IsAny<MessageGroups>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
// // Verify the override was used, not the default
|
||||
// overrideStrategy.Verify(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
// defaultStrategy.Verify(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
// }
|
||||
|
||||
[Fact]
|
||||
public async Task CompactStorageAsync_Throws_WhenNoStrategyAvailableAsync()
|
||||
{
|
||||
// Arrange
|
||||
InMemoryChatHistoryProvider provider = new();
|
||||
AgentSession session = CreateMockSession();
|
||||
// [Fact]
|
||||
// public async Task CompactStorageAsync_Throws_WhenNoStrategyAvailableAsync()
|
||||
// {
|
||||
// // Arrange
|
||||
// InMemoryChatHistoryProvider provider = new();
|
||||
// AgentSession session = CreateMockSession();
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<System.InvalidOperationException>(
|
||||
() => provider.CompactStorageAsync(session));
|
||||
}
|
||||
// // Act & Assert
|
||||
// await Assert.ThrowsAsync<System.InvalidOperationException>(
|
||||
// () => provider.CompactStorageAsync(session));
|
||||
// }
|
||||
|
||||
[Fact]
|
||||
public async Task CompactStorageAsync_WithCustomStrategy_AppliesCustomLogicAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<ICompactionStrategy> mockStrategy = new();
|
||||
mockStrategy.Setup(s => s.CompactAsync(It.IsAny<MessageGroups>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<MessageGroups, CancellationToken>((groups, _) =>
|
||||
{
|
||||
// Exclude all user groups
|
||||
foreach (MessageGroup group in groups.Groups)
|
||||
{
|
||||
if (group.Kind == MessageGroupKind.User)
|
||||
{
|
||||
group.IsExcluded = true;
|
||||
}
|
||||
}
|
||||
})
|
||||
.ReturnsAsync(true);
|
||||
// [Fact]
|
||||
// public async Task CompactStorageAsync_WithCustomStrategy_AppliesCustomLogicAsync()
|
||||
// {
|
||||
// // Arrange
|
||||
// Mock<ICompactionStrategy> mockStrategy = new();
|
||||
// mockStrategy.Setup(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()))
|
||||
// .Callback<MessageIndex, CancellationToken>((groups, _) =>
|
||||
// {
|
||||
// // Exclude all user groups
|
||||
// foreach (MessageGroup group in groups.Groups)
|
||||
// {
|
||||
// if (group.Kind == MessageGroupKind.User)
|
||||
// {
|
||||
// group.IsExcluded = true;
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// .ReturnsAsync(true);
|
||||
|
||||
InMemoryChatHistoryProvider provider = new();
|
||||
AgentSession session = CreateMockSession();
|
||||
provider.SetMessages(session,
|
||||
[
|
||||
new ChatMessage(ChatRole.System, "System"),
|
||||
new ChatMessage(ChatRole.User, "User message"),
|
||||
new ChatMessage(ChatRole.Assistant, "Response"),
|
||||
]);
|
||||
// InMemoryChatHistoryProvider provider = new();
|
||||
// AgentSession session = CreateMockSession();
|
||||
// provider.SetMessages(session,
|
||||
// [
|
||||
// new ChatMessage(ChatRole.System, "System"),
|
||||
// new ChatMessage(ChatRole.User, "User message"),
|
||||
// new ChatMessage(ChatRole.Assistant, "Response"),
|
||||
// ]);
|
||||
|
||||
// Act
|
||||
bool result = await provider.CompactStorageAsync(session, mockStrategy.Object);
|
||||
// // Act
|
||||
// bool result = await provider.CompactStorageAsync(session, mockStrategy.Object);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
List<ChatMessage> messages = provider.GetMessages(session);
|
||||
Assert.Equal(2, messages.Count);
|
||||
Assert.Equal(ChatRole.System, messages[0].Role);
|
||||
Assert.Equal(ChatRole.Assistant, messages[1].Role);
|
||||
}
|
||||
}
|
||||
// // Assert
|
||||
// Assert.True(result);
|
||||
// List<ChatMessage> messages = provider.GetMessages(session);
|
||||
// Assert.Equal(2, messages.Count);
|
||||
// Assert.Equal(ChatRole.System, messages[0].Role);
|
||||
// Assert.Equal(ChatRole.Assistant, messages[1].Role);
|
||||
// }
|
||||
//}
|
||||
|
||||
+30
-30
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Agents.AI.Compaction;
|
||||
@@ -7,9 +7,9 @@ using Microsoft.Extensions.AI;
|
||||
namespace Microsoft.Agents.AI.Abstractions.UnitTests.Compaction;
|
||||
|
||||
/// <summary>
|
||||
/// Contains tests for the <see cref="MessageGroups"/> class.
|
||||
/// Contains tests for the <see cref="MessageIndex"/> class.
|
||||
/// </summary>
|
||||
public class MessageGroupsTests
|
||||
public class MessageIndexTests
|
||||
{
|
||||
[Fact]
|
||||
public void Create_EmptyList_ReturnsEmptyGroups()
|
||||
@@ -18,7 +18,7 @@ public class MessageGroupsTests
|
||||
List<ChatMessage> messages = [];
|
||||
|
||||
// Act
|
||||
MessageGroups groups = MessageGroups.Create(messages);
|
||||
MessageIndex groups = MessageIndex.Create(messages);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(groups.Groups);
|
||||
@@ -34,7 +34,7 @@ public class MessageGroupsTests
|
||||
];
|
||||
|
||||
// Act
|
||||
MessageGroups groups = MessageGroups.Create(messages);
|
||||
MessageIndex groups = MessageIndex.Create(messages);
|
||||
|
||||
// Assert
|
||||
Assert.Single(groups.Groups);
|
||||
@@ -52,7 +52,7 @@ public class MessageGroupsTests
|
||||
];
|
||||
|
||||
// Act
|
||||
MessageGroups groups = MessageGroups.Create(messages);
|
||||
MessageIndex groups = MessageIndex.Create(messages);
|
||||
|
||||
// Assert
|
||||
Assert.Single(groups.Groups);
|
||||
@@ -69,7 +69,7 @@ public class MessageGroupsTests
|
||||
];
|
||||
|
||||
// Act
|
||||
MessageGroups groups = MessageGroups.Create(messages);
|
||||
MessageIndex groups = MessageIndex.Create(messages);
|
||||
|
||||
// Assert
|
||||
Assert.Single(groups.Groups);
|
||||
@@ -86,7 +86,7 @@ public class MessageGroupsTests
|
||||
List<ChatMessage> messages = [assistantMessage, toolResult];
|
||||
|
||||
// Act
|
||||
MessageGroups groups = MessageGroups.Create(messages);
|
||||
MessageIndex groups = MessageIndex.Create(messages);
|
||||
|
||||
// Assert
|
||||
Assert.Single(groups.Groups);
|
||||
@@ -109,7 +109,7 @@ public class MessageGroupsTests
|
||||
List<ChatMessage> messages = [systemMsg, userMsg, assistantToolCall, toolResult, assistantText];
|
||||
|
||||
// Act
|
||||
MessageGroups groups = MessageGroups.Create(messages);
|
||||
MessageIndex groups = MessageIndex.Create(messages);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(4, groups.Groups.Count);
|
||||
@@ -134,7 +134,7 @@ public class MessageGroupsTests
|
||||
List<ChatMessage> messages = [assistantToolCall, toolResult1, toolResult2];
|
||||
|
||||
// Act
|
||||
MessageGroups groups = MessageGroups.Create(messages);
|
||||
MessageIndex groups = MessageIndex.Create(messages);
|
||||
|
||||
// Assert
|
||||
Assert.Single(groups.Groups);
|
||||
@@ -150,7 +150,7 @@ public class MessageGroupsTests
|
||||
ChatMessage msg2 = new(ChatRole.Assistant, "Response");
|
||||
ChatMessage msg3 = new(ChatRole.User, "Second");
|
||||
|
||||
MessageGroups groups = MessageGroups.Create([msg1, msg2, msg3]);
|
||||
MessageIndex groups = MessageIndex.Create([msg1, msg2, msg3]);
|
||||
groups.Groups[1].IsExcluded = true;
|
||||
|
||||
// Act
|
||||
@@ -169,7 +169,7 @@ public class MessageGroupsTests
|
||||
ChatMessage msg1 = new(ChatRole.User, "First");
|
||||
ChatMessage msg2 = new(ChatRole.Assistant, "Response");
|
||||
|
||||
MessageGroups groups = MessageGroups.Create([msg1, msg2]);
|
||||
MessageIndex groups = MessageIndex.Create([msg1, msg2]);
|
||||
groups.Groups[0].IsExcluded = true;
|
||||
|
||||
// Act
|
||||
@@ -183,7 +183,7 @@ public class MessageGroupsTests
|
||||
public void IncludedGroupCount_ReflectsExclusions()
|
||||
{
|
||||
// Arrange
|
||||
MessageGroups groups = MessageGroups.Create(
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "A"),
|
||||
new ChatMessage(ChatRole.Assistant, "B"),
|
||||
@@ -207,7 +207,7 @@ public class MessageGroupsTests
|
||||
List<ChatMessage> messages = [summaryMessage];
|
||||
|
||||
// Act
|
||||
MessageGroups groups = MessageGroups.Create(messages);
|
||||
MessageIndex groups = MessageIndex.Create(messages);
|
||||
|
||||
// Assert
|
||||
Assert.Single(groups.Groups);
|
||||
@@ -227,7 +227,7 @@ public class MessageGroupsTests
|
||||
List<ChatMessage> messages = [systemMsg, summaryMsg, userMsg];
|
||||
|
||||
// Act
|
||||
MessageGroups groups = MessageGroups.Create(messages);
|
||||
MessageIndex groups = MessageIndex.Create(messages);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(3, groups.Groups.Count);
|
||||
@@ -264,7 +264,7 @@ public class MessageGroupsTests
|
||||
public void Create_ComputesByteCount_Utf8()
|
||||
{
|
||||
// Arrange — "Hello" is 5 UTF-8 bytes
|
||||
MessageGroups groups = MessageGroups.Create([new ChatMessage(ChatRole.User, "Hello")]);
|
||||
MessageIndex groups = MessageIndex.Create([new ChatMessage(ChatRole.User, "Hello")]);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(5, groups.Groups[0].ByteCount);
|
||||
@@ -274,7 +274,7 @@ public class MessageGroupsTests
|
||||
public void Create_ComputesByteCount_MultiByteChars()
|
||||
{
|
||||
// Arrange — "café" has a multi-byte 'é' (2 bytes in UTF-8) → 5 bytes total
|
||||
MessageGroups groups = MessageGroups.Create([new ChatMessage(ChatRole.User, "café")]);
|
||||
MessageIndex groups = MessageIndex.Create([new ChatMessage(ChatRole.User, "café")]);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(5, groups.Groups[0].ByteCount);
|
||||
@@ -286,7 +286,7 @@ public class MessageGroupsTests
|
||||
// Arrange — ToolCall group: assistant (tool call, null text) + tool result "OK" (2 bytes)
|
||||
ChatMessage assistantMsg = new(ChatRole.Assistant, [new FunctionCallContent("call1", "fn")]);
|
||||
ChatMessage toolResult = new(ChatRole.Tool, "OK");
|
||||
MessageGroups groups = MessageGroups.Create([assistantMsg, toolResult]);
|
||||
MessageIndex groups = MessageIndex.Create([assistantMsg, toolResult]);
|
||||
|
||||
// Assert — single ToolCall group with 2 messages
|
||||
Assert.Single(groups.Groups);
|
||||
@@ -298,7 +298,7 @@ public class MessageGroupsTests
|
||||
public void Create_DefaultTokenCount_IsHeuristic()
|
||||
{
|
||||
// Arrange — "Hello world test data!" = 22 UTF-8 bytes → 22 / 4 = 5 estimated tokens
|
||||
MessageGroups groups = MessageGroups.Create([new ChatMessage(ChatRole.User, "Hello world test data!")]);
|
||||
MessageIndex groups = MessageIndex.Create([new ChatMessage(ChatRole.User, "Hello world test data!")]);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(22, groups.Groups[0].ByteCount);
|
||||
@@ -311,7 +311,7 @@ public class MessageGroupsTests
|
||||
// Arrange — message with no text (e.g., pure function call)
|
||||
ChatMessage msg = new(ChatRole.Assistant, [new FunctionCallContent("call1", "get_weather")]);
|
||||
ChatMessage tool = new(ChatRole.Tool, string.Empty);
|
||||
MessageGroups groups = MessageGroups.Create([msg, tool]);
|
||||
MessageIndex groups = MessageIndex.Create([msg, tool]);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, groups.Groups[0].MessageCount);
|
||||
@@ -323,7 +323,7 @@ public class MessageGroupsTests
|
||||
public void TotalAggregates_SumAllGroups()
|
||||
{
|
||||
// Arrange
|
||||
MessageGroups groups = MessageGroups.Create(
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "AAAA"), // 4 bytes
|
||||
new ChatMessage(ChatRole.Assistant, "BBBB"), // 4 bytes
|
||||
@@ -342,7 +342,7 @@ public class MessageGroupsTests
|
||||
public void IncludedAggregates_ExcludeMarkedGroups()
|
||||
{
|
||||
// Arrange
|
||||
MessageGroups groups = MessageGroups.Create(
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "AAAA"), // 4 bytes
|
||||
new ChatMessage(ChatRole.Assistant, "BBBB"), // 4 bytes
|
||||
@@ -369,7 +369,7 @@ public class MessageGroupsTests
|
||||
ChatMessage assistantMsg = new(ChatRole.Assistant, [new FunctionCallContent("call1", "fn")]);
|
||||
ChatMessage toolResult = new(ChatRole.Tool, "OK");
|
||||
|
||||
MessageGroups groups = MessageGroups.Create([assistantMsg, toolResult]);
|
||||
MessageIndex groups = MessageIndex.Create([assistantMsg, toolResult]);
|
||||
|
||||
// Assert — single group with 2 messages
|
||||
Assert.Single(groups.Groups);
|
||||
@@ -383,7 +383,7 @@ public class MessageGroupsTests
|
||||
public void Create_AssignsTurnIndices_SingleTurn()
|
||||
{
|
||||
// Arrange — System (no turn), User + Assistant = turn 1
|
||||
MessageGroups groups = MessageGroups.Create(
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.System, "You are helpful."),
|
||||
new ChatMessage(ChatRole.User, "Hello"),
|
||||
@@ -402,7 +402,7 @@ public class MessageGroupsTests
|
||||
public void Create_AssignsTurnIndices_MultiTurn()
|
||||
{
|
||||
// Arrange — 3 user turns
|
||||
MessageGroups groups = MessageGroups.Create(
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.System, "System prompt."),
|
||||
new ChatMessage(ChatRole.User, "Q1"),
|
||||
@@ -429,7 +429,7 @@ public class MessageGroupsTests
|
||||
ChatMessage assistantToolCall = new(ChatRole.Assistant, [new FunctionCallContent("call1", "get_weather")]);
|
||||
ChatMessage toolResult = new(ChatRole.Tool, "Sunny");
|
||||
|
||||
MessageGroups groups = MessageGroups.Create(
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "What's the weather?"),
|
||||
assistantToolCall,
|
||||
@@ -449,7 +449,7 @@ public class MessageGroupsTests
|
||||
public void GetTurnGroups_ReturnsGroupsForSpecificTurn()
|
||||
{
|
||||
// Arrange
|
||||
MessageGroups groups = MessageGroups.Create(
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.System, "System."),
|
||||
new ChatMessage(ChatRole.User, "Q1"),
|
||||
@@ -475,7 +475,7 @@ public class MessageGroupsTests
|
||||
public void IncludedTurnCount_ReflectsExclusions()
|
||||
{
|
||||
// Arrange — 2 turns, exclude all groups in turn 1
|
||||
MessageGroups groups = MessageGroups.Create(
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Q1"),
|
||||
new ChatMessage(ChatRole.Assistant, "A1"),
|
||||
@@ -495,7 +495,7 @@ public class MessageGroupsTests
|
||||
public void TotalTurnCount_ZeroWhenNoUserMessages()
|
||||
{
|
||||
// Arrange — only system messages
|
||||
MessageGroups groups = MessageGroups.Create(
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.System, "System."),
|
||||
]);
|
||||
@@ -509,7 +509,7 @@ public class MessageGroupsTests
|
||||
public void IncludedTurnCount_PartialExclusion_StillCountsTurn()
|
||||
{
|
||||
// Arrange — turn 1 has 2 groups, only one excluded
|
||||
MessageGroups groups = MessageGroups.Create(
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Q1"),
|
||||
new ChatMessage(ChatRole.Assistant, "A1"),
|
||||
+46
-46
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
@@ -20,17 +20,17 @@ public class PipelineCompactionStrategyTests
|
||||
// Arrange
|
||||
List<string> executionOrder = [];
|
||||
Mock<ICompactionStrategy> strategy1 = new();
|
||||
strategy1.Setup(s => s.CompactAsync(It.IsAny<MessageGroups>(), It.IsAny<CancellationToken>()))
|
||||
strategy1.Setup(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()))
|
||||
.Callback(() => executionOrder.Add("first"))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
Mock<ICompactionStrategy> strategy2 = new();
|
||||
strategy2.Setup(s => s.CompactAsync(It.IsAny<MessageGroups>(), It.IsAny<CancellationToken>()))
|
||||
strategy2.Setup(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()))
|
||||
.Callback(() => executionOrder.Add("second"))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
PipelineCompactionStrategy pipeline = new(strategy1.Object, strategy2.Object);
|
||||
MessageGroups groups = MessageGroups.Create([new ChatMessage(ChatRole.User, "Hello")]);
|
||||
PipelineCompactionStrategy pipeline = new([strategy1.Object, strategy2.Object]);
|
||||
MessageIndex groups = MessageIndex.Create([new ChatMessage(ChatRole.User, "Hello")]);
|
||||
|
||||
// Act
|
||||
await pipeline.CompactAsync(groups);
|
||||
@@ -44,11 +44,11 @@ public class PipelineCompactionStrategyTests
|
||||
{
|
||||
// Arrange
|
||||
Mock<ICompactionStrategy> strategy1 = new();
|
||||
strategy1.Setup(s => s.CompactAsync(It.IsAny<MessageGroups>(), It.IsAny<CancellationToken>()))
|
||||
strategy1.Setup(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
PipelineCompactionStrategy pipeline = new(strategy1.Object);
|
||||
MessageGroups groups = MessageGroups.Create([new ChatMessage(ChatRole.User, "Hello")]);
|
||||
PipelineCompactionStrategy pipeline = new([strategy1.Object]);
|
||||
MessageIndex groups = MessageIndex.Create([new ChatMessage(ChatRole.User, "Hello")]);
|
||||
|
||||
// Act
|
||||
bool result = await pipeline.CompactAsync(groups);
|
||||
@@ -62,15 +62,15 @@ public class PipelineCompactionStrategyTests
|
||||
{
|
||||
// Arrange
|
||||
Mock<ICompactionStrategy> strategy1 = new();
|
||||
strategy1.Setup(s => s.CompactAsync(It.IsAny<MessageGroups>(), It.IsAny<CancellationToken>()))
|
||||
strategy1.Setup(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
Mock<ICompactionStrategy> strategy2 = new();
|
||||
strategy2.Setup(s => s.CompactAsync(It.IsAny<MessageGroups>(), It.IsAny<CancellationToken>()))
|
||||
strategy2.Setup(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
PipelineCompactionStrategy pipeline = new(strategy1.Object, strategy2.Object);
|
||||
MessageGroups groups = MessageGroups.Create([new ChatMessage(ChatRole.User, "Hello")]);
|
||||
PipelineCompactionStrategy pipeline = new([strategy1.Object, strategy2.Object]);
|
||||
MessageIndex groups = MessageIndex.Create([new ChatMessage(ChatRole.User, "Hello")]);
|
||||
|
||||
// Act
|
||||
bool result = await pipeline.CompactAsync(groups);
|
||||
@@ -84,22 +84,22 @@ public class PipelineCompactionStrategyTests
|
||||
{
|
||||
// Arrange
|
||||
Mock<ICompactionStrategy> strategy1 = new();
|
||||
strategy1.Setup(s => s.CompactAsync(It.IsAny<MessageGroups>(), It.IsAny<CancellationToken>()))
|
||||
strategy1.Setup(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
Mock<ICompactionStrategy> strategy2 = new();
|
||||
strategy2.Setup(s => s.CompactAsync(It.IsAny<MessageGroups>(), It.IsAny<CancellationToken>()))
|
||||
strategy2.Setup(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
PipelineCompactionStrategy pipeline = new(strategy1.Object, strategy2.Object);
|
||||
MessageGroups groups = MessageGroups.Create([new ChatMessage(ChatRole.User, "Hello")]);
|
||||
PipelineCompactionStrategy pipeline = new([strategy1.Object, strategy2.Object]);
|
||||
MessageIndex groups = MessageIndex.Create([new ChatMessage(ChatRole.User, "Hello")]);
|
||||
|
||||
// Act
|
||||
await pipeline.CompactAsync(groups);
|
||||
|
||||
// Assert — both strategies were called
|
||||
strategy1.Verify(s => s.CompactAsync(It.IsAny<MessageGroups>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
strategy2.Verify(s => s.CompactAsync(It.IsAny<MessageGroups>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
strategy1.Verify(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
strategy2.Verify(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -107,8 +107,8 @@ public class PipelineCompactionStrategyTests
|
||||
{
|
||||
// Arrange — first strategy reduces to target
|
||||
Mock<ICompactionStrategy> strategy1 = new();
|
||||
strategy1.Setup(s => s.CompactAsync(It.IsAny<MessageGroups>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<MessageGroups, CancellationToken>((groups, _) =>
|
||||
strategy1.Setup(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<MessageIndex, CancellationToken>((groups, _) =>
|
||||
{
|
||||
// Exclude the first group to bring count down
|
||||
groups.Groups[0].IsExcluded = true;
|
||||
@@ -116,16 +116,16 @@ public class PipelineCompactionStrategyTests
|
||||
.ReturnsAsync(true);
|
||||
|
||||
Mock<ICompactionStrategy> strategy2 = new();
|
||||
strategy2.Setup(s => s.CompactAsync(It.IsAny<MessageGroups>(), It.IsAny<CancellationToken>()))
|
||||
strategy2.Setup(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
PipelineCompactionStrategy pipeline = new(strategy1.Object, strategy2.Object)
|
||||
PipelineCompactionStrategy pipeline = new([strategy1.Object, strategy2.Object])
|
||||
{
|
||||
EarlyStop = true,
|
||||
TargetIncludedGroupCount = 2,
|
||||
};
|
||||
|
||||
MessageGroups groups = MessageGroups.Create(
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "First"),
|
||||
new ChatMessage(ChatRole.Assistant, "Response"),
|
||||
@@ -137,8 +137,8 @@ public class PipelineCompactionStrategyTests
|
||||
|
||||
// Assert — strategy2 should not have been called
|
||||
Assert.True(result);
|
||||
strategy1.Verify(s => s.CompactAsync(It.IsAny<MessageGroups>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
strategy2.Verify(s => s.CompactAsync(It.IsAny<MessageGroups>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
strategy1.Verify(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
strategy2.Verify(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -146,20 +146,20 @@ public class PipelineCompactionStrategyTests
|
||||
{
|
||||
// Arrange — first strategy does NOT bring count to target
|
||||
Mock<ICompactionStrategy> strategy1 = new();
|
||||
strategy1.Setup(s => s.CompactAsync(It.IsAny<MessageGroups>(), It.IsAny<CancellationToken>()))
|
||||
strategy1.Setup(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
Mock<ICompactionStrategy> strategy2 = new();
|
||||
strategy2.Setup(s => s.CompactAsync(It.IsAny<MessageGroups>(), It.IsAny<CancellationToken>()))
|
||||
strategy2.Setup(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
PipelineCompactionStrategy pipeline = new(strategy1.Object, strategy2.Object)
|
||||
PipelineCompactionStrategy pipeline = new([strategy1.Object, strategy2.Object])
|
||||
{
|
||||
EarlyStop = true,
|
||||
TargetIncludedGroupCount = 1,
|
||||
};
|
||||
|
||||
MessageGroups groups = MessageGroups.Create(
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "First"),
|
||||
new ChatMessage(ChatRole.User, "Second"),
|
||||
@@ -170,8 +170,8 @@ public class PipelineCompactionStrategyTests
|
||||
await pipeline.CompactAsync(groups);
|
||||
|
||||
// Assert — both strategies were called since target was never reached
|
||||
strategy1.Verify(s => s.CompactAsync(It.IsAny<MessageGroups>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
strategy2.Verify(s => s.CompactAsync(It.IsAny<MessageGroups>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
strategy1.Verify(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
strategy2.Verify(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -179,27 +179,27 @@ public class PipelineCompactionStrategyTests
|
||||
{
|
||||
// Arrange
|
||||
Mock<ICompactionStrategy> strategy1 = new();
|
||||
strategy1.Setup(s => s.CompactAsync(It.IsAny<MessageGroups>(), It.IsAny<CancellationToken>()))
|
||||
strategy1.Setup(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
Mock<ICompactionStrategy> strategy2 = new();
|
||||
strategy2.Setup(s => s.CompactAsync(It.IsAny<MessageGroups>(), It.IsAny<CancellationToken>()))
|
||||
strategy2.Setup(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
PipelineCompactionStrategy pipeline = new(strategy1.Object, strategy2.Object)
|
||||
PipelineCompactionStrategy pipeline = new([strategy1.Object, strategy2.Object])
|
||||
{
|
||||
EarlyStop = true,
|
||||
// TargetIncludedGroupCount is null
|
||||
};
|
||||
|
||||
MessageGroups groups = MessageGroups.Create([new ChatMessage(ChatRole.User, "Hello")]);
|
||||
MessageIndex groups = MessageIndex.Create([new ChatMessage(ChatRole.User, "Hello")]);
|
||||
|
||||
// Act
|
||||
await pipeline.CompactAsync(groups);
|
||||
|
||||
// Assert — both strategies called because no target to check against
|
||||
strategy1.Verify(s => s.CompactAsync(It.IsAny<MessageGroups>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
strategy2.Verify(s => s.CompactAsync(It.IsAny<MessageGroups>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
strategy1.Verify(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
strategy2.Verify(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -207,8 +207,8 @@ public class PipelineCompactionStrategyTests
|
||||
{
|
||||
// Arrange — pipeline: first exclude oldest 2 non-system groups, then exclude 2 more
|
||||
Mock<ICompactionStrategy> phase1 = new();
|
||||
phase1.Setup(s => s.CompactAsync(It.IsAny<MessageGroups>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<MessageGroups, CancellationToken>((groups, _) =>
|
||||
phase1.Setup(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<MessageIndex, CancellationToken>((groups, _) =>
|
||||
{
|
||||
int excluded = 0;
|
||||
foreach (MessageGroup group in groups.Groups)
|
||||
@@ -223,8 +223,8 @@ public class PipelineCompactionStrategyTests
|
||||
.ReturnsAsync(true);
|
||||
|
||||
Mock<ICompactionStrategy> phase2 = new();
|
||||
phase2.Setup(s => s.CompactAsync(It.IsAny<MessageGroups>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<MessageGroups, CancellationToken>((groups, _) =>
|
||||
phase2.Setup(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<MessageIndex, CancellationToken>((groups, _) =>
|
||||
{
|
||||
int excluded = 0;
|
||||
foreach (MessageGroup group in groups.Groups)
|
||||
@@ -238,9 +238,9 @@ public class PipelineCompactionStrategyTests
|
||||
})
|
||||
.ReturnsAsync(true);
|
||||
|
||||
PipelineCompactionStrategy pipeline = new(phase1.Object, phase2.Object);
|
||||
PipelineCompactionStrategy pipeline = new([phase1.Object, phase2.Object]);
|
||||
|
||||
MessageGroups groups = MessageGroups.Create(
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.System, "You are helpful."),
|
||||
new ChatMessage(ChatRole.User, "Q1"),
|
||||
@@ -262,8 +262,8 @@ public class PipelineCompactionStrategyTests
|
||||
Assert.Equal("You are helpful.", included[0].Text);
|
||||
Assert.Equal("Q3", included[1].Text);
|
||||
|
||||
phase1.Verify(s => s.CompactAsync(It.IsAny<MessageGroups>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
phase2.Verify(s => s.CompactAsync(It.IsAny<MessageGroups>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
phase1.Verify(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
phase2.Verify(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -271,7 +271,7 @@ public class PipelineCompactionStrategyTests
|
||||
{
|
||||
// Arrange
|
||||
PipelineCompactionStrategy pipeline = new(new List<ICompactionStrategy>());
|
||||
MessageGroups groups = MessageGroups.Create([new ChatMessage(ChatRole.User, "Hello")]);
|
||||
MessageIndex groups = MessageIndex.Create([new ChatMessage(ChatRole.User, "Hello")]);
|
||||
|
||||
// Act
|
||||
bool result = await pipeline.CompactAsync(groups);
|
||||
|
||||
+9
-9
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Compaction;
|
||||
@@ -16,7 +16,7 @@ public class TruncationCompactionStrategyTests
|
||||
{
|
||||
// Arrange
|
||||
TruncationCompactionStrategy strategy = new(maxGroups: 5);
|
||||
MessageGroups groups = MessageGroups.Create(
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Hello"),
|
||||
new ChatMessage(ChatRole.Assistant, "Hi!"),
|
||||
@@ -35,7 +35,7 @@ public class TruncationCompactionStrategyTests
|
||||
{
|
||||
// Arrange
|
||||
TruncationCompactionStrategy strategy = new(maxGroups: 2);
|
||||
MessageGroups groups = MessageGroups.Create(
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Hello"),
|
||||
new ChatMessage(ChatRole.Assistant, "Hi!"),
|
||||
@@ -58,7 +58,7 @@ public class TruncationCompactionStrategyTests
|
||||
ChatMessage msg3 = new(ChatRole.User, "Second");
|
||||
ChatMessage msg4 = new(ChatRole.Assistant, "Response 2");
|
||||
|
||||
MessageGroups groups = MessageGroups.Create([msg1, msg2, msg3, msg4]);
|
||||
MessageIndex groups = MessageIndex.Create([msg1, msg2, msg3, msg4]);
|
||||
|
||||
// Act
|
||||
bool result = await strategy.CompactAsync(groups);
|
||||
@@ -82,7 +82,7 @@ public class TruncationCompactionStrategyTests
|
||||
ChatMessage msg2 = new(ChatRole.Assistant, "Response 1");
|
||||
ChatMessage msg3 = new(ChatRole.User, "Second");
|
||||
|
||||
MessageGroups groups = MessageGroups.Create([systemMsg, msg1, msg2, msg3]);
|
||||
MessageIndex groups = MessageIndex.Create([systemMsg, msg1, msg2, msg3]);
|
||||
|
||||
// Act
|
||||
bool result = await strategy.CompactAsync(groups);
|
||||
@@ -109,7 +109,7 @@ public class TruncationCompactionStrategyTests
|
||||
ChatMessage msg2 = new(ChatRole.Assistant, "Response");
|
||||
ChatMessage msg3 = new(ChatRole.User, "Second");
|
||||
|
||||
MessageGroups groups = MessageGroups.Create([systemMsg, msg1, msg2, msg3]);
|
||||
MessageIndex groups = MessageIndex.Create([systemMsg, msg1, msg2, msg3]);
|
||||
|
||||
// Act
|
||||
bool result = await strategy.CompactAsync(groups);
|
||||
@@ -133,7 +133,7 @@ public class TruncationCompactionStrategyTests
|
||||
ChatMessage toolResult = new(ChatRole.Tool, "Sunny");
|
||||
ChatMessage finalResponse = new(ChatRole.User, "Thanks!");
|
||||
|
||||
MessageGroups groups = MessageGroups.Create([assistantToolCall, toolResult, finalResponse]);
|
||||
MessageIndex groups = MessageIndex.Create([assistantToolCall, toolResult, finalResponse]);
|
||||
|
||||
// Act
|
||||
bool result = await strategy.CompactAsync(groups);
|
||||
@@ -152,7 +152,7 @@ public class TruncationCompactionStrategyTests
|
||||
{
|
||||
// Arrange
|
||||
TruncationCompactionStrategy strategy = new(maxGroups: 1);
|
||||
MessageGroups groups = MessageGroups.Create(
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Old"),
|
||||
new ChatMessage(ChatRole.User, "New"),
|
||||
@@ -171,7 +171,7 @@ public class TruncationCompactionStrategyTests
|
||||
{
|
||||
// Arrange
|
||||
TruncationCompactionStrategy strategy = new(maxGroups: 1);
|
||||
MessageGroups groups = MessageGroups.Create(
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Already excluded"),
|
||||
new ChatMessage(ChatRole.User, "Included 1"),
|
||||
|
||||
Reference in New Issue
Block a user