Restructure UI with app-level tabs and chat history drawer; add dataset curation, HF import, Modelfile/QLoRA hooks, and link approved samples to the agent immediately via heard vector memory without waiting for fine-tuning. Co-authored-by: Cursor <cursoragent@cursor.com>
1264 lines
46 KiB
C#
1264 lines
46 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/writeback and the Civitai search hop loop.</summary>
|
|
public partial class SwarmAssistentExtension
|
|
{
|
|
const int MaxCivitaiHopsFallback = 2;
|
|
const int MaxToolHopsFallback = 4;
|
|
|
|
static bool IsSlimDebugPack(string packName) =>
|
|
string.Equals(packName, "debug_explain", StringComparison.OrdinalIgnoreCase);
|
|
|
|
(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 slimDebug = IsSlimDebugPack(packName);
|
|
|
|
if (Memory is not null && !slimDebug)
|
|
{
|
|
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 (!slimDebug)
|
|
{
|
|
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 slimDebug = IsSlimDebugPack(packName);
|
|
JArray hits = [];
|
|
if (!slimDebug)
|
|
{
|
|
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 (!slimDebug)
|
|
{
|
|
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;
|
|
JArray civitaiResults = [];
|
|
string reply = "";
|
|
JObject lastRaw = null;
|
|
int maxHops = slimDebug
|
|
? 1
|
|
: Math.Max(CfgInt("max_civitai_hops", MaxCivitaiHopsFallback), CfgInt("max_tool_hops", MaxToolHopsFallback));
|
|
HashSet<string> hopDone = new(StringComparer.OrdinalIgnoreCase);
|
|
var chain = Config.PersonaExtendsChain(pid);
|
|
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 (slimDebug)
|
|
{
|
|
break;
|
|
}
|
|
JObject patch = TryParsePatch(reply);
|
|
await ApplyMemoryActions(root, patch, embed, pid);
|
|
ApplyUserPrefActions(patch, pid);
|
|
ApplyPersonaActions(patch, ref pid);
|
|
if (hop + 1 >= maxHops)
|
|
{
|
|
break;
|
|
}
|
|
string follow = null;
|
|
JArray civitaiHop = null;
|
|
HashSet<string> hopSkip = new(StringComparer.OrdinalIgnoreCase);
|
|
while (true)
|
|
{
|
|
string tool = NextToolHop(patch, hopSkip);
|
|
if (string.IsNullOrWhiteSpace(tool))
|
|
{
|
|
follow = null;
|
|
break;
|
|
}
|
|
(follow, civitaiHop) = await RunToolHop(session, root, embed, pid, chain, patch, tool, hopDone);
|
|
if (follow is not null)
|
|
{
|
|
break;
|
|
}
|
|
hopSkip.Add(tool);
|
|
}
|
|
if (follow is null)
|
|
{
|
|
break;
|
|
}
|
|
if (civitaiHop is { Count: > 0 })
|
|
{
|
|
civitaiResults = civitaiHop;
|
|
}
|
|
// 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, civitaiResults, 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;
|
|
}
|
|
|
|
async Task<(string follow, JArray civitai)> RunToolHop(
|
|
Session session,
|
|
string root,
|
|
string embed,
|
|
string pid,
|
|
IEnumerable<string> chain,
|
|
JObject patch,
|
|
string tool,
|
|
HashSet<string> hopDone)
|
|
{
|
|
if (tool == "memory_get")
|
|
{
|
|
JArray got = [];
|
|
foreach (JToken t in patch["memories"] as JArray ?? [])
|
|
{
|
|
if (t is not JObject mo)
|
|
{
|
|
continue;
|
|
}
|
|
string kind = mo["kind"]?.ToString() ?? "note";
|
|
string key = mo["key"]?.ToString() ?? "";
|
|
if (string.IsNullOrWhiteSpace(key))
|
|
{
|
|
continue;
|
|
}
|
|
string sig = $"get:{kind}:{key}";
|
|
if (!hopDone.Add(sig))
|
|
{
|
|
continue;
|
|
}
|
|
JObject row = Memory.Get(kind, key, chain);
|
|
got.Add(row ?? new JObject { ["kind"] = kind, ["key"] = key, ["missing"] = true });
|
|
}
|
|
if (got.Count == 0)
|
|
{
|
|
return (null, null);
|
|
}
|
|
return (
|
|
"memory_get results (JSON). Use these facts; omit memory_get unless you need a different key.\n```json\n"
|
|
+ got.ToString(Newtonsoft.Json.Formatting.None) + "\n```",
|
|
null);
|
|
}
|
|
if (tool == "memory_search")
|
|
{
|
|
string q = ExtractMemoryQuery(patch);
|
|
if (string.IsNullOrWhiteSpace(q) || !hopDone.Add("search:" + q))
|
|
{
|
|
return (null, null);
|
|
}
|
|
string kind = patch["memory_kind"]?.ToString();
|
|
int topK = Config.LoadAssistant(pid)["memory_top_k"]?.Value<int?>() ?? 8;
|
|
JArray rows = await Memory.SearchAsync(root, q, kind, topK, embed, chain);
|
|
return (
|
|
"memory_search results (JSON, hybrid FTS+vector). Omit memory_search unless you need a different query.\n```json\n"
|
|
+ rows.ToString(Newtonsoft.Json.Formatting.None) + "\n```",
|
|
null);
|
|
}
|
|
if (tool == "heard_search")
|
|
{
|
|
if (Config.LoadTrainingAgent()["enabled"]?.Value<bool?>() == false)
|
|
{
|
|
return ("heard_search disabled in training-agent settings.", null);
|
|
}
|
|
string q = patch["memory_query"]?.ToString()?.Trim()
|
|
?? patch["search_query"]?.ToString()?.Trim()
|
|
?? ExtractMemoryQuery(patch);
|
|
if (string.IsNullOrWhiteSpace(q) || !hopDone.Add("heard:" + q))
|
|
{
|
|
return (null, null);
|
|
}
|
|
int topK = Config.LoadTrainingAgent()["heard_quota"]?.Value<int?>() ?? 3;
|
|
JArray rows = await Memory.SearchAsync(root, q, AssistentMemory.HeardKind, topK, embed, chain);
|
|
JArray examples = [];
|
|
foreach (JToken t in rows)
|
|
{
|
|
if (t is JObject ho)
|
|
{
|
|
JObject ex = Memory.BuildHeardExampleFromHit(ho, chain);
|
|
if (ex is not null)
|
|
{
|
|
examples.Add(ex);
|
|
}
|
|
}
|
|
}
|
|
return (
|
|
"heard_search — curated dialogue examples the assistant learned (style/reference, not hard rules). Use tone and structure; omit heard_search unless you need more examples.\n```json\n"
|
|
+ examples.ToString(Newtonsoft.Json.Formatting.None) + "\n```",
|
|
null);
|
|
}
|
|
if (tool == "lookup_tags")
|
|
{
|
|
string q = ExtractTagQuery(patch);
|
|
if (string.IsNullOrWhiteSpace(q) || !hopDone.Add("tags:" + q))
|
|
{
|
|
return (null, null);
|
|
}
|
|
int lim = Config.LoadAssistant(pid)["tag_lookup_limit"]?.Value<int?>() ?? 20;
|
|
JArray tags = Memory.LookupTags(q, lim);
|
|
return (
|
|
"lookup_tags results from Danbooru csv (canonical name, aliases, post_count). Krea prompts stay natural prose — use this to check spelling/aliases, do not dump tag soup.\n```json\n"
|
|
+ tags.ToString(Newtonsoft.Json.Formatting.None) + "\n```",
|
|
null);
|
|
}
|
|
if (tool == "list_inventory")
|
|
{
|
|
string q = patch["inventory_query"]?.ToString()?.Trim() ?? "";
|
|
string sig = "inv:" + q.ToLowerInvariant();
|
|
if (!hopDone.Add(sig))
|
|
{
|
|
return (null, null);
|
|
}
|
|
int lim = Config.LoadAssistant(pid)["inventory_hop_limit"]?.Value<int?>() ?? 20;
|
|
JArray rows = SearchInventoryForHop(q, lim);
|
|
return (
|
|
"list_inventory results (rich LoRA/checkpoint rows). Use exact names + listed triggers; omit list_inventory unless you need a different query.\n```json\n"
|
|
+ rows.ToString(Newtonsoft.Json.Formatting.None) + "\n```",
|
|
null);
|
|
}
|
|
if (tool == "skill_load")
|
|
{
|
|
List<string> ids = [];
|
|
if (patch["skills"] is JArray skArr)
|
|
{
|
|
foreach (JToken t in skArr)
|
|
{
|
|
string sid = AssistentConfig.SafeId(t?.ToString());
|
|
if (!string.IsNullOrWhiteSpace(sid))
|
|
{
|
|
ids.Add(sid);
|
|
}
|
|
}
|
|
}
|
|
if (ids.Count == 0)
|
|
{
|
|
ids.Add("memory");
|
|
}
|
|
StringBuilder sb = new();
|
|
foreach (string sid in ids)
|
|
{
|
|
string sig = "skill:" + sid;
|
|
if (!hopDone.Add(sig))
|
|
{
|
|
continue;
|
|
}
|
|
string text = Config.LoadSkillPrompt(pid, sid);
|
|
if (string.IsNullOrWhiteSpace(text))
|
|
{
|
|
continue;
|
|
}
|
|
sb.AppendLine($"## Skill: {sid}");
|
|
sb.AppendLine(text);
|
|
sb.AppendLine();
|
|
}
|
|
if (sb.Length == 0)
|
|
{
|
|
return (null, null);
|
|
}
|
|
return (
|
|
"skill_load results. Follow these skill rules on the next reply; omit skill_load unless you need another skill.\n\n"
|
|
+ sb.ToString().TrimEnd(),
|
|
null);
|
|
}
|
|
if (tool == "persona_read")
|
|
{
|
|
List<string> shelves = [];
|
|
if (patch["persona_shelves"] is JArray shArr)
|
|
{
|
|
foreach (JToken t in shArr)
|
|
{
|
|
string name = t?.ToString()?.Trim();
|
|
if (!string.IsNullOrWhiteSpace(name))
|
|
{
|
|
shelves.Add(name);
|
|
}
|
|
}
|
|
}
|
|
else if (patch["persona_shelves"] is JObject shObj)
|
|
{
|
|
// Model sometimes echoes shelf objects; treat keys as names.
|
|
foreach (JProperty p in shObj.Properties())
|
|
{
|
|
shelves.Add(p.Name);
|
|
}
|
|
}
|
|
string sig = "persona_read:" + string.Join(",", shelves);
|
|
if (!hopDone.Add(sig))
|
|
{
|
|
return (null, null);
|
|
}
|
|
string body = Config.RenderPersonaReadBlock(pid, shelves.Count > 0 ? shelves : null);
|
|
if (string.IsNullOrWhiteSpace(body))
|
|
{
|
|
return ("persona_read: no additional lore shelves for this persona.", null);
|
|
}
|
|
return (
|
|
"persona_read results (lore shelves). Use for roleplay/appearance/outfit detail; omit persona_read unless you need different shelves.\n\n"
|
|
+ body,
|
|
null);
|
|
}
|
|
if (tool == "civitai")
|
|
{
|
|
string query = ExtractSearchQuery(patch);
|
|
if (string.IsNullOrWhiteSpace(query))
|
|
{
|
|
if (!hopDone.Add("civitai:missing_query"))
|
|
{
|
|
return (null, null);
|
|
}
|
|
return (
|
|
"search_civitai skipped: provide a short search_query (LoRA keywords only). "
|
|
+ "Never search with the whole user message. Then retry with actions:[\"search_civitai\"] + search_query, "
|
|
+ "or continue using available_loras only.",
|
|
null);
|
|
}
|
|
if (!hopDone.Add("civitai:" + query))
|
|
{
|
|
return (null, null);
|
|
}
|
|
JObject search = await AssistentSearchCivitai(session, query, 8);
|
|
if (search["error"] is not null)
|
|
{
|
|
return ($"Civitai search failed: {search["error"]}. Continue without download — use only available_loras from context.", null);
|
|
}
|
|
JArray civitaiResults = search["results"] as JArray ?? [];
|
|
return (
|
|
"Civitai search results (JSON). Prefer `krea_likely: true`. Do NOT download yourself — the UI shows Confirm cards. " +
|
|
"Pick useful LoRAs from results or available_loras, emit a normal patch (prompt/loras). " +
|
|
"Omit search_civitai from actions unless you need a different query.\n```json\n" +
|
|
civitaiResults.ToString(Newtonsoft.Json.Formatting.None) + "\n```",
|
|
civitaiResults);
|
|
}
|
|
return (null, null);
|
|
}
|
|
|
|
/// <summary>Substring filter over current LoRA/checkpoint inventory for list_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
|
|
{
|
|
string pid = AssistentConfig.SafeId(ctx["persona"]?.ToString()) ?? Config?.DefaultPersonaId() ?? "neutral";
|
|
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;
|
|
}
|
|
if (fromDisk["has_card"]?.Value<bool>() == true)
|
|
{
|
|
outRow["has_card"] = 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}");
|
|
}
|
|
}
|
|
}
|
|
}
|