diff --git a/dotnet/samples/Catalog/AgentWithTextSearchRag/Program.cs b/dotnet/samples/Catalog/AgentWithTextSearchRag/Program.cs index 3e86534edd..65f3a9e98f 100644 --- a/dotnet/samples/Catalog/AgentWithTextSearchRag/Program.cs +++ b/dotnet/samples/Catalog/AgentWithTextSearchRag/Program.cs @@ -28,7 +28,7 @@ AIAgent agent = new AzureOpenAIClient( .CreateAIAgent(new ChatClientAgentOptions { Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available.", - AIContextProviderFactory = _ => new TextSearchProvider(MockSearchAsync, textSearchOptions) + AIContextProviderFactory = ctx => new TextSearchProvider(MockSearchAsync, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions) }); AgentThread thread = agent.GetNewThread(); diff --git a/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/Program.cs b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/Program.cs index 611e11c22c..ec665325a7 100644 --- a/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/Program.cs +++ b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/Program.cs @@ -63,9 +63,7 @@ AIAgent agent = azureOpenAIClient .CreateAIAgent(new ChatClientAgentOptions { Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available.", - AIContextProviderFactory = ctx => ctx.SerializedState.ValueKind is not System.Text.Json.JsonValueKind.Null and not System.Text.Json.JsonValueKind.Undefined - ? new TextSearchProvider(SearchAdapter, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions) - : new TextSearchProvider(SearchAdapter, textSearchOptions) + AIContextProviderFactory = ctx => new TextSearchProvider(SearchAdapter, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions) }); AgentThread thread = agent.GetNewThread(); diff --git a/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step02_ExternalDataSourceRAG/Program.cs b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step02_ExternalDataSourceRAG/Program.cs index e29bb58d04..4e8fbf0bde 100644 --- a/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step02_ExternalDataSourceRAG/Program.cs +++ b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step02_ExternalDataSourceRAG/Program.cs @@ -72,9 +72,7 @@ AIAgent agent = azureOpenAIClient .CreateAIAgent(new ChatClientAgentOptions { Instructions = "You are a helpful support specialist for the Microsoft Agent Framework. Answer questions using the provided context and cite the source document when available. Keep responses brief.", - AIContextProviderFactory = ctx => ctx.SerializedState.ValueKind is not System.Text.Json.JsonValueKind.Null and not System.Text.Json.JsonValueKind.Undefined - ? new TextSearchProvider(SearchAdapter, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions) - : new TextSearchProvider(SearchAdapter, textSearchOptions) + AIContextProviderFactory = ctx => new TextSearchProvider(SearchAdapter, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions) }); AgentThread thread = agent.GetNewThread(); diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step18_TextSearchRag/Program.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step18_TextSearchRag/Program.cs index 81a6b29152..65f3a9e98f 100644 --- a/dotnet/samples/GettingStarted/Agents/Agent_Step18_TextSearchRag/Program.cs +++ b/dotnet/samples/GettingStarted/Agents/Agent_Step18_TextSearchRag/Program.cs @@ -28,9 +28,7 @@ AIAgent agent = new AzureOpenAIClient( .CreateAIAgent(new ChatClientAgentOptions { Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available.", - AIContextProviderFactory = ctx => ctx.SerializedState.ValueKind is not System.Text.Json.JsonValueKind.Null and not System.Text.Json.JsonValueKind.Undefined - ? new TextSearchProvider(MockSearchAsync, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions) - : new TextSearchProvider(MockSearchAsync, textSearchOptions) + AIContextProviderFactory = ctx => new TextSearchProvider(MockSearchAsync, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions) }); AgentThread thread = agent.GetNewThread(); diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step19_Mem0Provider/Program.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step19_Mem0Provider/Program.cs index 21aa9d3e1d..539ebbaecb 100644 --- a/dotnet/samples/GettingStarted/Agents/Agent_Step19_Mem0Provider/Program.cs +++ b/dotnet/samples/GettingStarted/Agents/Agent_Step19_Mem0Provider/Program.cs @@ -33,9 +33,9 @@ AIAgent agent = new AzureOpenAIClient( Instructions = "You are a friendly travel assistant. Use known memories about the user when responding, and do not invent details.", AIContextProviderFactory = ctx => ctx.SerializedState.ValueKind is not JsonValueKind.Null or JsonValueKind.Undefined // If each thread should have its own Mem0 scope, you can create a new id per thread here: - // ? new Mem0Provider(mem0HttpClient, new Mem0ProviderOptions() { ThreadId = Guid.NewGuid().ToString() }) + // ? new Mem0Provider(mem0HttpClient, new Mem0ProviderScope() { ThreadId = Guid.NewGuid().ToString() }) // In this case we are storing memories scoped by application and user instead so that memories are retained across threads. - ? new Mem0Provider(mem0HttpClient, new Mem0ProviderOptions() { ApplicationId = "getting-started-agents", UserId = "sample-user" }) + ? new Mem0Provider(mem0HttpClient, new Mem0ProviderScope() { ApplicationId = "getting-started-agents", UserId = "sample-user" }) // For cases where we are restoring from serialized state: : new Mem0Provider(mem0HttpClient, ctx.SerializedState, ctx.JsonSerializerOptions) }); diff --git a/dotnet/src/Microsoft.Agents.AI.Mem0/Mem0Provider.cs b/dotnet/src/Microsoft.Agents.AI.Mem0/Mem0Provider.cs index d0f6f78b3f..4aae5de59b 100644 --- a/dotnet/src/Microsoft.Agents.AI.Mem0/Mem0Provider.cs +++ b/dotnet/src/Microsoft.Agents.AI.Mem0/Mem0Provider.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI; @@ -31,10 +32,15 @@ public sealed class Mem0Provider : AIContextProvider private readonly Mem0Client _client; private readonly ILogger? _logger; + private readonly Mem0ProviderScope _storageScope; + private readonly Mem0ProviderScope _searchScope; + /// /// Initializes a new instance of the class. /// /// Configured (base address + auth). + /// Optional values to scope the memory storage with. + /// Optional values to scope the memory search with. Defaults to if not provided. /// Provider options. /// Optional logger factory. /// @@ -47,21 +53,35 @@ public sealed class Mem0Provider : AIContextProvider /// new Mem0AIContextProvider(httpClient); /// /// - public Mem0Provider(HttpClient httpClient, Mem0ProviderOptions? options = null, ILoggerFactory? loggerFactory = null) + public Mem0Provider(HttpClient httpClient, Mem0ProviderScope storageScope, Mem0ProviderScope? searchScope = null, Mem0ProviderOptions? options = null, ILoggerFactory? loggerFactory = null) { if (string.IsNullOrWhiteSpace(httpClient.BaseAddress?.AbsoluteUri)) { throw new ArgumentException("The HttpClient BaseAddress must be set for Mem0 operations.", nameof(httpClient)); } - this.ApplicationId = options?.ApplicationId; - this.AgentId = options?.AgentId; - this.ThreadId = options?.ThreadId; - this.UserId = options?.UserId; - this._contextPrompt = options?.ContextPrompt ?? DefaultContextPrompt; - this._logger = loggerFactory?.CreateLogger(); this._client = new Mem0Client(httpClient); + + this._contextPrompt = options?.ContextPrompt ?? DefaultContextPrompt; + this._storageScope = new Mem0ProviderScope(Throw.IfNull(storageScope)); + this._searchScope = searchScope ?? storageScope; + + if (string.IsNullOrWhiteSpace(this._storageScope.ApplicationId) + && string.IsNullOrWhiteSpace(this._storageScope.AgentId) + && string.IsNullOrWhiteSpace(this._storageScope.ThreadId) + && string.IsNullOrWhiteSpace(this._storageScope.UserId)) + { + throw new ArgumentException("At least one of ApplicationId, AgentId, ThreadId, or UserId must be provided for the storage scope."); + } + + if (string.IsNullOrWhiteSpace(this._searchScope.ApplicationId) + && string.IsNullOrWhiteSpace(this._searchScope.AgentId) + && string.IsNullOrWhiteSpace(this._searchScope.ThreadId) + && string.IsNullOrWhiteSpace(this._searchScope.UserId)) + { + throw new ArgumentException("At least one of ApplicationId, AgentId, ThreadId, or UserId must be provided for the search scope."); + } } /// @@ -70,6 +90,7 @@ public sealed class Mem0Provider : AIContextProvider /// Configured (base address + auth). /// A representing the serialized state of the store. /// Optional settings for customizing the JSON deserialization process. + /// Provider options. /// Optional logger factory. /// /// @@ -82,46 +103,30 @@ public sealed class Mem0Provider : AIContextProvider /// new Mem0AIContextProvider(httpClient, state); /// /// - public Mem0Provider(HttpClient httpClient, JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, ILoggerFactory? loggerFactory = null) + public Mem0Provider(HttpClient httpClient, JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, Mem0ProviderOptions? options = null, ILoggerFactory? loggerFactory = null) { if (string.IsNullOrWhiteSpace(httpClient.BaseAddress?.AbsoluteUri)) { throw new ArgumentException("The HttpClient BaseAddress must be set for Mem0 operations.", nameof(httpClient)); } + this._logger = loggerFactory?.CreateLogger(); + this._client = new Mem0Client(httpClient); + + this._contextPrompt = options?.ContextPrompt ?? DefaultContextPrompt; + var jso = jsonSerializerOptions ?? Mem0JsonUtilities.DefaultOptions; var state = serializedState.Deserialize(jso.GetTypeInfo(typeof(Mem0State))) as Mem0State; - this.ApplicationId = state?.ApplicationId; - this.AgentId = state?.AgentId; - this.ThreadId = state?.ThreadId; - this.UserId = state?.UserId; - this._contextPrompt = state?.ContextPrompt ?? DefaultContextPrompt; + if (state == null || state.StorageScope == null || state.SearchScope == null) + { + throw new InvalidOperationException("The Mem0Provider state did not contain the required scope properties."); + } - this._logger = loggerFactory?.CreateLogger(); - this._client = new Mem0Client(httpClient); + this._storageScope = state.StorageScope; + this._searchScope = state.SearchScope; } - /// - /// Gets or sets an optional ID for the application to scope memories to. - /// - public string? ApplicationId { get; set; } - - /// - /// Gets or sets an optional ID for the agent to scope memories to. - /// - public string? AgentId { get; set; } - - /// - /// Gets or sets an optional ID for the thread to scope memories to. - /// - public string? ThreadId { get; set; } - - /// - /// Gets or sets an optional ID for the user to scope memories to. - /// - public string? UserId { get; set; } - /// public override async ValueTask InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default) { @@ -134,10 +139,10 @@ public sealed class Mem0Provider : AIContextProvider try { var memories = (await this._client.SearchAsync( - this.ApplicationId, - this.AgentId, - this.ThreadId, - this.UserId, + this._searchScope.ApplicationId, + this._searchScope.AgentId, + this._searchScope.ThreadId, + this._searchScope.UserId, queryText, cancellationToken).ConfigureAwait(false)).ToList(); @@ -150,20 +155,20 @@ public sealed class Mem0Provider : AIContextProvider this._logger.LogInformation( "Mem0AIContextProvider: Retrieved {Count} memories. ApplicationId: '{ApplicationId}', AgentId: '{AgentId}', ThreadId: '{ThreadId}', UserId: '{UserId}'", memories.Count, - this.ApplicationId, - this.AgentId, - this.ThreadId, - this.UserId); + this._searchScope.ApplicationId, + this._searchScope.AgentId, + this._searchScope.ThreadId, + this._searchScope.UserId); if (outputMessageText is not null) { this._logger.LogTrace( "Mem0AIContextProvider: Search Results\nInput:{Input}\nOutput:{MessageText}\nApplicationId: '{ApplicationId}', AgentId: '{AgentId}', ThreadId: '{ThreadId}', UserId: '{UserId}'", queryText, outputMessageText, - this.ApplicationId, - this.AgentId, - this.ThreadId, - this.UserId); + this._searchScope.ApplicationId, + this._searchScope.AgentId, + this._searchScope.ThreadId, + this._searchScope.UserId); } } @@ -181,10 +186,10 @@ public sealed class Mem0Provider : AIContextProvider this._logger?.LogError( ex, "Mem0AIContextProvider: Failed to search Mem0 for memories due to error. ApplicationId: '{ApplicationId}', AgentId: '{AgentId}', ThreadId: '{ThreadId}', UserId: '{UserId}'", - this.ApplicationId, - this.AgentId, - this.ThreadId, - this.UserId); + this._searchScope.ApplicationId, + this._searchScope.AgentId, + this._searchScope.ThreadId, + this._searchScope.UserId); return new AIContext(); } } @@ -207,10 +212,10 @@ public sealed class Mem0Provider : AIContextProvider this._logger?.LogError( ex, "Mem0AIContextProvider: Failed to send messages to Mem0 due to error. ApplicationId: '{ApplicationId}', AgentId: '{AgentId}', ThreadId: '{ThreadId}', UserId: '{UserId}'", - this.ApplicationId, - this.AgentId, - this.ThreadId, - this.UserId); + this._storageScope.ApplicationId, + this._storageScope.AgentId, + this._storageScope.ThreadId, + this._storageScope.UserId); } } @@ -220,23 +225,16 @@ public sealed class Mem0Provider : AIContextProvider /// Cancellation token. public Task ClearStoredMemoriesAsync(CancellationToken cancellationToken = default) => this._client.ClearMemoryAsync( - this.ApplicationId, - this.AgentId, - this.ThreadId, - this.UserId, + this._storageScope.ApplicationId, + this._storageScope.AgentId, + this._storageScope.ThreadId, + this._storageScope.UserId, cancellationToken); /// public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null) { - var state = new Mem0State - { - ApplicationId = this.ApplicationId, - AgentId = this.AgentId, - ThreadId = this.ThreadId, - UserId = this.UserId, - ContextPrompt = this._contextPrompt == DefaultContextPrompt ? null : this._contextPrompt - }; + var state = new Mem0State(this._storageScope, this._searchScope); var jso = jsonSerializerOptions ?? Mem0JsonUtilities.DefaultOptions; return JsonSerializer.SerializeToElement(state, jso.GetTypeInfo(typeof(Mem0State))); @@ -262,10 +260,10 @@ public sealed class Mem0Provider : AIContextProvider } await this._client.CreateMemoryAsync( - this.ApplicationId, - this.AgentId, - this.ThreadId, - this.UserId, + this._storageScope.ApplicationId, + this._storageScope.AgentId, + this._storageScope.ThreadId, + this._storageScope.UserId, message.Text, message.Role.Value, cancellationToken).ConfigureAwait(false); @@ -274,10 +272,14 @@ public sealed class Mem0Provider : AIContextProvider internal sealed class Mem0State { - public string? ApplicationId { get; set; } - public string? AgentId { get; set; } - public string? UserId { get; set; } - public string? ThreadId { get; set; } - public string? ContextPrompt { get; set; } + [JsonConstructor] + public Mem0State(Mem0ProviderScope storageScope, Mem0ProviderScope searchScope) + { + this.StorageScope = storageScope; + this.SearchScope = searchScope; + } + + public Mem0ProviderScope StorageScope { get; set; } + public Mem0ProviderScope SearchScope { get; set; } } } diff --git a/dotnet/src/Microsoft.Agents.AI.Mem0/Mem0ProviderOptions.cs b/dotnet/src/Microsoft.Agents.AI.Mem0/Mem0ProviderOptions.cs index f9987698a8..34b0392bec 100644 --- a/dotnet/src/Microsoft.Agents.AI.Mem0/Mem0ProviderOptions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Mem0/Mem0ProviderOptions.cs @@ -5,35 +5,8 @@ namespace Microsoft.Agents.AI.Mem0; /// /// Options for configuring the . /// -/// -/// Mem0 memories can be scoped by one or more of: application, agent, thread, and user. -/// At least one scope must be provided; otherwise Mem0 will reject requests. -/// public sealed class Mem0ProviderOptions { - /// - /// Gets or sets an optional ID for the application to scope memories to. - /// - /// If not set, the scope of the memories will span all applications. - public string? ApplicationId { get; set; } - - /// - /// Gets or sets an optional ID for the agent to scope memories to. - /// - /// If not set, the scope of the memories will span all agents. - public string? AgentId { get; set; } - - /// - /// Gets or sets an optional ID for the thread to scope memories to. - /// - public string? ThreadId { get; set; } - - /// - /// Gets or sets an optional ID for the user to scope memories to. - /// - /// If not set, the scope of the memories will span all users. - public string? UserId { get; set; } - /// /// When providing memories to the model, this string is prefixed to the retrieved memories to supply context. /// diff --git a/dotnet/src/Microsoft.Agents.AI.Mem0/Mem0ProviderScope.cs b/dotnet/src/Microsoft.Agents.AI.Mem0/Mem0ProviderScope.cs new file mode 100644 index 0000000000..ff47549b39 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Mem0/Mem0ProviderScope.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Agents.AI.Mem0; + +/// +/// Allows scoping of memories for the . +/// +/// +/// Mem0 memories can be scoped by one or more of: application, agent, thread, and user. +/// At least one scope must be provided; otherwise Mem0 will reject requests. +/// +public sealed class Mem0ProviderScope +{ + /// + /// Initializes a new instance of the class. + /// + public Mem0ProviderScope() { } + + /// + /// Initializes a new instance of the class by cloning an existing scope. + /// + /// The scope to clone. + public Mem0ProviderScope(Mem0ProviderScope sourceScope) + { + Throw.IfNull(sourceScope); + + this.ApplicationId = sourceScope.ApplicationId; + this.AgentId = sourceScope.AgentId; + this.ThreadId = sourceScope.ThreadId; + this.UserId = sourceScope.UserId; + } + + /// + /// Gets or sets an optional ID for the application to scope memories to. + /// + /// If not set, the scope of the memories will span all applications. + public string? ApplicationId { get; set; } + + /// + /// Gets or sets an optional ID for the agent to scope memories to. + /// + /// If not set, the scope of the memories will span all agents. + public string? AgentId { get; set; } + + /// + /// Gets or sets an optional ID for the thread to scope memories to. + /// + public string? ThreadId { get; set; } + + /// + /// Gets or sets an optional ID for the user to scope memories to. + /// + /// If not set, the scope of the memories will span all users. + public string? UserId { get; set; } +} diff --git a/dotnet/src/Microsoft.Agents.AI/Data/TextSearchProvider.cs b/dotnet/src/Microsoft.Agents.AI/Data/TextSearchProvider.cs index 8cb4582e37..f76629a577 100644 --- a/dotnet/src/Microsoft.Agents.AI/Data/TextSearchProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI/Data/TextSearchProvider.cs @@ -44,82 +44,72 @@ public sealed class TextSearchProvider : AIContextProvider private readonly ILogger? _logger; private readonly AITool[] _tools; private readonly Queue _recentMessagesText; - private readonly TextSearchProviderOptions _options; private readonly List _recentMessageRolesIncluded; + private readonly int _recentMessageMemoryLimit; + private readonly TextSearchProviderOptions.TextSearchBehavior _searchTime; + private readonly string _contextPrompt; + private readonly string _citationsPrompt; + private readonly Func, string>? _contextFormatter; /// /// Initializes a new instance of the class. /// /// Delegate that executes the search logic. Must not be . - /// Optional configuration options. - /// Optional logger factory. - /// Thrown when is . - public TextSearchProvider(Func>> searchAsync, TextSearchProviderOptions? options = null, ILoggerFactory? loggerFactory = null) - { - this._searchAsync = searchAsync ?? throw new ArgumentNullException(nameof(searchAsync)); - this._options = options ?? new(); - Throw.IfLessThan(this._options.RecentMessageMemoryLimit, 0); - this._logger = loggerFactory?.CreateLogger(); - this._recentMessagesText = new(); - this._recentMessageRolesIncluded = this._options.RecentMessageRolesIncluded ?? [ChatRole.User]; - - // Create the on-demand search tool (only used if behavior is OnDemandFunctionCalling) - this._tools = - [ - AIFunctionFactory.Create( - this.SearchAsync, - name: this._options.FunctionToolName ?? DefaultPluginSearchFunctionName, - description: this._options.FunctionToolDescription ?? DefaultPluginSearchFunctionDescription) - ]; - } - - /// - /// Initializes a new instance of the class from previously serialized state. - /// - /// Delegate that executes the search logic. Must not be . /// A representing the serialized provider state. /// Optional serializer options (unused - source generated context is used). /// Optional configuration options. /// Optional logger factory. /// Thrown when is . - /// - /// Only overridden prompts (function name, function description, context prompt, citations prompt) are restored. - /// If a value was not persisted or matches the defaults it will fall back to the built-in defaults. - /// Custom delegates are not serialized. - /// - public TextSearchProvider(Func>> searchAsync, JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, TextSearchProviderOptions? options = null, ILoggerFactory? loggerFactory = null) + public TextSearchProvider( + Func>> searchAsync, + JsonElement serializedState, + JsonSerializerOptions? jsonSerializerOptions = null, + TextSearchProviderOptions? options = null, + ILoggerFactory? loggerFactory = null) { - this._searchAsync = searchAsync ?? throw new ArgumentNullException(nameof(searchAsync)); - this._options = options ?? new(); - Throw.IfLessThan(this._options.RecentMessageMemoryLimit, 0); + // Validate and assign parameters + this._searchAsync = Throw.IfNull(searchAsync); this._logger = loggerFactory?.CreateLogger(); - this._recentMessageRolesIncluded = this._options.RecentMessageRolesIncluded ?? [ChatRole.User]; + this._recentMessageMemoryLimit = Throw.IfLessThan(options?.RecentMessageMemoryLimit ?? 0, 0); + this._recentMessageRolesIncluded = options?.RecentMessageRolesIncluded ?? [ChatRole.User]; + this._searchTime = options?.SearchTime ?? TextSearchProviderOptions.TextSearchBehavior.BeforeAIInvoke; + this._contextPrompt = options?.ContextPrompt ?? DefaultContextPrompt; + this._citationsPrompt = options?.CitationsPrompt ?? DefaultCitationsPrompt; + this._contextFormatter = options?.ContextFormatter; + // Restore recent messages from serialized state if provided List? restoredMessages = null; - - var state = serializedState.Deserialize(AgentJsonUtilities.DefaultOptions.GetTypeInfo(typeof(TextSearchProviderState))) as TextSearchProviderState; - if (state?.RecentMessagesText is { Count: > 0 }) + if (serializedState.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined) { - restoredMessages = state.RecentMessagesText; + this._recentMessagesText = new(); } + else + { + var jso = jsonSerializerOptions ?? AgentJsonUtilities.DefaultOptions; + var state = serializedState.Deserialize(jso.GetTypeInfo(typeof(TextSearchProviderState))) as TextSearchProviderState; + if (state?.RecentMessagesText is { Count: > 0 }) + { + restoredMessages = state.RecentMessagesText; + } - // Restore recent messages respecting the limit (may truncate if limit changed afterwards). - this._recentMessagesText = restoredMessages is null ? new() : new(restoredMessages.Take(this._options.RecentMessageMemoryLimit)); + // Restore recent messages respecting the limit (may truncate if limit changed afterwards). + this._recentMessagesText = restoredMessages is null ? new() : new(restoredMessages.Take(this._recentMessageMemoryLimit)); + } // Create the on-demand search tool (only used if behavior is OnDemandFunctionCalling) this._tools = [ AIFunctionFactory.Create( this.SearchAsync, - name: this._options.FunctionToolName ?? DefaultPluginSearchFunctionName, - description: this._options.FunctionToolDescription ?? DefaultPluginSearchFunctionDescription) + name: options?.FunctionToolName ?? DefaultPluginSearchFunctionName, + description: options?.FunctionToolDescription ?? DefaultPluginSearchFunctionDescription) ]; } /// public override async ValueTask InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default) { - if (this._options.SearchTime != TextSearchProviderOptions.TextSearchBehavior.BeforeAIInvoke) + if (this._searchTime != TextSearchProviderOptions.TextSearchBehavior.BeforeAIInvoke) { // Expose the search tool for on-demand invocation. return new AIContext { Tools = this._tools }; // No automatic message injection. @@ -171,7 +161,7 @@ public sealed class TextSearchProvider : AIContextProvider /// public override ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default) { - int limit = this._options.RecentMessageMemoryLimit; + int limit = this._recentMessageMemoryLimit; if (limit <= 0) { return default; // Memory disabled. @@ -220,9 +210,9 @@ public sealed class TextSearchProvider : AIContextProvider { // Only persist values that differ from defaults plus recent memory configuration & messages. TextSearchProviderState state = new(); - if (this._options.RecentMessageMemoryLimit > 0 && this._recentMessagesText.Count > 0) + if (this._recentMessageMemoryLimit > 0 && this._recentMessagesText.Count > 0) { - state.RecentMessagesText = this._recentMessagesText.Take(this._options.RecentMessageMemoryLimit).ToList(); + state.RecentMessagesText = this._recentMessagesText.Take(this._recentMessageMemoryLimit).ToList(); } return JsonSerializer.SerializeToElement(state, AgentJsonUtilities.DefaultOptions.GetTypeInfo(typeof(TextSearchProviderState))); @@ -253,9 +243,9 @@ public sealed class TextSearchProvider : AIContextProvider /// Formatted string (may be empty). private string FormatResults(IList results) { - if (this._options.ContextFormatter is not null) + if (this._contextFormatter is not null) { - return this._options.ContextFormatter(results) ?? string.Empty; + return this._contextFormatter(results) ?? string.Empty; } if (results.Count == 0) @@ -264,7 +254,7 @@ public sealed class TextSearchProvider : AIContextProvider } var sb = new StringBuilder(); - sb.AppendLine(this._options.ContextPrompt ?? DefaultContextPrompt); + sb.AppendLine(this._contextPrompt); for (int i = 0; i < results.Count; i++) { var result = results[i]; @@ -279,7 +269,7 @@ public sealed class TextSearchProvider : AIContextProvider sb.AppendLine($"Contents: {result.Text}"); sb.AppendLine("----"); } - sb.AppendLine(this._options.CitationsPrompt ?? DefaultCitationsPrompt); + sb.AppendLine(this._citationsPrompt); sb.AppendLine(); return sb.ToString(); } diff --git a/dotnet/tests/Microsoft.Agents.AI.Mem0.IntegrationTests/Mem0ProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.Mem0.IntegrationTests/Mem0ProviderTests.cs index 0b9594845a..5240eccd05 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Mem0.IntegrationTests/Mem0ProviderTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Mem0.IntegrationTests/Mem0ProviderTests.cs @@ -45,8 +45,8 @@ public sealed class Mem0ProviderTests : IDisposable // Arrange var question = new ChatMessage(ChatRole.User, "What is my name?"); var input = new ChatMessage(ChatRole.User, "Hello, my name is Caoimhe."); - var options = new Mem0ProviderOptions { ThreadId = "it-thread-1", UserId = "it-user-1" }; - var sut = new Mem0Provider(this._httpClient, options); + var storageScope = new Mem0ProviderScope { ThreadId = "it-thread-1", UserId = "it-user-1" }; + var sut = new Mem0Provider(this._httpClient, storageScope); await sut.ClearStoredMemoriesAsync(); var ctxBefore = await sut.InvokingAsync(new AIContextProvider.InvokingContext(new[] { question })); @@ -69,8 +69,8 @@ public sealed class Mem0ProviderTests : IDisposable // Arrange var question = new ChatMessage(ChatRole.User, "What is your name?"); var assistantIntro = new ChatMessage(ChatRole.Assistant, "Hello, I'm a friendly assistant and my name is Caoimhe."); - var options = new Mem0ProviderOptions { AgentId = "it-agent-1" }; - var sut = new Mem0Provider(this._httpClient, options); + var storageScope = new Mem0ProviderScope { AgentId = "it-agent-1" }; + var sut = new Mem0Provider(this._httpClient, storageScope); await sut.ClearStoredMemoriesAsync(); var ctxBefore = await sut.InvokingAsync(new AIContextProvider.InvokingContext(new[] { question })); @@ -93,8 +93,8 @@ public sealed class Mem0ProviderTests : IDisposable // Arrange var question = new ChatMessage(ChatRole.User, "What is your name?"); var assistantIntro = new ChatMessage(ChatRole.Assistant, "I'm an AI tutor and my name is Caoimhe."); - var sut1 = new Mem0Provider(this._httpClient, new Mem0ProviderOptions { AgentId = "it-agent-a" }); - var sut2 = new Mem0Provider(this._httpClient, new Mem0ProviderOptions { AgentId = "it-agent-b" }); + var sut1 = new Mem0Provider(this._httpClient, new Mem0ProviderScope { AgentId = "it-agent-a" }); + var sut2 = new Mem0Provider(this._httpClient, new Mem0ProviderScope { AgentId = "it-agent-b" }); await sut1.ClearStoredMemoriesAsync(); await sut2.ClearStoredMemoriesAsync(); diff --git a/dotnet/tests/Microsoft.Agents.AI.Mem0.UnitTests/Mem0ProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.Mem0.UnitTests/Mem0ProviderTests.cs index ae97aed78c..46a5482f15 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Mem0.UnitTests/Mem0ProviderTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Mem0.UnitTests/Mem0ProviderTests.cs @@ -48,35 +48,35 @@ public sealed class Mem0ProviderTests : IDisposable using HttpClient client = new(); // Act & Assert - var ex = Assert.Throws(() => new Mem0Provider(client)); + var ex = Assert.Throws(() => new Mem0Provider(client, new Mem0ProviderScope() { ThreadId = "tid" })); Assert.StartsWith("The HttpClient BaseAddress must be set for Mem0 operations.", ex.Message); } [Fact] - public void Constructor_Defaults_Scopes() + public void Constructor_Throws_WhenNoStorageScopeValueIsSet() { - // Arrange & Act - var sut = new Mem0Provider(this._httpClient); - - // Assert - Assert.Null(sut.ApplicationId); - Assert.Null(sut.AgentId); - Assert.Null(sut.ThreadId); - Assert.Null(sut.UserId); + // Act & Assert + var ex = Assert.Throws(() => new Mem0Provider(this._httpClient, new Mem0ProviderScope())); + Assert.StartsWith("At least one of ApplicationId, AgentId, ThreadId, or UserId must be provided for the storage scope.", ex.Message); } [Fact] - public void DeserializingConstructor_Defaults_Scopes() + public void Constructor_Throws_WhenNoSearchScopeValueIsSet() { - // Arrange & Act - var jsonElement = JsonSerializer.SerializeToElement(new object(), Mem0JsonUtilities.DefaultOptions); - var sut = new Mem0Provider(this._httpClient, jsonElement); + // Act & Assert + var ex = Assert.Throws(() => new Mem0Provider(this._httpClient, new Mem0ProviderScope() { ThreadId = "tid" }, new Mem0ProviderScope())); + Assert.StartsWith("At least one of ApplicationId, AgentId, ThreadId, or UserId must be provided for the search scope.", ex.Message); + } - // Assert - Assert.Null(sut.ApplicationId); - Assert.Null(sut.AgentId); - Assert.Null(sut.ThreadId); - Assert.Null(sut.UserId); + [Fact] + public void DeserializingConstructor_Throws_WithEmptyJsonElement() + { + // Arrange + var jsonElement = JsonSerializer.SerializeToElement(new object(), Mem0JsonUtilities.DefaultOptions); + + // Act & Assert + var ex = Assert.Throws(() => new Mem0Provider(this._httpClient, jsonElement)); + Assert.StartsWith("The Mem0Provider state did not contain the required scope properties.", ex.Message); } [Fact] @@ -84,14 +84,14 @@ public sealed class Mem0ProviderTests : IDisposable { // Arrange this._handler.EnqueueJsonResponse("[ { \"id\": \"1\", \"memory\": \"Name is Caoimhe\", \"hash\": \"h\", \"metadata\": null, \"score\": 0.9, \"created_at\": \"2023-01-01T00:00:00Z\", \"updated_at\": null, \"user_id\": \"u\", \"app_id\": null, \"agent_id\": \"agent\", \"session_id\": \"thread\" } ]"); - var options = new Mem0ProviderOptions + var storageScope = new Mem0ProviderScope { ApplicationId = "app", AgentId = "agent", ThreadId = "thread", UserId = "user" }; - var sut = new Mem0Provider(this._httpClient, options, this._loggerFactoryMock.Object); + var sut = new Mem0Provider(this._httpClient, storageScope, loggerFactory: this._loggerFactoryMock.Object); var invokingContext = new AIContextProvider.InvokingContext(new[] { new ChatMessage(ChatRole.User, "What is my name?") }); // Act @@ -137,8 +137,8 @@ public sealed class Mem0ProviderTests : IDisposable this._handler.EnqueueEmptyOk(); // For first CreateMemory this._handler.EnqueueEmptyOk(); // For second CreateMemory this._handler.EnqueueEmptyOk(); // For third CreateMemory - var options = new Mem0ProviderOptions { ApplicationId = "a", AgentId = "b", ThreadId = "c", UserId = "d" }; - var sut = new Mem0Provider(this._httpClient, options); + var storageScope = new Mem0ProviderScope { ApplicationId = "a", AgentId = "b", ThreadId = "c", UserId = "d" }; + var sut = new Mem0Provider(this._httpClient, storageScope); var requestMessages = new List { @@ -168,8 +168,8 @@ public sealed class Mem0ProviderTests : IDisposable public async Task InvokedAsync_PersistsNothingForFailedRequestAsync() { // Arrange - var options = new Mem0ProviderOptions { ApplicationId = "a", AgentId = "b", ThreadId = "c", UserId = "d" }; - var sut = new Mem0Provider(this._httpClient, options); + var storageScope = new Mem0ProviderScope { ApplicationId = "a", AgentId = "b", ThreadId = "c", UserId = "d" }; + var sut = new Mem0Provider(this._httpClient, storageScope); var requestMessages = new List { @@ -189,8 +189,8 @@ public sealed class Mem0ProviderTests : IDisposable public async Task InvokedAsync_ShouldNotThrow_WhenStorageFailsAsync() { // Arrange - var options = new Mem0ProviderOptions { ApplicationId = "a", AgentId = "b", ThreadId = "c", UserId = "d" }; - var sut = new Mem0Provider(this._httpClient, options, this._loggerFactoryMock.Object); + var storageScope = new Mem0ProviderScope { ApplicationId = "a", AgentId = "b", ThreadId = "c", UserId = "d" }; + var sut = new Mem0Provider(this._httpClient, storageScope, loggerFactory: this._loggerFactoryMock.Object); this._handler.EnqueueEmptyInternalServerError(); var requestMessages = new List @@ -222,8 +222,8 @@ public sealed class Mem0ProviderTests : IDisposable public async Task ClearStoredMemoriesAsync_SendsDeleteWithQueryAsync() { // Arrange - var options = new Mem0ProviderOptions { ApplicationId = "app", AgentId = "agent", ThreadId = "thread", UserId = "user" }; - var sut = new Mem0Provider(this._httpClient, options); + var storageScope = new Mem0ProviderScope { ApplicationId = "app", AgentId = "agent", ThreadId = "thread", UserId = "user" }; + var sut = new Mem0Provider(this._httpClient, storageScope); this._handler.EnqueueEmptyOk(); // for DELETE // Act @@ -235,80 +235,39 @@ public sealed class Mem0ProviderTests : IDisposable } [Fact] - public void Properties_Roundtrip() + public void Serialize_RoundTripsScopes() { // Arrange - var options = new Mem0ProviderOptions { ApplicationId = "app", AgentId = "agent", ThreadId = "thread", UserId = "user" }; - var sut = new Mem0Provider(this._httpClient, options); - - // Assert - Assert.Equal("app", sut.ApplicationId); - Assert.Equal("agent", sut.AgentId); - Assert.Equal("thread", sut.ThreadId); - Assert.Equal("user", sut.UserId); - - // Act - sut.ApplicationId = "app2"; - sut.AgentId = "agent2"; - sut.ThreadId = "thread2"; - sut.UserId = "user2"; - - // Assert - Assert.Equal("app2", sut.ApplicationId); - Assert.Equal("agent2", sut.AgentId); - Assert.Equal("thread2", sut.ThreadId); - Assert.Equal("user2", sut.UserId); - } - - [Fact] - public void Serialize_Deserialize_Roundtrips() - { - // Arrange - var options = new Mem0ProviderOptions { ApplicationId = "app", AgentId = "agent", ThreadId = "thread", UserId = "user" }; - var sut = new Mem0Provider(this._httpClient, options); - - // Act - var stateElement = sut.Serialize(); - var sut2 = new Mem0Provider(this._httpClient, stateElement); - - // Assert - Assert.Equal("app", sut.ApplicationId); - Assert.Equal("agent", sut.AgentId); - Assert.Equal("thread", sut.ThreadId); - Assert.Equal("user", sut.UserId); - - Assert.Equal("app", sut2.ApplicationId); - Assert.Equal("agent", sut2.AgentId); - Assert.Equal("thread", sut2.ThreadId); - Assert.Equal("user", sut2.UserId); - } - - [Fact] - public void Serialize_RoundTripsCustomContextPrompt() - { - // Arrange - var options = new Mem0ProviderOptions { ApplicationId = "app", AgentId = "agent", ThreadId = "thread", UserId = "user", ContextPrompt = "Custom:" }; - var sut = new Mem0Provider(this._httpClient, options); + var storageScope = new Mem0ProviderScope { ApplicationId = "app", AgentId = "agent", ThreadId = "thread", UserId = "user" }; + var sut = new Mem0Provider(this._httpClient, storageScope, options: new() { ContextPrompt = "Custom:" }, loggerFactory: this._loggerFactoryMock.Object); // Act var stateElement = sut.Serialize(); using JsonDocument doc = JsonDocument.Parse(stateElement.GetRawText()); - Assert.Equal("Custom:", doc.RootElement.GetProperty("contextPrompt").GetString()); + var storageScopeElement = doc.RootElement.GetProperty("storageScope"); + Assert.Equal("app", storageScopeElement.GetProperty("applicationId").GetString()); + Assert.Equal("agent", storageScopeElement.GetProperty("agentId").GetString()); + Assert.Equal("thread", storageScopeElement.GetProperty("threadId").GetString()); + Assert.Equal("user", storageScopeElement.GetProperty("userId").GetString()); var sut2 = new Mem0Provider(this._httpClient, stateElement); var stateElement2 = sut2.Serialize(); // Assert using JsonDocument doc2 = JsonDocument.Parse(stateElement2.GetRawText()); - Assert.Equal("Custom:", doc2.RootElement.GetProperty("contextPrompt").GetString()); + var storageScopeElement2 = doc2.RootElement.GetProperty("storageScope"); + Assert.Equal("app", storageScopeElement2.GetProperty("applicationId").GetString()); + Assert.Equal("agent", storageScopeElement2.GetProperty("agentId").GetString()); + Assert.Equal("thread", storageScopeElement2.GetProperty("threadId").GetString()); + Assert.Equal("user", storageScopeElement2.GetProperty("userId").GetString()); } [Fact] public void Serialize_DoesNotIncludeDefaultContextPrompt() { // Arrange - var options = new Mem0ProviderOptions { ApplicationId = "app" }; - var sut = new Mem0Provider(this._httpClient, options); + var storageScope = new Mem0ProviderScope { ApplicationId = "app" }; + var sut = new Mem0Provider(this._httpClient, storageScope); // Act var stateElement = sut.Serialize(); @@ -318,23 +277,12 @@ public sealed class Mem0ProviderTests : IDisposable Assert.False(doc.RootElement.TryGetProperty("contextPrompt", out _)); } - [Fact] - public async Task InvokingAsync_Throws_WhenNoScopesAsync() - { - // Arrange - var sut = new Mem0Provider(this._httpClient, new Mem0ProviderOptions()); - var ctx = new AIContextProvider.InvokingContext(new[] { new ChatMessage(ChatRole.User, "Test") }); - - // Act & Assert - await Assert.ThrowsAsync(() => sut.InvokingAsync(ctx).AsTask()); - } - [Fact] public async Task InvokingAsync_ShouldNotThrow_WhenSearchFailsAsync() { // Arrange - var options = new Mem0ProviderOptions { ApplicationId = "app" }; - var provider = new Mem0Provider(this._httpClient, options, loggerFactory: this._loggerFactoryMock.Object); + var storageScope = new Mem0ProviderScope { ApplicationId = "app" }; + var provider = new Mem0Provider(this._httpClient, storageScope, loggerFactory: this._loggerFactoryMock.Object); var invokingContext = new AIContextProvider.InvokingContext(new[] { new ChatMessage(ChatRole.User, "Q?") }); // Act diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Data/TextSearchProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Data/TextSearchProviderTests.cs index 831c843337..be1e901499 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Data/TextSearchProviderTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Data/TextSearchProviderTests.cs @@ -58,7 +58,7 @@ public sealed class TextSearchProviderTests ContextPrompt = overrideContextPrompt, CitationsPrompt = overrideCitationsPrompt }; - var provider = new TextSearchProvider(SearchDelegateAsync, options, withLogging ? this._loggerFactoryMock.Object : null); + var provider = new TextSearchProvider(SearchDelegateAsync, default, null, options, withLogging ? this._loggerFactoryMock.Object : null); var invokingContext = new AIContextProvider.InvokingContext(new[] { @@ -135,7 +135,7 @@ public sealed class TextSearchProviderTests FunctionToolName = overrideName, FunctionToolDescription = overrideDescription }; - var provider = new TextSearchProvider(this.NoResultSearchAsync, options); + var provider = new TextSearchProvider(this.NoResultSearchAsync, default, null, options); var invokingContext = new AIContextProvider.InvokingContext(new[] { new ChatMessage(ChatRole.User, "Q?") }); // Act @@ -154,7 +154,7 @@ public sealed class TextSearchProviderTests public async Task InvokingAsync_ShouldNotThrow_WhenSearchFailsAsync() { // Arrange - var provider = new TextSearchProvider(this.FailingSearchAsync, loggerFactory: this._loggerFactoryMock.Object); + var provider = new TextSearchProvider(this.FailingSearchAsync, default, null, loggerFactory: this._loggerFactoryMock.Object); var invokingContext = new AIContextProvider.InvokingContext(new[] { new ChatMessage(ChatRole.User, "Q?") }); // Act @@ -195,7 +195,7 @@ public sealed class TextSearchProviderTests ContextPrompt = overrideContextPrompt, CitationsPrompt = overrideCitationsPrompt }; - var provider = new TextSearchProvider(SearchDelegateAsync, options); + var provider = new TextSearchProvider(SearchDelegateAsync, default, null, options); // Act var formatted = await provider.SearchAsync("Sample user question?", CancellationToken.None); @@ -247,7 +247,7 @@ public sealed class TextSearchProviderTests SearchTime = TextSearchProviderOptions.TextSearchBehavior.BeforeAIInvoke, ContextFormatter = r => $"Custom formatted context with {r.Count} results." }; - var provider = new TextSearchProvider(SearchDelegateAsync, options); + var provider = new TextSearchProvider(SearchDelegateAsync, default, null, options); var invokingContext = new AIContextProvider.InvokingContext(new[] { new ChatMessage(ChatRole.User, "Q?") }); // Act @@ -281,7 +281,7 @@ public sealed class TextSearchProviderTests SearchTime = TextSearchProviderOptions.TextSearchBehavior.BeforeAIInvoke, ContextFormatter = r => string.Join(",", r.Select(x => ((RawPayload)x.RawRepresentation!).Id)) }; - var provider = new TextSearchProvider(SearchDelegateAsync, options); + var provider = new TextSearchProvider(SearchDelegateAsync, default, null, options); var invokingContext = new AIContextProvider.InvokingContext(new[] { new ChatMessage(ChatRole.User, "Q?") }); // Act @@ -298,7 +298,7 @@ public sealed class TextSearchProviderTests { // Arrange var options = new TextSearchProviderOptions { SearchTime = TextSearchProviderOptions.TextSearchBehavior.BeforeAIInvoke }; - var provider = new TextSearchProvider(this.NoResultSearchAsync, options); + var provider = new TextSearchProvider(this.NoResultSearchAsync, default, null, options); var invokingContext = new AIContextProvider.InvokingContext(new[] { new ChatMessage(ChatRole.User, "Q?") }); // Act @@ -327,7 +327,7 @@ public sealed class TextSearchProviderTests capturedInput = input; return Task.FromResult>([]); // No results needed. } - var provider = new TextSearchProvider(SearchDelegateAsync, options); + var provider = new TextSearchProvider(SearchDelegateAsync, default, null, options); // Populate memory with more messages than the limit (A,B,C,D) -> should retain B,C,D var initialMessages = new[] @@ -367,7 +367,7 @@ public sealed class TextSearchProviderTests capturedInput = input; return Task.FromResult>([]); // No results needed. } - var provider = new TextSearchProvider(SearchDelegateAsync, options); + var provider = new TextSearchProvider(SearchDelegateAsync, default, null, options); // Populate memory with more messages than the limit (A,B,C,D) -> should retain B,C,D var initialMessages = new[] @@ -407,7 +407,7 @@ public sealed class TextSearchProviderTests capturedInput = input; return Task.FromResult>([]); } - var provider = new TextSearchProvider(SearchDelegateAsync, options); + var provider = new TextSearchProvider(SearchDelegateAsync, default, null, options); // First memory update (A,B) await provider.InvokedAsync(new(new[] @@ -449,7 +449,7 @@ public sealed class TextSearchProviderTests capturedInput = input; return Task.FromResult>([]); // No results needed for this test. } - var provider = new TextSearchProvider(SearchDelegateAsync, options); + var provider = new TextSearchProvider(SearchDelegateAsync, default, null, options); // Populate memory with mixed roles; only Assistant messages (A1,A2) should be retained. var initialMessages = new[] @@ -486,7 +486,7 @@ public sealed class TextSearchProviderTests SearchTime = TextSearchProviderOptions.TextSearchBehavior.BeforeAIInvoke, RecentMessageMemoryLimit = 3 }; - var provider = new TextSearchProvider(this.NoResultSearchAsync, options); + var provider = new TextSearchProvider(this.NoResultSearchAsync, default, null, options); // Act var state = provider.Serialize(); @@ -506,7 +506,7 @@ public sealed class TextSearchProviderTests RecentMessageMemoryLimit = 3, RecentMessageRolesIncluded = [ChatRole.User, ChatRole.Assistant] }; - var provider = new TextSearchProvider(this.NoResultSearchAsync, options); + var provider = new TextSearchProvider(this.NoResultSearchAsync, default, null, options); var messages = new[] { new ChatMessage(ChatRole.User, "M1"), @@ -536,7 +536,7 @@ public sealed class TextSearchProviderTests RecentMessageMemoryLimit = 4, RecentMessageRolesIncluded = [ChatRole.User, ChatRole.Assistant] }; - var provider = new TextSearchProvider(this.NoResultSearchAsync, options); + var provider = new TextSearchProvider(this.NoResultSearchAsync, default, null, options); var messages = new[] { new ChatMessage(ChatRole.User, "A"), @@ -571,7 +571,7 @@ public sealed class TextSearchProviderTests public async Task Deserialize_WithChangedLowerLimit_ShouldTruncateToNewLimitAsync() { // Arrange - var initialProvider = new TextSearchProvider(this.NoResultSearchAsync, new TextSearchProviderOptions + var initialProvider = new TextSearchProvider(this.NoResultSearchAsync, default, null, new TextSearchProviderOptions { SearchTime = TextSearchProviderOptions.TextSearchBehavior.BeforeAIInvoke, RecentMessageMemoryLimit = 5,