mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Update tests
This commit is contained in:
@@ -67,11 +67,12 @@ public sealed class MessageIndex
|
||||
this.Groups = groups;
|
||||
this.Tokenizer = tokenizer;
|
||||
|
||||
// Restore turn counter from the last group that has a TurnIndex
|
||||
for (int index = groups.Count - 1; index >= 0; --index)
|
||||
{
|
||||
if (this.Groups[0].TurnIndex.HasValue)
|
||||
if (this.Groups[index].TurnIndex.HasValue)
|
||||
{
|
||||
this._currentTurn = this.Groups[0].TurnIndex!.Value;
|
||||
this._currentTurn = this.Groups[index].TurnIndex!.Value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -353,11 +354,6 @@ public sealed class MessageIndex
|
||||
|
||||
private static bool HasToolCalls(ChatMessage message)
|
||||
{
|
||||
if (message.Contents is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (AIContent content in message.Contents)
|
||||
{
|
||||
if (content is FunctionCallContent)
|
||||
|
||||
@@ -157,11 +157,6 @@ public sealed class SummarizationCompactionStrategy : CompactionStrategy
|
||||
}
|
||||
}
|
||||
|
||||
if (summarized == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Generate summary using the chat client (single LLM call for all marked groups)
|
||||
ChatResponse response = await this.ChatClient.GetResponseAsync(
|
||||
[
|
||||
@@ -180,14 +175,7 @@ public sealed class SummarizationCompactionStrategy : CompactionStrategy
|
||||
ChatMessage summaryMessage = new(ChatRole.Assistant, $"[Summary]\n{summaryText}");
|
||||
(summaryMessage.AdditionalProperties ??= [])[MessageGroup.SummaryPropertyKey] = true;
|
||||
|
||||
if (insertIndex >= 0)
|
||||
{
|
||||
index.InsertGroup(insertIndex, MessageGroupKind.Summary, [summaryMessage]);
|
||||
}
|
||||
else
|
||||
{
|
||||
index.AddGroup(MessageGroupKind.Summary, [summaryMessage]);
|
||||
}
|
||||
index.InsertGroup(insertIndex, MessageGroupKind.Summary, [summaryMessage]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,400 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Compaction;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Moq;
|
||||
|
||||
namespace Microsoft.Agents.AI.UnitTests.Compaction;
|
||||
|
||||
/// <summary>
|
||||
/// Contains tests for the <see cref="CompactingChatClient"/> class.
|
||||
/// </summary>
|
||||
public sealed class CompactingChatClientTests : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Restores the static <see cref="AIAgent.CurrentRunContext"/> after each test.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
SetCurrentRunContext(null);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstructorThrowsOnNullStrategyAsync()
|
||||
{
|
||||
Mock<IChatClient> mockInner = new();
|
||||
Assert.Throws<ArgumentNullException>(() => new CompactingChatClient(mockInner.Object, null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetResponseAsyncNoContextPassesThroughAsync()
|
||||
{
|
||||
// Arrange — no CurrentRunContext set → passthrough
|
||||
ChatResponse expectedResponse = new([new ChatMessage(ChatRole.Assistant, "Hi")]);
|
||||
Mock<IChatClient> mockInner = new();
|
||||
mockInner.Setup(c => c.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(expectedResponse);
|
||||
|
||||
TruncationCompactionStrategy strategy = new(CompactionTriggers.TokensExceed(100000));
|
||||
CompactingChatClient client = new(mockInner.Object, strategy);
|
||||
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Hello"),
|
||||
];
|
||||
|
||||
// Act
|
||||
ChatResponse response = await client.GetResponseAsync(messages);
|
||||
|
||||
// Assert
|
||||
Assert.Same(expectedResponse, response);
|
||||
mockInner.Verify(c => c.GetResponseAsync(
|
||||
messages,
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetResponseAsyncWithContextAppliesCompactionAsync()
|
||||
{
|
||||
// Arrange — set CurrentRunContext so compaction runs
|
||||
ChatResponse expectedResponse = new([new ChatMessage(ChatRole.Assistant, "Done")]);
|
||||
List<ChatMessage>? capturedMessages = null;
|
||||
Mock<IChatClient> mockInner = new();
|
||||
mockInner.Setup(c => c.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Callback<IEnumerable<ChatMessage>, ChatOptions?, CancellationToken>((msgs, _, _) =>
|
||||
capturedMessages = [.. msgs])
|
||||
.ReturnsAsync(expectedResponse);
|
||||
|
||||
// Strategy that always triggers and keeps only 1 group
|
||||
TruncationCompactionStrategy strategy = new(_ => true, minimumPreserved: 1);
|
||||
CompactingChatClient client = new(mockInner.Object, strategy);
|
||||
|
||||
TestAgentSession session = new();
|
||||
SetRunContext(session);
|
||||
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Q1"),
|
||||
new ChatMessage(ChatRole.Assistant, "A1"),
|
||||
new ChatMessage(ChatRole.User, "Q2"),
|
||||
];
|
||||
|
||||
// Act
|
||||
ChatResponse response = await client.GetResponseAsync(messages);
|
||||
|
||||
// Assert — compaction should have removed oldest groups
|
||||
Assert.Same(expectedResponse, response);
|
||||
Assert.NotNull(capturedMessages);
|
||||
Assert.True(capturedMessages!.Count < messages.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetResponseAsyncNoCompactionNeededReturnsOriginalMessagesAsync()
|
||||
{
|
||||
// Arrange — trigger never fires → no compaction
|
||||
ChatResponse expectedResponse = new([new ChatMessage(ChatRole.Assistant, "Hi")]);
|
||||
List<ChatMessage>? capturedMessages = null;
|
||||
Mock<IChatClient> mockInner = new();
|
||||
mockInner.Setup(c => c.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Callback<IEnumerable<ChatMessage>, ChatOptions?, CancellationToken>((msgs, _, _) =>
|
||||
capturedMessages = [.. msgs])
|
||||
.ReturnsAsync(expectedResponse);
|
||||
|
||||
TruncationCompactionStrategy strategy = new(CompactionTriggers.TokensExceed(100000));
|
||||
CompactingChatClient client = new(mockInner.Object, strategy);
|
||||
|
||||
TestAgentSession session = new();
|
||||
SetRunContext(session);
|
||||
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Hello"),
|
||||
];
|
||||
|
||||
// Act
|
||||
await client.GetResponseAsync(messages);
|
||||
|
||||
// Assert — original messages passed through
|
||||
Assert.NotNull(capturedMessages);
|
||||
Assert.Single(capturedMessages!);
|
||||
Assert.Equal("Hello", capturedMessages[0].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetResponseAsyncWithExistingIndexUpdatesAsync()
|
||||
{
|
||||
// Arrange — call twice to exercise the "existing index" path (state.MessageIndex.Count > 0)
|
||||
Mock<IChatClient> mockInner = new();
|
||||
mockInner.Setup(c => c.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new ChatResponse([new ChatMessage(ChatRole.Assistant, "OK")]));
|
||||
|
||||
// Strategy that always triggers, keeping 1 group
|
||||
TruncationCompactionStrategy strategy = new(_ => true, minimumPreserved: 1);
|
||||
CompactingChatClient client = new(mockInner.Object, strategy);
|
||||
|
||||
TestAgentSession session = new();
|
||||
SetRunContext(session);
|
||||
|
||||
List<ChatMessage> messages1 =
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Q1"),
|
||||
new ChatMessage(ChatRole.Assistant, "A1"),
|
||||
new ChatMessage(ChatRole.User, "Q2"),
|
||||
];
|
||||
|
||||
// First call — initializes state
|
||||
await client.GetResponseAsync(messages1);
|
||||
|
||||
List<ChatMessage> messages2 =
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Q1"),
|
||||
new ChatMessage(ChatRole.Assistant, "A1"),
|
||||
new ChatMessage(ChatRole.User, "Q2"),
|
||||
new ChatMessage(ChatRole.Assistant, "A2"),
|
||||
new ChatMessage(ChatRole.User, "Q3"),
|
||||
];
|
||||
|
||||
// Act — second call exercises the update path
|
||||
ChatResponse response = await client.GetResponseAsync(messages2);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(response);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetResponseAsyncNullSessionReturnsOriginalAsync()
|
||||
{
|
||||
// Arrange — CurrentRunContext exists but Session is null
|
||||
ChatResponse expectedResponse = new([new ChatMessage(ChatRole.Assistant, "Hi")]);
|
||||
Mock<IChatClient> mockInner = new();
|
||||
mockInner.Setup(c => c.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(expectedResponse);
|
||||
|
||||
TruncationCompactionStrategy strategy = new(CompactionTriggers.TokensExceed(100000));
|
||||
CompactingChatClient client = new(mockInner.Object, strategy);
|
||||
|
||||
// Set context with null session
|
||||
SetRunContext(null);
|
||||
|
||||
List<ChatMessage> messages = [new ChatMessage(ChatRole.User, "Hello")];
|
||||
|
||||
// Act
|
||||
ChatResponse response = await client.GetResponseAsync(messages);
|
||||
|
||||
// Assert
|
||||
Assert.Same(expectedResponse, response);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetStreamingResponseAsyncNoContextPassesThroughAsync()
|
||||
{
|
||||
// Arrange — no CurrentRunContext
|
||||
Mock<IChatClient> mockInner = new();
|
||||
ChatResponseUpdate[] updates = [new(ChatRole.Assistant, "Hi")];
|
||||
mockInner.Setup(c => c.GetStreamingResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Returns(ToAsyncEnumerableAsync(updates));
|
||||
|
||||
TruncationCompactionStrategy strategy = new(CompactionTriggers.TokensExceed(100000));
|
||||
CompactingChatClient client = new(mockInner.Object, strategy);
|
||||
|
||||
List<ChatMessage> messages = [new ChatMessage(ChatRole.User, "Hello")];
|
||||
|
||||
// Act
|
||||
List<ChatResponseUpdate> results = [];
|
||||
await foreach (ChatResponseUpdate update in client.GetStreamingResponseAsync(messages))
|
||||
{
|
||||
results.Add(update);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Single(results);
|
||||
Assert.Equal("Hi", results[0].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetStreamingResponseAsyncWithContextAppliesCompactionAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IChatClient> mockInner = new();
|
||||
ChatResponseUpdate[] updates = [new(ChatRole.Assistant, "Done")];
|
||||
mockInner.Setup(c => c.GetStreamingResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Returns(ToAsyncEnumerableAsync(updates));
|
||||
|
||||
TruncationCompactionStrategy strategy = new(_ => true, minimumPreserved: 1);
|
||||
CompactingChatClient client = new(mockInner.Object, strategy);
|
||||
|
||||
TestAgentSession session = new();
|
||||
SetRunContext(session);
|
||||
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Q1"),
|
||||
new ChatMessage(ChatRole.Assistant, "A1"),
|
||||
new ChatMessage(ChatRole.User, "Q2"),
|
||||
];
|
||||
|
||||
// Act
|
||||
List<ChatResponseUpdate> results = [];
|
||||
await foreach (ChatResponseUpdate update in client.GetStreamingResponseAsync(messages))
|
||||
{
|
||||
results.Add(update);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Single(results);
|
||||
Assert.Equal("Done", results[0].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetServiceReturnsStrategyForMatchingType()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IChatClient> mockInner = new();
|
||||
TruncationCompactionStrategy strategy = new(CompactionTriggers.TokensExceed(1000));
|
||||
CompactingChatClient client = new(mockInner.Object, strategy);
|
||||
|
||||
// Act — typeof(Type).IsInstanceOfType(typeof(CompactionStrategy)) is true
|
||||
object? result = client.GetService(typeof(Type));
|
||||
|
||||
// Assert
|
||||
Assert.Same(strategy, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetServiceDelegatesToBaseForNonMatchingType()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IChatClient> mockInner = new();
|
||||
TruncationCompactionStrategy strategy = new(CompactionTriggers.TokensExceed(1000));
|
||||
CompactingChatClient client = new(mockInner.Object, strategy);
|
||||
|
||||
// Act — typeof(string) doesn't match
|
||||
object? result = client.GetService(typeof(string));
|
||||
|
||||
// Assert — delegates to base (which returns null for unregistered types)
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetServiceThrowsOnNullType()
|
||||
{
|
||||
Mock<IChatClient> mockInner = new();
|
||||
TruncationCompactionStrategy strategy = new(CompactionTriggers.TokensExceed(1000));
|
||||
CompactingChatClient client = new(mockInner.Object, strategy);
|
||||
|
||||
Assert.Throws<ArgumentNullException>(() => client.GetService(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetServiceWithServiceKeyDelegatesToBase()
|
||||
{
|
||||
// Arrange — non-null serviceKey always delegates
|
||||
Mock<IChatClient> mockInner = new();
|
||||
TruncationCompactionStrategy strategy = new(CompactionTriggers.TokensExceed(1000));
|
||||
CompactingChatClient client = new(mockInner.Object, strategy);
|
||||
|
||||
// Act
|
||||
object? result = client.GetService(typeof(Type), serviceKey: "mykey");
|
||||
|
||||
// Assert — delegates to base because serviceKey is non-null
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetResponseAsyncMessagesNotListCreatesListCopyAsync()
|
||||
{
|
||||
// Arrange — pass IEnumerable (not List<ChatMessage>) to exercise the list copy branch
|
||||
ChatResponse expectedResponse = new([new ChatMessage(ChatRole.Assistant, "Hi")]);
|
||||
Mock<IChatClient> mockInner = new();
|
||||
mockInner.Setup(c => c.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(expectedResponse);
|
||||
|
||||
TruncationCompactionStrategy strategy = new(CompactionTriggers.TokensExceed(100000));
|
||||
CompactingChatClient client = new(mockInner.Object, strategy);
|
||||
|
||||
TestAgentSession session = new();
|
||||
SetRunContext(session);
|
||||
|
||||
// Use an IEnumerable (not a List) to trigger the copy path
|
||||
IEnumerable<ChatMessage> messages = new ChatMessage[] { new(ChatRole.User, "Hello") };
|
||||
|
||||
// Act
|
||||
ChatResponse response = await client.GetResponseAsync(messages);
|
||||
|
||||
// Assert
|
||||
Assert.Same(expectedResponse, response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets <see cref="AIAgent.CurrentRunContext"/> via reflection.
|
||||
/// </summary>
|
||||
private static void SetCurrentRunContext(AgentRunContext? context)
|
||||
{
|
||||
FieldInfo? field = typeof(AIAgent).GetField("s_currentContext", BindingFlags.NonPublic | BindingFlags.Static);
|
||||
Assert.NotNull(field);
|
||||
object? asyncLocal = field!.GetValue(null);
|
||||
Assert.NotNull(asyncLocal);
|
||||
PropertyInfo? valueProp = asyncLocal!.GetType().GetProperty("Value");
|
||||
Assert.NotNull(valueProp);
|
||||
valueProp!.SetValue(asyncLocal, context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="AgentRunContext"/> with the given session and sets it as the current context.
|
||||
/// </summary>
|
||||
private static void SetRunContext(AgentSession? session)
|
||||
{
|
||||
Mock<AIAgent> mockAgent = new() { CallBase = true };
|
||||
AgentRunContext context = new(
|
||||
mockAgent.Object,
|
||||
session,
|
||||
new List<ChatMessage> { new(ChatRole.User, "test") },
|
||||
null);
|
||||
SetCurrentRunContext(context);
|
||||
}
|
||||
|
||||
private static async IAsyncEnumerable<ChatResponseUpdate> ToAsyncEnumerableAsync(
|
||||
ChatResponseUpdate[] updates, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
foreach (ChatResponseUpdate update in updates)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
yield return update;
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestAgentSession : AgentSession;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Agents.AI.Compaction;
|
||||
using Microsoft.Extensions.AI;
|
||||
@@ -675,4 +676,174 @@ public class MessageIndexTests
|
||||
Assert.Equal(5, inserted.ByteCount);
|
||||
Assert.Equal(1, inserted.TokenCount); // 5 / 4 = 1 (integer division)
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstructorWithGroupsRestoresTurnIndex()
|
||||
{
|
||||
// Arrange — pre-existing groups with turn indices
|
||||
MessageGroup group1 = new(MessageGroupKind.User, [new ChatMessage(ChatRole.User, "Q1")], 2, 1, turnIndex: 1);
|
||||
MessageGroup group2 = new(MessageGroupKind.AssistantText, [new ChatMessage(ChatRole.Assistant, "A1")], 2, 1, turnIndex: 1);
|
||||
MessageGroup group3 = new(MessageGroupKind.User, [new ChatMessage(ChatRole.User, "Q2")], 2, 1, turnIndex: 2);
|
||||
List<MessageGroup> groups = [group1, group2, group3];
|
||||
|
||||
// Act — constructor should restore _currentTurn from the last group's TurnIndex
|
||||
MessageIndex index = new(groups);
|
||||
|
||||
// Assert — adding a new user message should get turn 3 (restored 2 + 1)
|
||||
index.Update(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Q1"),
|
||||
new ChatMessage(ChatRole.Assistant, "A1"),
|
||||
new ChatMessage(ChatRole.User, "Q2"),
|
||||
new ChatMessage(ChatRole.User, "Q3"),
|
||||
]);
|
||||
|
||||
// The new user group should have TurnIndex 3
|
||||
MessageGroup lastGroup = index.Groups[index.Groups.Count - 1];
|
||||
Assert.Equal(MessageGroupKind.User, lastGroup.Kind);
|
||||
Assert.NotNull(lastGroup.TurnIndex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstructorWithEmptyGroupsHandlesGracefully()
|
||||
{
|
||||
// Arrange & Act — constructor with empty list
|
||||
MessageIndex index = new([]);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(index.Groups);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstructorWithGroupsWithoutTurnIndexSkipsRestore()
|
||||
{
|
||||
// Arrange — groups without turn indices (system messages)
|
||||
MessageGroup systemGroup = new(MessageGroupKind.System, [new ChatMessage(ChatRole.System, "Be helpful")], 10, 3, turnIndex: null);
|
||||
List<MessageGroup> groups = [systemGroup];
|
||||
|
||||
// Act — constructor won't find a TurnIndex to restore
|
||||
MessageIndex index = new(groups);
|
||||
|
||||
// Assert
|
||||
Assert.Single(index.Groups);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeTokenCountReturnsTokenCount()
|
||||
{
|
||||
// Arrange — call the public static method directly
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Hello world"),
|
||||
new ChatMessage(ChatRole.Assistant, "Greetings"),
|
||||
];
|
||||
|
||||
// Act — use a simple tokenizer that counts words (each word = 1 token)
|
||||
SimpleWordTokenizer tokenizer = new();
|
||||
int tokenCount = MessageIndex.ComputeTokenCount(messages, tokenizer);
|
||||
|
||||
// Assert — "Hello world" = 2, "Greetings" = 1 → 3 total
|
||||
Assert.Equal(3, tokenCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeTokenCountEmptyTextReturnsZero()
|
||||
{
|
||||
// Arrange — message with no text content
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new ChatMessage(ChatRole.User, [new FunctionCallContent("c1", "fn")]),
|
||||
];
|
||||
|
||||
SimpleWordTokenizer tokenizer = new();
|
||||
int tokenCount = MessageIndex.ComputeTokenCount(messages, tokenizer);
|
||||
|
||||
// Assert — no text content → 0 tokens
|
||||
Assert.Equal(0, tokenCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateWithTokenizerUsesTokenizerForCounts()
|
||||
{
|
||||
// Arrange
|
||||
SimpleWordTokenizer tokenizer = new();
|
||||
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Hello world test"),
|
||||
];
|
||||
|
||||
// Act
|
||||
MessageIndex index = MessageIndex.Create(messages, tokenizer);
|
||||
|
||||
// Assert — tokenizer counts words: "Hello world test" = 3 tokens
|
||||
Assert.Single(index.Groups);
|
||||
Assert.Equal(3, index.Groups[0].TokenCount);
|
||||
Assert.NotNull(index.Tokenizer);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InsertGroupWithTokenizerUsesTokenizer()
|
||||
{
|
||||
// Arrange
|
||||
SimpleWordTokenizer tokenizer = new();
|
||||
MessageIndex index = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Hello"),
|
||||
], tokenizer);
|
||||
|
||||
// Act
|
||||
ChatMessage msg = new(ChatRole.Assistant, "Hello world test message");
|
||||
MessageGroup inserted = index.InsertGroup(0, MessageGroupKind.AssistantText, [msg]);
|
||||
|
||||
// Assert — tokenizer counts words: "Hello world test message" = 4 tokens
|
||||
Assert.Equal(4, inserted.TokenCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A simple tokenizer that counts whitespace-separated words as tokens.
|
||||
/// </summary>
|
||||
private sealed class SimpleWordTokenizer : Microsoft.ML.Tokenizers.Tokenizer
|
||||
{
|
||||
public override Microsoft.ML.Tokenizers.PreTokenizer? PreTokenizer => null;
|
||||
public override Microsoft.ML.Tokenizers.Normalizer? Normalizer => null;
|
||||
|
||||
protected override Microsoft.ML.Tokenizers.EncodeResults<Microsoft.ML.Tokenizers.EncodedToken> EncodeToTokens(string? text, System.ReadOnlySpan<char> textSpan, Microsoft.ML.Tokenizers.EncodeSettings settings)
|
||||
{
|
||||
// Simple word-based encoding
|
||||
string input = text ?? textSpan.ToString();
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
return new Microsoft.ML.Tokenizers.EncodeResults<Microsoft.ML.Tokenizers.EncodedToken>
|
||||
{
|
||||
Tokens = System.Array.Empty<Microsoft.ML.Tokenizers.EncodedToken>(),
|
||||
CharsConsumed = 0,
|
||||
NormalizedText = null,
|
||||
};
|
||||
}
|
||||
|
||||
string[] words = input.Split(' ');
|
||||
List<Microsoft.ML.Tokenizers.EncodedToken> tokens = [];
|
||||
int offset = 0;
|
||||
for (int i = 0; i < words.Length; i++)
|
||||
{
|
||||
tokens.Add(new Microsoft.ML.Tokenizers.EncodedToken(i, words[i], new System.Range(offset, offset + words[i].Length)));
|
||||
offset += words[i].Length + 1;
|
||||
}
|
||||
|
||||
return new Microsoft.ML.Tokenizers.EncodeResults<Microsoft.ML.Tokenizers.EncodedToken>
|
||||
{
|
||||
Tokens = tokens,
|
||||
CharsConsumed = input.Length,
|
||||
NormalizedText = null,
|
||||
};
|
||||
}
|
||||
|
||||
public override OperationStatus Decode(System.Collections.Generic.IEnumerable<int> ids, System.Span<char> destination, out int idsConsumed, out int charsWritten)
|
||||
{
|
||||
idsConsumed = 0;
|
||||
charsWritten = 0;
|
||||
return OperationStatus.Done;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
@@ -163,12 +163,12 @@ public class SlidingWindowCompactionStrategyTests
|
||||
{
|
||||
// Arrange — trigger on > 1 turn, custom target stops after removing 1 turn
|
||||
int removeCount = 0;
|
||||
CompactionTrigger targetAfterOne = _ => ++removeCount >= 1;
|
||||
bool TargetAfterOne(MessageIndex _) => ++removeCount >= 1;
|
||||
|
||||
SlidingWindowCompactionStrategy strategy = new(
|
||||
CompactionTriggers.TurnsExceed(1),
|
||||
minimumPreserved: 0,
|
||||
target: targetAfterOne);
|
||||
target: TargetAfterOne);
|
||||
|
||||
MessageIndex index = MessageIndex.Create(
|
||||
[
|
||||
|
||||
+15
-17
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -15,8 +15,6 @@ namespace Microsoft.Agents.AI.UnitTests.Compaction;
|
||||
/// </summary>
|
||||
public class SummarizationCompactionStrategyTests
|
||||
{
|
||||
private static readonly CompactionTrigger AlwaysTrigger = _ => true;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a mock <see cref="IChatClient"/> that returns the specified summary text.
|
||||
/// </summary>
|
||||
@@ -60,7 +58,7 @@ public class SummarizationCompactionStrategyTests
|
||||
// Arrange — always trigger, preserve 1 recent group
|
||||
SummarizationCompactionStrategy strategy = new(
|
||||
CreateMockChatClient("Key facts from earlier."),
|
||||
AlwaysTrigger,
|
||||
CompactionTriggers.Always,
|
||||
minimumPreserved: 1);
|
||||
|
||||
MessageIndex index = MessageIndex.Create(
|
||||
@@ -91,7 +89,7 @@ public class SummarizationCompactionStrategyTests
|
||||
// Arrange
|
||||
SummarizationCompactionStrategy strategy = new(
|
||||
CreateMockChatClient(),
|
||||
AlwaysTrigger,
|
||||
CompactionTriggers.Always,
|
||||
minimumPreserved: 1);
|
||||
|
||||
MessageIndex index = MessageIndex.Create(
|
||||
@@ -118,7 +116,7 @@ public class SummarizationCompactionStrategyTests
|
||||
// Arrange
|
||||
SummarizationCompactionStrategy strategy = new(
|
||||
CreateMockChatClient("Summary text."),
|
||||
AlwaysTrigger,
|
||||
CompactionTriggers.Always,
|
||||
minimumPreserved: 1);
|
||||
|
||||
MessageIndex index = MessageIndex.Create(
|
||||
@@ -145,7 +143,7 @@ public class SummarizationCompactionStrategyTests
|
||||
// Arrange — LLM returns whitespace
|
||||
SummarizationCompactionStrategy strategy = new(
|
||||
CreateMockChatClient(" "),
|
||||
AlwaysTrigger,
|
||||
CompactionTriggers.Always,
|
||||
minimumPreserved: 1);
|
||||
|
||||
MessageIndex index = MessageIndex.Create(
|
||||
@@ -168,7 +166,7 @@ public class SummarizationCompactionStrategyTests
|
||||
// Arrange — preserve 5 but only 2 non-system groups
|
||||
SummarizationCompactionStrategy strategy = new(
|
||||
CreateMockChatClient(),
|
||||
AlwaysTrigger,
|
||||
CompactionTriggers.Always,
|
||||
minimumPreserved: 5);
|
||||
|
||||
MessageIndex index = MessageIndex.Create(
|
||||
@@ -198,12 +196,12 @@ public class SummarizationCompactionStrategyTests
|
||||
capturedMessages = [.. msgs])
|
||||
.ReturnsAsync(new ChatResponse([new ChatMessage(ChatRole.Assistant, "Custom summary.")]));
|
||||
|
||||
const string customPrompt = "Summarize in bullet points only.";
|
||||
const string CustomPrompt = "Summarize in bullet points only.";
|
||||
SummarizationCompactionStrategy strategy = new(
|
||||
mockClient.Object,
|
||||
AlwaysTrigger,
|
||||
CompactionTriggers.Always,
|
||||
minimumPreserved: 1,
|
||||
summarizationPrompt: customPrompt);
|
||||
summarizationPrompt: CustomPrompt);
|
||||
|
||||
MessageIndex index = MessageIndex.Create(
|
||||
[
|
||||
@@ -216,7 +214,7 @@ public class SummarizationCompactionStrategyTests
|
||||
|
||||
// Assert — the custom prompt should be the first message sent to the LLM
|
||||
Assert.NotNull(capturedMessages);
|
||||
Assert.Equal(customPrompt, capturedMessages![0].Text);
|
||||
Assert.Equal(CustomPrompt, capturedMessages![0].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -225,7 +223,7 @@ public class SummarizationCompactionStrategyTests
|
||||
// Arrange
|
||||
SummarizationCompactionStrategy strategy = new(
|
||||
CreateMockChatClient(),
|
||||
AlwaysTrigger,
|
||||
CompactionTriggers.Always,
|
||||
minimumPreserved: 1);
|
||||
|
||||
MessageIndex index = MessageIndex.Create(
|
||||
@@ -248,13 +246,13 @@ public class SummarizationCompactionStrategyTests
|
||||
{
|
||||
// Arrange — 4 non-system groups, preserve 1, target met after 1 exclusion
|
||||
int exclusionCount = 0;
|
||||
CompactionTrigger targetAfterOne = _ => ++exclusionCount >= 1;
|
||||
CompactionTrigger TargetAfterOne = _ => ++exclusionCount >= 1;
|
||||
|
||||
SummarizationCompactionStrategy strategy = new(
|
||||
CreateMockChatClient("Partial summary."),
|
||||
AlwaysTrigger,
|
||||
CompactionTriggers.Always,
|
||||
minimumPreserved: 1,
|
||||
target: targetAfterOne);
|
||||
target: TargetAfterOne);
|
||||
|
||||
MessageIndex index = MessageIndex.Create(
|
||||
[
|
||||
@@ -278,7 +276,7 @@ public class SummarizationCompactionStrategyTests
|
||||
// Arrange — preserve 2
|
||||
SummarizationCompactionStrategy strategy = new(
|
||||
CreateMockChatClient("Summary."),
|
||||
AlwaysTrigger,
|
||||
CompactionTriggers.Always,
|
||||
minimumPreserved: 2);
|
||||
|
||||
MessageIndex index = MessageIndex.Create(
|
||||
|
||||
+12
-14
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
@@ -12,13 +12,11 @@ namespace Microsoft.Agents.AI.UnitTests.Compaction;
|
||||
/// </summary>
|
||||
public class TruncationCompactionStrategyTests
|
||||
{
|
||||
private static readonly CompactionTrigger s_alwaysTrigger = _ => true;
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsyncAlwaysTriggerCompactsToPreserveRecentAsync()
|
||||
{
|
||||
// Arrange — always-trigger means always compact
|
||||
TruncationCompactionStrategy strategy = new(s_alwaysTrigger, minimumPreserved: 1);
|
||||
TruncationCompactionStrategy strategy = new(CompactionTriggers.Always, minimumPreserved: 1);
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "First"),
|
||||
@@ -89,7 +87,7 @@ public class TruncationCompactionStrategyTests
|
||||
public async Task CompactAsyncPreservesSystemMessagesAsync()
|
||||
{
|
||||
// Arrange
|
||||
TruncationCompactionStrategy strategy = new(s_alwaysTrigger, minimumPreserved: 1);
|
||||
TruncationCompactionStrategy strategy = new(CompactionTriggers.Always, minimumPreserved: 1);
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.System, "You are helpful."),
|
||||
@@ -117,9 +115,9 @@ public class TruncationCompactionStrategyTests
|
||||
public async Task CompactAsyncPreservesToolCallGroupAtomicityAsync()
|
||||
{
|
||||
// Arrange
|
||||
TruncationCompactionStrategy strategy = new(s_alwaysTrigger, minimumPreserved: 1);
|
||||
TruncationCompactionStrategy strategy = new(CompactionTriggers.Always, minimumPreserved: 1);
|
||||
|
||||
ChatMessage assistantToolCall= new(ChatRole.Assistant, [new FunctionCallContent("call1", "get_weather")]);
|
||||
ChatMessage assistantToolCall = new(ChatRole.Assistant, [new FunctionCallContent("call1", "get_weather")]);
|
||||
ChatMessage toolResult = new(ChatRole.Tool, "Sunny");
|
||||
ChatMessage finalResponse = new(ChatRole.User, "Thanks!");
|
||||
|
||||
@@ -141,7 +139,7 @@ public class TruncationCompactionStrategyTests
|
||||
public async Task CompactAsyncSetsExcludeReasonAsync()
|
||||
{
|
||||
// Arrange
|
||||
TruncationCompactionStrategy strategy = new(s_alwaysTrigger, minimumPreserved: 1);
|
||||
TruncationCompactionStrategy strategy = new(CompactionTriggers.Always, minimumPreserved: 1);
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Old"),
|
||||
@@ -160,7 +158,7 @@ public class TruncationCompactionStrategyTests
|
||||
public async Task CompactAsyncSkipsAlreadyExcludedGroupsAsync()
|
||||
{
|
||||
// Arrange
|
||||
TruncationCompactionStrategy strategy = new(s_alwaysTrigger, minimumPreserved: 1);
|
||||
TruncationCompactionStrategy strategy = new(CompactionTriggers.Always, minimumPreserved: 1);
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Already excluded"),
|
||||
@@ -183,7 +181,7 @@ public class TruncationCompactionStrategyTests
|
||||
public async Task CompactAsyncMinimumPreservedKeepsMultipleAsync()
|
||||
{
|
||||
// Arrange — keep 2 most recent
|
||||
TruncationCompactionStrategy strategy = new(s_alwaysTrigger, minimumPreserved: 2);
|
||||
TruncationCompactionStrategy strategy = new(CompactionTriggers.Always, minimumPreserved: 2);
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Q1"),
|
||||
@@ -207,7 +205,7 @@ public class TruncationCompactionStrategyTests
|
||||
public async Task CompactAsyncNothingToRemoveReturnsFalseAsync()
|
||||
{
|
||||
// Arrange — preserve 5 but only 2 groups
|
||||
TruncationCompactionStrategy strategy = new(s_alwaysTrigger, minimumPreserved: 5);
|
||||
TruncationCompactionStrategy strategy = new(CompactionTriggers.Always, minimumPreserved: 5);
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Hello"),
|
||||
@@ -226,12 +224,12 @@ public class TruncationCompactionStrategyTests
|
||||
{
|
||||
// Arrange — always trigger, custom target stops after 1 exclusion
|
||||
int targetChecks = 0;
|
||||
CompactionTrigger targetAfterOne = _ => ++targetChecks >= 1;
|
||||
bool TargetAfterOne(MessageIndex _) => ++targetChecks >= 1;
|
||||
|
||||
TruncationCompactionStrategy strategy = new(
|
||||
s_alwaysTrigger,
|
||||
CompactionTriggers.Always,
|
||||
minimumPreserved: 1,
|
||||
target: targetAfterOne);
|
||||
target: TargetAfterOne);
|
||||
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" />
|
||||
<PackageReference Include="Microsoft.ML.Tokenizers" />
|
||||
<PackageReference Include="OpenTelemetry" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.InMemory" />
|
||||
<PackageReference Include="System.Linq.AsyncEnumerable" />
|
||||
|
||||
Reference in New Issue
Block a user