244 lines
8.6 KiB
C#
244 lines
8.6 KiB
C#
using Nexus.Api.Models;
|
|
|
|
namespace Nexus.Api.Services;
|
|
|
|
public sealed class MemoryService(
|
|
IOpenClawAgentConfigurationService configuration) : IMemoryService
|
|
{
|
|
private const string MemoryDirectory = "memory";
|
|
|
|
public async Task<IReadOnlyList<MemoryFileInfo>> GetAllAsync(
|
|
string agentId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var normalizedAgentId = NormalizeAgentId(agentId);
|
|
var result = new List<MemoryFileInfo>();
|
|
var agentFiles = await configuration.GetAgentFilesAsync(
|
|
normalizedAgentId,
|
|
cancellationToken);
|
|
var longTerm = agentFiles.Files.SingleOrDefault(file =>
|
|
string.Equals(file.Name, "MEMORY.md", StringComparison.OrdinalIgnoreCase));
|
|
if (longTerm is { Missing: false })
|
|
{
|
|
result.Add(new MemoryFileInfo(
|
|
"MEMORY.md",
|
|
"MEMORY.md",
|
|
longTerm.Size ?? 0,
|
|
(longTerm.UpdatedAt ?? agentFiles.CheckedAt).UtcDateTime,
|
|
normalizedAgentId,
|
|
"MEMORY.md"));
|
|
}
|
|
|
|
var workspace = await TryGetMemoryWorkspaceAsync(
|
|
normalizedAgentId,
|
|
cancellationToken);
|
|
if (workspace is not null)
|
|
{
|
|
result.AddRange(workspace.Entries
|
|
.Where(OpenClawContentReadHelpers.IsMarkdownFile)
|
|
.OrderByDescending(
|
|
entry => entry.Name,
|
|
StringComparer.OrdinalIgnoreCase)
|
|
.Select(entry => new MemoryFileInfo(
|
|
entry.Name,
|
|
OpenClawContentReadHelpers.LegacyPath(
|
|
entry.Path,
|
|
MemoryDirectory),
|
|
entry.Size ?? 0,
|
|
(entry.UpdatedAt ?? workspace.CheckedAt).UtcDateTime,
|
|
normalizedAgentId,
|
|
entry.Path)));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
public async Task<IReadOnlyList<MemorySearchResult>> SearchAsync(
|
|
string query,
|
|
string agentId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var normalizedAgentId = NormalizeAgentId(agentId);
|
|
var normalizedQuery = query.Trim();
|
|
var candidates = new List<MemoryCandidate>();
|
|
var agentFiles = await configuration.GetAgentFilesAsync(
|
|
normalizedAgentId,
|
|
cancellationToken);
|
|
if (agentFiles.Files.Any(file =>
|
|
string.Equals(file.Name, "MEMORY.md", StringComparison.OrdinalIgnoreCase)
|
|
&& !file.Missing
|
|
&& (file.Size is null
|
|
|| file.Size <= OpenClawContentReadHelpers.MaxContentBytes)))
|
|
{
|
|
candidates.Add(new MemoryCandidate(
|
|
"MEMORY.md",
|
|
"MEMORY.md",
|
|
"MEMORY.md",
|
|
true,
|
|
0));
|
|
}
|
|
|
|
var workspace = await TryGetMemoryWorkspaceAsync(
|
|
normalizedAgentId,
|
|
cancellationToken);
|
|
if (workspace is not null)
|
|
{
|
|
candidates.AddRange(workspace.Entries
|
|
.Where(OpenClawContentReadHelpers.IsMarkdownFile)
|
|
.Select(entry => new MemoryCandidate(
|
|
entry.Name,
|
|
OpenClawContentReadHelpers.LegacyPath(
|
|
entry.Path,
|
|
MemoryDirectory),
|
|
entry.Path,
|
|
false,
|
|
entry.Size ?? 0)));
|
|
}
|
|
|
|
return await OpenClawContentReadHelpers.SelectBoundedAsync<
|
|
MemoryCandidate,
|
|
MemorySearchResult>(
|
|
candidates,
|
|
async (candidate, token) =>
|
|
{
|
|
string? content;
|
|
long size;
|
|
if (candidate.AgentFile)
|
|
{
|
|
var file = await configuration.GetAgentFileAsync(
|
|
normalizedAgentId,
|
|
"MEMORY.md",
|
|
token);
|
|
content = file.Missing
|
|
|| file.Content is null
|
|
|| file.Size > OpenClawContentReadHelpers.MaxContentBytes
|
|
|| System.Text.Encoding.UTF8.GetByteCount(
|
|
file.Content) >
|
|
OpenClawContentReadHelpers.MaxContentBytes
|
|
? null
|
|
: file.Content;
|
|
size = file.Size ?? 0;
|
|
}
|
|
else
|
|
{
|
|
var file = await configuration.GetWorkspaceFileAsync(
|
|
normalizedAgentId,
|
|
candidate.WorkspacePath,
|
|
token);
|
|
content = OpenClawContentReadHelpers.ReadText(file);
|
|
size = file.Size;
|
|
}
|
|
|
|
if (content is null)
|
|
return null;
|
|
var index = content.IndexOf(
|
|
normalizedQuery,
|
|
StringComparison.OrdinalIgnoreCase);
|
|
if (index < 0)
|
|
return null;
|
|
var start = Math.Max(0, index - 60);
|
|
var length = Math.Min(200, content.Length - start);
|
|
var excerpt =
|
|
(start > 0 ? "…" : string.Empty)
|
|
+ content.Substring(start, length)
|
|
+ (start + length < content.Length ? "…" : string.Empty);
|
|
return new MemorySearchResult(
|
|
candidate.Name,
|
|
candidate.LegacyPath,
|
|
excerpt,
|
|
size,
|
|
normalizedAgentId,
|
|
candidate.WorkspacePath);
|
|
},
|
|
cancellationToken);
|
|
}
|
|
|
|
public async Task<MemoryFileContent?> GetFileAsync(
|
|
string name,
|
|
string agentId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var normalizedAgentId = NormalizeAgentId(agentId);
|
|
if (string.Equals(name, "MEMORY.md", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var file = await configuration.GetAgentFileAsync(
|
|
normalizedAgentId,
|
|
"MEMORY.md",
|
|
cancellationToken);
|
|
if (file.Missing
|
|
|| file.Content is null
|
|
|| file.Size > OpenClawContentReadHelpers.MaxContentBytes
|
|
|| System.Text.Encoding.UTF8.GetByteCount(file.Content) >
|
|
OpenClawContentReadHelpers.MaxContentBytes)
|
|
return null;
|
|
return new MemoryFileContent(
|
|
"MEMORY.md",
|
|
"MEMORY.md",
|
|
file.Content,
|
|
file.Size ?? 0,
|
|
(file.UpdatedAt ?? file.CheckedAt).UtcDateTime,
|
|
normalizedAgentId,
|
|
"MEMORY.md");
|
|
}
|
|
|
|
if (!OpenClawContentReadHelpers.IsSafeFileName(name))
|
|
return null;
|
|
var workspacePath = $"{MemoryDirectory}/{name}";
|
|
try
|
|
{
|
|
var file = await configuration.GetWorkspaceFileAsync(
|
|
normalizedAgentId,
|
|
workspacePath,
|
|
cancellationToken);
|
|
var content = OpenClawContentReadHelpers.ReadText(file);
|
|
if (content is null)
|
|
return null;
|
|
return new MemoryFileContent(
|
|
file.Name,
|
|
name,
|
|
content,
|
|
file.Size,
|
|
(file.UpdatedAt ?? file.CheckedAt).UtcDateTime,
|
|
normalizedAgentId,
|
|
file.Path);
|
|
}
|
|
catch (OpenClawGatewayRpcException exception)
|
|
when (OpenClawContentReadHelpers.IsNotFound(exception))
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static string NormalizeAgentId(string? agentId)
|
|
=> string.IsNullOrWhiteSpace(agentId)
|
|
? "iris"
|
|
: agentId.Trim().ToLowerInvariant();
|
|
|
|
private async Task<OpenClawWorkspaceCollectionDto?>
|
|
TryGetMemoryWorkspaceAsync(
|
|
string agentId,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
return await configuration.GetWorkspaceAsync(
|
|
agentId,
|
|
MemoryDirectory,
|
|
0,
|
|
OpenClawContentReadHelpers.MaxFiles,
|
|
cancellationToken);
|
|
}
|
|
catch (OpenClawGatewayRpcException exception)
|
|
when (OpenClawContentReadHelpers.IsNotFound(exception))
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private sealed record MemoryCandidate(
|
|
string Name,
|
|
string LegacyPath,
|
|
string WorkspacePath,
|
|
bool AgentFile,
|
|
long Size);
|
|
}
|