.NET: Update AIContextProviders to use Microsoft.Extensions.Compliance.Redaction (#4854)

* Update providers to use Microsoft.Extensions.Compliance.Redaction

* Fix formatting.

* Fix readme
This commit is contained in:
westey
2026-03-24 18:12:55 +00:00
committed by GitHub
parent cc85bbc2dc
commit 2c000b032d
19 changed files with 277 additions and 40 deletions
@@ -148,11 +148,15 @@ public sealed class Mem0ProviderTests : IDisposable
}
[Theory]
[InlineData(false, false, 4)]
[InlineData(true, false, 4)]
[InlineData(false, true, 2)]
[InlineData(true, true, 2)]
public async Task InvokingAsync_LogsUserIdBasedOnEnableSensitiveTelemetryDataAsync(bool enableSensitiveTelemetryData, bool requestThrows, int expectedLogInvocations)
[InlineData(false, false, false, 4)]
[InlineData(false, false, true, 4)]
[InlineData(true, false, false, 4)]
[InlineData(true, false, true, 4)]
[InlineData(false, true, false, 2)]
[InlineData(false, true, true, 2)]
[InlineData(true, true, false, 2)]
[InlineData(true, true, true, 2)]
public async Task InvokingAsync_RedactsLogDataBasedOnOptionsAsync(bool enableSensitiveTelemetryData, bool requestThrows, bool useCustomRedactor, int expectedLogInvocations)
{
// Arrange
if (requestThrows)
@@ -171,7 +175,11 @@ public sealed class Mem0ProviderTests : IDisposable
ThreadId = "session",
UserId = "user"
};
var options = new Mem0ProviderOptions { EnableSensitiveTelemetryData = enableSensitiveTelemetryData };
var options = new Mem0ProviderOptions
{
EnableSensitiveTelemetryData = enableSensitiveTelemetryData,
Redactor = useCustomRedactor ? new ReplacingRedactor("***") : null
};
var mockSession = new TestAgentSession();
var sut = new Mem0Provider(this._httpClient, _ => new Mem0Provider.State(storageScope), options: options, loggerFactory: this._loggerFactoryMock.Object);
@@ -180,7 +188,8 @@ public sealed class Mem0ProviderTests : IDisposable
// Act
await sut.InvokingAsync(invokingContext, CancellationToken.None);
// Assert
// Assert — EnableSensitiveTelemetryData takes precedence over Redactor
string expectedRedaction = enableSensitiveTelemetryData ? "user" : (useCustomRedactor ? "***" : "<redacted>");
Assert.Equal(expectedLogInvocations, this._loggerMock.Invocations.Count);
foreach (var logInvocation in this._loggerMock.Invocations)
{
@@ -191,18 +200,18 @@ public sealed class Mem0ProviderTests : IDisposable
var state = Assert.IsType<IReadOnlyList<KeyValuePair<string, object?>>>(logInvocation.Arguments[2], exactMatch: false);
var userIdValue = state.First(kvp => kvp.Key == "UserId").Value;
Assert.Equal(enableSensitiveTelemetryData ? "user" : "<redacted>", userIdValue);
Assert.Equal(expectedRedaction, userIdValue);
var inputValue = state.FirstOrDefault(kvp => kvp.Key == "Input").Value;
if (inputValue != null)
{
Assert.Equal(enableSensitiveTelemetryData ? "Who am I?" : "<redacted>", inputValue);
Assert.Equal(enableSensitiveTelemetryData ? "Who am I?" : expectedRedaction, inputValue);
}
var messageTextValue = state.FirstOrDefault(kvp => kvp.Key == "MessageText").Value;
if (messageTextValue != null)
{
Assert.Equal(enableSensitiveTelemetryData ? "## Memories\nConsider the following memories when answering user questions:\nName is Caoimhe" : "<redacted>", messageTextValue);
Assert.Equal(enableSensitiveTelemetryData ? "## Memories\nConsider the following memories when answering user questions:\nName is Caoimhe" : expectedRedaction, messageTextValue);
}
}
}
@@ -85,7 +85,8 @@ public sealed class TextSearchProviderTests
{
SearchTime = TextSearchProviderOptions.TextSearchBehavior.BeforeAIInvoke,
ContextPrompt = overrideContextPrompt,
CitationsPrompt = overrideCitationsPrompt
CitationsPrompt = overrideCitationsPrompt,
EnableSensitiveTelemetryData = true
};
var provider = new TextSearchProvider(SearchDelegateAsync, options, withLogging ? this._loggerFactoryMock.Object : null);
@@ -164,6 +165,65 @@ public sealed class TextSearchProviderTests
}
}
[Theory]
[InlineData(false, false)]
[InlineData(false, true)]
[InlineData(true, false)]
[InlineData(true, true)]
public async Task InvokingAsync_RedactsLogDataBasedOnOptionsAsync(bool enableSensitiveTelemetryData, bool useCustomRedactor)
{
// Arrange
List<TextSearchProvider.TextSearchResult> results =
[
new() { SourceName = "Doc1", SourceLink = "http://example.com/doc1", Text = "Content of Doc1" }
];
Task<IEnumerable<TextSearchProvider.TextSearchResult>> SearchDelegateAsync(string input, CancellationToken ct)
{
return Task.FromResult<IEnumerable<TextSearchProvider.TextSearchResult>>(results);
}
var options = new TextSearchProviderOptions
{
SearchTime = TextSearchProviderOptions.TextSearchBehavior.BeforeAIInvoke,
EnableSensitiveTelemetryData = enableSensitiveTelemetryData,
Redactor = useCustomRedactor ? new ReplacingRedactor("***") : null
};
var provider = new TextSearchProvider(SearchDelegateAsync, options, this._loggerFactoryMock.Object);
var invokingContext = new AIContextProvider.InvokingContext(
s_mockAgent,
new TestAgentSession(),
new AIContext { Messages = new List<ChatMessage> { new(ChatRole.User, "Sample user question?") } });
// Act
await provider.InvokingAsync(invokingContext, CancellationToken.None);
// Assert — EnableSensitiveTelemetryData takes precedence over Redactor
var traceInvocation = this._loggerMock.Invocations
.Where(i => i.Method.Name == nameof(ILogger.Log))
.FirstOrDefault(i => (LogLevel)i.Arguments[0]! == LogLevel.Trace);
Assert.NotNull(traceInvocation);
var state = Assert.IsType<IReadOnlyList<KeyValuePair<string, object?>>>(traceInvocation.Arguments[2], exactMatch: false);
var inputValue = state.First(kvp => kvp.Key == "Input").Value;
var messageTextValue = state.First(kvp => kvp.Key == "MessageText").Value;
if (enableSensitiveTelemetryData)
{
// EnableSensitiveTelemetryData=true: raw data passes through regardless of Redactor
Assert.Equal("Sample user question?", inputValue);
Assert.Contains("Content of Doc1", messageTextValue?.ToString()!);
}
else
{
// EnableSensitiveTelemetryData=false: custom redactor or default placeholder
string expectedRedaction = useCustomRedactor ? "***" : "<redacted>";
Assert.Equal(expectedRedaction, inputValue);
Assert.Equal(expectedRedaction, messageTextValue);
}
}
[Theory]
[InlineData(null, null, "Search", "Allows searching for additional information to help answer the user question.")]
[InlineData("CustomSearch", "CustomDescription", "CustomSearch", "CustomDescription")]
@@ -270,16 +270,21 @@ public class ChatHistoryMemoryProviderTests
}
[Theory]
[InlineData(false, false, 0)]
[InlineData(true, false, 0)]
[InlineData(false, true, 2)]
[InlineData(true, true, 2)]
public async Task InvokedAsync_LogsUserIdBasedOnEnableSensitiveTelemetryDataAsync(bool enableSensitiveTelemetryData, bool requestThrows, int expectedLogInvocations)
[InlineData(false, false, false, 0)]
[InlineData(false, false, true, 0)]
[InlineData(true, false, false, 0)]
[InlineData(true, false, true, 0)]
[InlineData(false, true, false, 2)]
[InlineData(false, true, true, 2)]
[InlineData(true, true, false, 2)]
[InlineData(true, true, true, 2)]
public async Task InvokedAsync_RedactsLogDataBasedOnOptionsAsync(bool enableSensitiveTelemetryData, bool requestThrows, bool useCustomRedactor, int expectedLogInvocations)
{
// Arrange
var options = new ChatHistoryMemoryProviderOptions
{
EnableSensitiveTelemetryData = enableSensitiveTelemetryData
EnableSensitiveTelemetryData = enableSensitiveTelemetryData,
Redactor = useCustomRedactor ? new ReplacingRedactor("***") : null
};
if (requestThrows)
@@ -309,7 +314,7 @@ public class ChatHistoryMemoryProviderTests
// Act
await provider.InvokedAsync(invokedContext, CancellationToken.None);
// Assert
// Assert — EnableSensitiveTelemetryData takes precedence over Redactor
Assert.Equal(expectedLogInvocations, this._loggerMock.Invocations.Count);
foreach (var logInvocation in this._loggerMock.Invocations)
{
@@ -320,7 +325,8 @@ public class ChatHistoryMemoryProviderTests
var state = Assert.IsType<IReadOnlyList<KeyValuePair<string, object?>>>(logInvocation.Arguments[2], exactMatch: false);
var userIdValue = state.First(kvp => kvp.Key == "UserId").Value;
Assert.Equal(enableSensitiveTelemetryData ? "user1" : "<redacted>", userIdValue);
string expectedRedaction = enableSensitiveTelemetryData ? "user1" : (useCustomRedactor ? "***" : "<redacted>");
Assert.Equal(expectedRedaction, userIdValue);
}
}
@@ -526,17 +532,22 @@ public class ChatHistoryMemoryProviderTests
}
[Theory]
[InlineData(false, false, 2)]
[InlineData(true, false, 2)]
[InlineData(false, true, 2)]
[InlineData(true, true, 2)]
public async Task InvokingAsync_LogsUserIdBasedOnEnableSensitiveTelemetryDataAsync(bool enableSensitiveTelemetryData, bool requestThrows, int expectedLogInvocations)
[InlineData(false, false, false, 2)]
[InlineData(false, false, true, 2)]
[InlineData(true, false, false, 2)]
[InlineData(true, false, true, 2)]
[InlineData(false, true, false, 2)]
[InlineData(false, true, true, 2)]
[InlineData(true, true, false, 2)]
[InlineData(true, true, true, 2)]
public async Task InvokingAsync_RedactsLogDataBasedOnOptionsAsync(bool enableSensitiveTelemetryData, bool requestThrows, bool useCustomRedactor, int expectedLogInvocations)
{
// Arrange
var options = new ChatHistoryMemoryProviderOptions
{
SearchTime = ChatHistoryMemoryProviderOptions.SearchBehavior.BeforeAIInvoke,
EnableSensitiveTelemetryData = enableSensitiveTelemetryData
EnableSensitiveTelemetryData = enableSensitiveTelemetryData,
Redactor = useCustomRedactor ? new ReplacingRedactor("***") : null
};
var scope = new ChatHistoryMemoryProviderScope
@@ -578,7 +589,8 @@ public class ChatHistoryMemoryProviderTests
// Act
await provider.InvokingAsync(invokingContext, CancellationToken.None);
// Assert
// Assert — EnableSensitiveTelemetryData takes precedence over Redactor
string expectedRedaction = enableSensitiveTelemetryData ? "user1" : (useCustomRedactor ? "***" : "<redacted>");
Assert.Equal(expectedLogInvocations, this._loggerMock.Invocations.Count);
foreach (var logInvocation in this._loggerMock.Invocations)
{
@@ -589,18 +601,18 @@ public class ChatHistoryMemoryProviderTests
var state = Assert.IsType<IReadOnlyList<KeyValuePair<string, object?>>>(logInvocation.Arguments[2], exactMatch: false);
var userIdValue = state.First(kvp => kvp.Key == "UserId").Value;
Assert.Equal(enableSensitiveTelemetryData ? "user1" : "<redacted>", userIdValue);
Assert.Equal(expectedRedaction, userIdValue);
var inputValue = state.FirstOrDefault(kvp => kvp.Key == "Input").Value;
if (inputValue != null)
{
Assert.Equal(enableSensitiveTelemetryData ? "Who am I?" : "<redacted>", inputValue);
Assert.Equal(enableSensitiveTelemetryData ? "Who am I?" : expectedRedaction, inputValue);
}
var messageTextValue = state.FirstOrDefault(kvp => kvp.Key == "MessageText").Value;
if (messageTextValue != null)
{
Assert.Equal(enableSensitiveTelemetryData ? "## Memories\nConsider the following memories when answering user questions:\nName is Caoimhe" : "<redacted>", messageTextValue);
Assert.Equal(enableSensitiveTelemetryData ? "## Memories\nConsider the following memories when answering user questions:\nName is Caoimhe" : expectedRedaction, messageTextValue);
}
}
}