-
Notifications
You must be signed in to change notification settings - Fork 1.3k
.NET Compaction - Add AsChatReducer() extension to expose CompactionStrategy as IChatReducer
#4664
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+218
−0
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
9912431
Initial plan
Copilot 576eb38
Add ChatStrategyExtensions.cs with AsChatReducer() extension method a…
Copilot 865fb9e
Refactor message list creation in ReduceAsync method
crickman c6f4c1e
Remove unnecessary blank line in AsChatReducer method
crickman 995def9
Potential fix for pull request finding
crickman 0873595
Potential fix for pull request finding
crickman f5e55b4
Merge branch 'main' into copilot/add-chat-strategy-extensions
crickman 2f31f6d
Fix test
crickman 6f79a63
Merge branch 'main' into copilot/add-chat-strategy-extensions
crickman File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
59 changes: 59 additions & 0 deletions
59
dotnet/src/Microsoft.Agents.AI/Compaction/ChatStrategyExtensions.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| // Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| using System.Collections.Generic; | ||
| using System.Diagnostics.CodeAnalysis; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
| using Microsoft.Extensions.AI; | ||
| using Microsoft.Shared.DiagnosticIds; | ||
| using Microsoft.Shared.Diagnostics; | ||
|
|
||
| namespace Microsoft.Agents.AI.Compaction; | ||
|
|
||
| /// <summary> | ||
| /// Provides extension methods for <see cref="CompactionStrategy"/>. | ||
| /// </summary> | ||
| [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] | ||
| public static class ChatStrategyExtensions | ||
| { | ||
| /// <summary> | ||
| /// Returns an <see cref="IChatReducer"/> that applies this <see cref="CompactionStrategy"/> to reduce a list of messages. | ||
| /// </summary> | ||
| /// <param name="strategy">The compaction strategy to wrap as an <see cref="IChatReducer"/>.</param> | ||
| /// <returns> | ||
| /// An <see cref="IChatReducer"/> that, on each call to <see cref="IChatReducer.ReduceAsync"/>, builds a | ||
| /// <see cref="CompactionMessageIndex"/> from the supplied messages and applies the strategy's compaction logic, | ||
| /// returning the resulting included messages. | ||
| /// </returns> | ||
| /// <remarks> | ||
| /// This allows any <see cref="CompactionStrategy"/> to be used wherever an <see cref="IChatReducer"/> is expected, | ||
| /// bridging the compaction pipeline into systems bound to the <c>Microsoft.Extensions.AI</c> <see cref="IChatReducer"/> contract. | ||
| /// </remarks> | ||
| public static IChatReducer AsChatReducer(this CompactionStrategy strategy) | ||
| { | ||
| Throw.IfNull(strategy); | ||
|
|
||
| return new CompactionStrategyChatReducer(strategy); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// An <see cref="IChatReducer"/> adapter that delegates to a <see cref="CompactionStrategy"/>. | ||
| /// </summary> | ||
| private sealed class CompactionStrategyChatReducer : IChatReducer | ||
| { | ||
| private readonly CompactionStrategy _strategy; | ||
|
|
||
| public CompactionStrategyChatReducer(CompactionStrategy strategy) | ||
| { | ||
| this._strategy = strategy; | ||
| } | ||
|
|
||
| /// <inheritdoc/> | ||
| public async Task<IEnumerable<ChatMessage>> ReduceAsync(IEnumerable<ChatMessage> messages, CancellationToken cancellationToken = default) | ||
| { | ||
| CompactionMessageIndex index = CompactionMessageIndex.Create([.. messages]); | ||
| await this._strategy.CompactAsync(index, cancellationToken: cancellationToken).ConfigureAwait(false); | ||
crickman marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return index.GetIncludedMessages(); | ||
| } | ||
| } | ||
| } | ||
159 changes: 159 additions & 0 deletions
159
dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/ChatStrategyExtensionsTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,159 @@ | ||
| // Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Linq; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
| using Microsoft.Agents.AI.Compaction; | ||
| using Microsoft.Extensions.AI; | ||
|
|
||
| namespace Microsoft.Agents.AI.UnitTests.Compaction; | ||
|
|
||
| /// <summary> | ||
| /// Contains tests for the <see cref="ChatStrategyExtensions"/> class. | ||
| /// </summary> | ||
| public class ChatStrategyExtensionsTests | ||
| { | ||
| [Fact] | ||
| public void AsChatReducerNullStrategyThrows() | ||
| { | ||
| // Act & Assert | ||
| Assert.Throws<ArgumentNullException>(() => ((CompactionStrategy)null!).AsChatReducer()); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void AsChatReducerReturnsIChatReducer() | ||
| { | ||
| // Arrange | ||
| ChatReducerCompactionStrategy strategy = new(new IdentityReducer(), CompactionTriggers.Always); | ||
|
|
||
| // Act | ||
| IChatReducer reducer = strategy.AsChatReducer(); | ||
|
|
||
| // Assert | ||
| Assert.NotNull(reducer); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task ReduceAsyncReturnsAllMessagesWhenStrategyDoesNotCompactAsync() | ||
| { | ||
| // Arrange — trigger never fires, so no compaction occurs | ||
| ChatReducerCompactionStrategy strategy = new(new IdentityReducer(), CompactionTriggers.Never); | ||
| IChatReducer reducer = strategy.AsChatReducer(); | ||
|
|
||
| List<ChatMessage> messages = | ||
| [ | ||
| new(ChatRole.User, "Hello"), | ||
| new(ChatRole.Assistant, "Hi!"), | ||
| ]; | ||
|
|
||
| // Act | ||
| IEnumerable<ChatMessage> result = await reducer.ReduceAsync(messages, CancellationToken.None); | ||
|
|
||
| // Assert | ||
| Assert.Equal(messages, result); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task ReduceAsyncCompactsMessagesWhenStrategyFiresAsync() | ||
| { | ||
| // Arrange — reducer keeps only the last message | ||
| ChatReducerCompactionStrategy strategy = new( | ||
| new TakeLastReducer(1), | ||
| CompactionTriggers.Always); | ||
| IChatReducer reducer = strategy.AsChatReducer(); | ||
|
|
||
| List<ChatMessage> messages = | ||
| [ | ||
| new(ChatRole.User, "First"), | ||
| new(ChatRole.Assistant, "Response 1"), | ||
| new(ChatRole.User, "Second"), | ||
| ]; | ||
|
|
||
| // Act | ||
| IEnumerable<ChatMessage> result = await reducer.ReduceAsync(messages, CancellationToken.None); | ||
|
|
||
| // Assert | ||
| List<ChatMessage> resultList = [.. result]; | ||
| Assert.Single(resultList); | ||
| Assert.Equal("Second", resultList[0].Text); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task ReduceAsyncPassesCancellationTokenToStrategyAsync() | ||
| { | ||
| // Arrange | ||
| using CancellationTokenSource cts = new(); | ||
| CancellationToken capturedToken = default; | ||
|
|
||
| CapturingReducer capturingReducer = new(token => capturedToken = token); | ||
| ChatReducerCompactionStrategy strategy = new(capturingReducer, CompactionTriggers.Always); | ||
| IChatReducer reducer = strategy.AsChatReducer(); | ||
|
|
||
| List<ChatMessage> messages = | ||
| [ | ||
| new(ChatRole.User, "Hello"), | ||
| new(ChatRole.User, "World"), | ||
| ]; | ||
|
|
||
| // Act | ||
| await reducer.ReduceAsync(messages, cts.Token); | ||
|
|
||
| // Assert | ||
| Assert.Equal(cts.Token, capturedToken); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task ReduceAsyncEmptyMessagesReturnsEmptyAsync() | ||
| { | ||
| // Arrange | ||
| ChatReducerCompactionStrategy strategy = new(new IdentityReducer(), CompactionTriggers.Always); | ||
| IChatReducer reducer = strategy.AsChatReducer(); | ||
|
|
||
| // Act | ||
| IEnumerable<ChatMessage> result = await reducer.ReduceAsync([], CancellationToken.None); | ||
|
|
||
| // Assert | ||
| Assert.Empty(result); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// An <see cref="IChatReducer"/> that returns messages unchanged. | ||
| /// </summary> | ||
| private sealed class IdentityReducer : IChatReducer | ||
| { | ||
| public Task<IEnumerable<ChatMessage>> ReduceAsync(IEnumerable<ChatMessage> messages, CancellationToken cancellationToken = default) | ||
| => Task.FromResult(messages); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// An <see cref="IChatReducer"/> that keeps only the last <c>n</c> messages. | ||
| /// </summary> | ||
| private sealed class TakeLastReducer : IChatReducer | ||
| { | ||
| private readonly int _count; | ||
|
|
||
| public TakeLastReducer(int count) => this._count = count; | ||
|
|
||
| public Task<IEnumerable<ChatMessage>> ReduceAsync(IEnumerable<ChatMessage> messages, CancellationToken cancellationToken = default) | ||
| => Task.FromResult(messages.Reverse().Take(this._count)); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// An <see cref="IChatReducer"/> that captures the <see cref="CancellationToken"/> passed to <see cref="ReduceAsync"/>. | ||
| /// </summary> | ||
| private sealed class CapturingReducer : IChatReducer | ||
| { | ||
| private readonly Action<CancellationToken> _capture; | ||
|
|
||
| public CapturingReducer(Action<CancellationToken> capture) => this._capture = capture; | ||
|
|
||
| public Task<IEnumerable<ChatMessage>> ReduceAsync(IEnumerable<ChatMessage> messages, CancellationToken cancellationToken = default) | ||
| { | ||
| this._capture(cancellationToken); | ||
| IEnumerable<ChatMessage> reducedMessages = [messages.Reverse().First()]; | ||
| return Task.FromResult(reducedMessages); | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.