Add personas and model Cards (0.5.1): tone presets, stem.assistent.json, wanted queue.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+715
-32
@@ -39,11 +39,16 @@ public class SwarmAssistentExtension : Extension
|
||||
"fix_params",
|
||||
"inpaint_edit",
|
||||
"describe_ref",
|
||||
"catalog_card",
|
||||
];
|
||||
|
||||
public static readonly string[] DefaultPersonaIds = ["neutral", "lewd", "aggressive"];
|
||||
|
||||
const int MaxCivitaiHops = 2;
|
||||
const int MaxLorasInInventory = 120;
|
||||
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;
|
||||
|
||||
@@ -54,9 +59,9 @@ 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, slash commands, img2img/inpaint, Generate loop, Civitai Confirm.";
|
||||
Description = "Collaborative Krea 2 assistant: Ollama chat, multi-window board, personas, model cards, Generate loop, Civitai Confirm.";
|
||||
License = "MIT";
|
||||
Version = "0.5.0";
|
||||
Version = "0.5.3";
|
||||
Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint"];
|
||||
}
|
||||
|
||||
@@ -65,11 +70,16 @@ public class SwarmAssistentExtension : Extension
|
||||
HttpClient ??= new HttpClient { Timeout = TimeSpan.FromMinutes(10) };
|
||||
API.RegisterAPICall(AssistentListModels, false, PermUse);
|
||||
API.RegisterAPICall(AssistentGetPacks, false, PermUse);
|
||||
API.RegisterAPICall(AssistentListPersonas, false, PermUse);
|
||||
API.RegisterAPICall(AssistentListInventory, false, PermUse);
|
||||
API.RegisterAPICall(AssistentGetCard, false, PermUse);
|
||||
API.RegisterAPICall(AssistentSaveCard, true, PermUse);
|
||||
API.RegisterAPICall(AssistentEnqueueWanted, true, PermUse);
|
||||
API.RegisterAPICall(AssistentGetCardMeta, false, PermUse);
|
||||
API.RegisterAPICall(AssistentSearchCivitai, false, PermUse);
|
||||
API.RegisterAPICall(AssistentChat, true, PermUse);
|
||||
API.RegisterAPICall(AssistentChatWS, true, PermUse);
|
||||
Logs.Init("Swarm Assistent extension loaded (Ollama proxy + Krea 2 packs + inventory/Civitai)");
|
||||
Logs.Init("Swarm Assistent extension loaded (Ollama proxy + personas + model cards)");
|
||||
}
|
||||
|
||||
static string Clip(string text, int max)
|
||||
@@ -145,41 +155,551 @@ public class SwarmAssistentExtension : Extension
|
||||
return new JObject { ["success"] = true, ["packs"] = packs, ["order"] = new JArray(PackNames) };
|
||||
}
|
||||
|
||||
/// <summary>Server-side LoRA / checkpoint / wildcard inventory (not DOM scrape).</summary>
|
||||
public async Task<JObject> AssistentListInventory(Session session)
|
||||
static string DataRoot()
|
||||
{
|
||||
if (Directory.Exists("/mnt/swarm_data"))
|
||||
{
|
||||
return "/mnt/swarm_data";
|
||||
}
|
||||
try
|
||||
{
|
||||
string models = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, "Models"));
|
||||
if (Directory.Exists(models))
|
||||
{
|
||||
return Path.GetDirectoryName(models) ?? Environment.CurrentDirectory;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
return Environment.CurrentDirectory;
|
||||
}
|
||||
|
||||
string PersonasOverlayJsonPath() => Path.Combine(DataRoot(), "Assistent", "personas.json");
|
||||
|
||||
string WantedModelsPath() => Path.Combine(DataRoot(), ".gpu-rent-wanted-models.yaml");
|
||||
|
||||
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" => "Агрессивный",
|
||||
_ => "Нейтральный",
|
||||
},
|
||||
["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;
|
||||
}
|
||||
byId[id] = new JObject
|
||||
{
|
||||
["id"] = id,
|
||||
["title"] = po["title"]?.ToString() ?? id,
|
||||
["prompt"] = po["prompt"]?.ToString() ?? "",
|
||||
["source"] = "overlay",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.Debug($"AssistentListPersonas overlay: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
JArray list = [];
|
||||
foreach (JObject p in byId.Values.OrderBy(p => p["id"]?.ToString()))
|
||||
{
|
||||
list.Add(p);
|
||||
}
|
||||
if (!byId.ContainsKey(def) && list.Count > 0)
|
||||
{
|
||||
def = list[0]?["id"]?.ToString() ?? "neutral";
|
||||
}
|
||||
return new JObject
|
||||
{
|
||||
["success"] = true,
|
||||
["default"] = def,
|
||||
["personas"] = list,
|
||||
};
|
||||
}
|
||||
|
||||
static string ModelWeightPath(string setName, string modelName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(modelName) || !Program.T2IModelSets.TryGetValue(setName, out T2IModelHandler handler))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (!handler.Models.TryGetValue(modelName, out T2IModel model) && !handler.Models.TryGetValue(modelName.Replace('\\', '/'), out model))
|
||||
{
|
||||
// Try suffix match
|
||||
model = handler.Models.Values.FirstOrDefault(m =>
|
||||
string.Equals(m.Name, modelName, StringComparison.OrdinalIgnoreCase)
|
||||
|| m.Name.EndsWith("/" + modelName, StringComparison.OrdinalIgnoreCase)
|
||||
|| Path.GetFileNameWithoutExtension(m.Name) == Path.GetFileNameWithoutExtension(modelName));
|
||||
}
|
||||
if (model is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
try
|
||||
{
|
||||
// SwarmUI T2IModel exposes RawFilePath in recent builds.
|
||||
return model.RawFilePath;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static string CardPathForWeight(string weightPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(weightPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
string dir = Path.GetDirectoryName(weightPath);
|
||||
string stem = Path.GetFileNameWithoutExtension(weightPath);
|
||||
if (string.IsNullOrWhiteSpace(dir) || string.IsNullOrWhiteSpace(stem))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return Path.Combine(dir, $"{stem}.assistent.json");
|
||||
}
|
||||
|
||||
static string SetNameForKind(string kind)
|
||||
{
|
||||
return (kind ?? "").Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"lora" => "LoRA",
|
||||
"checkpoint" or "ckpt" or "stable-diffusion" => "Stable-Diffusion",
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
JObject ReadCardObject(string kind, string name)
|
||||
{
|
||||
string set = SetNameForKind(kind);
|
||||
string weight = ModelWeightPath(set, name);
|
||||
string card = CardPathForWeight(weight);
|
||||
if (card is null || !File.Exists(card))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
try
|
||||
{
|
||||
return JObject.Parse(File.ReadAllText(card, Encoding.UTF8));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<JObject> AssistentGetCard(Session session, string kind, string name)
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
if (string.IsNullOrWhiteSpace(kind) || string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return new JObject { ["error"] = "kind and name required" };
|
||||
}
|
||||
JObject card = ReadCardObject(kind, name);
|
||||
string set = SetNameForKind(kind);
|
||||
string weight = ModelWeightPath(set, name);
|
||||
return new JObject
|
||||
{
|
||||
["success"] = true,
|
||||
["kind"] = kind,
|
||||
["name"] = name,
|
||||
["has_card"] = card is not null,
|
||||
["weight_path"] = weight,
|
||||
["card"] = card,
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<JObject> AssistentSaveCard(Session session, string kind, string name, JObject card, bool enqueue_wanted = false)
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
if (card is null)
|
||||
{
|
||||
return new JObject { ["error"] = "card required" };
|
||||
}
|
||||
kind = (kind ?? card["kind"]?.ToString() ?? "").Trim();
|
||||
name = (name ?? card["name"]?.ToString() ?? "").Trim();
|
||||
if (string.IsNullOrWhiteSpace(kind) || string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return new JObject { ["error"] = "kind and name required" };
|
||||
}
|
||||
card["kind"] = kind;
|
||||
card["name"] = name;
|
||||
|
||||
string set = SetNameForKind(kind);
|
||||
string weight = ModelWeightPath(set, name);
|
||||
if (!string.IsNullOrWhiteSpace(weight) && File.Exists(weight))
|
||||
{
|
||||
string path = CardPathForWeight(weight);
|
||||
File.WriteAllText(path, card.ToString(Newtonsoft.Json.Formatting.Indented), Encoding.UTF8);
|
||||
return new JObject { ["success"] = true, ["path"] = path, ["installed"] = true };
|
||||
}
|
||||
|
||||
// Not installed — draft into wanted-cards + optionally enqueue download for next up.
|
||||
Directory.CreateDirectory(WantedCardsDir());
|
||||
string vid = card["version_id"]?.ToString() ?? "draft";
|
||||
string draft = Path.Combine(WantedCardsDir(), $"{vid}.assistent.json");
|
||||
File.WriteAllText(draft, card.ToString(Newtonsoft.Json.Formatting.Indented), Encoding.UTF8);
|
||||
if (enqueue_wanted || !string.IsNullOrWhiteSpace(card["civitai_url"]?.ToString()))
|
||||
{
|
||||
await AssistentEnqueueWanted(session, kind, card["civitai_url"]?.ToString(), card["version_id"]?.Value<int?>() ?? 0, card["title"]?.ToString() ?? name, card);
|
||||
}
|
||||
return new JObject { ["success"] = true, ["path"] = draft, ["installed"] = false, ["wanted"] = true };
|
||||
}
|
||||
|
||||
public async Task<JObject> AssistentEnqueueWanted(Session session, string kind, string url, int version_id = 0, string title = null, JObject card = null)
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
kind = (kind ?? "lora").Trim().ToLowerInvariant();
|
||||
if (kind is not ("lora" or "checkpoint" or "vae" or "embedding" or "controlnet" or "upscaler" or "clip"))
|
||||
{
|
||||
kind = "lora";
|
||||
}
|
||||
url = (url ?? "").Trim();
|
||||
if (string.IsNullOrWhiteSpace(url) && version_id > 0)
|
||||
{
|
||||
url = $"https://civitai.red/models/0?modelVersionId={version_id}";
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
return new JObject { ["error"] = "url or version_id required" };
|
||||
}
|
||||
if (version_id <= 0)
|
||||
{
|
||||
Match m = Regex.Match(url, @"modelVersionId=(\d+)", RegexOptions.IgnoreCase);
|
||||
if (m.Success)
|
||||
{
|
||||
version_id = int.Parse(m.Groups[1].Value);
|
||||
}
|
||||
}
|
||||
|
||||
string path = WantedModelsPath();
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path) ?? DataRoot());
|
||||
Dictionary<string, List<WantedEntry>> sections = LoadWantedYaml(File.Exists(path) ? File.ReadAllText(path, Encoding.UTF8) : "");
|
||||
|
||||
if (version_id > 0)
|
||||
{
|
||||
foreach (List<WantedEntry> list in sections.Values)
|
||||
{
|
||||
if (list.Any(e => e.VersionId == version_id))
|
||||
{
|
||||
return new JObject { ["success"] = true, ["already"] = true, ["path"] = path, ["version_id"] = version_id };
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (List<WantedEntry> list in sections.Values)
|
||||
{
|
||||
if (list.Any(e => string.Equals(e.Url, url, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return new JObject { ["success"] = true, ["already"] = true, ["path"] = path };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!sections.TryGetValue(kind, out List<WantedEntry> bucket))
|
||||
{
|
||||
bucket = [];
|
||||
sections[kind] = bucket;
|
||||
}
|
||||
bucket.Add(new WantedEntry { Url = url, Title = title, VersionId = version_id });
|
||||
File.WriteAllText(path, WriteWantedYaml(sections), Encoding.UTF8);
|
||||
|
||||
if (card is not null && version_id > 0)
|
||||
{
|
||||
Directory.CreateDirectory(WantedCardsDir());
|
||||
string draft = Path.Combine(WantedCardsDir(), $"{version_id}.assistent.json");
|
||||
File.WriteAllText(draft, card.ToString(Newtonsoft.Json.Formatting.Indented), Encoding.UTF8);
|
||||
}
|
||||
return new JObject { ["success"] = true, ["path"] = path, ["version_id"] = version_id };
|
||||
}
|
||||
|
||||
sealed class WantedEntry
|
||||
{
|
||||
public string Url;
|
||||
public string Title;
|
||||
public int VersionId;
|
||||
}
|
||||
|
||||
static Dictionary<string, List<WantedEntry>> LoadWantedYaml(string raw)
|
||||
{
|
||||
Dictionary<string, List<WantedEntry>> sections = new(StringComparer.OrdinalIgnoreCase);
|
||||
string currentKind = null;
|
||||
WantedEntry cur = null;
|
||||
void Flush()
|
||||
{
|
||||
if (cur is null || string.IsNullOrWhiteSpace(cur.Url) || string.IsNullOrWhiteSpace(currentKind))
|
||||
{
|
||||
cur = null;
|
||||
return;
|
||||
}
|
||||
if (!sections.TryGetValue(currentKind, out List<WantedEntry> list))
|
||||
{
|
||||
list = [];
|
||||
sections[currentKind] = list;
|
||||
}
|
||||
list.Add(cur);
|
||||
cur = null;
|
||||
}
|
||||
foreach (string line in (raw ?? "").Split('\n'))
|
||||
{
|
||||
string t = line.TrimEnd();
|
||||
if (string.IsNullOrWhiteSpace(t) || t.TrimStart().StartsWith('#'))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Match kindLine = Regex.Match(t, @"^([A-Za-z0-9_-]+):\s*$");
|
||||
if (kindLine.Success && !t.TrimStart().StartsWith('-'))
|
||||
{
|
||||
Flush();
|
||||
currentKind = kindLine.Groups[1].Value.Trim().ToLowerInvariant();
|
||||
continue;
|
||||
}
|
||||
Match urlLine = Regex.Match(t, @"^\s*-\s*url:\s*[""']?(.+?)[""']?\s*$");
|
||||
if (urlLine.Success)
|
||||
{
|
||||
Flush();
|
||||
cur = new WantedEntry { Url = urlLine.Groups[1].Value.Trim() };
|
||||
continue;
|
||||
}
|
||||
if (cur is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Match titleLine = Regex.Match(t, @"^\s*title:\s*[""']?(.+?)[""']?\s*$");
|
||||
if (titleLine.Success)
|
||||
{
|
||||
cur.Title = titleLine.Groups[1].Value.Trim();
|
||||
continue;
|
||||
}
|
||||
Match vidLine = Regex.Match(t, @"^\s*version_id:\s*(\d+)\s*$");
|
||||
if (vidLine.Success && int.TryParse(vidLine.Groups[1].Value, out int vid))
|
||||
{
|
||||
cur.VersionId = vid;
|
||||
}
|
||||
}
|
||||
Flush();
|
||||
return sections;
|
||||
}
|
||||
|
||||
static string WriteWantedYaml(Dictionary<string, List<WantedEntry>> sections)
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
sb.AppendLine("# Assistent wanted queue — merged into local models.yaml on gpu-rent up/capture");
|
||||
string[] order = ["checkpoint", "lora", "vae", "embedding", "controlnet", "upscaler", "clip"];
|
||||
HashSet<string> seen = new(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (string kind in order.Concat(sections.Keys.OrderBy(k => k, StringComparer.OrdinalIgnoreCase)))
|
||||
{
|
||||
if (!seen.Add(kind) || !sections.TryGetValue(kind, out List<WantedEntry> list) || list.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
sb.AppendLine($"{kind}:");
|
||||
foreach (WantedEntry e in list)
|
||||
{
|
||||
sb.AppendLine($" - url: \"{e.Url.Replace("\"", "%22")}\"");
|
||||
if (!string.IsNullOrWhiteSpace(e.Title))
|
||||
{
|
||||
sb.AppendLine($" title: \"{e.Title.Replace("\"", "'")}\"");
|
||||
}
|
||||
if (e.VersionId > 0)
|
||||
{
|
||||
sb.AppendLine($" version_id: {e.VersionId}");
|
||||
}
|
||||
}
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public async Task<JObject> AssistentGetCardMeta(Session session, string kind, string name, int version_id = 0)
|
||||
{
|
||||
// Pull Civitai sidecar next to weight + optional API version for examples.
|
||||
await Task.CompletedTask;
|
||||
string set = SetNameForKind(kind);
|
||||
string weight = ModelWeightPath(set, name);
|
||||
JObject civitai = null;
|
||||
JArray exampleUrls = [];
|
||||
if (!string.IsNullOrWhiteSpace(weight))
|
||||
{
|
||||
string stem = Path.GetFileNameWithoutExtension(weight);
|
||||
string dir = Path.GetDirectoryName(weight);
|
||||
string side = Path.Combine(dir ?? "", $"{stem}.civitai.json");
|
||||
if (File.Exists(side))
|
||||
{
|
||||
try
|
||||
{
|
||||
civitai = JObject.Parse(File.ReadAllText(side, Encoding.UTF8));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
if (civitai is not null)
|
||||
{
|
||||
if (version_id <= 0)
|
||||
{
|
||||
version_id = civitai["id"]?.Value<int?>() ?? 0;
|
||||
}
|
||||
if (civitai["images"] is JArray imgs)
|
||||
{
|
||||
foreach (JToken img in imgs.Take(3))
|
||||
{
|
||||
string u = img?["url"]?.ToString();
|
||||
if (!string.IsNullOrWhiteSpace(u))
|
||||
{
|
||||
exampleUrls.Add(u);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (civitai["trainedWords"] is null && civitai["model"] is JObject)
|
||||
{
|
||||
// keep as-is
|
||||
}
|
||||
}
|
||||
JObject card = ReadCardObject(kind, name);
|
||||
string trigger = null;
|
||||
try
|
||||
{
|
||||
if (Program.T2IModelSets.TryGetValue(set, out T2IModelHandler h)
|
||||
&& h.Models.TryGetValue(name, out T2IModel m))
|
||||
{
|
||||
trigger = m.Metadata?.TriggerPhrase;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
return new JObject
|
||||
{
|
||||
["success"] = true,
|
||||
["kind"] = kind,
|
||||
["name"] = name,
|
||||
["version_id"] = version_id,
|
||||
["trigger_phrase"] = trigger,
|
||||
["has_card"] = card is not null,
|
||||
["card"] = card,
|
||||
["civitai"] = civitai,
|
||||
["example_urls"] = exampleUrls,
|
||||
["weight_path"] = weight,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Server-side LoRA / checkpoint / wildcard inventory (not DOM scrape).
|
||||
/// Pass rescan=true after downloads so new files appear (calls Program.RefreshAllModelSets).</summary>
|
||||
public async Task<JObject> AssistentListInventory(Session session, bool rescan = false)
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
if (rescan)
|
||||
{
|
||||
try
|
||||
{
|
||||
Program.RefreshAllModelSets();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.Debug($"AssistentListInventory rescan: {ex.Message}");
|
||||
try
|
||||
{
|
||||
Program.ModelRefreshEvent?.Invoke();
|
||||
}
|
||||
catch (Exception ex2)
|
||||
{
|
||||
Logs.Debug($"AssistentListInventory ModelRefreshEvent: {ex2.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
JArray loras = [];
|
||||
JArray checkpoints = [];
|
||||
JArray wildcards = [];
|
||||
|
||||
if (Program.T2IModelSets.TryGetValue("LoRA", out T2IModelHandler loraHandler))
|
||||
{
|
||||
foreach (T2IModel model in loraHandler.Models.Values.OrderBy(m => m.Name).Take(MaxLorasInInventory))
|
||||
foreach (T2IModel model in loraHandler.Models.Values
|
||||
.OrderByDescending(m => LooksLikeKreaArch(m))
|
||||
.ThenBy(m => m.Name)
|
||||
.Take(MaxLorasInInventory))
|
||||
{
|
||||
loras.Add(new JObject
|
||||
{
|
||||
["name"] = model.Name,
|
||||
["title"] = model.Metadata?.Title ?? model.Title ?? model.Name,
|
||||
["trigger_phrase"] = model.Metadata?.TriggerPhrase,
|
||||
["architecture"] = model.ModelClass?.ID,
|
||||
["compat_class"] = model.ModelClass?.CompatClass?.ID,
|
||||
["hash"] = model.Metadata?.Hash ?? "",
|
||||
});
|
||||
loras.Add(BuildInventoryModelEntry(model, "lora"));
|
||||
}
|
||||
}
|
||||
|
||||
if (Program.T2IModelSets.TryGetValue("Stable-Diffusion", out T2IModelHandler ckptHandler))
|
||||
{
|
||||
foreach (T2IModel model in ckptHandler.Models.Values.OrderBy(m => m.Name).Take(60))
|
||||
foreach (T2IModel model in ckptHandler.Models.Values
|
||||
.OrderByDescending(m => LooksLikeKreaArch(m))
|
||||
.ThenBy(m => m.Name)
|
||||
.Take(MaxCheckpointsInInventory))
|
||||
{
|
||||
checkpoints.Add(new JObject
|
||||
{
|
||||
["name"] = model.Name,
|
||||
["title"] = model.Metadata?.Title ?? model.Title ?? model.Name,
|
||||
["architecture"] = model.ModelClass?.ID,
|
||||
["compat_class"] = model.ModelClass?.CompatClass?.ID,
|
||||
});
|
||||
checkpoints.Add(BuildInventoryModelEntry(model, "checkpoint"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,9 +724,125 @@ public class SwarmAssistentExtension : Extension
|
||||
["checkpoints"] = checkpoints,
|
||||
["wildcards"] = wildcards,
|
||||
["has_civitai_key"] = hasCivitaiKey,
|
||||
["rescanned"] = rescan,
|
||||
["inventory_at"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
|
||||
};
|
||||
}
|
||||
|
||||
static bool LooksLikeKreaArch(T2IModel model)
|
||||
{
|
||||
string arch = model?.ModelClass?.ID ?? "";
|
||||
string compat = model?.ModelClass?.CompatClass?.ID ?? "";
|
||||
string name = model?.Name ?? "";
|
||||
string blob = $"{arch} {compat} {name}".ToLowerInvariant();
|
||||
return blob.Contains("krea");
|
||||
}
|
||||
|
||||
JObject BuildInventoryModelEntry(T2IModel model, string kind)
|
||||
{
|
||||
string weight = null;
|
||||
try { weight = model.RawFilePath; } catch { /* ignore */ }
|
||||
string cardPath = CardPathForWeight(weight);
|
||||
bool hasCard = !string.IsNullOrWhiteSpace(cardPath) && File.Exists(cardPath);
|
||||
|
||||
string usage = model.Metadata?.UsageHint;
|
||||
string desc = model.Metadata?.Description;
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(desc) && !string.IsNullOrWhiteSpace(model.Description))
|
||||
{
|
||||
desc = model.Description;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// older Swarm builds
|
||||
}
|
||||
|
||||
string blurb = null;
|
||||
if (hasCard)
|
||||
{
|
||||
try
|
||||
{
|
||||
JObject card = JObject.Parse(File.ReadAllText(cardPath, Encoding.UTF8));
|
||||
string fromCard = (card["notes"] ?? card["when"] ?? card["prompt_hint"])?.ToString();
|
||||
if (!string.IsNullOrWhiteSpace(fromCard))
|
||||
{
|
||||
blurb = Clip(fromCard.Trim(), InventoryBlurbMax);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore bad card json
|
||||
}
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(blurb))
|
||||
{
|
||||
string raw = !string.IsNullOrWhiteSpace(usage) ? usage : desc;
|
||||
if (!string.IsNullOrWhiteSpace(raw))
|
||||
{
|
||||
blurb = Clip(CollapseWs(raw), InventoryBlurbMax);
|
||||
}
|
||||
}
|
||||
|
||||
JArray tags = null;
|
||||
if (model.Metadata?.Tags is { Length: > 0 } tagArr)
|
||||
{
|
||||
tags = new JArray(tagArr.Where(t => !string.IsNullOrWhiteSpace(t)).Take(8));
|
||||
}
|
||||
|
||||
string trigger = model.Metadata?.TriggerPhrase;
|
||||
JArray triggers = null;
|
||||
if (!string.IsNullOrWhiteSpace(trigger))
|
||||
{
|
||||
triggers = new JArray(trigger.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).Take(12));
|
||||
}
|
||||
|
||||
JObject entry = new()
|
||||
{
|
||||
["name"] = model.Name,
|
||||
["title"] = model.Metadata?.Title ?? model.Title ?? model.Name,
|
||||
["kind"] = kind,
|
||||
["trigger_phrase"] = trigger,
|
||||
["architecture"] = model.ModelClass?.ID,
|
||||
["compat_class"] = model.ModelClass?.CompatClass?.ID,
|
||||
["hash"] = model.Metadata?.Hash ?? "",
|
||||
["has_card"] = hasCard,
|
||||
["krea_likely"] = LooksLikeKreaArch(model),
|
||||
};
|
||||
if (triggers is not null && triggers.Count > 0)
|
||||
{
|
||||
entry["triggers"] = triggers;
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(blurb))
|
||||
{
|
||||
entry["blurb"] = blurb;
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(usage))
|
||||
{
|
||||
entry["usage_hint"] = Clip(CollapseWs(usage), 120);
|
||||
}
|
||||
if (tags is not null && tags.Count > 0)
|
||||
{
|
||||
entry["tags"] = tags;
|
||||
}
|
||||
string defW = model.Metadata?.LoraDefaultWeight;
|
||||
if (!string.IsNullOrWhiteSpace(defW) && kind == "lora")
|
||||
{
|
||||
entry["default_weight"] = defW;
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
static string CollapseWs(string text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
return Regex.Replace(text.Trim(), @"\s+", " ");
|
||||
}
|
||||
|
||||
/// <summary>Search Civitai for LoRAs (prefers Krea 2 base). Uses Swarm-stored civitai_api key.</summary>
|
||||
public async Task<JObject> AssistentSearchCivitai(Session session, string query, int limit = 8)
|
||||
{
|
||||
@@ -368,7 +1004,7 @@ public class SwarmAssistentExtension : Extension
|
||||
};
|
||||
}
|
||||
|
||||
List<JObject> BuildOllamaMessages(string packName, bool includeBase, string contextJson, JArray userMessages, string extraSystem = null)
|
||||
List<JObject> BuildOllamaMessages(string packName, bool includeBase, string contextJson, JArray userMessages, string extraSystem = null, string personaId = null)
|
||||
{
|
||||
List<JObject> ollamaMessages = [];
|
||||
StringBuilder system = new();
|
||||
@@ -380,6 +1016,13 @@ public class SwarmAssistentExtension : Extension
|
||||
system.AppendLine(basePack);
|
||||
}
|
||||
}
|
||||
string personaPrompt = ResolvePersonaPrompt(personaId);
|
||||
if (!string.IsNullOrWhiteSpace(personaPrompt))
|
||||
{
|
||||
system.AppendLine();
|
||||
system.AppendLine($"## Persona: {personaId ?? "neutral"}");
|
||||
system.AppendLine(personaPrompt);
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(packName) && packName != "base_krea2")
|
||||
{
|
||||
string situational = ReadPackFile(packName);
|
||||
@@ -431,6 +1074,42 @@ 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);
|
||||
}
|
||||
|
||||
static JObject TryParsePatch(string reply)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(reply))
|
||||
@@ -528,9 +1207,10 @@ public class SwarmAssistentExtension : Extension
|
||||
string contextJson,
|
||||
JArray userMessages,
|
||||
Func<string, Task> onDelta = null,
|
||||
Func<int, Task> onHopStart = null)
|
||||
Func<int, Task> onHopStart = null,
|
||||
string personaId = null)
|
||||
{
|
||||
List<JObject> messages = BuildOllamaMessages(packName, includeBase, contextJson, userMessages);
|
||||
List<JObject> messages = BuildOllamaMessages(packName, includeBase, contextJson, userMessages, personaId: personaId);
|
||||
JArray civitaiResults = [];
|
||||
string reply = "";
|
||||
JObject lastRaw = null;
|
||||
@@ -653,7 +1333,7 @@ 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)
|
||||
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;
|
||||
@@ -678,12 +1358,13 @@ public class SwarmAssistentExtension : Extension
|
||||
}
|
||||
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";
|
||||
}
|
||||
|
||||
/// <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);
|
||||
ExtractChatPayload(raw, ref baseUrl, ref model, ref pack, ref includeBase, out JArray userMessages, out string contextJson, out string persona);
|
||||
string root = NormalizeBaseUrl(baseUrl);
|
||||
string modelName = (model ?? "").Trim();
|
||||
if (string.IsNullOrWhiteSpace(modelName))
|
||||
@@ -698,13 +1379,14 @@ public class SwarmAssistentExtension : Extension
|
||||
try
|
||||
{
|
||||
(string reply, JObject parsed, JArray civitai) = await RunChatWithHops(
|
||||
session, root, modelName, packName, includeBase, contextJson, userMessages);
|
||||
session, root, modelName, packName, includeBase, contextJson, userMessages, personaId: persona);
|
||||
return new JObject
|
||||
{
|
||||
["success"] = true,
|
||||
["reply"] = reply,
|
||||
["model"] = modelName,
|
||||
["pack"] = packName,
|
||||
["persona"] = persona,
|
||||
["raw"] = parsed,
|
||||
["civitai_results"] = civitai,
|
||||
};
|
||||
@@ -718,7 +1400,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);
|
||||
ExtractChatPayload(raw, ref baseUrl, ref model, ref pack, ref includeBase, out JArray userMessages, out string contextJson, out string persona);
|
||||
string root = NormalizeBaseUrl(baseUrl);
|
||||
string modelName = (model ?? "").Trim();
|
||||
if (string.IsNullOrWhiteSpace(modelName))
|
||||
@@ -762,7 +1444,7 @@ public class SwarmAssistentExtension : Extension
|
||||
}
|
||||
}
|
||||
(string reply, JObject parsed, JArray civitai) = await RunChatWithHops(
|
||||
session, root, modelName, packName, includeBase, contextJson, userMessages, OnDelta, OnHopStart);
|
||||
session, root, modelName, packName, includeBase, contextJson, userMessages, OnDelta, OnHopStart, persona);
|
||||
await ws.SendJson(new JObject
|
||||
{
|
||||
["success"] = true,
|
||||
@@ -770,6 +1452,7 @@ public class SwarmAssistentExtension : Extension
|
||||
["reply"] = reply,
|
||||
["model"] = modelName,
|
||||
["pack"] = packName,
|
||||
["persona"] = persona,
|
||||
["raw"] = parsed,
|
||||
["civitai_results"] = civitai,
|
||||
}, API.WebsocketTimeout);
|
||||
|
||||
Reference in New Issue
Block a user