using System; using System.Collections.Generic; using System.Linq; using Microsoft.Data.Sqlite; using Newtonsoft.Json.Linq; using SwarmUI.Utils; namespace Mrleo1nid.SwarmAssistent; /// Facts about the human at the desk — global + per-persona, separate from craft RAG. public sealed partial class AssistentMemory { void EnsureUserPrefsSchema() { Exec( """ CREATE TABLE IF NOT EXISTS user_prefs ( id INTEGER PRIMARY KEY AUTOINCREMENT, key TEXT NOT NULL, text TEXT NOT NULL, scope TEXT NOT NULL DEFAULT 'global', persona_id TEXT NOT NULL DEFAULT '', source TEXT NOT NULL DEFAULT 'user', pinned INTEGER NOT NULL DEFAULT 0, updated INTEGER NOT NULL, UNIQUE(key, scope, persona_id) ); CREATE INDEX IF NOT EXISTS idx_user_prefs_scope ON user_prefs(scope, persona_id); """); } static string NormalizePrefScope(string scope) { string s = (scope ?? "").Trim().ToLowerInvariant(); return s is "persona" or "personal" or "agent" ? "persona" : "global"; } static string NormalizePrefPersona(string scope, string personaId) { if (NormalizePrefScope(scope) == "global") { return ""; } return AssistentConfig.SafeId(personaId) ?? "neutral"; } public JArray ListUserPrefs(string scope = null, string personaId = null, int limit = 200) { lock (_lock) { EnsureOpen(); List rows = []; string wantScope = (scope ?? "").Trim().ToLowerInvariant(); string wantPersona = AssistentConfig.SafeId(personaId) ?? ""; using SqliteCommand cmd = _conn.CreateCommand(); List where = []; if (wantScope is "global" or "shared" or "common") { where.Add("scope = 'global'"); } else if (wantScope is "persona" or "personal") { where.Add("scope = 'persona'"); if (!string.IsNullOrWhiteSpace(wantPersona)) { where.Add("persona_id = $persona"); cmd.Parameters.AddWithValue("$persona", wantPersona); } } else if (!string.IsNullOrWhiteSpace(wantPersona)) { // Prompt path: global ∪ this persona where.Add("(scope = 'global' OR (scope = 'persona' AND persona_id = $persona))"); cmd.Parameters.AddWithValue("$persona", wantPersona); } string whereSql = where.Count > 0 ? "WHERE " + string.Join(" AND ", where) : ""; cmd.CommandText = $""" SELECT id, key, text, scope, persona_id, source, pinned, updated FROM user_prefs {whereSql} ORDER BY pinned DESC, updated DESC LIMIT $lim """; cmd.Parameters.AddWithValue("$lim", Math.Clamp(limit, 1, 2000)); using SqliteDataReader reader = cmd.ExecuteReader(); while (reader.Read()) { string sc = reader.GetString(3); string pid = reader.IsDBNull(4) ? "" : reader.GetString(4) ?? ""; rows.Add(new JObject { ["id"] = reader.GetInt64(0), ["key"] = reader.GetString(1), ["text"] = reader.GetString(2), ["scope"] = sc, ["persona"] = string.IsNullOrEmpty(pid) ? "" : pid, ["persona_id"] = pid, ["source"] = reader.GetString(5), ["pinned"] = !reader.IsDBNull(6) && reader.GetInt64(6) != 0, ["updated"] = reader.IsDBNull(7) ? 0 : reader.GetInt64(7), }); } return new JArray(rows); } } public JObject UpsertUserPref(string key, string text, string scope = "global", string personaId = null, string source = "user", bool? pinned = null) { lock (_lock) { EnsureOpen(); long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); UpsertUserPrefUnlocked(key, text, scope, personaId, source, pinned ?? false, now); return new JObject { ["key"] = (key ?? "").Trim(), ["text"] = (text ?? "").Trim(), ["scope"] = NormalizePrefScope(scope), ["persona_id"] = NormalizePrefPersona(scope, personaId), ["source"] = string.IsNullOrWhiteSpace(source) ? "user" : source.Trim().ToLowerInvariant(), ["pinned"] = pinned ?? false, ["updated"] = now, }; } } void UpsertUserPrefUnlocked(string key, string text, string scope, string personaId, string source, bool pinned, long updated) { key = (key ?? "").Trim(); text = (text ?? "").Trim(); if (string.IsNullOrWhiteSpace(key) || string.IsNullOrWhiteSpace(text)) { throw new ArgumentException("key and text required"); } if (key.Length > 120) { key = key[..120]; } string sc = NormalizePrefScope(scope); string pid = NormalizePrefPersona(sc, personaId); string src = string.IsNullOrWhiteSpace(source) ? "user" : source.Trim().ToLowerInvariant(); using SqliteCommand cmd = _conn.CreateCommand(); cmd.CommandText = """ INSERT INTO user_prefs(key, text, scope, persona_id, source, pinned, updated) VALUES ($key, $text, $scope, $persona, $source, $pinned, $upd) ON CONFLICT(key, scope, persona_id) DO UPDATE SET text = excluded.text, source = excluded.source, pinned = excluded.pinned, updated = excluded.updated """; cmd.Parameters.AddWithValue("$key", key); cmd.Parameters.AddWithValue("$text", text); cmd.Parameters.AddWithValue("$scope", sc); cmd.Parameters.AddWithValue("$persona", pid); cmd.Parameters.AddWithValue("$source", src); cmd.Parameters.AddWithValue("$pinned", pinned ? 1 : 0); cmd.Parameters.AddWithValue("$upd", updated); cmd.ExecuteNonQuery(); } public bool ForgetUserPref(string key, string scope = "global", string personaId = null) { lock (_lock) { EnsureOpen(); string sc = NormalizePrefScope(scope); string pid = NormalizePrefPersona(sc, personaId); using SqliteCommand cmd = _conn.CreateCommand(); cmd.CommandText = "DELETE FROM user_prefs WHERE key = $key AND scope = $scope AND persona_id = $persona"; cmd.Parameters.AddWithValue("$key", (key ?? "").Trim()); cmd.Parameters.AddWithValue("$scope", sc); cmd.Parameters.AddWithValue("$persona", pid); return cmd.ExecuteNonQuery() > 0; } } public int ClearUserPrefs(string scope = null, string personaId = null) { lock (_lock) { EnsureOpen(); string want = (scope ?? "all").Trim().ToLowerInvariant(); using SqliteCommand cmd = _conn.CreateCommand(); if (want is "global" or "shared") { cmd.CommandText = "DELETE FROM user_prefs WHERE scope = 'global'"; } else if (want is "persona" or "personal") { string pid = AssistentConfig.SafeId(personaId) ?? ""; if (string.IsNullOrWhiteSpace(pid)) { cmd.CommandText = "DELETE FROM user_prefs WHERE scope = 'persona'"; } else { cmd.CommandText = "DELETE FROM user_prefs WHERE scope = 'persona' AND persona_id = $persona"; cmd.Parameters.AddWithValue("$persona", pid); } } else { cmd.CommandText = "DELETE FROM user_prefs"; } return cmd.ExecuteNonQuery(); } } public int CountUserPrefs() { lock (_lock) { EnsureOpen(); using SqliteCommand cmd = _conn.CreateCommand(); cmd.CommandText = "SELECT COUNT(*) FROM user_prefs"; return Convert.ToInt32(cmd.ExecuteScalar()); } } /// Select prefs for prompt injection according to weight (0=off, <1=short, 1=full, >1=must). public List SelectUserPrefsForPrompt(string personaId, double weight, int maxRows) { if (weight <= 0) { return []; } string pid = AssistentConfig.SafeId(personaId) ?? "neutral"; JArray all = ListUserPrefs(null, pid, 500); List list = all.OfType().ToList(); maxRows = Math.Clamp(maxRows, 1, 40); int take = weight >= 1 ? maxRows : Math.Max(3, (int)Math.Ceiling(maxRows * weight)); return list.Take(take).ToList(); } public string FormatUserPrefsBlock(string personaId, double weight, int maxRows) { List rows = SelectUserPrefsForPrompt(personaId, weight, maxRows); if (rows.Count == 0 || weight <= 0) { return null; } var sb = new System.Text.StringBuilder(); sb.AppendLine("## About the user"); if (weight > 1.05) { sb.AppendLine("MUST respect these preferences unless the current user message overrides them this turn."); } sb.AppendLine("Facts about the human (global = every persona; persona = this agent only):"); foreach (JObject row in rows) { string sc = row["scope"]?.ToString() ?? "global"; string key = row["key"]?.ToString() ?? ""; string text = row["text"]?.ToString() ?? ""; string pin = row["pinned"]?.Value() == true ? " ★" : ""; sb.AppendLine($"- [{sc}] {key}{pin}: {text}"); } return sb.ToString().TrimEnd(); } /// Batch-delete craft vector rows (never bundled). Returns deleted count. public int ClearCraftMemory(string scope = null, string kind = null, string persona = null) { lock (_lock) { EnsureOpen(); using SqliteCommand cmd = _conn.CreateCommand(); List where = ["source != 'bundled'"]; string wantScope = (scope ?? "").Trim().ToLowerInvariant(); if (wantScope is "shared" or "common" or "global") { where.Add("persona = ''"); } else if (wantScope is "personal" || !string.IsNullOrWhiteSpace(persona)) { string pid = NormalizePersona(persona); where.Add("persona = $persona"); cmd.Parameters.AddWithValue("$persona", pid); } string k = (kind ?? "").Trim().ToLowerInvariant(); if (!string.IsNullOrWhiteSpace(k) && k != "all") { where.Add("kind = $kind"); cmd.Parameters.AddWithValue("$kind", k); } cmd.CommandText = "DELETE FROM memories WHERE " + string.Join(" AND ", where); return cmd.ExecuteNonQuery(); } } }