.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
Unverified
parent cc85bbc2dc
commit 2c000b032d
19 changed files with 277 additions and 40 deletions
@@ -10,6 +10,7 @@ using System.Threading;
using System.Threading.Tasks;
using Azure.AI.Projects;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Compliance.Redaction;
using Microsoft.Extensions.Logging;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;
@@ -37,7 +38,7 @@ public sealed class FoundryMemoryProvider : AIContextProvider
private readonly string _memoryStoreName;
private readonly int _maxMemories;
private readonly int _updateDelay;
private readonly bool _enableSensitiveTelemetryData;
private readonly Redactor _redactor;
private readonly AIProjectClient _client;
private readonly ILogger<FoundryMemoryProvider>? _logger;
@@ -79,7 +80,7 @@ public sealed class FoundryMemoryProvider : AIContextProvider
this._memoryStoreName = memoryStoreName;
this._maxMemories = effectiveOptions.MaxMemories;
this._updateDelay = effectiveOptions.UpdateDelay;
this._enableSensitiveTelemetryData = effectiveOptions.EnableSensitiveTelemetryData;
this._redactor = effectiveOptions.EnableSensitiveTelemetryData ? NullRedactor.Instance : (effectiveOptions.Redactor ?? new ReplacingRedactor("<redacted>"));
}
/// <inheritdoc />
@@ -416,7 +417,7 @@ public sealed class FoundryMemoryProvider : AIContextProvider
private static bool IsAllowedRole(ChatRole role) =>
role == ChatRole.User || role == ChatRole.Assistant || role == ChatRole.System;
private string? SanitizeLogData(string? data) => this._enableSensitiveTelemetryData ? data : "<redacted>";
private string SanitizeLogData(string? data) => this._redactor.Redact(data);
/// <summary>
/// Represents the state of a <see cref="FoundryMemoryProvider"/> stored in the <see cref="AgentSession.StateBag"/>.
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Compliance.Redaction;
namespace Microsoft.Agents.AI.FoundryMemory;
@@ -37,8 +38,22 @@ public sealed class FoundryMemoryProviderOptions
/// Gets or sets a value indicating whether sensitive data such as user ids and user messages may appear in logs.
/// </summary>
/// <value>Defaults to <see langword="false"/>.</value>
/// <remarks>
/// When set to <see langword="true"/>, sensitive data is passed through to logs unchanged and any
/// configured <see cref="Redactor"/> is ignored. This property takes precedence over <see cref="Redactor"/>.
/// </remarks>
public bool EnableSensitiveTelemetryData { get; set; }
/// <summary>
/// Gets or sets a custom <see cref="Redactor"/> used to redact sensitive data in log output.
/// </summary>
/// <value>
/// When <see langword="null"/> (the default), sensitive data is replaced with a placeholder.
/// When set, this redactor is used to transform sensitive values before they are logged.
/// Ignored when <see cref="EnableSensitiveTelemetryData"/> is <see langword="true"/>.
/// </value>
public Redactor? Redactor { get; set; }
/// <summary>
/// Gets or sets the key used to store the provider state in the session's <see cref="AgentSessionStateBag"/>.
/// </summary>
@@ -8,6 +8,7 @@
<PropertyGroup>
<InjectSharedThrow>true</InjectSharedThrow>
<InjectSharedDiagnosticIds>true</InjectSharedDiagnosticIds>
<InjectSharedRedaction>true</InjectSharedRedaction>
<InjectExperimentalAttributeOnLegacy>true</InjectExperimentalAttributeOnLegacy>
<InjectTrimAttributesOnLegacy>true</InjectTrimAttributesOnLegacy>
</PropertyGroup>
@@ -20,6 +21,7 @@
<ItemGroup>
<PackageReference Include="Azure.AI.Projects" />
<PackageReference Include="Microsoft.Extensions.Compliance.Abstractions" />
<PackageReference Include="OpenAI" />
</ItemGroup>
@@ -8,6 +8,7 @@ using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Compliance.Redaction;
using Microsoft.Extensions.Logging;
using Microsoft.Shared.Diagnostics;
@@ -51,7 +52,7 @@ public sealed class Mem0Provider : MessageAIContextProvider
private readonly ProviderSessionState<State> _sessionState;
private IReadOnlyList<string>? _stateKeys;
private readonly string _contextPrompt;
private readonly bool _enableSensitiveTelemetryData;
private readonly Redactor _redactor;
private readonly Mem0Client _client;
private readonly ILogger<Mem0Provider>? _logger;
@@ -91,7 +92,7 @@ public sealed class Mem0Provider : MessageAIContextProvider
this._client = new Mem0Client(httpClient);
this._contextPrompt = options?.ContextPrompt ?? DefaultContextPrompt;
this._enableSensitiveTelemetryData = options?.EnableSensitiveTelemetryData ?? false;
this._redactor = options?.EnableSensitiveTelemetryData == true ? NullRedactor.Instance : (options?.Redactor ?? new ReplacingRedactor("<redacted>"));
}
/// <inheritdoc />
@@ -297,5 +298,5 @@ public sealed class Mem0Provider : MessageAIContextProvider
public Mem0ProviderScope SearchScope { get; }
}
private string? SanitizeLogData(string? data) => this._enableSensitiveTelemetryData ? data : "<redacted>";
private string SanitizeLogData(string? data) => this._redactor.Redact(data);
}
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Compliance.Redaction;
namespace Microsoft.Agents.AI.Mem0;
@@ -21,8 +22,22 @@ public sealed class Mem0ProviderOptions
/// Gets or sets a value indicating whether sensitive data such as user ids and user messages may appear in logs.
/// </summary>
/// <value>Defaults to <see langword="false"/>.</value>
/// <remarks>
/// When set to <see langword="true"/>, sensitive data is passed through to logs unchanged and any
/// configured <see cref="Redactor"/> is ignored. This property takes precedence over <see cref="Redactor"/>.
/// </remarks>
public bool EnableSensitiveTelemetryData { get; set; }
/// <summary>
/// Gets or sets a custom <see cref="Redactor"/> used to redact sensitive data in log output.
/// </summary>
/// <value>
/// When <see langword="null"/> (the default), sensitive data is replaced with a placeholder.
/// When set, this redactor is used to transform sensitive values before they are logged.
/// Ignored when <see cref="EnableSensitiveTelemetryData"/> is <see langword="true"/>.
/// </value>
public Redactor? Redactor { get; set; }
/// <summary>
/// Gets or sets the key used to store the provider state in the session's <see cref="AgentSessionStateBag"/>.
/// </summary>
@@ -6,6 +6,7 @@
<PropertyGroup>
<InjectSharedThrow>true</InjectSharedThrow>
<InjectSharedRedaction>true</InjectSharedRedaction>
<InjectTrimAttributesOnLegacy>true</InjectTrimAttributesOnLegacy>
</PropertyGroup>
@@ -23,6 +24,10 @@
<ProjectReference Include="..\Microsoft.Agents.AI.Abstractions\Microsoft.Agents.AI.Abstractions.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Compliance.Abstractions" />
</ItemGroup>
<PropertyGroup>
<!-- NuGet Package Settings -->
<Title>Microsoft Agent Framework - Mem0 integration</Title>
@@ -7,6 +7,7 @@ using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Compliance.Redaction;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.VectorData;
using Microsoft.Shared.Diagnostics;
@@ -80,7 +81,7 @@ public sealed class ChatHistoryMemoryProvider : MessageAIContextProvider, IDispo
private readonly VectorStoreCollection<object, Dictionary<string, object?>> _collection;
private readonly int _maxResults;
private readonly string _contextPrompt;
private readonly bool _enableSensitiveTelemetryData;
private readonly Redactor _redactor;
private readonly ChatHistoryMemoryProviderOptions.SearchBehavior _searchTime;
private readonly string _toolName;
private readonly string _toolDescription;
@@ -118,7 +119,7 @@ public sealed class ChatHistoryMemoryProvider : MessageAIContextProvider, IDispo
options ??= new ChatHistoryMemoryProviderOptions();
this._maxResults = options.MaxResults.HasValue ? Throw.IfLessThanOrEqual(options.MaxResults.Value, 0) : DefaultMaxResults;
this._contextPrompt = options.ContextPrompt ?? DefaultContextPrompt;
this._enableSensitiveTelemetryData = options.EnableSensitiveTelemetryData;
this._redactor = options.EnableSensitiveTelemetryData ? NullRedactor.Instance : (options.Redactor ?? new ReplacingRedactor("<redacted>"));
this._searchTime = options.SearchTime;
this._logger = loggerFactory?.CreateLogger<ChatHistoryMemoryProvider>();
this._toolName = options.FunctionToolName ?? DefaultFunctionToolName;
@@ -485,7 +486,7 @@ public sealed class ChatHistoryMemoryProvider : MessageAIContextProvider, IDispo
GC.SuppressFinalize(this);
}
private string? SanitizeLogData(string? data) => this._enableSensitiveTelemetryData ? data : "<redacted>";
private string SanitizeLogData(string? data) => this._redactor.Redact(data);
/// <summary>
/// Rebinds a filter expression's body to use the specified shared parameter,
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Compliance.Redaction;
namespace Microsoft.Agents.AI;
@@ -46,8 +47,22 @@ public sealed class ChatHistoryMemoryProviderOptions
/// Gets or sets a value indicating whether sensitive data such as user ids and user messages may appear in logs.
/// </summary>
/// <value>Defaults to <see langword="false"/>.</value>
/// <remarks>
/// When set to <see langword="true"/>, sensitive data is passed through to logs unchanged and any
/// configured <see cref="Redactor"/> is ignored. This property takes precedence over <see cref="Redactor"/>.
/// </remarks>
public bool EnableSensitiveTelemetryData { get; set; }
/// <summary>
/// Gets or sets a custom <see cref="Redactor"/> used to redact sensitive data in log output.
/// </summary>
/// <value>
/// When <see langword="null"/> (the default), sensitive data is replaced with a placeholder.
/// When set, this redactor is used to transform sensitive values before they are logged.
/// Ignored when <see cref="EnableSensitiveTelemetryData"/> is <see langword="true"/>.
/// </value>
public Redactor? Redactor { get; set; }
/// <summary>
/// Gets or sets the key used to store provider state in the <see cref="AgentSession.StateBag"/>.
/// </summary>
@@ -8,6 +8,7 @@
<PropertyGroup>
<InjectSharedThrow>true</InjectSharedThrow>
<InjectSharedDiagnosticIds>true</InjectSharedDiagnosticIds>
<InjectSharedRedaction>true</InjectSharedRedaction>
<InjectDiagnosticClassesOnLegacy>true</InjectDiagnosticClassesOnLegacy>
<InjectExperimentalAttributeOnLegacy>true</InjectExperimentalAttributeOnLegacy>
<InjectTrimAttributesOnLegacy>true</InjectTrimAttributesOnLegacy>
@@ -22,6 +23,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.AI" />
<PackageReference Include="Microsoft.Extensions.Compliance.Abstractions" />
<PackageReference Include="Microsoft.Extensions.VectorData.Abstractions" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
@@ -7,6 +7,7 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Compliance.Redaction;
using Microsoft.Extensions.Logging;
using Microsoft.Shared.Diagnostics;
@@ -62,6 +63,7 @@ public sealed class TextSearchProvider : MessageAIContextProvider
private readonly string _contextPrompt;
private readonly string _citationsPrompt;
private readonly Func<IList<TextSearchResult>, string>? _contextFormatter;
private readonly Redactor _redactor;
/// <summary>
/// Initializes a new instance of the <see cref="TextSearchProvider"/> class.
@@ -89,6 +91,7 @@ public sealed class TextSearchProvider : MessageAIContextProvider
this._contextPrompt = options?.ContextPrompt ?? DefaultContextPrompt;
this._citationsPrompt = options?.CitationsPrompt ?? DefaultCitationsPrompt;
this._contextFormatter = options?.ContextFormatter;
this._redactor = options?.EnableSensitiveTelemetryData == true ? NullRedactor.Instance : (options?.Redactor ?? new ReplacingRedactor("<redacted>"));
// Create the on-demand search tool (only used if behavior is OnDemandFunctionCalling)
this._tools =
@@ -180,7 +183,7 @@ public sealed class TextSearchProvider : MessageAIContextProvider
if (this._logger?.IsEnabled(LogLevel.Trace) is true)
{
this._logger.LogTrace("TextSearchProvider: Search Results\nInput:{Input}\nOutput:{MessageText}", input, formatted);
this._logger.LogTrace("TextSearchProvider: Search Results\nInput:{Input}\nOutput:{MessageText}", this.SanitizeLogData(input), this.SanitizeLogData(formatted));
}
return [new ChatMessage(ChatRole.User, formatted)];
@@ -249,7 +252,7 @@ public sealed class TextSearchProvider : MessageAIContextProvider
if (this._logger.IsEnabled(LogLevel.Trace))
{
this._logger.LogTrace("TextSearchProvider Input:{UserQuestion}\nOutput:{MessageText}", userQuestion, outputText);
this._logger.LogTrace("TextSearchProvider Input:{UserQuestion}\nOutput:{MessageText}", this.SanitizeLogData(userQuestion), this.SanitizeLogData(outputText));
}
}
@@ -325,6 +328,8 @@ public sealed class TextSearchProvider : MessageAIContextProvider
public object? RawRepresentation { get; set; }
}
private string SanitizeLogData(string? data) => this._redactor.Redact(data);
/// <summary>
/// Represents the per-session state of a <see cref="TextSearchProvider"/> stored in the <see cref="AgentSession.StateBag"/>.
/// </summary>
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Compliance.Redaction;
namespace Microsoft.Agents.AI;
@@ -117,6 +118,26 @@ public sealed class TextSearchProviderOptions
/// </value>
public List<ChatRole>? RecentMessageRolesIncluded { get; set; }
/// <summary>
/// Gets or sets a value indicating whether sensitive data such as user queries and search results may appear in logs.
/// </summary>
/// <value>Defaults to <see langword="false"/>.</value>
/// <remarks>
/// When set to <see langword="true"/>, sensitive data is passed through to logs unchanged and any
/// configured <see cref="Redactor"/> is ignored. This property takes precedence over <see cref="Redactor"/>.
/// </remarks>
public bool EnableSensitiveTelemetryData { get; set; }
/// <summary>
/// Gets or sets a custom <see cref="Redactor"/> used to redact sensitive data in log output.
/// </summary>
/// <value>
/// When <see langword="null"/> (the default), sensitive data is replaced with a placeholder.
/// When set, this redactor is used to transform sensitive values before they are logged.
/// Ignored when <see cref="EnableSensitiveTelemetryData"/> is <see langword="true"/>.
/// </value>
public Redactor? Redactor { get; set; }
/// <summary>
/// Behavior choices for the provider.
/// </summary>
+30
View File
@@ -0,0 +1,30 @@
# Redaction
Log data redaction utilities built on `Microsoft.Extensions.Compliance.Redaction.Redactor`.
Provides `ReplacingRedactor`, an internal `Redactor` implementation that replaces
any input with a fixed replacement string (e.g. `"<redacted>"`).
To use this in your project, add the following to your `.csproj` file:
```xml
<PropertyGroup>
<InjectSharedRedaction>true</InjectSharedRedaction>
</PropertyGroup>
```
You will also need to add a package reference to `Microsoft.Extensions.Compliance.Abstractions`:
```xml
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Compliance.Abstractions" />
</ItemGroup>
```
And finally, this also depends on the shared Throw class, so when using redaction, InjectSharedThrow should also be enabled:
```xml
<PropertyGroup>
<InjectSharedThrow>true</InjectSharedThrow>
</PropertyGroup>
```
@@ -0,0 +1,35 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Microsoft.Extensions.Compliance.Redaction;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
/// <summary>
/// A <see cref="Redactor"/> that replaces the entire input with a fixed replacement string.
/// </summary>
internal sealed class ReplacingRedactor : Redactor
{
private readonly string _replacementText;
/// <summary>
/// Initializes a new instance of the <see cref="ReplacingRedactor"/> class.
/// </summary>
/// <param name="replacementText">The text to substitute for any input value.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="replacementText"/> is <see langword="null"/>.</exception>
public ReplacingRedactor(string replacementText)
{
this._replacementText = Throw.IfNull(replacementText);
}
/// <inheritdoc />
public override int GetRedactedLength(ReadOnlySpan<char> input) => this._replacementText.Length;
/// <inheritdoc />
public override int Redact(ReadOnlySpan<char> source, Span<char> destination)
{
this._replacementText.AsSpan().CopyTo(destination);
return this._replacementText.Length;
}
}