Files
swarm-assistent/AssistentMemoryApi.cs
T
Leonid PershinandCursor 880e2dbea2 Ship Assistent 0.8.1: split modules and shared+personal vector memory.
Personal RAG never leaks into the shared store; retrieve merges shared plus the persona chain, with personal overwrite on kind+key.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-22 00:38:54 +03:00

216 lines
8.2 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using SwarmUI.Accounts;
using SwarmUI.Utils;
namespace Mrleo1nid.SwarmAssistent;
/// <summary>Read/write routes for the vector memory list in ⚙ and the gpu-rent wanted queue badge.</summary>
public partial class SwarmAssistentExtension
{
/// <summary>Embed model the UI should use: settings overlay wins, then persona assistant.json.</summary>
string MemoryEmbedModel(string requested = null)
{
if (!string.IsNullOrWhiteSpace(requested))
{
return requested.Trim();
}
return Config?.LoadSettings()["embed_model"]?.ToString()
?? Config?.LoadAssistant(Config.DefaultPersonaId())["embed_model"]?.ToString()
?? "nomic-embed-text";
}
string MemoryBaseUrl(string requested = null)
{
return NormalizeBaseUrl(string.IsNullOrWhiteSpace(requested)
? Config?.LoadSettings()["base_url"]?.ToString()
: requested);
}
string ResolveApiPersona(string persona, string scope)
{
string s = (scope ?? "").Trim().ToLowerInvariant();
if (s is "shared" or "common" or "global")
{
return AssistentMemory.SharedPersona;
}
if (s is "personal")
{
return AssistentConfig.SafeId(persona) ?? Config?.DefaultPersonaId() ?? "neutral";
}
if (string.IsNullOrWhiteSpace(persona) || AssistentMemory.IsShared(persona))
{
return AssistentMemory.SharedPersona;
}
return AssistentMemory.NormalizePersona(persona);
}
public async Task<JObject> AssistentListMemory(Session session, int limit = 200, string kind = null, string persona = null, string scope = null)
{
await Task.CompletedTask;
if (Memory is null)
{
return new JObject { ["error"] = "memory not ready" };
}
try
{
JArray all = Memory.ListAll(limit);
string filter = (kind ?? "").Trim().ToLowerInvariant();
string wantScope = (scope ?? "").Trim().ToLowerInvariant();
string wantPersona = (persona ?? "").Trim();
IEnumerable<JToken> q = all;
if (!string.IsNullOrWhiteSpace(filter) && filter != "all")
{
q = q.Where(t => string.Equals(t?["kind"]?.ToString(), filter, StringComparison.OrdinalIgnoreCase));
}
bool personaGiven = !string.IsNullOrWhiteSpace(wantPersona);
if (wantScope is "shared" or "common" or "global"
|| (personaGiven && AssistentMemory.IsShared(wantPersona)))
{
q = q.Where(t => string.Equals(t?["scope"]?.ToString(), "shared", StringComparison.OrdinalIgnoreCase));
}
else if (wantScope is "personal" || personaGiven)
{
string pid = AssistentMemory.NormalizePersona(personaGiven ? wantPersona : Config?.DefaultPersonaId());
q = q.Where(t => string.Equals(t?["persona"]?.ToString(), pid, StringComparison.OrdinalIgnoreCase));
}
JArray rows = new(q);
JArray kinds = new(all
.Select(t => t?["kind"]?.ToString())
.Where(s => !string.IsNullOrWhiteSpace(s))
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(s => s, StringComparer.OrdinalIgnoreCase));
return new JObject
{
["success"] = true,
["memories"] = rows,
["kinds"] = kinds,
["total"] = Memory.CountAll(),
["embed_model"] = Memory.EmbedModel,
["dims"] = Memory.Dims,
};
}
catch (Exception ex)
{
return new JObject { ["error"] = $"memory list: {ex.Message}" };
}
}
public async Task<JObject> AssistentUpsertMemory(Session session, string kind, string key, string text, string source = "user", string baseUrl = null, string embed_model = null, string persona = null, string scope = null)
{
if (Memory is null)
{
return new JObject { ["error"] = "memory not ready" };
}
kind = (kind ?? "note").Trim().ToLowerInvariant();
key = (key ?? "").Trim();
text = (text ?? "").Trim();
if (string.IsNullOrWhiteSpace(key) || string.IsNullOrWhiteSpace(text))
{
return new JObject { ["error"] = "key and text required" };
}
string src = (source ?? "user").Trim().ToLowerInvariant();
if (src == "bundled")
{
return new JObject { ["error"] = "bundled memories are read-only — use memory-seed/" };
}
string target = ResolveApiPersona(persona, scope);
try
{
await Memory.UpsertTextAsync(MemoryBaseUrl(baseUrl), kind, key, text, src, null, MemoryEmbedModel(embed_model), target);
return new JObject
{
["success"] = true,
["kind"] = kind,
["key"] = key,
["source"] = src,
["scope"] = AssistentMemory.IsShared(target) ? "shared" : "personal",
["persona"] = AssistentMemory.IsShared(target) ? "shared" : target,
};
}
catch (Exception ex)
{
return new JObject { ["error"] = $"memory upsert: {ex.Message}" };
}
}
public async Task<JObject> AssistentForgetMemory(Session session, string kind, string key, string source = null, string persona = null, string scope = null)
{
await Task.CompletedTask;
if (Memory is null)
{
return new JObject { ["error"] = "memory not ready" };
}
if (string.IsNullOrWhiteSpace(kind) || string.IsNullOrWhiteSpace(key))
{
return new JObject { ["error"] = "kind and key required" };
}
if (string.Equals((source ?? "").Trim(), "bundled", StringComparison.OrdinalIgnoreCase))
{
return new JObject { ["error"] = "bundled memories come back on reseed — edit memory-seed/ instead" };
}
string target = ResolveApiPersona(persona, scope);
try
{
Memory.Forget(kind, key, string.IsNullOrWhiteSpace(source) ? null : source.Trim(), target);
return new JObject
{
["success"] = true,
["kind"] = kind.Trim().ToLowerInvariant(),
["key"] = key.Trim(),
["scope"] = AssistentMemory.IsShared(target) ? "shared" : "personal",
["persona"] = AssistentMemory.IsShared(target) ? "shared" : target,
};
}
catch (Exception ex)
{
return new JObject { ["error"] = $"memory forget: {ex.Message}" };
}
}
/// <summary>The gpu-rent wanted queue (models pending the next <c>up</c>) — count + entries.</summary>
public async Task<JObject> AssistentListWanted(Session session)
{
await Task.CompletedTask;
string path = WantedModelsPath();
JArray items = [];
if (!File.Exists(path))
{
return new JObject { ["success"] = true, ["count"] = 0, ["items"] = items, ["path"] = path };
}
try
{
Dictionary<string, List<WantedEntry>> sections = LoadWantedYaml(File.ReadAllText(path, Encoding.UTF8));
foreach ((string kind, List<WantedEntry> list) in sections.OrderBy(p => p.Key, StringComparer.OrdinalIgnoreCase))
{
foreach (WantedEntry entry in list)
{
items.Add(new JObject
{
["kind"] = kind,
["url"] = entry.Url,
["title"] = entry.Title,
["version_id"] = entry.VersionId,
});
}
}
return new JObject
{
["success"] = true,
["count"] = items.Count,
["items"] = items,
["path"] = path,
};
}
catch (Exception ex)
{
return new JObject { ["error"] = $"wanted queue: {ex.Message}" };
}
}
}