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>
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
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}" };
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user