Files
swarm-assistent/AssistentChatPipeline.cs
T
Leonid PershinandCursor e2d48b4bf9 Add FTS lookup for seeded Civitai Krea2 prompt examples.
Index Assistent/civitai-examples.jsonl without embeddings and expose AssistentLookupExamples plus ask:examples hop for remix references.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-23 08:15:40 +03:00

1145 lines
42 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.Core;
using SwarmUI.Text2Image;
using SwarmUI.Utils;
namespace Mrleo1nid.SwarmAssistent;
/// <summary>Prompt assembly, memory retrieval, and ask-only server hop loop (settings / inventory).</summary>
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<JObject> messages, JObject systemLayers) BuildOllamaMessages(string packName, bool includeBase, string contextJson, JArray userMessages, string extraSystem = null, string personaId = null, IEnumerable<string> skillIds = null)
{
List<JObject> 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<double?>() ?? 1.0;
int maxPrefs = asst["user_prefs_max"]?.Value<int?>() ?? 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));
}
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);
}
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, int systemChars, JObject systemLayers)> RunChatWithHops(
Session session,
string root,
string modelName,
string packName,
bool includeBase,
string contextJson,
JArray userMessages,
Func<string, Task> onDelta = null,
Func<int, Task> onHopStart = null,
string personaId = null,
JArray skillIds = null,
string embedModel = null)
{
string pid = AssistentConfig.SafeId(personaId) ?? Config.DefaultPersonaId();
List<string> 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<JObject> messages, JObject systemLayers) = BuildOllamaMessages(packName, includeBase, enrichedContext, userMessages, personaId: pid, skillIds: skills);
int systemChars = systemLayers["total"]?.Value<int?>()
?? messages.FirstOrDefault(m => string.Equals(m["role"]?.ToString(), "system", StringComparison.OrdinalIgnoreCase))?["content"]?.ToString()?.Length
?? 0;
string reply = "";
JObject lastRaw = null;
int maxHops = slimUtility ? 1 : CfgInt("max_tool_hops", MaxToolHopsFallback);
HashSet<string> 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<string> 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);
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 });
}
return (reply, lastRaw, [], 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<int?>() ?? 8,
MinScore = a["memory_min_score"]?.Value<float?>() ?? 0.32f,
ApplyQuotas = true,
};
Dictionary<string, int> quotas = AssistentMemory.CopyDefaultQuotas();
if (a["memory_quotas"] is JObject qOverrides)
{
foreach (JProperty p in qOverrides.Properties())
{
quotas[p.Name] = p.Value?.Value<int?>() ?? 2;
}
}
if (agent["enabled"]?.Value<bool?>() != false)
{
quotas["heard"] = agent["heard_quota"]?.Value<int?>() ?? 3;
}
else
{
quotas.Remove("heard");
}
opt.Quotas = quotas;
return opt;
}
JArray FilterHeardHitsIfDisabled(JArray hits)
{
if (Config.LoadTrainingAgent()["enabled"]?.Value<bool?>() != 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;
}
/// <summary>Ask-only server hops: Exact/assistant settings dump or truncated inventory.</summary>
async Task<string> RunToolHop(
Session session,
string pid,
JObject patch,
string tool,
HashSet<string> hopDone)
{
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<int?>() ?? 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_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<int?>() ?? 5;
JArray examples = Memory?.LookupExamples(q, lim, rating) ?? [];
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,
};
}
/// <summary>Substring filter over current LoRA/checkpoint inventory for ask:inventory hop.</summary>
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<T2IModel> 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<int?>() ?? 240;
}
catch
{
hitChars = 240;
}
hitChars = Math.Max(80, Math.Min(hitChars, 800));
string pid = AssistentConfig.SafeId(ctx["persona"]?.ToString()) ?? personaId ?? Config?.DefaultPersonaId() ?? "neutral";
IEnumerable<string> 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<double?>() ?? 1.0;
int maxPrefs = asst["user_prefs_max"]?.Value<int?>() ?? 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?>();
int? h = ctx["height"]?.Value<int?>();
return w is > 0 && h is > 0;
}
catch
{
return false;
}
}
/// <summary>Drop RAG rows that only point at Exact (legacy krea_facts seed / "see Exact memory…").</summary>
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<int?>() ?? 12;
namesCap = asst["inventory_prompt_names"]?.Value<int?>() ?? 24;
}
catch { /* defaults */ }
richCap = Math.Max(4, Math.Min(richCap, 40));
namesCap = Math.Max(richCap, Math.Min(namesCap, 80));
HashSet<string> 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<JToken> ordered = allLoras
.OrderByDescending(t => keepRich.Contains(t?["name"]?.ToString() ?? "") ? 1000 : 0)
.ThenByDescending(t => t?["krea_likely"]?.Value<bool>() == 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<bool>() == 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<bool>() == 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;
}
}
}
/// <summary>Ensure rich LoRA rows have triggers/blurb from Swarm inventory when the client sent name-only.</summary>
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<bool>() == 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<string> 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;
}
/// <summary>Apply overlay persona clone/write from patch. Ignores persona_delete. Updates pid ref after switch.</summary>
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<bool?>() == 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<bool>() == 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}");
}
}
}
}