// Copyright (c) Microsoft. All rights reserved.
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI.Compaction;
///
/// A compaction strategy that summarizes older message groups using an ,
/// replacing them with a single summary message.
///
///
///
/// When the number of included message groups exceeds ,
/// this strategy extracts the oldest non-system groups (up to the threshold), sends them
/// to an for summarization, and replaces those groups with a single
/// assistant message containing the summary.
///
///
/// System message groups are always preserved and never included in summarization.
///
///
public sealed class SummarizationCompactionStrategy : ICompactionStrategy
{
private const string DefaultSummarizationPrompt =
"Summarize the following conversation concisely, preserving key facts, decisions, and context. " +
"Focus on information that would be needed to continue the conversation effectively.";
///
/// Initializes a new instance of the class.
///
/// The chat client to use for generating summaries.
/// The maximum number of included groups allowed before summarization is triggered.
/// Optional custom prompt for the summarization request. If , a default prompt is used.
public SummarizationCompactionStrategy(IChatClient chatClient, int maxGroupsBeforeSummary, string? summarizationPrompt = null)
{
this.ChatClient = Throw.IfNull(chatClient);
this.MaxGroupsBeforeSummary = maxGroupsBeforeSummary;
this.SummarizationPrompt = summarizationPrompt ?? DefaultSummarizationPrompt;
}
///
/// Gets the chat client used for generating summaries.
///
public IChatClient ChatClient { get; }
///
/// Gets the maximum number of included groups allowed before summarization is triggered.
///
public int MaxGroupsBeforeSummary { get; }
///
/// Gets the prompt used when requesting summaries from the chat client.
///
public string SummarizationPrompt { get; }
///
public async Task CompactAsync(MessageIndex groups, CancellationToken cancellationToken = default)
{
int includedCount = groups.IncludedGroupCount;
if (includedCount <= this.MaxGroupsBeforeSummary)
{
return false;
}
// Determine how many groups to summarize (keep the most recent MaxGroupsBeforeSummary groups)
int groupsToSummarize = includedCount - this.MaxGroupsBeforeSummary;
// Collect the oldest non-system included groups for summarization
StringBuilder conversationText = new();
int summarized = 0;
int insertIndex = -1;
for (int i = 0; i < groups.Groups.Count && summarized < groupsToSummarize; i++)
{
MessageGroup group = groups.Groups[i];
if (group.IsExcluded || group.Kind == MessageGroupKind.System)
{
continue;
}
if (insertIndex < 0)
{
insertIndex = i;
}
// Build text representation of the group for summarization
foreach (ChatMessage message in group.Messages)
{
string text = message.Text ?? string.Empty;
if (!string.IsNullOrEmpty(text))
{
conversationText.AppendLine($"{message.Role}: {text}");
}
}
group.IsExcluded = true;
group.ExcludeReason = "Summarized by SummarizationCompactionStrategy";
summarized++;
}
if (summarized == 0)
{
return false;
}
// Generate summary using the chat client
ChatResponse response = await this.ChatClient.GetResponseAsync(
[
new ChatMessage(ChatRole.System, this.SummarizationPrompt),
new ChatMessage(ChatRole.User, conversationText.ToString()),
],
cancellationToken: cancellationToken).ConfigureAwait(false);
string summaryText = response.Text ?? string.Empty;
// Insert a summary group at the position of the first summarized group
ChatMessage summaryMessage = new(ChatRole.Assistant, $"[Summary of earlier conversation]: {summaryText}");
(summaryMessage.AdditionalProperties ??= [])[MessageGroup.SummaryPropertyKey] = true;
if (insertIndex >= 0)
{
groups.InsertGroup(insertIndex, MessageGroupKind.Summary, [summaryMessage]);
}
else
{
groups.AddGroup(MessageGroupKind.Summary, [summaryMessage]);
}
return true;
}
}