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.Core; using SwarmUI.Text2Image; using SwarmUI.Utils; namespace Mrleo1nid.SwarmAssistent; /// Prompt assembly, memory retrieval, and ask-only server hop loop (settings / inventory). public partial class SwarmAssistentExtension { const int MaxToolHopsFallback = 2; static bool IsSlimUtilityPack(string packName) => string.Equals(packName, "debug_explain", StringComparison.OrdinalIgnoreCase) || string.Equals(packName, "compress_history", StringComparison.OrdinalIgnoreCase); [Obsolete("Use IsSlimUtilityPack")] static bool IsSlimDebugPack(string packName) => IsSlimUtilityPack(packName); (List messages, JObject systemLayers) BuildOllamaMessages(string packName, bool includeBase, string contextJson, JArray userMessages, string extraSystem = null, string personaId = null, IEnumerable skillIds = null) { List ollamaMessages = []; StringBuilder system = new(); JObject layers = new(); string pid = AssistentConfig.SafeId(personaId) ?? Config.DefaultPersonaId(); void AddLayer(string name, string block) { if (string.IsNullOrWhiteSpace(block)) { return; } if (system.Length > 0) { system.AppendLine(); } int before = system.Length; system.AppendLine(block.TrimEnd()); layers[name] = system.Length - before; } if (includeBase) { AddLayer("core", Config.LoadCorePrompt(pid)); } bool slimUtility = IsSlimUtilityPack(packName); if (Memory is not null && !slimUtility) { try { JObject asst = Config.LoadAssistant(pid); double weight = asst["user_prefs_weight"]?.Value() ?? 1.0; int maxPrefs = asst["user_prefs_max"]?.Value() ?? 16; AddLayer("prefs", Memory.FormatUserPrefsBlock(pid, weight, maxPrefs)); } catch (Exception ex) { Logs.Debug($"BuildOllamaMessages user prefs: {ex.Message}"); } } JObject exact = Config.LoadExactForPrompt(pid); if (exact is not null && exact.Count > 0 && LiveContextHasSize(contextJson)) { exact.Remove("aspect_table"); } if (exact is not null && exact.Count > 0) { AddLayer("exact", "## Exact memory (canonical KV defaults — prefer over RAG for numbers)\n```json\n" + exact.ToString(Newtonsoft.Json.Formatting.None) + "\n```"); } if (!slimUtility) { StringBuilder skillsBlock = new(); foreach (string skillId in skillIds ?? Config.ResolveEnabledSkills(pid, null)) { string skillText = Config.LoadSkillPrompt(pid, skillId); if (!string.IsNullOrWhiteSpace(skillText)) { if (skillsBlock.Length > 0) { skillsBlock.AppendLine(); } skillsBlock.AppendLine($"## Skill: {skillId}"); skillsBlock.AppendLine(skillText.TrimEnd()); } } AddLayer("skills", skillsBlock.ToString()); AddLayer("identity", Config.RenderIdentityBlock(pid)); string knowledgeLayer = BuildKnowledgeSystemLayer(pid); if (!string.IsNullOrWhiteSpace(knowledgeLayer)) { AddLayer("knowledge", knowledgeLayer); } } if (!string.IsNullOrWhiteSpace(packName)) { string situational = Config.LoadPackPrompt(pid, packName); if (!string.IsNullOrWhiteSpace(situational)) { AddLayer("pack", $"## Active mode: {packName}\n{situational.TrimEnd()}"); } } if (!string.IsNullOrWhiteSpace(contextJson)) { AddLayer("live", "## Live SwarmUI context (JSON — trust this over guesses)\n```json\n" + contextJson + "\n```"); } if (!string.IsNullOrWhiteSpace(extraSystem)) { AddLayer("extra", extraSystem); } bool priorAssistant = false; foreach (JToken msg in userMessages ?? []) { if (string.Equals((msg as JObject)?["role"]?.ToString(), "assistant", StringComparison.OrdinalIgnoreCase)) { priorAssistant = true; break; } } if (!priorAssistant && !string.IsNullOrWhiteSpace(contextJson)) { try { JObject ctx = JObject.Parse(contextJson); priorAssistant = ctx.Value("do_not_greet") == true || (ctx["prior_assistant_turns"]?.Value() ?? 0) > 0; } catch { // not json } } if (priorAssistant && !slimUtility) { AddLayer("continuity", "Conversation continuity: this thread already has your prior replies. " + "Do NOT greet (no «О, привет», no hello, no 👋). " + "Do NOT re-introduce yourself or dump your bio/tagline. " + "Answer only the latest user message. Name yourself only if asked who you are."); } layers["total"] = system.Length; if (system.Length > 0) { ollamaMessages.Add(new JObject { ["role"] = "system", ["content"] = system.ToString(), }); } foreach (JToken msg in userMessages ?? []) { if (msg is not JObject mo) { continue; } JObject copy = new() { ["role"] = mo["role"]?.ToString() ?? "user", ["content"] = mo["content"]?.ToString() ?? "", }; if (mo["images"] is JArray images && images.Count > 0) { copy["images"] = images; } ollamaMessages.Add(copy); } return (ollamaMessages, layers); } async Task<(string reply, JObject raw, JArray civitaiResults, JObject knowledge, int systemChars, JObject systemLayers)> RunChatWithHops( Session session, string root, string modelName, string packName, bool includeBase, string contextJson, JArray userMessages, Func onDelta = null, Func onHopStart = null, string personaId = null, JArray skillIds = null, string embedModel = null) { string pid = AssistentConfig.SafeId(personaId) ?? Config.DefaultPersonaId(); List skills = Config.ResolveEnabledSkills(pid, skillIds); string embed = string.IsNullOrWhiteSpace(embedModel) ? (Config.LoadSettings()["embed_model"]?.ToString() ?? Config.LoadAssistant(pid)["embed_model"]?.ToString() ?? "nomic-embed-text") : embedModel; try { await Memory.EnsureSeedAsync(root, Config, embed); } catch (Exception ex) { Logs.Debug($"Assistent memory seed: {ex.Message}"); } bool slimUtility = IsSlimUtilityPack(packName); JArray hits = []; if (!slimUtility) { string retrieveQuery = BuildRetrieveQuery(userMessages, contextJson, packName); try { AssistentMemory.RetrieveOptions opt = MemoryRetrieveOptions(pid); hits = await Memory.RetrieveAsync(root, retrieveQuery, opt.TopK, embed, Config.PersonaExtendsChain(pid), opt); hits = FilterHeardHitsIfDisabled(hits); } catch (Exception ex) { Logs.Debug($"Assistent memory retrieve: {ex.Message}"); } } string enrichedContext = InjectMemoryHits(contextJson, hits, pid); if (!slimUtility) { enrichedContext = EnrichPersonaContext(enrichedContext, pid, packName); } (List messages, JObject systemLayers) = BuildOllamaMessages(packName, includeBase, enrichedContext, userMessages, personaId: pid, skillIds: skills); int systemChars = systemLayers["total"]?.Value() ?? messages.FirstOrDefault(m => string.Equals(m["role"]?.ToString(), "system", StringComparison.OrdinalIgnoreCase))?["content"]?.ToString()?.Length ?? 0; string reply = ""; JObject lastRaw = null; JArray knowledgeHops = []; JArray knowledgeResults = []; int maxHops = slimUtility ? 1 : CfgInt("max_tool_hops", MaxToolHopsFallback); HashSet hopDone = new(StringComparer.OrdinalIgnoreCase); for (int hop = 0; hop < maxHops; hop++) { if (onHopStart is not null) { await onHopStart(hop); } (reply, lastRaw) = await CallOllamaChat(root, modelName, messages, stream: onDelta is not null, onDelta, pid); if (slimUtility) { break; } JObject patch = TryParsePatch(reply); // 0.14: no ApplyMemoryActions / ApplyUserPrefActions / ApplyPersonaActions / Civitai from server hop loop. if (hop + 1 >= maxHops) { break; } string follow = null; HashSet hopSkip = new(StringComparer.OrdinalIgnoreCase); while (true) { string tool = NextToolHop(patch, hopSkip); if (string.IsNullOrWhiteSpace(tool)) { follow = null; break; } follow = await RunToolHop(session, pid, patch, tool, hopDone, knowledgeHops, knowledgeResults); if (follow is not null) { break; } hopSkip.Add(tool); } if (follow is null) { break; } // Re-feed only the parsed patch JSON (not full prose) to save hop tokens. string assistantContent = patch is not null ? patch.ToString(Newtonsoft.Json.Formatting.None) : reply; messages.Add(new JObject { ["role"] = "assistant", ["content"] = assistantContent }); messages.Add(new JObject { ["role"] = "user", ["content"] = follow }); } JObject knowledge = BuildKnowledgeResponse(pid, knowledgeHops, knowledgeResults); JArray civitai = CivitaiResultsShim(knowledgeResults); return (reply, lastRaw, civitai, knowledge, systemChars, systemLayers); } static string BuildRetrieveQuery(JArray userMessages, string contextJson, string packName = null) { StringBuilder sb = new(); if (!string.IsNullOrWhiteSpace(packName)) { sb.Append(packName).Append(' '); } if (!string.IsNullOrWhiteSpace(contextJson)) { try { JObject ctx = JObject.Parse(contextJson); string ckpt = ctx["checkpoint"]?.ToString() ?? ctx["current_model"]?.ToString(); if (!string.IsNullOrWhiteSpace(ckpt)) { sb.Append(ckpt).Append(' '); } if (ctx["selected_loras"] is JArray selLoras) { foreach (JToken t in selLoras.Take(12)) { string n = t?["name"]?.ToString() ?? t?.ToString(); if (!string.IsNullOrWhiteSpace(n)) { sb.Append(n).Append(' '); } } } else if (ctx["enabled_loras"] is JArray en) { foreach (JToken t in en.Take(12)) { string n = t?["name"]?.ToString() ?? t?.ToString(); if (!string.IsNullOrWhiteSpace(n)) { sb.Append(n).Append(' '); } } } if (ctx["krea_profile"] != null) { sb.Append("krea ").Append(ctx["krea_profile"]).Append(' '); } string aspect = ctx["aspect"]?.ToString(); if (!string.IsNullOrWhiteSpace(aspect)) { sb.Append(aspect).Append(' '); } string prompt = ctx["prompt"]?.ToString(); if (!string.IsNullOrWhiteSpace(prompt)) { sb.Append(prompt.Length > 400 ? prompt[..400] : prompt).Append(' '); } } catch { // ignore } } foreach (JToken msg in (userMessages ?? []).Reverse().Take(3)) { if (msg is JObject mo && string.Equals(mo["role"]?.ToString(), "user", StringComparison.OrdinalIgnoreCase)) { string c = mo["content"]?.ToString() ?? ""; sb.Append(c.Length > 500 ? c[..500] : c).Append(' '); } } string q = CollapseWs(sb.ToString()); return string.IsNullOrWhiteSpace(q) ? "krea2 prompting" : q; } AssistentMemory.RetrieveOptions MemoryRetrieveOptions(string pid) { JObject a = Config.LoadAssistant(pid) ?? new JObject(); JObject agent = Config.LoadTrainingAgent(); AssistentMemory.RetrieveOptions opt = new() { TopK = a["memory_top_k"]?.Value() ?? 8, MinScore = a["memory_min_score"]?.Value() ?? 0.32f, ApplyQuotas = true, }; Dictionary quotas = AssistentMemory.CopyDefaultQuotas(); if (a["memory_quotas"] is JObject qOverrides) { foreach (JProperty p in qOverrides.Properties()) { quotas[p.Name] = p.Value?.Value() ?? 2; } } if (agent["enabled"]?.Value() != false) { quotas["heard"] = agent["heard_quota"]?.Value() ?? 3; } else { quotas.Remove("heard"); } opt.Quotas = quotas; return opt; } JArray FilterHeardHitsIfDisabled(JArray hits) { if (Config.LoadTrainingAgent()["enabled"]?.Value() != false) { return hits; } JArray filtered = []; foreach (JToken t in hits ?? []) { if (t is JObject ho && string.Equals(ho["kind"]?.ToString(), AssistentMemory.HeardKind, StringComparison.OrdinalIgnoreCase)) { continue; } filtered.Add(t); } return filtered; } /// Ask-only server hops: Exact/assistant settings dump or truncated inventory. async Task RunToolHop( Session session, string pid, JObject patch, string tool, HashSet hopDone, JArray knowledgeHops = null, JArray knowledgeResults = null) { if (tool == "ask_settings") { if (!hopDone.Add("ask_settings")) { return null; } JObject dump = BuildAskSettingsDump(pid); return "ask:settings dump (Exact + assistant knobs from server). " + "Live session fields arrive via client compact context. " + "Reply with a sparse delta JSON only if needed; omit ask:settings unless you need a refresh.\n```json\n" + dump.ToString(Newtonsoft.Json.Formatting.None) + "\n```"; } if (tool == "ask_inventory") { string q = patch?["inventory_query"]?.ToString()?.Trim() ?? ""; string sig = "ask_inventory:" + q.ToLowerInvariant(); if (!hopDone.Add(sig)) { return null; } int lim = Config.LoadAssistant(pid)["inventory_hop_limit"]?.Value() ?? 20; JArray rows; if (string.IsNullOrWhiteSpace(q)) { JObject inv = await AssistentListInventory(session, rescan: false); rows = TruncateInventoryForAsk(inv, lim); } else { rows = SearchInventoryForHop(q, lim); } return "ask:inventory truncated LoRA/checkpoint list. Use exact names + listed triggers; " + "omit ask:inventory unless you need a different query.\n```json\n" + rows.ToString(Newtonsoft.Json.Formatting.None) + "\n```"; } if (tool == "ask_knowledge") { string q = patch?["knowledge_query"]?.ToString()?.Trim() ?? patch?["example_query"]?.ToString()?.Trim() ?? patch?["memory_query"]?.ToString()?.Trim() ?? ""; string rating = patch?["knowledge_rating"]?.ToString()?.Trim() ?? patch?["example_rating"]?.ToString()?.Trim(); string sig = "ask_knowledge:" + q.ToLowerInvariant() + ":" + (rating ?? ""); if (!hopDone.Add(sig)) { return null; } if (string.IsNullOrWhiteSpace(q)) { return "ask:knowledge needs knowledge_query (tags / scene / style). " + "Retry with \"ask\":[\"knowledge\"], \"knowledge_query\":\"redhead stockings\"."; } int lim = Config.LoadAssistant(pid)["knowledge_hop_limit"]?.Value() ?? Config.LoadAssistant(pid)["examples_hop_limit"]?.Value() ?? 6; JArray rows = SearchKnowledge(pid, q, lim, rating); knowledgeHops?.Add(new JObject { ["tool"] = "ask_knowledge", ["query"] = q, ["count"] = rows.Count }); MergeKnowledgeResults(knowledgeResults, rows); return "ask:knowledge — FTS over attached books (read-only references). " + "Remix ideas; do not paste long verbatim.\n```json\n" + rows.ToString(Newtonsoft.Json.Formatting.None) + "\n```"; } if (tool == "ask_examples") { string q = patch?["example_query"]?.ToString()?.Trim() ?? patch?["memory_query"]?.ToString()?.Trim() ?? ""; string rating = patch?["example_rating"]?.ToString()?.Trim(); string sig = "ask_examples:" + q.ToLowerInvariant() + ":" + (rating ?? ""); if (!hopDone.Add(sig)) { return null; } if (string.IsNullOrWhiteSpace(q)) { return "ask:examples needs example_query (tags / short scene). " + "Retry with \"ask\":[\"examples\"], \"example_query\":\"redhead stockings cinematic\"."; } int lim = Config.LoadAssistant(pid)["examples_hop_limit"]?.Value() ?? 5; JArray examples = Memory?.LookupExamples(q, lim, rating) ?? []; knowledgeHops?.Add(new JObject { ["tool"] = "ask_examples", ["query"] = q, ["count"] = examples.Count }); MergeKnowledgeResults(knowledgeResults, examples, legacyExamples: true); return "ask:examples — Civitai Krea2 prompt references (FTS, no embeddings). " + "These are EXAMPLES to remix, not copy 1:1. Prefer craft over pasting.\n```json\n" + examples.ToString(Newtonsoft.Json.Formatting.None) + "\n```"; } return null; } static JArray TruncateInventoryForAsk(JObject inv, int limit) { int lim = Math.Max(1, Math.Min(limit, 40)); JArray outRows = []; foreach (JToken t in inv?["loras"] as JArray ?? []) { if (outRows.Count >= lim) { break; } outRows.Add(t); } foreach (JToken t in inv?["checkpoints"] as JArray ?? []) { if (outRows.Count >= lim) { break; } outRows.Add(t); } return outRows; } JObject BuildAskSettingsDump(string pid) { JObject exact = Config.LoadExact(pid) ?? new JObject(); JObject asst = Config.LoadAssistant(pid) ?? new JObject(); // Knobs the model may need — not the full assistant.json blob (quotas/seed noise). JObject knobs = new() { ["num_ctx"] = asst["num_ctx"], ["num_predict"] = asst["num_predict"], ["max_tool_hops"] = asst["max_tool_hops"], ["inventory_hop_limit"] = asst["inventory_hop_limit"], ["max_loras_inventory"] = asst["max_loras_inventory"], ["max_checkpoints_inventory"] = asst["max_checkpoints_inventory"], ["max_gen_variants"] = asst["max_gen_variants"], ["max_ref_slots"] = asst["max_ref_slots"], ["default_pack"] = asst["default_pack"], ["default_persona"] = asst["default_persona"], ["gate"] = asst["gate"], ["context_prompt_max"] = asst["context_prompt_max"], ["history_keep_turns"] = asst["history_keep_turns"], ["compress_at"] = asst["compress_at"], ["chars_per_token"] = asst["chars_per_token"], ["compress_auto"] = asst["compress_auto"], }; return new JObject { ["detail"] = "settings", ["exact"] = exact, ["assistant"] = knobs, ["persona"] = pid, }; } /// Substring filter over current LoRA/checkpoint inventory for ask:inventory hop. JArray SearchInventoryForHop(string query, int limit) { int lim = Math.Max(1, Math.Min(limit, 40)); string q = (query ?? "").Trim().ToLowerInvariant(); JArray outRows = []; void AddFromHandler(string setName, string kind) { if (!Program.T2IModelSets.TryGetValue(setName, out T2IModelHandler handler)) { return; } IEnumerable models = handler.Models.Values .OrderByDescending(LooksLikeKreaArch) .ThenBy(m => m.Name); foreach (T2IModel model in models) { if (outRows.Count >= lim) { return; } string name = model.Name ?? ""; if (!string.IsNullOrEmpty(q)) { string blob = $"{name} {model.Metadata?.UsageHint} {model.Metadata?.Description}".ToLowerInvariant(); if (!blob.Contains(q, StringComparison.Ordinal)) { continue; } } outRows.Add(BuildInventoryModelEntry(model, kind)); } } AddFromHandler("LoRA", "lora"); if (outRows.Count < lim) { AddFromHandler("Stable-Diffusion", "checkpoint"); } return outRows; } string InjectMemoryHits(string contextJson, JArray hits, string personaId = null) { JObject ctx; try { ctx = string.IsNullOrWhiteSpace(contextJson) ? new JObject() : JObject.Parse(contextJson); } catch { ctx = new JObject { ["_raw_context"] = contextJson }; } int hitChars = 240; try { string pidHit = AssistentConfig.SafeId(ctx["persona"]?.ToString()) ?? personaId ?? Config?.DefaultPersonaId() ?? "neutral"; hitChars = Config?.LoadAssistant(pidHit)?["memory_hit_chars"]?.Value() ?? 240; } catch { hitChars = 240; } hitChars = Math.Max(80, Math.Min(hitChars, 800)); string pid = AssistentConfig.SafeId(ctx["persona"]?.ToString()) ?? personaId ?? Config?.DefaultPersonaId() ?? "neutral"; IEnumerable chain = Config?.PersonaExtendsChain(pid) ?? []; JArray clippedHits = []; JArray heardExamples = []; foreach (JToken t in hits ?? []) { if (t is not JObject ho) { continue; } if (IsExactPointerHit(ho)) { continue; } if (string.Equals(ho["kind"]?.ToString(), AssistentMemory.HeardKind, StringComparison.OrdinalIgnoreCase)) { JObject ex = Memory?.BuildHeardExampleFromHit(ho, chain); if (ex is not null) { heardExamples.Add(ex); } continue; } JObject copy = (JObject)ho.DeepClone(); string text = copy["text"]?.ToString() ?? ""; if (text.Length > hitChars) { copy["text"] = text[..hitChars] + "…"; copy["truncated"] = true; } clippedHits.Add(copy); } ctx["memory_hits"] = clippedHits; if (heardExamples.Count > 0) { ctx["heard_examples"] = heardExamples; } else { ctx.Remove("heard_examples"); } ctx.Remove("taste_profile"); ctx.Remove("enabled_loras"); // alias of selected_loras — do not double-feed try { JObject asst = Config?.LoadAssistant(pid) ?? new JObject(); double weight = asst["user_prefs_weight"]?.Value() ?? 1.0; int maxPrefs = asst["user_prefs_max"]?.Value() ?? 16; ctx["user_prefs_count"] = Memory?.SelectUserPrefsForPrompt(pid, weight, maxPrefs).Count ?? 0; } catch { ctx["user_prefs_count"] = 0; } // Never re-inject full Exact into live context (already in system prompt). ctx.Remove("exact"); if (ctx["session_exact"] is JObject se && !se.Properties().Any()) { ctx.Remove("session_exact"); } // Always normalize inventory: keep enabled + rich top-N; name-only for the rest. SlimAvailableLorasInContext(ctx, hits); DropNullOrEmpty(ctx); return ctx.ToString(Newtonsoft.Json.Formatting.None); } static bool LiveContextHasSize(string contextJson) { if (string.IsNullOrWhiteSpace(contextJson)) { return false; } try { JObject ctx = JObject.Parse(contextJson); int? w = ctx["width"]?.Value(); int? h = ctx["height"]?.Value(); return w is > 0 && h is > 0; } catch { return false; } } /// Drop RAG rows that only point at Exact (legacy krea_facts seed / "see Exact memory…"). static bool IsExactPointerHit(JObject ho) { if (ho is null) { return false; } string key = (ho["key"]?.ToString() ?? "").Trim().ToLowerInvariant(); if (key.StartsWith("krea2_", StringComparison.Ordinal)) { return true; } string text = (ho["text"]?.ToString() ?? "").ToLowerInvariant(); if (string.IsNullOrWhiteSpace(text)) { return false; } return text.Contains("live in exact memory", StringComparison.Ordinal) || text.Contains("see exact memory", StringComparison.Ordinal) || text.Contains("prefer exact kv", StringComparison.Ordinal) || text.Contains("exact.facts.", StringComparison.Ordinal) || text.Contains("exact memory profiles.", StringComparison.Ordinal); } void SlimAvailableLorasInContext(JObject ctx, JArray hits) { if (ctx["available_loras"] is not JArray allLoras || allLoras.Count == 0) { return; } int richCap = 12; int namesCap = 24; try { string pid = AssistentConfig.SafeId(ctx["persona"]?.ToString()) ?? Config?.DefaultPersonaId() ?? "neutral"; JObject asst = Config?.LoadAssistant(pid) ?? new JObject(); richCap = asst["inventory_prompt_rich"]?.Value() ?? 12; namesCap = asst["inventory_prompt_names"]?.Value() ?? 24; } catch { /* defaults */ } richCap = Math.Max(4, Math.Min(richCap, 40)); namesCap = Math.Max(richCap, Math.Min(namesCap, 80)); HashSet keepRich = new(StringComparer.OrdinalIgnoreCase); if (ctx["enabled_loras"] is JArray en) { foreach (JToken t in en) { string n = t?["name"]?.ToString() ?? t?.ToString(); if (!string.IsNullOrWhiteSpace(n)) { keepRich.Add(n); } } } if (ctx["selected_loras"] is JArray sel) { foreach (JToken t in sel) { string n = t?["name"]?.ToString() ?? t?.ToString(); if (!string.IsNullOrWhiteSpace(n)) { keepRich.Add(n); } } } foreach (JToken hit in hits ?? []) { if (string.Equals(hit?["kind"]?.ToString(), "lora", StringComparison.OrdinalIgnoreCase) || string.Equals(hit?["kind"]?.ToString(), "card", StringComparison.OrdinalIgnoreCase)) { string k = hit?["key"]?.ToString(); if (!string.IsNullOrWhiteSpace(k)) { keepRich.Add(k); } } } List ordered = allLoras .OrderByDescending(t => keepRich.Contains(t?["name"]?.ToString() ?? "") ? 1000 : 0) .ThenByDescending(t => t?["krea_likely"]?.Value() == true ? 50 : 0) .ThenBy(t => t?["name"]?.ToString() ?? "", StringComparer.OrdinalIgnoreCase) .ToList(); JArray slim = []; int richCount = 0; foreach (JToken t in ordered) { if (slim.Count >= namesCap) { break; } string n = t?["name"]?.ToString(); if (string.IsNullOrWhiteSpace(n)) { continue; } bool wantRich = keepRich.Contains(n) || (t?["krea_likely"]?.Value() == true && richCount < richCap); if (wantRich) { JObject rich = EnrichLoraRowForPrompt(t as JObject ?? new JObject { ["name"] = n }); slim.Add(rich); if (!keepRich.Contains(n)) { richCount++; } } else { JObject nameOnly = new() { ["name"] = n }; if (t?["krea_likely"]?.Value() == true) { nameOnly["krea_likely"] = true; } slim.Add(nameOnly); } } ctx["available_loras"] = slim; if (allLoras.Count > slim.Count) { ctx["available_loras_truncated"] = true; // Prefer client total if already set (full disk inventory count). if (ctx["available_loras_total"] is null) { ctx["available_loras_total"] = allLoras.Count; } } } /// Ensure rich LoRA rows have triggers/blurb from Swarm inventory when the client sent name-only. JObject EnrichLoraRowForPrompt(JObject row) { if (row is null) { return new JObject(); } JObject outRow = (JObject)row.DeepClone(); string name = outRow["name"]?.ToString(); bool needsTriggers = string.IsNullOrWhiteSpace(outRow["trigger_phrase"]?.ToString()) && (outRow["triggers"] is not JArray tr || tr.Count == 0); bool needsBlurb = string.IsNullOrWhiteSpace(outRow["blurb"]?.ToString()); if (!needsTriggers && !needsBlurb) { return outRow; } JObject fromDisk = FindInventoryLoraByName(name); if (fromDisk is null) { return outRow; } if (needsTriggers) { if (!string.IsNullOrWhiteSpace(fromDisk["trigger_phrase"]?.ToString())) { outRow["trigger_phrase"] = fromDisk["trigger_phrase"]; } if (fromDisk["triggers"] is JArray ft && ft.Count > 0) { outRow["triggers"] = ft.DeepClone(); } } if (needsBlurb && !string.IsNullOrWhiteSpace(fromDisk["blurb"]?.ToString())) { outRow["blurb"] = fromDisk["blurb"]; } if (outRow["default_weight"] is null && fromDisk["default_weight"] is not null) { outRow["default_weight"] = fromDisk["default_weight"]; } if (fromDisk["krea_likely"]?.Value() == true) { outRow["krea_likely"] = true; } return outRow; } JObject FindInventoryLoraByName(string name) { if (string.IsNullOrWhiteSpace(name) || !Program.T2IModelSets.TryGetValue("LoRA", out T2IModelHandler handler)) { return null; } T2IModel model = handler.Models.Values.FirstOrDefault(m => string.Equals(m.Name, name, StringComparison.OrdinalIgnoreCase) || string.Equals(Path.GetFileNameWithoutExtension(m.Name), Path.GetFileNameWithoutExtension(name), StringComparison.OrdinalIgnoreCase) || (m.Name?.EndsWith("/" + name, StringComparison.OrdinalIgnoreCase) ?? false)); return model is null ? null : BuildInventoryModelEntry(model, "lora"); } static void DropNullOrEmpty(JObject ctx) { List remove = []; foreach (JProperty p in ctx.Properties()) { if (p.Value is null || p.Value.Type == JTokenType.Null) { remove.Add(p.Name); } else if (p.Value is JObject jo && !jo.Properties().Any()) { remove.Add(p.Name); } else if (p.Value is JArray ja && ja.Count == 0 && p.Name is not "memory_hits") { remove.Add(p.Name); } } foreach (string k in remove) { ctx.Remove(k); } } string EnrichPersonaContext(string contextJson, string personaId, string packName) { JObject ctx; try { ctx = string.IsNullOrWhiteSpace(contextJson) ? new JObject() : JObject.Parse(contextJson); } catch { ctx = new JObject { ["_raw_context"] = contextJson }; } string pid = AssistentConfig.SafeId(personaId) ?? Config.DefaultPersonaId(); ctx["persona_source"] = Config.PersonaSource(pid); JObject schema = Config.LoadControlsSchema(pid); JObject values = Config.LoadControlValues(pid); bool authorPack = string.Equals(packName, "author_persona", StringComparison.OrdinalIgnoreCase) || string.Equals(packName, "persona", StringComparison.OrdinalIgnoreCase); // Values only outside author pack (schema is fat). Author pack gets full schema. if (values.Properties().Any()) { if (authorPack && schema.Properties().Any()) { ctx["persona_controls"] = new JObject { ["schema"] = schema, ["values"] = values, }; } else { ctx["persona_controls"] = new JObject { ["values"] = values }; } } JArray catalog = []; if (authorPack) { foreach (var p in Config.ListPersonaCatalog()) { catalog.Add(new JObject { ["id"] = p.id, ["title"] = p.title, }); } ctx["personas"] = catalog; JObject shelves = Config.LoadIdentityParts(pid); shelves.Remove("extra"); ctx["persona_shelves"] = shelves; if (schema.Properties().Any()) { ctx["persona_controls_schema"] = schema; } } DropNullOrEmpty(ctx); return ctx.ToString(Newtonsoft.Json.Formatting.None); } static string MemoryWritePersona(JObject mo, string currentPersonaId) { string scope = (mo?["scope"]?.ToString() ?? "").Trim().ToLowerInvariant(); if (scope is "shared" or "common" or "global") { return AssistentMemory.SharedPersona; } // Personal only — never let the model write into another personality's store. return AssistentConfig.SafeId(currentPersonaId) ?? AssistentMemory.SharedPersona; } /// Apply overlay persona clone/write from patch. Ignores persona_delete. Updates pid ref after switch. void ApplyPersonaActions(JObject patch, ref string personaId) { if (patch is null || Config is null) { return; } // Never honor delete from the model. bool wantClone = false, wantWrite = false; if (patch["actions"] is JArray acts) { foreach (JToken a in acts) { string s = a?.ToString() ?? ""; if (string.Equals(s, "persona_clone", StringComparison.OrdinalIgnoreCase)) { wantClone = true; } if (string.Equals(s, "persona_write", StringComparison.OrdinalIgnoreCase)) { wantWrite = true; } } } if (patch["persona_clone"] is JObject) { wantClone = true; } // persona_shelves as object of content = write; as array of names = persona_read hop (ignore here). if (patch["persona_shelves"] is JObject && !ActionsContain(patch, "persona_read")) { wantWrite = true; } try { if (wantClone && patch["persona_clone"] is JObject clone) { string from = AssistentConfig.SafeId(clone["from"]?.ToString()) ?? personaId; string to = AssistentConfig.SafeId(clone["to"]?.ToString()); string title = clone["title"]?.ToString(); bool overwrite = clone["overwrite"]?.Value() == true; if (to is not null) { Config.ClonePersonaToOverlay(from, to, title, overwrite); personaId = to; patch["_persona_cloned"] = to; } } if (wantWrite && patch["persona_shelves"] is JObject shelves) { string target = AssistentConfig.SafeId(patch["persona"]?.ToString()) ?? AssistentConfig.SafeId(patch["persona_clone"]?["to"]?.ToString()) ?? personaId; if (target is not null) { Config.SavePersonaShelves(target, shelves); patch["_persona_written"] = target; } } // Control values from model patch (Exact). Generate patches never touch sliders. if (patch["controls"] is JObject ctrlVals) { if (AssistentConfig.PatchLooksLikeGeneration(patch)) { patch.Remove("controls"); } else { string ctrlPid = AssistentConfig.SafeId(patch["persona"]?.ToString()) ?? personaId; JObject schema = Config.LoadControlsSchema(ctrlPid); JObject current = Config.LoadControlValues(ctrlPid); JObject filtered = AssistentConfig.FilterEchoedControlDefaults( schema, current, ctrlVals, patchLooksLikeGen: false); if (filtered.Count > 0) { Config.SaveControlValues(ctrlPid, filtered); patch["controls"] = filtered; patch["_controls_saved"] = true; } else { patch.Remove("controls"); } } } } catch (Exception ex) { Logs.Warning($"Assistent persona actions: {ex.Message}"); patch["_persona_error"] = ex.Message; } } async Task ApplyMemoryActions(string root, JObject patch, string embedModel, string personaId) { if (patch is null || Memory is null) { return; } bool upsert = false, forget = false; if (patch["actions"] is JArray acts) { foreach (JToken a in acts) { string s = a?.ToString() ?? ""; if (string.Equals(s, "memory_upsert", StringComparison.OrdinalIgnoreCase)) { upsert = true; } if (string.Equals(s, "memory_forget", StringComparison.OrdinalIgnoreCase)) { forget = true; } } } JArray memories = patch["memories"] as JArray; if (memories is null || memories.Count == 0) { return; } foreach (JToken t in memories) { if (t is not JObject mo) { continue; } string kind = mo["kind"]?.ToString() ?? "note"; string key = mo["key"]?.ToString() ?? ""; string text = mo["text"]?.ToString() ?? ""; string target = MemoryWritePersona(mo, personaId); try { if (forget && string.IsNullOrWhiteSpace(text)) { Memory.Forget(kind, key, persona: target); } else if (upsert || !string.IsNullOrWhiteSpace(text)) { await Memory.UpsertTextAsync(root, kind, key, text, "user", mo, embedModel, target); } } catch (Exception ex) { Logs.Debug($"ApplyMemoryActions: {ex.Message}"); } } } void ApplyUserPrefActions(JObject patch, string personaId) { if (patch is null || Memory is null) { return; } bool upsert = false, forget = false; if (patch["actions"] is JArray acts) { foreach (JToken a in acts) { string s = a?.ToString() ?? ""; if (string.Equals(s, "user_pref_upsert", StringComparison.OrdinalIgnoreCase)) { upsert = true; } if (string.Equals(s, "user_pref_forget", StringComparison.OrdinalIgnoreCase)) { forget = true; } } } JArray prefs = patch["user_prefs"] as JArray; if (prefs is null || prefs.Count == 0) { return; } string pid = AssistentConfig.SafeId(personaId) ?? Config.DefaultPersonaId(); foreach (JToken t in prefs) { if (t is not JObject mo) { continue; } string key = mo["key"]?.ToString() ?? ""; string text = mo["text"]?.ToString() ?? ""; string scope = mo["scope"]?.ToString() ?? "global"; bool pinned = mo["pinned"]?.Value() == true; try { if (forget && string.IsNullOrWhiteSpace(text)) { Memory.ForgetUserPref(key, scope, pid); } else if (upsert || !string.IsNullOrWhiteSpace(text)) { Memory.UpsertUserPref(key, text, scope, pid, "agent", pinned); } } catch (Exception ex) { Logs.Debug($"ApplyUserPrefActions: {ex.Message}"); } } } }