mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET: [BREAKING] Simplify TextSearchProvider construction and improve Mem0Provider scoping. (#1905)
* Simplify TextSearchProvider construction and improve Mem0Provider scoping * Fixing indentation.
This commit is contained in:
committed by
GitHub
Unverified
parent
bb8ef466de
commit
5e38c63455
@@ -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();
|
||||
|
||||
+1
-3
@@ -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();
|
||||
|
||||
+1
-3
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
|
||||
@@ -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<Mem0Provider>? _logger;
|
||||
|
||||
private readonly Mem0ProviderScope _storageScope;
|
||||
private readonly Mem0ProviderScope _searchScope;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Mem0Provider"/> class.
|
||||
/// </summary>
|
||||
/// <param name="httpClient">Configured <see cref="HttpClient"/> (base address + auth).</param>
|
||||
/// <param name="storageScope">Optional values to scope the memory storage with.</param>
|
||||
/// <param name="searchScope">Optional values to scope the memory search with. Defaults to <paramref name="storageScope"/> if not provided.</param>
|
||||
/// <param name="options">Provider options.</param>
|
||||
/// <param name="loggerFactory">Optional logger factory.</param>
|
||||
/// <remarks>
|
||||
@@ -47,21 +53,35 @@ public sealed class Mem0Provider : AIContextProvider
|
||||
/// new Mem0AIContextProvider(httpClient);
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
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<Mem0Provider>();
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -70,6 +90,7 @@ public sealed class Mem0Provider : AIContextProvider
|
||||
/// <param name="httpClient">Configured <see cref="HttpClient"/> (base address + auth).</param>
|
||||
/// <param name="serializedState">A <see cref="JsonElement"/> representing the serialized state of the store.</param>
|
||||
/// <param name="jsonSerializerOptions">Optional settings for customizing the JSON deserialization process.</param>
|
||||
/// <param name="options">Provider options.</param>
|
||||
/// <param name="loggerFactory">Optional logger factory.</param>
|
||||
/// <exception cref="ArgumentException"></exception>
|
||||
/// <remarks>
|
||||
@@ -82,46 +103,30 @@ public sealed class Mem0Provider : AIContextProvider
|
||||
/// new Mem0AIContextProvider(httpClient, state);
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
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<Mem0Provider>();
|
||||
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<Mem0Provider>();
|
||||
this._client = new Mem0Client(httpClient);
|
||||
this._storageScope = state.StorageScope;
|
||||
this._searchScope = state.SearchScope;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an optional ID for the application to scope memories to.
|
||||
/// </summary>
|
||||
public string? ApplicationId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an optional ID for the agent to scope memories to.
|
||||
/// </summary>
|
||||
public string? AgentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an optional ID for the thread to scope memories to.
|
||||
/// </summary>
|
||||
public string? ThreadId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an optional ID for the user to scope memories to.
|
||||
/// </summary>
|
||||
public string? UserId { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask<AIContext> 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
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
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);
|
||||
|
||||
/// <inheritdoc />
|
||||
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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,35 +5,8 @@ namespace Microsoft.Agents.AI.Mem0;
|
||||
/// <summary>
|
||||
/// Options for configuring the <see cref="Mem0Provider"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
public sealed class Mem0ProviderOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets an optional ID for the application to scope memories to.
|
||||
/// </summary>
|
||||
/// <remarks>If not set, the scope of the memories will span all applications.</remarks>
|
||||
public string? ApplicationId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an optional ID for the agent to scope memories to.
|
||||
/// </summary>
|
||||
/// <remarks>If not set, the scope of the memories will span all agents.</remarks>
|
||||
public string? AgentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an optional ID for the thread to scope memories to.
|
||||
/// </summary>
|
||||
public string? ThreadId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an optional ID for the user to scope memories to.
|
||||
/// </summary>
|
||||
/// <remarks>If not set, the scope of the memories will span all users.</remarks>
|
||||
public string? UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When providing memories to the model, this string is prefixed to the retrieved memories to supply context.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI.Mem0;
|
||||
|
||||
/// <summary>
|
||||
/// Allows scoping of memories for the <see cref="Mem0Provider"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
public sealed class Mem0ProviderScope
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Mem0ProviderScope"/> class.
|
||||
/// </summary>
|
||||
public Mem0ProviderScope() { }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Mem0ProviderScope"/> class by cloning an existing scope.
|
||||
/// </summary>
|
||||
/// <param name="sourceScope">The scope to clone.</param>
|
||||
public Mem0ProviderScope(Mem0ProviderScope sourceScope)
|
||||
{
|
||||
Throw.IfNull(sourceScope);
|
||||
|
||||
this.ApplicationId = sourceScope.ApplicationId;
|
||||
this.AgentId = sourceScope.AgentId;
|
||||
this.ThreadId = sourceScope.ThreadId;
|
||||
this.UserId = sourceScope.UserId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an optional ID for the application to scope memories to.
|
||||
/// </summary>
|
||||
/// <remarks>If not set, the scope of the memories will span all applications.</remarks>
|
||||
public string? ApplicationId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an optional ID for the agent to scope memories to.
|
||||
/// </summary>
|
||||
/// <remarks>If not set, the scope of the memories will span all agents.</remarks>
|
||||
public string? AgentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an optional ID for the thread to scope memories to.
|
||||
/// </summary>
|
||||
public string? ThreadId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an optional ID for the user to scope memories to.
|
||||
/// </summary>
|
||||
/// <remarks>If not set, the scope of the memories will span all users.</remarks>
|
||||
public string? UserId { get; set; }
|
||||
}
|
||||
@@ -44,82 +44,72 @@ public sealed class TextSearchProvider : AIContextProvider
|
||||
private readonly ILogger<TextSearchProvider>? _logger;
|
||||
private readonly AITool[] _tools;
|
||||
private readonly Queue<string> _recentMessagesText;
|
||||
private readonly TextSearchProviderOptions _options;
|
||||
private readonly List<ChatRole> _recentMessageRolesIncluded;
|
||||
private readonly int _recentMessageMemoryLimit;
|
||||
private readonly TextSearchProviderOptions.TextSearchBehavior _searchTime;
|
||||
private readonly string _contextPrompt;
|
||||
private readonly string _citationsPrompt;
|
||||
private readonly Func<IList<TextSearchResult>, string>? _contextFormatter;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TextSearchProvider"/> class.
|
||||
/// </summary>
|
||||
/// <param name="searchAsync">Delegate that executes the search logic. Must not be <see langword="null"/>.</param>
|
||||
/// <param name="options">Optional configuration options.</param>
|
||||
/// <param name="loggerFactory">Optional logger factory.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="searchAsync"/> is <see langword="null"/>.</exception>
|
||||
public TextSearchProvider(Func<string, CancellationToken, Task<IEnumerable<TextSearchResult>>> 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<TextSearchProvider>();
|
||||
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)
|
||||
];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TextSearchProvider"/> class from previously serialized state.
|
||||
/// </summary>
|
||||
/// <param name="searchAsync">Delegate that executes the search logic. Must not be <see langword="null"/>.</param>
|
||||
/// <param name="serializedState">A <see cref="JsonElement"/> representing the serialized provider state.</param>
|
||||
/// <param name="jsonSerializerOptions">Optional serializer options (unused - source generated context is used).</param>
|
||||
/// <param name="options">Optional configuration options.</param>
|
||||
/// <param name="loggerFactory">Optional logger factory.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="searchAsync"/> is <see langword="null"/>.</exception>
|
||||
/// <remarks>
|
||||
/// 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 <see cref="TextSearchProviderOptions.ContextFormatter"/> delegates are not serialized.
|
||||
/// </remarks>
|
||||
public TextSearchProvider(Func<string, CancellationToken, Task<IEnumerable<TextSearchResult>>> searchAsync, JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, TextSearchProviderOptions? options = null, ILoggerFactory? loggerFactory = null)
|
||||
public TextSearchProvider(
|
||||
Func<string, CancellationToken, Task<IEnumerable<TextSearchResult>>> 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<TextSearchProvider>();
|
||||
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<string>? 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)
|
||||
];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask<AIContext> 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
|
||||
/// <inheritdoc />
|
||||
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
|
||||
/// <returns>Formatted string (may be empty).</returns>
|
||||
private string FormatResults(IList<TextSearchResult> 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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -48,35 +48,35 @@ public sealed class Mem0ProviderTests : IDisposable
|
||||
using HttpClient client = new();
|
||||
|
||||
// Act & Assert
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Mem0Provider(client));
|
||||
var ex = Assert.Throws<ArgumentException>(() => 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<ArgumentException>(() => 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<ArgumentException>(() => 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<InvalidOperationException>(() => 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<ChatMessage>
|
||||
{
|
||||
@@ -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<ChatMessage>
|
||||
{
|
||||
@@ -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<ChatMessage>
|
||||
@@ -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<ArgumentException>(() => 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
|
||||
|
||||
@@ -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<IEnumerable<TextSearchProvider.TextSearchResult>>([]); // 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<IEnumerable<TextSearchProvider.TextSearchResult>>([]); // 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<IEnumerable<TextSearchProvider.TextSearchResult>>([]);
|
||||
}
|
||||
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<IEnumerable<TextSearchProvider.TextSearchResult>>([]); // 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,
|
||||
|
||||
Reference in New Issue
Block a user