Bump Assistent to 0.7.0: Config personas, skills, and vector memory.
Move prompts into Config/_base and persona folders; seed model facts into SQLite via Ollama embed and retrieve as memory_hits each turn. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+484
-227
@@ -30,27 +30,15 @@ public class SwarmAssistentExtension : Extension
|
||||
|
||||
public static HttpClient HttpClient;
|
||||
|
||||
public static readonly string[] PackNames =
|
||||
[
|
||||
"base_krea2",
|
||||
"write_prompt",
|
||||
"critique_image",
|
||||
"compose_scene",
|
||||
"fix_params",
|
||||
"inpaint_edit",
|
||||
"describe_ref",
|
||||
"catalog_card",
|
||||
];
|
||||
public AssistentConfig Config;
|
||||
public AssistentMemory Memory;
|
||||
|
||||
public static readonly string[] DefaultPersonaIds = ["neutral", "lewd", "aggressive", "cinema", "terse"];
|
||||
|
||||
const int MaxCivitaiHops = 2;
|
||||
const int MaxLorasInInventory = 150;
|
||||
const int MaxWildcardsInInventory = 80;
|
||||
const int MaxCheckpointsInInventory = 60;
|
||||
const int InventoryBlurbMax = 140;
|
||||
/// <summary>Ollama default num_ctx is 4096; Assistent system+inventory+vision exceeds that.</summary>
|
||||
const int DefaultNumCtx = 16384;
|
||||
const int MaxCivitaiHopsFallback = 2;
|
||||
const int MaxLorasInInventoryFallback = 150;
|
||||
const int MaxWildcardsInInventoryFallback = 80;
|
||||
const int MaxCheckpointsInInventoryFallback = 60;
|
||||
const int InventoryBlurbMaxFallback = 140;
|
||||
const int DefaultNumCtxFallback = 16384;
|
||||
|
||||
static readonly Regex JsonFenceRe = new(@"```(?:json)?\s*([\s\S]*?)```", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
@@ -59,18 +47,23 @@ public class SwarmAssistentExtension : Extension
|
||||
ScriptFiles.Add("Assets/assistent.js");
|
||||
StyleSheetFiles.Add("Assets/assistent.css");
|
||||
ExtensionAuthor = "mrleo1nid";
|
||||
Description = "Collaborative Krea 2 assistant: Ollama chat, multi-window board, personas, model cards, Generate loop, Civitai Confirm.";
|
||||
Description = "Collaborative Krea 2 assistant: Ollama chat, persona presets, vector memory, model cards, Generate loop.";
|
||||
License = "MIT";
|
||||
Version = "0.6.0";
|
||||
Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint"];
|
||||
Version = "0.7.0";
|
||||
Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory"];
|
||||
}
|
||||
|
||||
public override void OnInit()
|
||||
{
|
||||
HttpClient ??= new HttpClient { Timeout = TimeSpan.FromMinutes(10) };
|
||||
Config = new AssistentConfig(FilePath, DataRoot());
|
||||
Memory = new AssistentMemory(DataRoot(), HttpClient, Config.LoadAssistant(Config.DefaultPersonaId())["embed_model"]?.ToString() ?? "nomic-embed-text");
|
||||
API.RegisterAPICall(AssistentListModels, false, PermUse);
|
||||
API.RegisterAPICall(AssistentGetPacks, false, PermUse);
|
||||
API.RegisterAPICall(AssistentListPersonas, false, PermUse);
|
||||
API.RegisterAPICall(AssistentGetConfig, false, PermUse);
|
||||
API.RegisterAPICall(AssistentGetSettings, false, PermUse);
|
||||
API.RegisterAPICall(AssistentSaveSettings, true, PermUse);
|
||||
API.RegisterAPICall(AssistentListInventory, false, PermUse);
|
||||
API.RegisterAPICall(AssistentGetCard, false, PermUse);
|
||||
API.RegisterAPICall(AssistentSaveCard, true, PermUse);
|
||||
@@ -81,7 +74,19 @@ public class SwarmAssistentExtension : Extension
|
||||
API.RegisterAPICall(AssistentSaveTaste, true, PermUse);
|
||||
API.RegisterAPICall(AssistentChat, true, PermUse);
|
||||
API.RegisterAPICall(AssistentChatWS, true, PermUse);
|
||||
Logs.Init("Swarm Assistent extension loaded (Ollama proxy + personas + model cards)");
|
||||
Logs.Init("Swarm Assistent extension loaded (Config presets + vector memory)");
|
||||
}
|
||||
|
||||
int CfgInt(string key, int fallback)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Config?.LoadAssistant(Config.DefaultPersonaId())[key]?.Value<int?>() ?? fallback;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
static string Clip(string text, int max)
|
||||
@@ -105,17 +110,7 @@ public class SwarmAssistentExtension : Extension
|
||||
|
||||
public string ReadPackFile(string name)
|
||||
{
|
||||
string safe = name.Replace('\\', '/').AfterLast('/').Replace("..", "");
|
||||
if (!PackNames.Contains(safe))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
string path = Path.Combine(FilePath, "Prompts", $"{safe}.md");
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return File.ReadAllText(path, Encoding.UTF8);
|
||||
return Config?.LoadPackPrompt(Config.DefaultPersonaId(), name);
|
||||
}
|
||||
|
||||
public async Task<JObject> AssistentListModels(Session session, string baseUrl)
|
||||
@@ -130,12 +125,79 @@ public class SwarmAssistentExtension : Extension
|
||||
return new JObject { ["error"] = $"Ollama /api/tags HTTP {(int)resp.StatusCode}: {Clip(body, 400)}" };
|
||||
}
|
||||
JObject parsed = JObject.Parse(body);
|
||||
JArray models = [];
|
||||
JArray all = [];
|
||||
foreach (JToken m in parsed["models"] as JArray ?? [])
|
||||
{
|
||||
models.Add(m["name"]?.ToString() ?? "");
|
||||
string name = m["name"]?.ToString() ?? m["model"]?.ToString() ?? "";
|
||||
if (!string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
all.Add(name);
|
||||
}
|
||||
}
|
||||
return new JObject { ["success"] = true, ["base_url"] = root, ["models"] = models };
|
||||
JObject roles = Config?.LoadOllamaRoles() ?? new JObject();
|
||||
HashSet<string> chatSet = new(
|
||||
(roles["chat"] as JArray)?.Select(t => t?.ToString()).Where(s => !string.IsNullOrWhiteSpace(s)) ?? [],
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
HashSet<string> memSet = new(
|
||||
(roles["memory"] as JArray)?.Select(t => t?.ToString()).Where(s => !string.IsNullOrWhiteSpace(s)) ?? [],
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
// Heuristic fallbacks when sidecar missing
|
||||
if (chatSet.Count == 0 && memSet.Count == 0)
|
||||
{
|
||||
foreach (JToken t in all)
|
||||
{
|
||||
string n = t.ToString();
|
||||
if (LooksLikeEmbedModel(n))
|
||||
{
|
||||
memSet.Add(n);
|
||||
}
|
||||
else
|
||||
{
|
||||
chatSet.Add(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Keep only tags that exist; anything unlabeled goes to chat if not memory
|
||||
foreach (JToken t in all)
|
||||
{
|
||||
string n = t.ToString();
|
||||
if (memSet.Contains(n) || LooksLikeEmbedModel(n))
|
||||
{
|
||||
memSet.Add(n);
|
||||
chatSet.Remove(n);
|
||||
}
|
||||
else if (chatSet.Count == 0 || chatSet.Contains(n))
|
||||
{
|
||||
chatSet.Add(n);
|
||||
}
|
||||
else if (!memSet.Contains(n))
|
||||
{
|
||||
chatSet.Add(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
JArray models = new(all.Select(t => t.ToString()).Where(n => chatSet.Contains(n) && !memSet.Contains(n) && !LooksLikeEmbedModel(n)));
|
||||
JArray memoryModels = new(all.Select(t => t.ToString()).Where(n => memSet.Contains(n) || LooksLikeEmbedModel(n)).Distinct(StringComparer.OrdinalIgnoreCase).ToList());
|
||||
if (memoryModels.Count == 0)
|
||||
{
|
||||
string fallback = Config?.LoadAssistant(Config.DefaultPersonaId())["embed_model"]?.ToString() ?? "nomic-embed-text";
|
||||
if (all.Any(t => string.Equals(t.ToString(), fallback, StringComparison.OrdinalIgnoreCase)
|
||||
|| t.ToString().StartsWith(fallback.Split(':')[0], StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
memoryModels.Add(all.Select(t => t.ToString()).First(n =>
|
||||
string.Equals(n, fallback, StringComparison.OrdinalIgnoreCase)
|
||||
|| n.StartsWith(fallback.Split(':')[0], StringComparison.OrdinalIgnoreCase)));
|
||||
}
|
||||
}
|
||||
return new JObject
|
||||
{
|
||||
["success"] = true,
|
||||
["base_url"] = root,
|
||||
["models"] = models,
|
||||
["memory_models"] = memoryModels,
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -143,18 +205,65 @@ public class SwarmAssistentExtension : Extension
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<JObject> AssistentGetPacks(Session session)
|
||||
static bool LooksLikeEmbedModel(string name)
|
||||
{
|
||||
string n = (name ?? "").ToLowerInvariant();
|
||||
return n.Contains("embed") || n.Contains("nomic") || n.Contains("bge-") || n.Contains("minilm") || n.Contains("e5-");
|
||||
}
|
||||
|
||||
public async Task<JObject> AssistentGetPacks(Session session, string persona = null)
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
string pid = AssistentConfig.SafeId(persona) ?? Config.DefaultPersonaId();
|
||||
JObject packs = new();
|
||||
foreach (string name in PackNames)
|
||||
JArray order = [];
|
||||
foreach (var p in Config.ListPacks(pid))
|
||||
{
|
||||
string text = ReadPackFile(name);
|
||||
string text = Config.LoadPackPrompt(pid, p.id);
|
||||
if (text is not null)
|
||||
{
|
||||
packs[name] = text;
|
||||
packs[p.id] = text;
|
||||
}
|
||||
order.Add(p.id);
|
||||
}
|
||||
return new JObject { ["success"] = true, ["packs"] = packs, ["order"] = order, ["persona"] = pid };
|
||||
}
|
||||
|
||||
public async Task<JObject> AssistentGetConfig(Session session, string persona = null)
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
string pid = AssistentConfig.SafeId(persona) ?? Config.DefaultPersonaId();
|
||||
return Config.BuildMergedConfigPayload(pid);
|
||||
}
|
||||
|
||||
public async Task<JObject> AssistentGetSettings(Session session)
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
return new JObject { ["success"] = true, ["settings"] = Config.LoadSettings() };
|
||||
}
|
||||
|
||||
public async Task<JObject> AssistentSaveSettings(Session session, JObject settings)
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
if (settings is null)
|
||||
{
|
||||
return new JObject { ["error"] = "settings required" };
|
||||
}
|
||||
string prevEmbed = Config.LoadSettings()["embed_model"]?.ToString();
|
||||
Config.SaveSettings(settings);
|
||||
string nextEmbed = settings["embed_model"]?.ToString();
|
||||
if (!string.IsNullOrWhiteSpace(nextEmbed) && !string.Equals(prevEmbed, nextEmbed, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
try
|
||||
{
|
||||
await Memory.ReembedAllAsync(NormalizeBaseUrl(settings["base_url"]?.ToString()), nextEmbed);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.Debug($"AssistentSaveSettings reembed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
return new JObject { ["success"] = true, ["packs"] = packs, ["order"] = new JArray(PackNames) };
|
||||
return new JObject { ["success"] = true, ["path"] = Path.Combine(Config.OverlayRoot, "settings.json") };
|
||||
}
|
||||
|
||||
static string DataRoot()
|
||||
@@ -184,110 +293,26 @@ public class SwarmAssistentExtension : Extension
|
||||
|
||||
string WantedCardsDir() => Path.Combine(DataRoot(), ".gpu-rent-wanted-cards");
|
||||
|
||||
public string ReadPersonaFile(string id)
|
||||
{
|
||||
string safe = (id ?? "").Replace('\\', '/').AfterLast('/').Replace("..", "");
|
||||
if (string.IsNullOrWhiteSpace(safe))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
string path = Path.Combine(FilePath, "Personas", $"{safe}.md");
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return File.ReadAllText(path, Encoding.UTF8);
|
||||
}
|
||||
|
||||
public async Task<JObject> AssistentListPersonas(Session session)
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
Dictionary<string, JObject> byId = new(StringComparer.OrdinalIgnoreCase);
|
||||
string def = "neutral";
|
||||
|
||||
foreach (string id in DefaultPersonaIds)
|
||||
{
|
||||
string text = ReadPersonaFile(id);
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
byId[id] = new JObject
|
||||
{
|
||||
["id"] = id,
|
||||
["title"] = id switch
|
||||
{
|
||||
"lewd" => "Пошляк",
|
||||
"aggressive" => "Агрессивный",
|
||||
"cinema" => "Кинооператор",
|
||||
"terse" => "Короткий",
|
||||
_ => "Нейтральный",
|
||||
},
|
||||
["prompt"] = text,
|
||||
["source"] = "bundled",
|
||||
};
|
||||
}
|
||||
|
||||
string overlay = PersonasOverlayJsonPath();
|
||||
if (File.Exists(overlay))
|
||||
{
|
||||
try
|
||||
{
|
||||
JObject parsed = JObject.Parse(File.ReadAllText(overlay, Encoding.UTF8));
|
||||
if (parsed["default"] != null)
|
||||
{
|
||||
def = parsed["default"]?.ToString() ?? def;
|
||||
}
|
||||
if (parsed["personas"] is JArray arr)
|
||||
{
|
||||
foreach (JToken t in arr)
|
||||
{
|
||||
if (t is not JObject po)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
string id = (po["id"]?.ToString() ?? "").Trim();
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
string overlayPrompt = po["prompt"]?.ToString() ?? "";
|
||||
if (string.IsNullOrWhiteSpace(overlayPrompt) && byId.ContainsKey(id))
|
||||
{
|
||||
// Keep bundled prompt when overlay prompt is empty.
|
||||
byId[id]["title"] = po["title"]?.ToString() ?? byId[id]["title"];
|
||||
byId[id]["source"] = "overlay+bundled";
|
||||
continue;
|
||||
}
|
||||
byId[id] = new JObject
|
||||
{
|
||||
["id"] = id,
|
||||
["title"] = po["title"]?.ToString() ?? id,
|
||||
["prompt"] = overlayPrompt,
|
||||
["source"] = "overlay",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.Debug($"AssistentListPersonas overlay: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
var catalog = Config.ListPersonaCatalog();
|
||||
JArray list = [];
|
||||
foreach (JObject p in byId.Values.OrderBy(p => p["id"]?.ToString()))
|
||||
foreach (var p in catalog)
|
||||
{
|
||||
list.Add(p);
|
||||
}
|
||||
if (!byId.ContainsKey(def) && list.Count > 0)
|
||||
{
|
||||
def = list[0]?["id"]?.ToString() ?? "neutral";
|
||||
list.Add(new JObject
|
||||
{
|
||||
["id"] = p.id,
|
||||
["title"] = p.title,
|
||||
["accent"] = p.accent,
|
||||
["prompt"] = Config.RenderIdentityBlock(p.id),
|
||||
["source"] = p.source,
|
||||
});
|
||||
}
|
||||
return new JObject
|
||||
{
|
||||
["success"] = true,
|
||||
["default"] = def,
|
||||
["default"] = Config.DefaultPersonaId(),
|
||||
["personas"] = list,
|
||||
};
|
||||
}
|
||||
@@ -408,6 +433,7 @@ public class SwarmAssistentExtension : Extension
|
||||
{
|
||||
string path = CardPathForWeight(weight);
|
||||
File.WriteAllText(path, card.ToString(Newtonsoft.Json.Formatting.Indented), Encoding.UTF8);
|
||||
_ = IngestCardToMemory(card, name);
|
||||
return new JObject { ["success"] = true, ["path"] = path, ["installed"] = true };
|
||||
}
|
||||
|
||||
@@ -421,9 +447,53 @@ public class SwarmAssistentExtension : Extension
|
||||
{
|
||||
await AssistentEnqueueWanted(session, kind, card["civitai_url"]?.ToString(), card["version_id"]?.Value<int?>() ?? 0, card["title"]?.ToString() ?? name, card);
|
||||
}
|
||||
_ = IngestCardToMemory(card, name);
|
||||
return new JObject { ["success"] = true, ["path"] = draft, ["installed"] = false, ["wanted"] = true };
|
||||
}
|
||||
|
||||
async Task IngestCardToMemory(JObject card, string name)
|
||||
{
|
||||
if (Memory is null || card is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
string kind = (card["kind"]?.ToString() ?? "lora").Trim().ToLowerInvariant();
|
||||
string key = (card["name"]?.ToString() ?? name ?? "").Trim();
|
||||
List<string> bits = [];
|
||||
foreach (string field in new[] { "when", "avoid", "prompt_hint", "notes" })
|
||||
{
|
||||
string v = card[field]?.ToString();
|
||||
if (!string.IsNullOrWhiteSpace(v))
|
||||
{
|
||||
bits.Add($"{field}: {v.Trim()}");
|
||||
}
|
||||
}
|
||||
if (card["triggers"] is JArray tr)
|
||||
{
|
||||
string joined = string.Join(", ", tr.Select(t => t?.ToString()).Where(s => !string.IsNullOrWhiteSpace(s)));
|
||||
if (!string.IsNullOrWhiteSpace(joined))
|
||||
{
|
||||
bits.Add("triggers: " + joined);
|
||||
}
|
||||
}
|
||||
if (bits.Count == 0 || string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
return;
|
||||
}
|
||||
string text = $"{kind} {key}. " + string.Join(" ", bits);
|
||||
string baseUrl = NormalizeBaseUrl(Config.LoadSettings()["base_url"]?.ToString());
|
||||
string embedModel = Config.LoadSettings()["embed_model"]?.ToString()
|
||||
?? Config.LoadAssistant(Config.DefaultPersonaId())["embed_model"]?.ToString();
|
||||
await Memory.UpsertTextAsync(baseUrl, "card", key, text, "user", card, embedModel);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.Debug($"IngestCardToMemory: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<JObject> AssistentEnqueueWanted(Session session, string kind, string url, int version_id = 0, string title = null, JObject card = null)
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
@@ -923,7 +993,7 @@ public class SwarmAssistentExtension : Extension
|
||||
foreach (T2IModel model in loraHandler.Models.Values
|
||||
.OrderByDescending(m => LooksLikeKreaArch(m))
|
||||
.ThenBy(m => m.Name)
|
||||
.Take(MaxLorasInInventory))
|
||||
.Take(CfgInt("max_loras_inventory", MaxLorasInInventoryFallback)))
|
||||
{
|
||||
loras.Add(BuildInventoryModelEntry(model, "lora"));
|
||||
}
|
||||
@@ -934,7 +1004,7 @@ public class SwarmAssistentExtension : Extension
|
||||
foreach (T2IModel model in ckptHandler.Models.Values
|
||||
.OrderByDescending(m => LooksLikeKreaArch(m))
|
||||
.ThenBy(m => m.Name)
|
||||
.Take(MaxCheckpointsInInventory))
|
||||
.Take(CfgInt("max_checkpoints_inventory", MaxCheckpointsInInventoryFallback)))
|
||||
{
|
||||
checkpoints.Add(BuildInventoryModelEntry(model, "checkpoint"));
|
||||
}
|
||||
@@ -942,7 +1012,7 @@ public class SwarmAssistentExtension : Extension
|
||||
|
||||
try
|
||||
{
|
||||
foreach (string name in WildcardsHelper.ListFiles.OrderBy(n => n).Take(MaxWildcardsInInventory))
|
||||
foreach (string name in WildcardsHelper.ListFiles.OrderBy(n => n).Take(CfgInt("max_wildcards_inventory", MaxWildcardsInInventoryFallback)))
|
||||
{
|
||||
wildcards.Add(new JObject { ["name"] = name });
|
||||
}
|
||||
@@ -1005,7 +1075,7 @@ public class SwarmAssistentExtension : Extension
|
||||
string fromCard = (card["notes"] ?? card["when"] ?? card["prompt_hint"])?.ToString();
|
||||
if (!string.IsNullOrWhiteSpace(fromCard))
|
||||
{
|
||||
blurb = Clip(fromCard.Trim(), InventoryBlurbMax);
|
||||
blurb = Clip(fromCard.Trim(), CfgInt("inventory_blurb_max", InventoryBlurbMaxFallback));
|
||||
}
|
||||
}
|
||||
catch
|
||||
@@ -1018,7 +1088,7 @@ public class SwarmAssistentExtension : Extension
|
||||
string raw = !string.IsNullOrWhiteSpace(usage) ? usage : desc;
|
||||
if (!string.IsNullOrWhiteSpace(raw))
|
||||
{
|
||||
blurb = Clip(CollapseWs(raw), InventoryBlurbMax);
|
||||
blurb = Clip(CollapseWs(raw), CfgInt("inventory_blurb_max", InventoryBlurbMaxFallback));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1262,28 +1332,42 @@ public class SwarmAssistentExtension : Extension
|
||||
};
|
||||
}
|
||||
|
||||
List<JObject> BuildOllamaMessages(string packName, bool includeBase, string contextJson, JArray userMessages, string extraSystem = null, string personaId = null)
|
||||
List<JObject> 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();
|
||||
string pid = AssistentConfig.SafeId(personaId) ?? Config.DefaultPersonaId();
|
||||
|
||||
if (includeBase)
|
||||
{
|
||||
string basePack = ReadPackFile("base_krea2");
|
||||
if (!string.IsNullOrWhiteSpace(basePack))
|
||||
string core = Config.LoadCorePrompt(pid);
|
||||
if (!string.IsNullOrWhiteSpace(core))
|
||||
{
|
||||
system.AppendLine(basePack);
|
||||
system.AppendLine(core);
|
||||
}
|
||||
}
|
||||
string personaPrompt = ResolvePersonaPrompt(personaId);
|
||||
if (!string.IsNullOrWhiteSpace(personaPrompt))
|
||||
|
||||
foreach (string skillId in skillIds ?? Config.ResolveEnabledSkills(pid, null))
|
||||
{
|
||||
string skillText = Config.LoadSkillPrompt(pid, skillId);
|
||||
if (!string.IsNullOrWhiteSpace(skillText))
|
||||
{
|
||||
system.AppendLine();
|
||||
system.AppendLine($"## Skill: {skillId}");
|
||||
system.AppendLine(skillText);
|
||||
}
|
||||
}
|
||||
|
||||
string identity = Config.RenderIdentityBlock(pid);
|
||||
if (!string.IsNullOrWhiteSpace(identity))
|
||||
{
|
||||
system.AppendLine();
|
||||
system.AppendLine($"## Persona: {personaId ?? "neutral"}");
|
||||
system.AppendLine(personaPrompt);
|
||||
system.AppendLine(identity);
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(packName) && packName != "base_krea2")
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(packName) && packName != "base_krea2" && packName != "core")
|
||||
{
|
||||
string situational = ReadPackFile(packName);
|
||||
string situational = Config.LoadPackPrompt(pid, packName);
|
||||
if (!string.IsNullOrWhiteSpace(situational))
|
||||
{
|
||||
system.AppendLine();
|
||||
@@ -1332,41 +1416,7 @@ public class SwarmAssistentExtension : Extension
|
||||
return ollamaMessages;
|
||||
}
|
||||
|
||||
string ResolvePersonaPrompt(string personaId)
|
||||
{
|
||||
string id = (personaId ?? "neutral").Trim();
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
id = "neutral";
|
||||
}
|
||||
string overlay = PersonasOverlayJsonPath();
|
||||
if (File.Exists(overlay))
|
||||
{
|
||||
try
|
||||
{
|
||||
JObject parsed = JObject.Parse(File.ReadAllText(overlay, Encoding.UTF8));
|
||||
if (parsed["personas"] is JArray arr)
|
||||
{
|
||||
foreach (JToken t in arr)
|
||||
{
|
||||
if (t is JObject po && string.Equals(po["id"]?.ToString(), id, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
string p = po["prompt"]?.ToString();
|
||||
if (!string.IsNullOrWhiteSpace(p))
|
||||
{
|
||||
return p;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// fall through to bundled
|
||||
}
|
||||
}
|
||||
return ReadPersonaFile(id);
|
||||
}
|
||||
string ResolvePersonaPrompt(string personaId) => Config.RenderIdentityBlock(personaId);
|
||||
|
||||
static JObject TryParsePatch(string reply)
|
||||
{
|
||||
@@ -1396,7 +1446,7 @@ public class SwarmAssistentExtension : Extension
|
||||
|| obj["creativity"] != null || obj["intensity"] != null
|
||||
|| obj["complexity"] != null || obj["movement"] != null
|
||||
|| obj["clear_prompt_images"] != null || obj["slot_to_prompt_image"] != null
|
||||
|| obj["pack"] != null))
|
||||
|| obj["pack"] != null || obj["memories"] != null || obj["memory"] != null))
|
||||
{
|
||||
return obj;
|
||||
}
|
||||
@@ -1442,6 +1492,35 @@ public class SwarmAssistentExtension : Extension
|
||||
return !string.IsNullOrWhiteSpace(ExtractSearchQuery(patch));
|
||||
}
|
||||
|
||||
static void ExtractChatPayload(JObject raw, ref string baseUrl, ref string model, ref string pack, ref bool includeBase, out JArray userMessages, out string contextJson, out string persona, out JArray skills)
|
||||
{
|
||||
JObject whole = raw ?? [];
|
||||
JObject nested = whole["raw"] as JObject;
|
||||
if (string.IsNullOrWhiteSpace(baseUrl))
|
||||
{
|
||||
baseUrl = whole["base_url"]?.ToString()
|
||||
?? whole["baseUrl"]?.ToString()
|
||||
?? nested?["base_url"]?.ToString()
|
||||
?? nested?["baseUrl"]?.ToString();
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(model))
|
||||
{
|
||||
model = whole["model"]?.ToString() ?? nested?["model"]?.ToString();
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(pack))
|
||||
{
|
||||
pack = whole["pack"]?.ToString() ?? nested?["pack"]?.ToString();
|
||||
}
|
||||
if (whole["includeBase"] is not null)
|
||||
{
|
||||
includeBase = whole.Value<bool?>("includeBase") ?? includeBase;
|
||||
}
|
||||
userMessages = (whole["messages"] as JArray) ?? (nested?["messages"] as JArray);
|
||||
contextJson = whole["context_json"]?.ToString() ?? nested?["context_json"]?.ToString();
|
||||
persona = whole["persona"]?.ToString() ?? nested?["persona"]?.ToString() ?? "neutral";
|
||||
skills = (whole["skills"] as JArray) ?? (nested?["skills"] as JArray);
|
||||
}
|
||||
|
||||
async Task<(string reply, JObject raw, JArray civitaiResults)> RunChatWithHops(
|
||||
Session session,
|
||||
string root,
|
||||
@@ -1452,21 +1531,55 @@ public class SwarmAssistentExtension : Extension
|
||||
JArray userMessages,
|
||||
Func<string, Task> onDelta = null,
|
||||
Func<int, Task> onHopStart = null,
|
||||
string personaId = null)
|
||||
string personaId = null,
|
||||
JArray skillIds = null,
|
||||
string embedModel = null)
|
||||
{
|
||||
List<JObject> messages = BuildOllamaMessages(packName, includeBase, contextJson, userMessages, personaId: personaId);
|
||||
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}");
|
||||
}
|
||||
|
||||
string retrieveQuery = BuildRetrieveQuery(userMessages, contextJson);
|
||||
JArray hits = [];
|
||||
try
|
||||
{
|
||||
int topK = Config.LoadAssistant(pid)["memory_top_k"]?.Value<int?>() ?? 10;
|
||||
hits = await Memory.RetrieveAsync(root, retrieveQuery, topK, embed);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.Debug($"Assistent memory retrieve: {ex.Message}");
|
||||
}
|
||||
|
||||
string enrichedContext = InjectMemoryHits(contextJson, hits);
|
||||
List<JObject> messages = BuildOllamaMessages(packName, includeBase, enrichedContext, userMessages, personaId: pid, skillIds: skills);
|
||||
JArray civitaiResults = [];
|
||||
string reply = "";
|
||||
JObject lastRaw = null;
|
||||
for (int hop = 0; hop < MaxCivitaiHops; hop++)
|
||||
int maxHops = CfgInt("max_civitai_hops", MaxCivitaiHopsFallback);
|
||||
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);
|
||||
(reply, lastRaw) = await CallOllamaChat(root, modelName, messages, stream: onDelta is not null, onDelta, pid);
|
||||
JObject patch = TryParsePatch(reply);
|
||||
if (hop + 1 >= MaxCivitaiHops || !WantsCivitaiSearch(patch))
|
||||
await ApplyMemoryActions(root, patch, embed);
|
||||
if (hop + 1 >= maxHops || !WantsCivitaiSearch(patch))
|
||||
{
|
||||
break;
|
||||
}
|
||||
@@ -1501,13 +1614,180 @@ public class SwarmAssistentExtension : Extension
|
||||
return (reply, lastRaw, civitaiResults);
|
||||
}
|
||||
|
||||
static string BuildRetrieveQuery(JArray userMessages, string contextJson)
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
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["enabled_loras"] is JArray en)
|
||||
{
|
||||
foreach (JToken t in en.Take(8))
|
||||
{
|
||||
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(' ');
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
foreach (JToken msg in (userMessages ?? []).Reverse().Take(2))
|
||||
{
|
||||
if (msg is JObject mo && string.Equals(mo["role"]?.ToString(), "user", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
sb.Append(mo["content"]?.ToString()).Append(' ');
|
||||
}
|
||||
}
|
||||
string q = CollapseWs(sb.ToString());
|
||||
return string.IsNullOrWhiteSpace(q) ? "krea2 prompting" : q;
|
||||
}
|
||||
|
||||
static string InjectMemoryHits(string contextJson, JArray hits)
|
||||
{
|
||||
JObject ctx;
|
||||
try
|
||||
{
|
||||
ctx = string.IsNullOrWhiteSpace(contextJson) ? new JObject() : JObject.Parse(contextJson);
|
||||
}
|
||||
catch
|
||||
{
|
||||
ctx = new JObject { ["_raw_context"] = contextJson };
|
||||
}
|
||||
ctx["memory_hits"] = hits ?? new JArray();
|
||||
// Slim inventory for LLM: keep enabled + current, drop full dump if present
|
||||
if (ctx["available_loras"] is JArray allLoras && allLoras.Count > 24)
|
||||
{
|
||||
HashSet<string> keep = 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))
|
||||
{
|
||||
keep.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))
|
||||
{
|
||||
keep.Add(k);
|
||||
}
|
||||
}
|
||||
}
|
||||
JArray slim = [];
|
||||
foreach (JToken t in allLoras)
|
||||
{
|
||||
string n = t?["name"]?.ToString();
|
||||
if (!string.IsNullOrWhiteSpace(n) && (keep.Contains(n) || slim.Count < 12))
|
||||
{
|
||||
if (keep.Contains(n) || t?["krea_likely"]?.Value<bool>() == true)
|
||||
{
|
||||
slim.Add(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (slim.Count == 0)
|
||||
{
|
||||
foreach (JToken t in allLoras.Take(12))
|
||||
{
|
||||
slim.Add(t);
|
||||
}
|
||||
}
|
||||
ctx["available_loras"] = slim;
|
||||
ctx["available_loras_truncated"] = true;
|
||||
ctx["available_loras_total"] = allLoras.Count;
|
||||
}
|
||||
return ctx.ToString(Newtonsoft.Json.Formatting.None);
|
||||
}
|
||||
|
||||
async Task ApplyMemoryActions(string root, JObject patch, string embedModel)
|
||||
{
|
||||
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() ?? "";
|
||||
try
|
||||
{
|
||||
if (forget && string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
Memory.Forget(kind, key);
|
||||
}
|
||||
else if (upsert || !string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
await Memory.UpsertTextAsync(root, kind, key, text, "user", mo, embedModel);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.Debug($"ApplyMemoryActions: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async Task<(string reply, JObject raw)> CallOllamaChat(
|
||||
string root,
|
||||
string modelName,
|
||||
List<JObject> ollamaMessages,
|
||||
bool stream,
|
||||
Func<string, Task> onDelta)
|
||||
Func<string, Task> onDelta,
|
||||
string personaId = null)
|
||||
{
|
||||
int numCtx = Config.LoadAssistant(AssistentConfig.SafeId(personaId) ?? Config.DefaultPersonaId())["num_ctx"]?.Value<int?>()
|
||||
?? DefaultNumCtxFallback;
|
||||
JObject payload = new()
|
||||
{
|
||||
["model"] = modelName,
|
||||
@@ -1515,8 +1795,9 @@ public class SwarmAssistentExtension : Extension
|
||||
["messages"] = new JArray(ollamaMessages),
|
||||
["options"] = new JObject
|
||||
{
|
||||
["num_ctx"] = DefaultNumCtx,
|
||||
["num_ctx"] = numCtx,
|
||||
},
|
||||
["keep_alive"] = "15m",
|
||||
};
|
||||
using StringContent content = new(payload.ToString(Newtonsoft.Json.Formatting.None), Encoding.UTF8, "application/json");
|
||||
using HttpRequestMessage req = new(HttpMethod.Post, $"{root}/api/chat") { Content = content };
|
||||
@@ -1573,38 +1854,12 @@ public class SwarmAssistentExtension : Extension
|
||||
/// SwarmUI passes the whole request as the JObject param (not only a nested key).
|
||||
/// Support both flat fields and legacy nested <c>raw</c>.
|
||||
/// </summary>
|
||||
static void ExtractChatPayload(JObject raw, ref string baseUrl, ref string model, ref string pack, ref bool includeBase, out JArray userMessages, out string contextJson, out string persona)
|
||||
{
|
||||
JObject whole = raw ?? [];
|
||||
JObject nested = whole["raw"] as JObject;
|
||||
if (string.IsNullOrWhiteSpace(baseUrl))
|
||||
{
|
||||
baseUrl = whole["base_url"]?.ToString()
|
||||
?? whole["baseUrl"]?.ToString()
|
||||
?? nested?["base_url"]?.ToString()
|
||||
?? nested?["baseUrl"]?.ToString();
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(model))
|
||||
{
|
||||
model = whole["model"]?.ToString() ?? nested?["model"]?.ToString();
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(pack))
|
||||
{
|
||||
pack = whole["pack"]?.ToString() ?? nested?["pack"]?.ToString();
|
||||
}
|
||||
if (whole["includeBase"] is not null)
|
||||
{
|
||||
includeBase = whole.Value<bool?>("includeBase") ?? includeBase;
|
||||
}
|
||||
userMessages = (whole["messages"] as JArray) ?? (nested?["messages"] as JArray);
|
||||
contextJson = whole["context_json"]?.ToString() ?? nested?["context_json"]?.ToString();
|
||||
persona = whole["persona"]?.ToString() ?? nested?["persona"]?.ToString() ?? "neutral";
|
||||
}
|
||||
// ExtractChatPayload defined above
|
||||
|
||||
/// <summary>Proxy to Ollama /api/chat (non-stream), with optional Civitai search hop.</summary>
|
||||
public async Task<JObject> AssistentChat(Session session, string baseUrl, string model, string pack, bool includeBase, JObject raw)
|
||||
{
|
||||
ExtractChatPayload(raw, ref baseUrl, ref model, ref pack, ref includeBase, out JArray userMessages, out string contextJson, out string persona);
|
||||
ExtractChatPayload(raw, ref baseUrl, ref model, ref pack, ref includeBase, out JArray userMessages, out string contextJson, out string persona, out JArray skills);
|
||||
string root = NormalizeBaseUrl(baseUrl);
|
||||
string modelName = (model ?? "").Trim();
|
||||
if (string.IsNullOrWhiteSpace(modelName))
|
||||
@@ -1616,10 +1871,11 @@ public class SwarmAssistentExtension : Extension
|
||||
return new JObject { ["error"] = "messages required" };
|
||||
}
|
||||
string packName = (pack ?? "write_prompt").Trim();
|
||||
string embedModel = raw?["embed_model"]?.ToString() ?? Config.LoadSettings()["embed_model"]?.ToString();
|
||||
try
|
||||
{
|
||||
(string reply, JObject parsed, JArray civitai) = await RunChatWithHops(
|
||||
session, root, modelName, packName, includeBase, contextJson, userMessages, personaId: persona);
|
||||
session, root, modelName, packName, includeBase, contextJson, userMessages, personaId: persona, skillIds: skills, embedModel: embedModel);
|
||||
return new JObject
|
||||
{
|
||||
["success"] = true,
|
||||
@@ -1640,7 +1896,7 @@ public class SwarmAssistentExtension : Extension
|
||||
/// <summary>WebSocket streaming chat (Ollama stream:true) + Civitai hops.</summary>
|
||||
public async Task<JObject> AssistentChatWS(Session session, WebSocket ws, string baseUrl, string model, string pack, bool includeBase, JObject raw)
|
||||
{
|
||||
ExtractChatPayload(raw, ref baseUrl, ref model, ref pack, ref includeBase, out JArray userMessages, out string contextJson, out string persona);
|
||||
ExtractChatPayload(raw, ref baseUrl, ref model, ref pack, ref includeBase, out JArray userMessages, out string contextJson, out string persona, out JArray skills);
|
||||
string root = NormalizeBaseUrl(baseUrl);
|
||||
string modelName = (model ?? "").Trim();
|
||||
if (string.IsNullOrWhiteSpace(modelName))
|
||||
@@ -1654,6 +1910,7 @@ public class SwarmAssistentExtension : Extension
|
||||
return null;
|
||||
}
|
||||
string packName = (pack ?? "write_prompt").Trim();
|
||||
string embedModel = raw?["embed_model"]?.ToString() ?? Config.LoadSettings()["embed_model"]?.ToString();
|
||||
try
|
||||
{
|
||||
if (ws.State == WebSocketState.Open)
|
||||
@@ -1684,7 +1941,7 @@ public class SwarmAssistentExtension : Extension
|
||||
}
|
||||
}
|
||||
(string reply, JObject parsed, JArray civitai) = await RunChatWithHops(
|
||||
session, root, modelName, packName, includeBase, contextJson, userMessages, OnDelta, OnHopStart, persona);
|
||||
session, root, modelName, packName, includeBase, contextJson, userMessages, OnDelta, OnHopStart, persona, skills, embedModel);
|
||||
await ws.SendJson(new JObject
|
||||
{
|
||||
["success"] = true,
|
||||
|
||||
Reference in New Issue
Block a user