mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET: [BREAKING] Add consistent message filtering to all providers. (#3851)
* Add consistent message filtering to all providers. * Remove old chat history filtering classes * Fix merge issues * Fix unit test * Enforce non-nullable property * Fix merging bug and make troubleshooting source info easier by adding tostring implementation
This commit is contained in:
committed by
GitHub
Unverified
parent
c99df98547
commit
de82ffd40a
+43
@@ -390,6 +390,49 @@ public sealed class AgentRequestMessageSourceAttributionTests
|
||||
|
||||
#endregion
|
||||
|
||||
#region ToString Tests
|
||||
|
||||
[Fact]
|
||||
public void ToString_WithSourceId_ReturnsTypeColonId()
|
||||
{
|
||||
// Arrange
|
||||
AgentRequestMessageSourceAttribution attribution = new(AgentRequestMessageSourceType.AIContextProvider, "MyProvider");
|
||||
|
||||
// Act
|
||||
string result = attribution.ToString();
|
||||
|
||||
// Assert
|
||||
Assert.Equal("AIContextProvider:MyProvider", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToString_WithNullSourceId_ReturnsTypeOnly()
|
||||
{
|
||||
// Arrange
|
||||
AgentRequestMessageSourceAttribution attribution = new(AgentRequestMessageSourceType.ChatHistory, null);
|
||||
|
||||
// Act
|
||||
string result = attribution.ToString();
|
||||
|
||||
// Assert
|
||||
Assert.Equal("ChatHistory", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToString_Default_ReturnsExternalOnly()
|
||||
{
|
||||
// Arrange
|
||||
AgentRequestMessageSourceAttribution attribution = default;
|
||||
|
||||
// Act
|
||||
string result = attribution.ToString();
|
||||
|
||||
// Assert
|
||||
Assert.Equal("External", result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Inequality Operator Tests
|
||||
|
||||
[Fact]
|
||||
|
||||
+40
@@ -414,6 +414,46 @@ public sealed class AgentRequestMessageSourceTypeTests
|
||||
|
||||
#endregion
|
||||
|
||||
#region ToString Tests
|
||||
|
||||
[Fact]
|
||||
public void ToString_ReturnsValue()
|
||||
{
|
||||
// Arrange
|
||||
AgentRequestMessageSourceType source = new("CustomSource");
|
||||
|
||||
// Act
|
||||
string result = source.ToString();
|
||||
|
||||
// Assert
|
||||
Assert.Equal("CustomSource", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToString_StaticExternal_ReturnsExternal()
|
||||
{
|
||||
// Arrange & Act
|
||||
string result = AgentRequestMessageSourceType.External.ToString();
|
||||
|
||||
// Assert
|
||||
Assert.Equal("External", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToString_Default_ReturnsExternal()
|
||||
{
|
||||
// Arrange
|
||||
AgentRequestMessageSourceType source = default;
|
||||
|
||||
// Act
|
||||
string result = source.ToString();
|
||||
|
||||
// Assert
|
||||
Assert.Equal("External", result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IEquatable Tests
|
||||
|
||||
[Fact]
|
||||
|
||||
-141
@@ -1,141 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Moq;
|
||||
using Moq.Protected;
|
||||
|
||||
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Contains tests for the <see cref="ChatHistoryProviderExtensions"/> class.
|
||||
/// </summary>
|
||||
public sealed class ChatHistoryProviderExtensionsTests
|
||||
{
|
||||
private static readonly AIAgent s_mockAgent = new Mock<AIAgent>().Object;
|
||||
private static readonly AgentSession s_mockSession = new Mock<AgentSession>().Object;
|
||||
|
||||
[Fact]
|
||||
public void WithMessageFilters_ReturnsChatHistoryProviderMessageFilter()
|
||||
{
|
||||
// Arrange
|
||||
Mock<ChatHistoryProvider> providerMock = new();
|
||||
|
||||
// Act
|
||||
ChatHistoryProvider result = providerMock.Object.WithMessageFilters(
|
||||
invokingMessagesFilter: msgs => msgs,
|
||||
invokedMessagesFilter: ctx => ctx);
|
||||
|
||||
// Assert
|
||||
Assert.IsType<ChatHistoryProviderMessageFilter>(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WithMessageFilters_InvokingFilter_IsAppliedAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<ChatHistoryProvider> providerMock = new();
|
||||
List<ChatMessage> innerMessages = [new(ChatRole.User, "Hello"), new(ChatRole.Assistant, "Hi")];
|
||||
ChatHistoryProvider.InvokingContext context = new(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Test")]);
|
||||
|
||||
providerMock
|
||||
.Protected()
|
||||
.Setup<ValueTask<IEnumerable<ChatMessage>>>("InvokingCoreAsync", ItExpr.IsAny<ChatHistoryProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>())
|
||||
.ReturnsAsync(innerMessages);
|
||||
|
||||
ChatHistoryProvider filtered = providerMock.Object.WithMessageFilters(
|
||||
invokingMessagesFilter: msgs => msgs.Where(m => m.Role == ChatRole.User));
|
||||
|
||||
// Act
|
||||
List<ChatMessage> result = (await filtered.InvokingAsync(context, CancellationToken.None)).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal(ChatRole.User, result[0].Role);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WithMessageFilters_InvokedFilter_IsAppliedAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<ChatHistoryProvider> providerMock = new();
|
||||
List<ChatMessage> requestMessages =
|
||||
[
|
||||
new(ChatRole.System, "System") { AdditionalProperties = new() { { AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.ChatHistory, "TestSource") } } },
|
||||
new(ChatRole.User, "Hello")
|
||||
];
|
||||
ChatHistoryProvider.InvokedContext context = new(s_mockAgent, s_mockSession, requestMessages)
|
||||
{
|
||||
ResponseMessages = [new ChatMessage(ChatRole.Assistant, "Response")]
|
||||
};
|
||||
|
||||
ChatHistoryProvider.InvokedContext? capturedContext = null;
|
||||
providerMock
|
||||
.Protected()
|
||||
.Setup<ValueTask>("InvokedCoreAsync", ItExpr.IsAny<ChatHistoryProvider.InvokedContext>(), ItExpr.IsAny<CancellationToken>())
|
||||
.Callback<ChatHistoryProvider.InvokedContext, CancellationToken>((ctx, _) => capturedContext = ctx)
|
||||
.Returns(default(ValueTask));
|
||||
|
||||
ChatHistoryProvider filtered = providerMock.Object.WithMessageFilters(
|
||||
invokedMessagesFilter: ctx =>
|
||||
{
|
||||
ctx.ResponseMessages = null;
|
||||
return ctx;
|
||||
});
|
||||
|
||||
// Act
|
||||
await filtered.InvokedAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(capturedContext);
|
||||
Assert.Null(capturedContext.ResponseMessages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithAIContextProviderMessageRemoval_ReturnsChatHistoryProviderMessageFilter()
|
||||
{
|
||||
// Arrange
|
||||
Mock<ChatHistoryProvider> providerMock = new();
|
||||
|
||||
// Act
|
||||
ChatHistoryProvider result = providerMock.Object.WithAIContextProviderMessageRemoval();
|
||||
|
||||
// Assert
|
||||
Assert.IsType<ChatHistoryProviderMessageFilter>(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WithAIContextProviderMessageRemoval_RemovesAIContextProviderMessagesAsync()
|
||||
{
|
||||
// Arrange
|
||||
Mock<ChatHistoryProvider> providerMock = new();
|
||||
List<ChatMessage> requestMessages =
|
||||
[
|
||||
new(ChatRole.System, "System") { AdditionalProperties = new() { { AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.ChatHistory, "TestSource") } } },
|
||||
new(ChatRole.User, "Hello"),
|
||||
new(ChatRole.System, "Context") { AdditionalProperties = new() { { AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.AIContextProvider, "TestContextSource") } } }
|
||||
];
|
||||
ChatHistoryProvider.InvokedContext context = new(s_mockAgent, s_mockSession, requestMessages);
|
||||
|
||||
ChatHistoryProvider.InvokedContext? capturedContext = null;
|
||||
providerMock
|
||||
.Protected()
|
||||
.Setup<ValueTask>("InvokedCoreAsync", ItExpr.IsAny<ChatHistoryProvider.InvokedContext>(), ItExpr.IsAny<CancellationToken>())
|
||||
.Callback<ChatHistoryProvider.InvokedContext, CancellationToken>((ctx, _) => capturedContext = ctx)
|
||||
.Returns(default(ValueTask));
|
||||
|
||||
ChatHistoryProvider filtered = providerMock.Object.WithAIContextProviderMessageRemoval();
|
||||
|
||||
// Act
|
||||
await filtered.InvokedAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(capturedContext);
|
||||
Assert.Equal(2, capturedContext.RequestMessages.Count());
|
||||
Assert.Contains("System", capturedContext.RequestMessages.Select(x => x.Text));
|
||||
Assert.Contains("Hello", capturedContext.RequestMessages.Select(x => x.Text));
|
||||
}
|
||||
}
|
||||
-213
@@ -1,213 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Moq;
|
||||
using Moq.Protected;
|
||||
|
||||
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Contains tests for the <see cref="ChatHistoryProviderMessageFilter"/> class.
|
||||
/// </summary>
|
||||
public sealed class ChatHistoryProviderMessageFilterTests
|
||||
{
|
||||
private static readonly AIAgent s_mockAgent = new Mock<AIAgent>().Object;
|
||||
private static readonly AgentSession s_mockSession = new Mock<AgentSession>().Object;
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithNullInnerProvider_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => new ChatHistoryProviderMessageFilter(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithOnlyInnerProvider_Throws()
|
||||
{
|
||||
// Arrange
|
||||
var innerProviderMock = new Mock<ChatHistoryProvider>();
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentException>(() => new ChatHistoryProviderMessageFilter(innerProviderMock.Object));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithAllParameters_CreatesInstance()
|
||||
{
|
||||
// Arrange
|
||||
var innerProviderMock = new Mock<ChatHistoryProvider>();
|
||||
|
||||
IEnumerable<ChatMessage> InvokingFilter(IEnumerable<ChatMessage> msgs) => msgs;
|
||||
ChatHistoryProvider.InvokedContext InvokedFilter(ChatHistoryProvider.InvokedContext ctx) => ctx;
|
||||
|
||||
// Act
|
||||
var filter = new ChatHistoryProviderMessageFilter(innerProviderMock.Object, InvokingFilter, InvokedFilter);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(filter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StateKey_DelegatesToInnerProvider()
|
||||
{
|
||||
// Arrange
|
||||
var innerProviderMock = new Mock<ChatHistoryProvider>();
|
||||
innerProviderMock.Setup(p => p.StateKey).Returns("inner-state-key");
|
||||
|
||||
// Act
|
||||
var filter = new ChatHistoryProviderMessageFilter(innerProviderMock.Object, x => x);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("inner-state-key", filter.StateKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokingAsync_WithNoOpFilters_ReturnsInnerProviderMessagesAsync()
|
||||
{
|
||||
// Arrange
|
||||
var innerProviderMock = new Mock<ChatHistoryProvider>();
|
||||
var expectedMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "Hello"),
|
||||
new(ChatRole.Assistant, "Hi there!")
|
||||
};
|
||||
var context = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Test")]);
|
||||
|
||||
innerProviderMock
|
||||
.Protected()
|
||||
.Setup<ValueTask<IEnumerable<ChatMessage>>>("InvokingCoreAsync", ItExpr.IsAny<ChatHistoryProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>())
|
||||
.ReturnsAsync(expectedMessages);
|
||||
|
||||
var filter = new ChatHistoryProviderMessageFilter(innerProviderMock.Object, x => x, x => x);
|
||||
|
||||
// Act
|
||||
var result = (await filter.InvokingAsync(context, CancellationToken.None)).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Equal("Hello", result[0].Text);
|
||||
Assert.Equal("Hi there!", result[1].Text);
|
||||
innerProviderMock
|
||||
.Protected()
|
||||
.Verify<ValueTask<IEnumerable<ChatMessage>>>("InvokingCoreAsync", Times.Once(), ItExpr.IsAny<ChatHistoryProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokingAsync_WithInvokingFilter_AppliesFilterAsync()
|
||||
{
|
||||
// Arrange
|
||||
var innerProviderMock = new Mock<ChatHistoryProvider>();
|
||||
var innerMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "Hello"),
|
||||
new(ChatRole.Assistant, "Hi there!"),
|
||||
new(ChatRole.User, "How are you?")
|
||||
};
|
||||
var context = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Test")]);
|
||||
|
||||
innerProviderMock
|
||||
.Protected()
|
||||
.Setup<ValueTask<IEnumerable<ChatMessage>>>("InvokingCoreAsync", ItExpr.IsAny<ChatHistoryProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>())
|
||||
.ReturnsAsync(innerMessages);
|
||||
|
||||
// Filter to only user messages
|
||||
IEnumerable<ChatMessage> InvokingFilter(IEnumerable<ChatMessage> msgs) => msgs.Where(m => m.Role == ChatRole.User);
|
||||
|
||||
var filter = new ChatHistoryProviderMessageFilter(innerProviderMock.Object, InvokingFilter);
|
||||
|
||||
// Act
|
||||
var result = (await filter.InvokingAsync(context, CancellationToken.None)).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.All(result, msg => Assert.Equal(ChatRole.User, msg.Role));
|
||||
innerProviderMock
|
||||
.Protected()
|
||||
.Verify<ValueTask<IEnumerable<ChatMessage>>>("InvokingCoreAsync", Times.Once(), ItExpr.IsAny<ChatHistoryProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokingAsync_WithInvokingFilter_CanModifyMessagesAsync()
|
||||
{
|
||||
// Arrange
|
||||
var innerProviderMock = new Mock<ChatHistoryProvider>();
|
||||
var innerMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "Hello"),
|
||||
new(ChatRole.Assistant, "Hi there!")
|
||||
};
|
||||
var context = new ChatHistoryProvider.InvokingContext(s_mockAgent, s_mockSession, [new ChatMessage(ChatRole.User, "Test")]);
|
||||
|
||||
innerProviderMock
|
||||
.Protected()
|
||||
.Setup<ValueTask<IEnumerable<ChatMessage>>>("InvokingCoreAsync", ItExpr.IsAny<ChatHistoryProvider.InvokingContext>(), ItExpr.IsAny<CancellationToken>())
|
||||
.ReturnsAsync(innerMessages);
|
||||
|
||||
// Filter that transforms messages
|
||||
IEnumerable<ChatMessage> InvokingFilter(IEnumerable<ChatMessage> msgs) =>
|
||||
msgs.Select(m => new ChatMessage(m.Role, $"[FILTERED] {m.Text}"));
|
||||
|
||||
var filter = new ChatHistoryProviderMessageFilter(innerProviderMock.Object, InvokingFilter);
|
||||
|
||||
// Act
|
||||
var result = (await filter.InvokingAsync(context, CancellationToken.None)).ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Equal("[FILTERED] Hello", result[0].Text);
|
||||
Assert.Equal("[FILTERED] Hi there!", result[1].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokedAsync_WithInvokedFilter_AppliesFilterAsync()
|
||||
{
|
||||
// Arrange
|
||||
var innerProviderMock = new Mock<ChatHistoryProvider>();
|
||||
List<ChatMessage> requestMessages =
|
||||
[
|
||||
new(ChatRole.System, "System") { AdditionalProperties = new() { { AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.ChatHistory, "TestSource") } } },
|
||||
new(ChatRole.User, "Hello"),
|
||||
];
|
||||
var responseMessages = new List<ChatMessage> { new(ChatRole.Assistant, "Response") };
|
||||
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, requestMessages)
|
||||
{
|
||||
ResponseMessages = responseMessages
|
||||
};
|
||||
|
||||
ChatHistoryProvider.InvokedContext? capturedContext = null;
|
||||
innerProviderMock
|
||||
.Protected()
|
||||
.Setup<ValueTask>("InvokedCoreAsync", ItExpr.IsAny<ChatHistoryProvider.InvokedContext>(), ItExpr.IsAny<CancellationToken>())
|
||||
.Callback<ChatHistoryProvider.InvokedContext, CancellationToken>((ctx, ct) => capturedContext = ctx)
|
||||
.Returns(default(ValueTask));
|
||||
|
||||
// Filter that modifies the context
|
||||
ChatHistoryProvider.InvokedContext InvokedFilter(ChatHistoryProvider.InvokedContext ctx)
|
||||
{
|
||||
var modifiedRequestMessages = ctx.RequestMessages.Where(x => x.GetAgentRequestMessageSourceType() == AgentRequestMessageSourceType.External).Select(m => new ChatMessage(m.Role, $"[FILTERED] {m.Text}")).ToList();
|
||||
return new ChatHistoryProvider.InvokedContext(s_mockAgent, s_mockSession, modifiedRequestMessages)
|
||||
{
|
||||
ResponseMessages = ctx.ResponseMessages,
|
||||
InvokeException = ctx.InvokeException
|
||||
};
|
||||
}
|
||||
|
||||
var filter = new ChatHistoryProviderMessageFilter(innerProviderMock.Object, invokedMessagesFilter: InvokedFilter);
|
||||
|
||||
// Act
|
||||
await filter.InvokedAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(capturedContext);
|
||||
Assert.Single(capturedContext.RequestMessages);
|
||||
Assert.Equal("[FILTERED] Hello", capturedContext.RequestMessages.First().Text);
|
||||
innerProviderMock
|
||||
.Protected()
|
||||
.Verify<ValueTask>("InvokedCoreAsync", Times.Once(), ItExpr.IsAny<ChatHistoryProvider.InvokedContext>(), ItExpr.IsAny<CancellationToken>());
|
||||
}
|
||||
}
|
||||
+86
-1
@@ -83,7 +83,7 @@ public class InMemoryChatHistoryProviderTests
|
||||
};
|
||||
|
||||
var provider = new InMemoryChatHistoryProvider();
|
||||
provider.SetMessages(session, [providerMessages[0]]);
|
||||
provider.SetMessages(session, providerMessages);
|
||||
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, session, requestMessages)
|
||||
{
|
||||
ResponseMessages = responseMessages
|
||||
@@ -397,6 +397,91 @@ public class InMemoryChatHistoryProviderTests
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(() => provider.InvokingAsync(null!, CancellationToken.None).AsTask());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokedAsync_DefaultFilter_ExcludesChatHistoryMessagesAsync()
|
||||
{
|
||||
// Arrange
|
||||
var session = CreateMockSession();
|
||||
var provider = new InMemoryChatHistoryProvider();
|
||||
var requestMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "External message"),
|
||||
new(ChatRole.System, "From history") { AdditionalProperties = new() { { AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.ChatHistory, "HistorySource") } } },
|
||||
new(ChatRole.System, "From context provider") { AdditionalProperties = new() { { AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.AIContextProvider, "ContextSource") } } },
|
||||
};
|
||||
|
||||
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, session, requestMessages)
|
||||
{
|
||||
ResponseMessages = [new ChatMessage(ChatRole.Assistant, "Response")]
|
||||
};
|
||||
|
||||
// Act
|
||||
await provider.InvokedAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert - ChatHistory message excluded, AIContextProvider message included
|
||||
var messages = provider.GetMessages(session);
|
||||
Assert.Equal(3, messages.Count);
|
||||
Assert.Equal("External message", messages[0].Text);
|
||||
Assert.Equal("From context provider", messages[1].Text);
|
||||
Assert.Equal("Response", messages[2].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokedAsync_CustomFilter_OverridesDefaultAsync()
|
||||
{
|
||||
// Arrange
|
||||
var session = CreateMockSession();
|
||||
var provider = new InMemoryChatHistoryProvider(new InMemoryChatHistoryProviderOptions
|
||||
{
|
||||
StorageInputMessageFilter = messages => messages.Where(m => m.GetAgentRequestMessageSourceType() == AgentRequestMessageSourceType.External)
|
||||
});
|
||||
var requestMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "External message"),
|
||||
new(ChatRole.System, "From history") { AdditionalProperties = new() { { AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.ChatHistory, "HistorySource") } } },
|
||||
new(ChatRole.System, "From context provider") { AdditionalProperties = new() { { AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.AIContextProvider, "ContextSource") } } },
|
||||
};
|
||||
|
||||
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, session, requestMessages)
|
||||
{
|
||||
ResponseMessages = [new ChatMessage(ChatRole.Assistant, "Response")]
|
||||
};
|
||||
|
||||
// Act
|
||||
await provider.InvokedAsync(context, CancellationToken.None);
|
||||
|
||||
// Assert - Custom filter keeps only External messages (both ChatHistory and AIContextProvider excluded)
|
||||
var messages = provider.GetMessages(session);
|
||||
Assert.Equal(2, messages.Count);
|
||||
Assert.Equal("External message", messages[0].Text);
|
||||
Assert.Equal("Response", messages[1].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokingAsync_OutputFilter_FiltersOutputMessagesAsync()
|
||||
{
|
||||
// Arrange
|
||||
var session = CreateMockSession();
|
||||
var provider = new InMemoryChatHistoryProvider(new InMemoryChatHistoryProviderOptions
|
||||
{
|
||||
RetrievalOutputMessageFilter = messages => messages.Where(m => m.Role == ChatRole.User)
|
||||
});
|
||||
provider.SetMessages(session,
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "User message"),
|
||||
new ChatMessage(ChatRole.Assistant, "Assistant message"),
|
||||
new ChatMessage(ChatRole.System, "System message")
|
||||
]);
|
||||
|
||||
// Act
|
||||
var context = new ChatHistoryProvider.InvokingContext(s_mockAgent, session, []);
|
||||
var result = (await provider.InvokingAsync(context, CancellationToken.None)).ToList();
|
||||
|
||||
// Assert - Only user messages pass through the output filter
|
||||
Assert.Single(result);
|
||||
Assert.Equal("User message", result[0].Text);
|
||||
}
|
||||
|
||||
public class TestAIContent(string testData) : AIContent
|
||||
{
|
||||
public string TestData => testData;
|
||||
|
||||
+124
@@ -841,4 +841,128 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Message Filter Tests
|
||||
|
||||
[SkippableFact]
|
||||
[Trait("Category", "CosmosDB")]
|
||||
public async Task InvokedAsync_DefaultFilter_ExcludesChatHistoryMessagesFromStorageAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.SkipIfEmulatorNotAvailable();
|
||||
var session = CreateMockSession();
|
||||
var conversationId = Guid.NewGuid().ToString();
|
||||
using var provider = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId,
|
||||
_ => new CosmosChatHistoryProvider.State(conversationId));
|
||||
|
||||
var requestMessages = new[]
|
||||
{
|
||||
new ChatMessage(ChatRole.User, "External message"),
|
||||
new ChatMessage(ChatRole.System, "From history") { AdditionalProperties = new() { { AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.ChatHistory, "HistorySource") } } },
|
||||
new ChatMessage(ChatRole.System, "From context provider") { AdditionalProperties = new() { { AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.AIContextProvider, "ContextSource") } } },
|
||||
};
|
||||
|
||||
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, session, requestMessages)
|
||||
{
|
||||
ResponseMessages = [new ChatMessage(ChatRole.Assistant, "Response")]
|
||||
};
|
||||
|
||||
// Act
|
||||
await provider.InvokedAsync(context);
|
||||
|
||||
// Wait for eventual consistency
|
||||
await Task.Delay(100);
|
||||
|
||||
// Assert - ChatHistory message excluded, External + AIContextProvider + Response stored
|
||||
var invokingContext = new ChatHistoryProvider.InvokingContext(s_mockAgent, session, []);
|
||||
var messages = (await provider.InvokingAsync(invokingContext)).ToList();
|
||||
Assert.Equal(3, messages.Count);
|
||||
Assert.Equal("External message", messages[0].Text);
|
||||
Assert.Equal("From context provider", messages[1].Text);
|
||||
Assert.Equal("Response", messages[2].Text);
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
[Trait("Category", "CosmosDB")]
|
||||
public async Task InvokedAsync_CustomStorageInputFilter_OverridesDefaultAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.SkipIfEmulatorNotAvailable();
|
||||
var session = CreateMockSession();
|
||||
var conversationId = Guid.NewGuid().ToString();
|
||||
using var provider = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId,
|
||||
_ => new CosmosChatHistoryProvider.State(conversationId))
|
||||
{
|
||||
// Custom filter: only store External messages (also exclude AIContextProvider)
|
||||
StorageInputMessageFilter = messages => messages.Where(m => m.GetAgentRequestMessageSourceType() == AgentRequestMessageSourceType.External)
|
||||
};
|
||||
|
||||
var requestMessages = new[]
|
||||
{
|
||||
new ChatMessage(ChatRole.User, "External message"),
|
||||
new ChatMessage(ChatRole.System, "From history") { AdditionalProperties = new() { { AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.ChatHistory, "HistorySource") } } },
|
||||
new ChatMessage(ChatRole.System, "From context provider") { AdditionalProperties = new() { { AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.AIContextProvider, "ContextSource") } } },
|
||||
};
|
||||
|
||||
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, session, requestMessages)
|
||||
{
|
||||
ResponseMessages = [new ChatMessage(ChatRole.Assistant, "Response")]
|
||||
};
|
||||
|
||||
// Act
|
||||
await provider.InvokedAsync(context);
|
||||
|
||||
// Wait for eventual consistency
|
||||
await Task.Delay(100);
|
||||
|
||||
// Assert - Custom filter: only External + Response stored (both ChatHistory and AIContextProvider excluded)
|
||||
var invokingContext = new ChatHistoryProvider.InvokingContext(s_mockAgent, session, []);
|
||||
var messages = (await provider.InvokingAsync(invokingContext)).ToList();
|
||||
Assert.Equal(2, messages.Count);
|
||||
Assert.Equal("External message", messages[0].Text);
|
||||
Assert.Equal("Response", messages[1].Text);
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
[Trait("Category", "CosmosDB")]
|
||||
public async Task InvokingAsync_RetrievalOutputFilter_FiltersRetrievedMessagesAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.SkipIfEmulatorNotAvailable();
|
||||
var session = CreateMockSession();
|
||||
var conversationId = Guid.NewGuid().ToString();
|
||||
using var provider = new CosmosChatHistoryProvider(this._connectionString, s_testDatabaseId, TestContainerId,
|
||||
_ => new CosmosChatHistoryProvider.State(conversationId))
|
||||
{
|
||||
// Only return User messages when retrieving
|
||||
RetrievalOutputMessageFilter = messages => messages.Where(m => m.Role == ChatRole.User)
|
||||
};
|
||||
|
||||
var requestMessages = new[]
|
||||
{
|
||||
new ChatMessage(ChatRole.User, "User message"),
|
||||
new ChatMessage(ChatRole.System, "System message"),
|
||||
};
|
||||
|
||||
var context = new ChatHistoryProvider.InvokedContext(s_mockAgent, session, requestMessages)
|
||||
{
|
||||
ResponseMessages = [new ChatMessage(ChatRole.Assistant, "Assistant response")]
|
||||
};
|
||||
|
||||
await provider.InvokedAsync(context);
|
||||
|
||||
// Wait for eventual consistency
|
||||
await Task.Delay(100);
|
||||
|
||||
// Act
|
||||
var invokingContext = new ChatHistoryProvider.InvokingContext(s_mockAgent, session, []);
|
||||
var messages = (await provider.InvokingAsync(invokingContext)).ToList();
|
||||
|
||||
// Assert - Only User messages returned (System and Assistant filtered by RetrievalOutputMessageFilter)
|
||||
Assert.Single(messages);
|
||||
Assert.Equal("User message", messages[0].Text);
|
||||
Assert.Equal(ChatRole.User, messages[0].Role);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -436,6 +436,116 @@ public sealed class Mem0ProviderTests : IDisposable
|
||||
Assert.NotNull(state);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokingAsync_DefaultFilter_ExcludesNonExternalMessagesFromSearchAsync()
|
||||
{
|
||||
// Arrange
|
||||
this._handler.EnqueueJsonResponse("[]"); // Empty search results
|
||||
var storageScope = new Mem0ProviderScope { ApplicationId = "app", AgentId = "agent", ThreadId = "session", UserId = "user" };
|
||||
var mockSession = new TestAgentSession();
|
||||
var sut = new Mem0Provider(this._httpClient, _ => new Mem0Provider.State(storageScope));
|
||||
|
||||
var requestMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "External message"),
|
||||
new(ChatRole.System, "From history") { AdditionalProperties = new() { { AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.ChatHistory, "HistorySource") } } },
|
||||
new(ChatRole.System, "From context provider") { AdditionalProperties = new() { { AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.AIContextProvider, "ContextSource") } } },
|
||||
};
|
||||
|
||||
var invokingContext = new AIContextProvider.InvokingContext(s_mockAgent, mockSession, new AIContext { Messages = requestMessages });
|
||||
|
||||
// Act
|
||||
await sut.InvokingAsync(invokingContext, CancellationToken.None);
|
||||
|
||||
// Assert - Search query should only contain the External message
|
||||
var searchRequest = Assert.Single(this._handler.Requests, r => r.RequestMessage.Method == HttpMethod.Post);
|
||||
using JsonDocument doc = JsonDocument.Parse(searchRequest.RequestBody);
|
||||
Assert.Equal("External message", doc.RootElement.GetProperty("query").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokingAsync_CustomSearchInputFilter_OverridesDefaultAsync()
|
||||
{
|
||||
// Arrange
|
||||
this._handler.EnqueueJsonResponse("[]"); // Empty search results
|
||||
var storageScope = new Mem0ProviderScope { ApplicationId = "app", AgentId = "agent", ThreadId = "session", UserId = "user" };
|
||||
var mockSession = new TestAgentSession();
|
||||
var sut = new Mem0Provider(this._httpClient, _ => new Mem0Provider.State(storageScope), options: new Mem0ProviderOptions
|
||||
{
|
||||
SearchInputMessageFilter = messages => messages // No filtering
|
||||
});
|
||||
|
||||
var requestMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "External message"),
|
||||
new(ChatRole.System, "From history") { AdditionalProperties = new() { { AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.ChatHistory, "HistorySource") } } },
|
||||
};
|
||||
|
||||
var invokingContext = new AIContextProvider.InvokingContext(s_mockAgent, mockSession, new AIContext { Messages = requestMessages });
|
||||
|
||||
// Act
|
||||
await sut.InvokingAsync(invokingContext, CancellationToken.None);
|
||||
|
||||
// Assert - Search query should contain all messages (custom identity filter)
|
||||
var searchRequest = Assert.Single(this._handler.Requests, r => r.RequestMessage.Method == HttpMethod.Post);
|
||||
using JsonDocument doc = JsonDocument.Parse(searchRequest.RequestBody);
|
||||
var queryText = doc.RootElement.GetProperty("query").GetString();
|
||||
Assert.Contains("External message", queryText);
|
||||
Assert.Contains("From history", queryText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokedAsync_DefaultFilter_ExcludesNonExternalMessagesFromStorageAsync()
|
||||
{
|
||||
// Arrange
|
||||
this._handler.EnqueueEmptyOk(); // For the one message that should be stored
|
||||
var storageScope = new Mem0ProviderScope { ApplicationId = "a", AgentId = "b", ThreadId = "c", UserId = "d" };
|
||||
var mockSession = new TestAgentSession();
|
||||
var sut = new Mem0Provider(this._httpClient, _ => new Mem0Provider.State(storageScope));
|
||||
|
||||
var requestMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "External message"),
|
||||
new(ChatRole.System, "From history") { AdditionalProperties = new() { { AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.ChatHistory, "HistorySource") } } },
|
||||
};
|
||||
|
||||
// Act
|
||||
await sut.InvokedAsync(new AIContextProvider.InvokedContext(s_mockAgent, mockSession, requestMessages));
|
||||
|
||||
// Assert - Only the External message should be persisted
|
||||
var memoryPosts = this._handler.Requests.Where(r => r.RequestMessage.RequestUri!.AbsolutePath == "/v1/memories/" && r.RequestMessage.Method == HttpMethod.Post).ToList();
|
||||
Assert.Single(memoryPosts);
|
||||
Assert.Contains("External message", memoryPosts[0].RequestBody);
|
||||
Assert.DoesNotContain(memoryPosts, r => ContainsOrdinal(r.RequestBody, "From history"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokedAsync_CustomStorageInputFilter_OverridesDefaultAsync()
|
||||
{
|
||||
// Arrange
|
||||
this._handler.EnqueueEmptyOk(); // For first CreateMemory
|
||||
this._handler.EnqueueEmptyOk(); // For second CreateMemory
|
||||
var storageScope = new Mem0ProviderScope { ApplicationId = "a", AgentId = "b", ThreadId = "c", UserId = "d" };
|
||||
var mockSession = new TestAgentSession();
|
||||
var sut = new Mem0Provider(this._httpClient, _ => new Mem0Provider.State(storageScope), options: new Mem0ProviderOptions
|
||||
{
|
||||
StorageInputMessageFilter = messages => messages // No filtering - store everything
|
||||
});
|
||||
|
||||
var requestMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "External message"),
|
||||
new(ChatRole.System, "From history") { AdditionalProperties = new() { { AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.ChatHistory, "HistorySource") } } },
|
||||
};
|
||||
|
||||
// Act
|
||||
await sut.InvokedAsync(new AIContextProvider.InvokedContext(s_mockAgent, mockSession, requestMessages));
|
||||
|
||||
// Assert - Both messages should be persisted (identity filter overrides default)
|
||||
var memoryPosts = this._handler.Requests.Where(r => r.RequestMessage.RequestUri!.AbsolutePath == "/v1/memories/" && r.RequestMessage.Method == HttpMethod.Post).ToList();
|
||||
Assert.Equal(2, memoryPosts.Count);
|
||||
}
|
||||
|
||||
private static bool ContainsOrdinal(string source, string value) => source.IndexOf(value, StringComparison.Ordinal) >= 0;
|
||||
|
||||
public void Dispose()
|
||||
|
||||
@@ -356,6 +356,140 @@ public sealed class TextSearchProviderTests
|
||||
Assert.Null(aiContext.Tools);
|
||||
}
|
||||
|
||||
#region Message Filter Tests
|
||||
|
||||
[Fact]
|
||||
public async Task InvokingAsync_DefaultFilter_ExcludesNonExternalMessagesFromSearchInputAsync()
|
||||
{
|
||||
// Arrange
|
||||
string? capturedInput = null;
|
||||
Task<IEnumerable<TextSearchProvider.TextSearchResult>> SearchDelegateAsync(string input, CancellationToken ct)
|
||||
{
|
||||
capturedInput = input;
|
||||
return Task.FromResult<IEnumerable<TextSearchProvider.TextSearchResult>>([]);
|
||||
}
|
||||
|
||||
var provider = new TextSearchProvider(SearchDelegateAsync);
|
||||
var requestMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "External message"),
|
||||
new(ChatRole.System, "From history") { AdditionalProperties = new() { { AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.ChatHistory, "HistorySource") } } },
|
||||
new(ChatRole.System, "From context provider") { AdditionalProperties = new() { { AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.AIContextProvider, "ContextSource") } } },
|
||||
};
|
||||
|
||||
var invokingContext = new AIContextProvider.InvokingContext(s_mockAgent, new TestAgentSession(), new AIContext { Messages = requestMessages });
|
||||
|
||||
// Act
|
||||
await provider.InvokingAsync(invokingContext, CancellationToken.None);
|
||||
|
||||
// Assert - Only external messages should be used for search input
|
||||
Assert.Equal("External message", capturedInput);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokingAsync_CustomSearchInputFilter_OverridesDefaultAsync()
|
||||
{
|
||||
// Arrange
|
||||
string? capturedInput = null;
|
||||
Task<IEnumerable<TextSearchProvider.TextSearchResult>> SearchDelegateAsync(string input, CancellationToken ct)
|
||||
{
|
||||
capturedInput = input;
|
||||
return Task.FromResult<IEnumerable<TextSearchProvider.TextSearchResult>>([]);
|
||||
}
|
||||
|
||||
var provider = new TextSearchProvider(SearchDelegateAsync, new TextSearchProviderOptions
|
||||
{
|
||||
SearchInputMessageFilter = messages => messages.Where(m => m.Role == ChatRole.System)
|
||||
});
|
||||
var requestMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "User message"),
|
||||
new(ChatRole.System, "System message"),
|
||||
};
|
||||
|
||||
var invokingContext = new AIContextProvider.InvokingContext(s_mockAgent, new TestAgentSession(), new AIContext { Messages = requestMessages });
|
||||
|
||||
// Act
|
||||
await provider.InvokingAsync(invokingContext, CancellationToken.None);
|
||||
|
||||
// Assert - Custom filter keeps only System messages
|
||||
Assert.Equal("System message", capturedInput);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokedAsync_DefaultFilter_ExcludesNonExternalMessagesFromStorageAsync()
|
||||
{
|
||||
// Arrange
|
||||
var options = new TextSearchProviderOptions
|
||||
{
|
||||
RecentMessageMemoryLimit = 10,
|
||||
RecentMessageRolesIncluded = [ChatRole.User, ChatRole.System]
|
||||
};
|
||||
string? capturedInput = null;
|
||||
Task<IEnumerable<TextSearchProvider.TextSearchResult>> SearchDelegateAsync(string input, CancellationToken ct)
|
||||
{
|
||||
capturedInput = input;
|
||||
return Task.FromResult<IEnumerable<TextSearchProvider.TextSearchResult>>([]);
|
||||
}
|
||||
var provider = new TextSearchProvider(SearchDelegateAsync, options);
|
||||
var session = new TestAgentSession();
|
||||
|
||||
var requestMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "External message"),
|
||||
new(ChatRole.System, "From history") { AdditionalProperties = new() { { AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.ChatHistory, "HistorySource") } } },
|
||||
new(ChatRole.System, "From context provider") { AdditionalProperties = new() { { AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.AIContextProvider, "ContextSource") } } },
|
||||
};
|
||||
|
||||
// Store messages via InvokedAsync
|
||||
await provider.InvokedAsync(new(s_mockAgent, session, requestMessages));
|
||||
|
||||
// Now invoke to read stored memory
|
||||
var invokingContext = new AIContextProvider.InvokingContext(s_mockAgent, session, new AIContext { Messages = [new ChatMessage(ChatRole.User, "Next")] });
|
||||
await provider.InvokingAsync(invokingContext, CancellationToken.None);
|
||||
|
||||
// Assert - Only "External message" was stored in memory, so search input = "External message" + "Next"
|
||||
Assert.Equal("External message\nNext", capturedInput);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokedAsync_CustomStorageInputFilter_OverridesDefaultAsync()
|
||||
{
|
||||
// Arrange
|
||||
var options = new TextSearchProviderOptions
|
||||
{
|
||||
RecentMessageMemoryLimit = 10,
|
||||
RecentMessageRolesIncluded = [ChatRole.User, ChatRole.System],
|
||||
StorageInputMessageFilter = messages => messages // No filtering - store everything
|
||||
};
|
||||
string? capturedInput = null;
|
||||
Task<IEnumerable<TextSearchProvider.TextSearchResult>> SearchDelegateAsync(string input, CancellationToken ct)
|
||||
{
|
||||
capturedInput = input;
|
||||
return Task.FromResult<IEnumerable<TextSearchProvider.TextSearchResult>>([]);
|
||||
}
|
||||
var provider = new TextSearchProvider(SearchDelegateAsync, options);
|
||||
var session = new TestAgentSession();
|
||||
|
||||
var requestMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "External message"),
|
||||
new(ChatRole.System, "From history") { AdditionalProperties = new() { { AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.ChatHistory, "HistorySource") } } },
|
||||
};
|
||||
|
||||
// Store messages via InvokedAsync
|
||||
await provider.InvokedAsync(new(s_mockAgent, session, requestMessages));
|
||||
|
||||
// Now invoke to read stored memory
|
||||
var invokingContext = new AIContextProvider.InvokingContext(s_mockAgent, session, new AIContext { Messages = [new ChatMessage(ChatRole.User, "Next")] });
|
||||
await provider.InvokingAsync(invokingContext, CancellationToken.None);
|
||||
|
||||
// Assert - Both messages stored (identity filter), so search input includes all + current
|
||||
Assert.Equal("External message\nFrom history\nNext", capturedInput);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Recent Message Memory Tests
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -539,6 +539,188 @@ public class ChatHistoryMemoryProviderTests
|
||||
|
||||
#endregion
|
||||
|
||||
#region Message Filter Tests
|
||||
|
||||
[Fact]
|
||||
public async Task InvokingAsync_DefaultFilter_ExcludesNonExternalMessagesFromSearchAsync()
|
||||
{
|
||||
// Arrange
|
||||
var providerOptions = new ChatHistoryMemoryProviderOptions
|
||||
{
|
||||
SearchTime = ChatHistoryMemoryProviderOptions.SearchBehavior.BeforeAIInvoke,
|
||||
};
|
||||
|
||||
string? capturedQuery = null;
|
||||
this._vectorStoreCollectionMock
|
||||
.Setup(c => c.SearchAsync(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<int>(),
|
||||
It.IsAny<VectorSearchOptions<Dictionary<string, object?>>>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Callback<string, int, VectorSearchOptions<Dictionary<string, object?>>, CancellationToken>((query, _, _, _) => capturedQuery = query)
|
||||
.Returns(ToAsyncEnumerableAsync(new List<VectorSearchResult<Dictionary<string, object?>>>()));
|
||||
|
||||
var provider = new ChatHistoryMemoryProvider(
|
||||
this._vectorStoreMock.Object,
|
||||
TestCollectionName,
|
||||
1,
|
||||
_ => new ChatHistoryMemoryProvider.State(new ChatHistoryMemoryProviderScope { UserId = "UID" }),
|
||||
options: providerOptions);
|
||||
|
||||
var requestMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "External message"),
|
||||
new(ChatRole.System, "From history") { AdditionalProperties = new() { { AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.ChatHistory, "HistorySource") } } },
|
||||
new(ChatRole.System, "From context provider") { AdditionalProperties = new() { { AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.AIContextProvider, "ContextSource") } } },
|
||||
};
|
||||
|
||||
var invokingContext = new AIContextProvider.InvokingContext(s_mockAgent, new TestAgentSession(), new AIContext { Messages = requestMessages });
|
||||
|
||||
// Act
|
||||
await provider.InvokingAsync(invokingContext, CancellationToken.None);
|
||||
|
||||
// Assert - Only External message used for search query
|
||||
Assert.Equal("External message", capturedQuery);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokingAsync_CustomSearchInputFilter_OverridesDefaultAsync()
|
||||
{
|
||||
// Arrange
|
||||
var providerOptions = new ChatHistoryMemoryProviderOptions
|
||||
{
|
||||
SearchTime = ChatHistoryMemoryProviderOptions.SearchBehavior.BeforeAIInvoke,
|
||||
SearchInputMessageFilter = messages => messages // No filtering
|
||||
};
|
||||
|
||||
string? capturedQuery = null;
|
||||
this._vectorStoreCollectionMock
|
||||
.Setup(c => c.SearchAsync(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<int>(),
|
||||
It.IsAny<VectorSearchOptions<Dictionary<string, object?>>>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Callback<string, int, VectorSearchOptions<Dictionary<string, object?>>, CancellationToken>((query, _, _, _) => capturedQuery = query)
|
||||
.Returns(ToAsyncEnumerableAsync(new List<VectorSearchResult<Dictionary<string, object?>>>()));
|
||||
|
||||
var provider = new ChatHistoryMemoryProvider(
|
||||
this._vectorStoreMock.Object,
|
||||
TestCollectionName,
|
||||
1,
|
||||
_ => new ChatHistoryMemoryProvider.State(new ChatHistoryMemoryProviderScope { UserId = "UID" }),
|
||||
options: providerOptions);
|
||||
|
||||
var requestMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "External message"),
|
||||
new(ChatRole.System, "From history") { AdditionalProperties = new() { { AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.ChatHistory, "HistorySource") } } },
|
||||
};
|
||||
|
||||
var invokingContext = new AIContextProvider.InvokingContext(s_mockAgent, new TestAgentSession(), new AIContext { Messages = requestMessages });
|
||||
|
||||
// Act
|
||||
await provider.InvokingAsync(invokingContext, CancellationToken.None);
|
||||
|
||||
// Assert - Both messages should be included in search query (identity filter)
|
||||
Assert.NotNull(capturedQuery);
|
||||
Assert.Contains("External message", capturedQuery);
|
||||
Assert.Contains("From history", capturedQuery);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokedAsync_DefaultFilter_ExcludesNonExternalMessagesFromStorageAsync()
|
||||
{
|
||||
// Arrange
|
||||
var stored = new List<Dictionary<string, object?>>();
|
||||
|
||||
this._vectorStoreCollectionMock
|
||||
.Setup(c => c.UpsertAsync(It.IsAny<IEnumerable<Dictionary<string, object?>>>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<IEnumerable<Dictionary<string, object?>>, CancellationToken>((items, ct) =>
|
||||
{
|
||||
if (items != null)
|
||||
{
|
||||
stored.AddRange(items);
|
||||
}
|
||||
})
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
var provider = new ChatHistoryMemoryProvider(
|
||||
this._vectorStoreMock.Object,
|
||||
TestCollectionName,
|
||||
1,
|
||||
_ => new ChatHistoryMemoryProvider.State(new ChatHistoryMemoryProviderScope { UserId = "UID" }));
|
||||
|
||||
var requestMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "External message"),
|
||||
new(ChatRole.System, "From history") { AdditionalProperties = new() { { AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.ChatHistory, "HistorySource") } } },
|
||||
new(ChatRole.System, "From context provider") { AdditionalProperties = new() { { AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.AIContextProvider, "ContextSource") } } },
|
||||
};
|
||||
|
||||
var invokedContext = new AIContextProvider.InvokedContext(s_mockAgent, new TestAgentSession(), requestMessages)
|
||||
{
|
||||
ResponseMessages = [new ChatMessage(ChatRole.Assistant, "Response")]
|
||||
};
|
||||
|
||||
// Act
|
||||
await provider.InvokedAsync(invokedContext, CancellationToken.None);
|
||||
|
||||
// Assert - Only External message + response stored (ChatHistory and AIContextProvider excluded by default)
|
||||
Assert.Equal(2, stored.Count);
|
||||
Assert.Equal("External message", stored[0]["Content"]);
|
||||
Assert.Equal("Response", stored[1]["Content"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokedAsync_CustomStorageInputFilter_OverridesDefaultAsync()
|
||||
{
|
||||
// Arrange
|
||||
var stored = new List<Dictionary<string, object?>>();
|
||||
|
||||
this._vectorStoreCollectionMock
|
||||
.Setup(c => c.UpsertAsync(It.IsAny<IEnumerable<Dictionary<string, object?>>>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<IEnumerable<Dictionary<string, object?>>, CancellationToken>((items, ct) =>
|
||||
{
|
||||
if (items != null)
|
||||
{
|
||||
stored.AddRange(items);
|
||||
}
|
||||
})
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
var provider = new ChatHistoryMemoryProvider(
|
||||
this._vectorStoreMock.Object,
|
||||
TestCollectionName,
|
||||
1,
|
||||
_ => new ChatHistoryMemoryProvider.State(new ChatHistoryMemoryProviderScope { UserId = "UID" }),
|
||||
options: new ChatHistoryMemoryProviderOptions
|
||||
{
|
||||
StorageInputMessageFilter = messages => messages // No filtering - store everything
|
||||
});
|
||||
|
||||
var requestMessages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "External message"),
|
||||
new(ChatRole.System, "From history") { AdditionalProperties = new() { { AgentRequestMessageSourceAttribution.AdditionalPropertiesKey, new AgentRequestMessageSourceAttribution(AgentRequestMessageSourceType.ChatHistory, "HistorySource") } } },
|
||||
};
|
||||
|
||||
var invokedContext = new AIContextProvider.InvokedContext(s_mockAgent, new TestAgentSession(), requestMessages)
|
||||
{
|
||||
ResponseMessages = [new ChatMessage(ChatRole.Assistant, "Response")]
|
||||
};
|
||||
|
||||
// Act
|
||||
await provider.InvokedAsync(invokedContext, CancellationToken.None);
|
||||
|
||||
// Assert - All messages stored (identity filter overrides default)
|
||||
Assert.Equal(3, stored.Count);
|
||||
Assert.Equal("External message", stored[0]["Content"]);
|
||||
Assert.Equal("From history", stored[1]["Content"]);
|
||||
Assert.Equal("Response", stored[2]["Content"]);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static async IAsyncEnumerable<T> ToAsyncEnumerableAsync<T>(IEnumerable<T> values)
|
||||
{
|
||||
await Task.Yield();
|
||||
|
||||
Reference in New Issue
Block a user