mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Merge branch 'main' into copilot-agent-updates
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -12,9 +13,39 @@ namespace Microsoft.Extensions.AI.Agents;
|
||||
/// <summary>
|
||||
/// Represents an in-memory store for chat messages associated with a specific thread.
|
||||
/// </summary>
|
||||
internal sealed class InMemoryChatMessageStore : IList<ChatMessage>, IChatMessageStore
|
||||
public sealed class InMemoryChatMessageStore : IList<ChatMessage>, IChatMessageStore
|
||||
{
|
||||
private readonly List<ChatMessage> _messages = new();
|
||||
private readonly IChatReducer? _chatReducer;
|
||||
private readonly ChatReducerTriggerEvent _reducerTriggerEvent;
|
||||
private List<ChatMessage> _messages = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InMemoryChatMessageStore"/> class.
|
||||
/// </summary>
|
||||
public InMemoryChatMessageStore()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InMemoryChatMessageStore"/> class.
|
||||
/// </summary>
|
||||
/// <param name="chatReducer">An optional <see cref="IChatReducer"/> instance used to process or reduce chat messages. If null, no reduction logic will be applied.</param>
|
||||
/// <param name="reducerTriggerEvent">The event that should trigger the reducer invocation.</param>
|
||||
public InMemoryChatMessageStore(IChatReducer chatReducer, ChatReducerTriggerEvent reducerTriggerEvent = ChatReducerTriggerEvent.BeforeMessagesRetrieval)
|
||||
{
|
||||
this._chatReducer = Throw.IfNull(chatReducer);
|
||||
this._reducerTriggerEvent = reducerTriggerEvent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the chat reducer used to process or reduce chat messages. If null, no reduction logic will be applied.
|
||||
/// </summary>
|
||||
public IChatReducer? ChatReducer => this._chatReducer;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the event that triggers the reducer invocation in this store.
|
||||
/// </summary>
|
||||
public ChatReducerTriggerEvent ReducerTriggerEvent => this._reducerTriggerEvent;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Count => this._messages.Count;
|
||||
@@ -30,17 +61,32 @@ internal sealed class InMemoryChatMessageStore : IList<ChatMessage>, IChatMessag
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task AddMessagesAsync(IReadOnlyCollection<ChatMessage> messages, CancellationToken cancellationToken)
|
||||
public async Task AddMessagesAsync(IReadOnlyCollection<ChatMessage> messages, CancellationToken cancellationToken)
|
||||
{
|
||||
_ = Throw.IfNull(messages);
|
||||
|
||||
if (messages.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this._messages.AddRange(messages);
|
||||
return Task.CompletedTask;
|
||||
|
||||
if (this._reducerTriggerEvent == ChatReducerTriggerEvent.AfterMessageAdded && this._chatReducer is not null)
|
||||
{
|
||||
this._messages = (await this._chatReducer.ReduceAsync(this._messages, cancellationToken).ConfigureAwait(false)).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IEnumerable<ChatMessage>> GetMessagesAsync(CancellationToken cancellationToken)
|
||||
public async Task<IEnumerable<ChatMessage>> GetMessagesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult<IEnumerable<ChatMessage>>(this._messages);
|
||||
if (this._reducerTriggerEvent == ChatReducerTriggerEvent.BeforeMessagesRetrieval && this._chatReducer is not null)
|
||||
{
|
||||
this._messages = (await this._chatReducer.ReduceAsync(this._messages, cancellationToken).ConfigureAwait(false)).ToList();
|
||||
}
|
||||
|
||||
return this._messages;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -118,4 +164,22 @@ internal sealed class InMemoryChatMessageStore : IList<ChatMessage>, IChatMessag
|
||||
{
|
||||
public IList<ChatMessage> Messages { get; set; } = new List<ChatMessage>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines the events that can trigger a reducer in the <see cref="InMemoryChatMessageStore"/>.
|
||||
/// </summary>
|
||||
public enum ChatReducerTriggerEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// Trigger the reducer when a new message is added.
|
||||
/// <see cref="AddMessagesAsync(IReadOnlyCollection{ChatMessage}, CancellationToken)"/> will only complete when reducer processing is done.
|
||||
/// </summary>
|
||||
AfterMessageAdded,
|
||||
|
||||
/// <summary>
|
||||
/// Trigger the reducer before messages are retrieved from the store.
|
||||
/// The reducer will process the messages before they are returned to the caller.
|
||||
/// </summary>
|
||||
BeforeMessagesRetrieval
|
||||
}
|
||||
}
|
||||
|
||||
+137
@@ -6,6 +6,7 @@ using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Moq;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.Abstractions.UnitTests;
|
||||
|
||||
@@ -14,6 +15,36 @@ namespace Microsoft.Extensions.AI.Agents.Abstractions.UnitTests;
|
||||
/// </summary>
|
||||
public class InMemoryChatMessageStoreTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_Throws_ForNullReducer()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => new InMemoryChatMessageStore(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultsToBeforeMessageRetrieval_ForNotProvidedTriggerEvent()
|
||||
{
|
||||
// Arrange & Act
|
||||
var reducerMock = new Mock<IChatReducer>();
|
||||
var store = new InMemoryChatMessageStore(reducerMock.Object);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(InMemoryChatMessageStore.ChatReducerTriggerEvent.BeforeMessagesRetrieval, store.ReducerTriggerEvent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Arguments_SetOnPropertiesCorrectly()
|
||||
{
|
||||
// Arrange & Act
|
||||
var reducerMock = new Mock<IChatReducer>();
|
||||
var store = new InMemoryChatMessageStore(reducerMock.Object, InMemoryChatMessageStore.ChatReducerTriggerEvent.AfterMessageAdded);
|
||||
|
||||
// Assert
|
||||
Assert.Same(reducerMock.Object, store.ChatReducer);
|
||||
Assert.Equal(InMemoryChatMessageStore.ChatReducerTriggerEvent.AfterMessageAdded, store.ReducerTriggerEvent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddMessagesAsyncAddsMessagesAndReturnsNullThreadIdAsync()
|
||||
{
|
||||
@@ -413,4 +444,110 @@ public class InMemoryChatMessageStoreTests
|
||||
Assert.Same(message1, messages[0]);
|
||||
Assert.Same(message2, messages[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddMessagesAsync_WithReducer_AfterMessageAdded_InvokesReducerAsync()
|
||||
{
|
||||
// Arrange
|
||||
var originalMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "Hello"),
|
||||
new(ChatRole.Assistant, "Hi there!")
|
||||
};
|
||||
var reducedMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "Reduced")
|
||||
};
|
||||
|
||||
var reducerMock = new Mock<IChatReducer>();
|
||||
reducerMock
|
||||
.Setup(r => r.ReduceAsync(It.Is<List<ChatMessage>>(x => x.SequenceEqual(originalMessages)), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(reducedMessages);
|
||||
|
||||
var store = new InMemoryChatMessageStore(reducerMock.Object, InMemoryChatMessageStore.ChatReducerTriggerEvent.AfterMessageAdded);
|
||||
|
||||
// Act
|
||||
await store.AddMessagesAsync(originalMessages, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Single(store);
|
||||
Assert.Equal("Reduced", store[0].Text);
|
||||
reducerMock.Verify(r => r.ReduceAsync(It.Is<List<ChatMessage>>(x => x.SequenceEqual(originalMessages)), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetMessagesAsync_WithReducer_BeforeMessagesRetrieval_InvokesReducerAsync()
|
||||
{
|
||||
// Arrange
|
||||
var originalMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "Hello"),
|
||||
new(ChatRole.Assistant, "Hi there!")
|
||||
};
|
||||
var reducedMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "Reduced")
|
||||
};
|
||||
|
||||
var reducerMock = new Mock<IChatReducer>();
|
||||
reducerMock
|
||||
.Setup(r => r.ReduceAsync(It.Is<List<ChatMessage>>(x => x.SequenceEqual(originalMessages)), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(reducedMessages);
|
||||
|
||||
var store = new InMemoryChatMessageStore(reducerMock.Object, InMemoryChatMessageStore.ChatReducerTriggerEvent.BeforeMessagesRetrieval);
|
||||
await store.AddMessagesAsync(originalMessages, CancellationToken.None);
|
||||
|
||||
// Act
|
||||
var result = (await store.GetMessagesAsync(CancellationToken.None)).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal("Reduced", result[0].Text);
|
||||
reducerMock.Verify(r => r.ReduceAsync(It.Is<List<ChatMessage>>(x => x.SequenceEqual(originalMessages)), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddMessagesAsync_WithReducer_ButWrongTrigger_DoesNotInvokeReducerAsync()
|
||||
{
|
||||
// Arrange
|
||||
var originalMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "Hello")
|
||||
};
|
||||
|
||||
var reducerMock = new Mock<IChatReducer>();
|
||||
|
||||
var store = new InMemoryChatMessageStore(reducerMock.Object, InMemoryChatMessageStore.ChatReducerTriggerEvent.BeforeMessagesRetrieval);
|
||||
|
||||
// Act
|
||||
await store.AddMessagesAsync(originalMessages, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Single(store);
|
||||
Assert.Equal("Hello", store[0].Text);
|
||||
reducerMock.Verify(r => r.ReduceAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetMessagesAsync_WithReducer_ButWrongTrigger_DoesNotInvokeReducerAsync()
|
||||
{
|
||||
// Arrange
|
||||
var originalMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "Hello")
|
||||
};
|
||||
|
||||
var reducerMock = new Mock<IChatReducer>();
|
||||
|
||||
var store = new InMemoryChatMessageStore(reducerMock.Object, InMemoryChatMessageStore.ChatReducerTriggerEvent.AfterMessageAdded);
|
||||
store.Add(originalMessages[0]);
|
||||
|
||||
// Act
|
||||
var result = (await store.GetMessagesAsync(CancellationToken.None)).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal("Hello", result[0].Text);
|
||||
reducerMock.Verify(r => r.ReduceAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user