Ship Assistent 0.11.3: variants grid, EN Krea prep, and stream fence fix.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-22 04:58:03 +03:00
co-authored by Cursor
parent 036484c7b7
commit 56e089d8da
18 changed files with 10244 additions and 9218 deletions
+136
View File
@@ -5,6 +5,7 @@
min-height: 28rem; min-height: 28rem;
padding: 0.5rem 0.65rem 0.65rem; padding: 0.5rem 0.65rem 0.65rem;
box-sizing: border-box; box-sizing: border-box;
position: relative;
} }
.sa-gate { .sa-gate {
@@ -1847,6 +1848,137 @@
align-items: center; align-items: center;
} }
.sa-board.sa-board-variants {
grid-template-columns: 1fr 1fr;
grid-auto-rows: minmax(7.5rem, 1fr);
}
.sa-slot.sa-slot-gen-result .sa-slot-open {
appearance: none;
border: 1px solid color-mix(in srgb, currentColor 28%, transparent);
background: color-mix(in srgb, currentColor 10%, transparent);
color: inherit;
border-radius: 999px;
padding: 0.05rem 0.45rem;
font-size: 0.68rem;
cursor: pointer;
margin-left: auto;
opacity: 0.85;
}
.sa-slot.sa-slot-gen-result .sa-slot-open:hover {
opacity: 1;
}
.sa-lightbox {
position: absolute;
inset: 0;
z-index: 80;
display: flex;
align-items: center;
justify-content: center;
pointer-events: auto;
}
.sa-lightbox[hidden] {
display: none !important;
}
.sa-lightbox-backdrop {
position: absolute;
inset: 0;
background: color-mix(in srgb, #000 62%, transparent);
}
.sa-lightbox-panel {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
gap: 0.45rem;
width: min(96%, 52rem);
max-height: 94%;
padding: 0.55rem 0.65rem 0.65rem;
border-radius: 0.65rem;
border: 1px solid color-mix(in srgb, currentColor 28%, transparent);
background: color-mix(in srgb, Canvas 92%, transparent);
box-shadow: 0 12px 40px color-mix(in srgb, #000 35%, transparent);
}
.sa-lightbox-head {
display: flex;
align-items: center;
gap: 0.45rem;
}
.sa-lightbox-title {
font-weight: 650;
font-size: 0.9rem;
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sa-lightbox-idx {
font-size: 0.75rem;
opacity: 0.7;
}
.sa-lightbox-body {
position: relative;
display: flex;
align-items: center;
justify-content: center;
min-height: 12rem;
flex: 1;
overflow: hidden;
}
.sa-lightbox-body img {
max-width: 100%;
max-height: min(70vh, 36rem);
object-fit: contain;
border-radius: 0.35rem;
}
.sa-lightbox-nav {
appearance: none;
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 2rem;
height: 2.4rem;
border: 1px solid color-mix(in srgb, currentColor 30%, transparent);
border-radius: 0.4rem;
background: color-mix(in srgb, Canvas 80%, transparent);
color: inherit;
font-size: 1.4rem;
line-height: 1;
cursor: pointer;
opacity: 0.85;
}
.sa-lightbox-nav:hover {
opacity: 1;
}
.sa-lightbox-prev {
left: 0.25rem;
}
.sa-lightbox-next {
right: 0.25rem;
}
.sa-lightbox-actions {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
justify-content: flex-end;
}
@media (max-width: 900px) { @media (max-width: 900px) {
.sa-layout { .sa-layout {
flex-direction: column; flex-direction: column;
@@ -1872,4 +2004,8 @@
border-right: none; border-right: none;
border-bottom: 1px solid color-mix(in srgb, currentColor 18%, transparent); border-bottom: 1px solid color-mix(in srgb, currentColor 18%, transparent);
} }
.sa-lightbox-panel {
width: 98%;
max-height: 96%;
}
} }
+9391 -8671
View File
File diff suppressed because it is too large Load Diff
+45 -1
View File
@@ -16,6 +16,7 @@ window.SA = window.SA || {};
'clear_prompt_images', 'slot_to_prompt_image', 'pack', 'clear_prompt_images', 'slot_to_prompt_image', 'pack',
'memories', 'memory_query', 'memory_kind', 'tag_query', 'user_prefs', 'memories', 'memory_query', 'memory_kind', 'tag_query', 'user_prefs',
'controls', 'persona_clone', 'persona_shelves', 'persona', 'notes', 'controls', 'persona_clone', 'persona_shelves', 'persona', 'notes',
'variants',
]; ];
const FENCE_RE = /```(?:json)?\s*([\s\S]*?)```/gi; const FENCE_RE = /```(?:json)?\s*([\s\S]*?)```/gi;
@@ -92,10 +93,53 @@ window.SA = window.SA || {};
return { prose, patch: lastPatch }; return { prose, patch: lastPatch };
} }
/**
* Closed fence worth freezing the stream / stopping Ollama early.
* Weak fences (pack / creativity / empty) must NOT stop — model often continues with the real patch.
*/
function isTerminalStreamPatch(obj) {
if (!obj || typeof obj !== 'object') {
return false;
}
if (isCardObject(obj)) {
return true;
}
if (Array.isArray(obj.variants) && obj.variants.length) {
return true;
}
if (obj.look_at != null || obj.vision_from != null || obj.vision_slots != null) {
return true;
}
if (obj.search_query != null || obj.civitai_query != null
|| obj.memory_query != null || obj.tag_query != null || obj.inventory_query != null) {
return true;
}
const acts = Array.isArray(obj.actions) ? obj.actions.map(String) : [];
const hopOrGen = [
'skill_load', 'persona_read', 'memory_get', 'memory_search', 'lookup_tags',
'list_inventory', 'search_civitai', 'interrupt', 'generate',
'memory_upsert', 'user_pref_upsert',
];
if (acts.some((a) => hopOrGen.includes(a))) {
return true;
}
if (String(obj.prompt || '').trim().length >= 48) {
return true;
}
if (obj.loras != null || obj.aspect != null || obj.steps != null
|| obj.width != null || obj.height != null || obj.cfg != null
|| obj.seed != null || obj.controls != null
|| obj.memories != null || obj.user_prefs != null) {
return true;
}
return false;
}
SA.PATCH_KEYS = PATCH_KEYS; SA.PATCH_KEYS = PATCH_KEYS;
SA.isCardObject = isCardObject; SA.isCardObject = isCardObject;
SA.isPatchObject = isPatchObject; SA.isPatchObject = isPatchObject;
SA.isTerminalStreamPatch = isTerminalStreamPatch;
SA.normalizePatch = normalizePatch; SA.normalizePatch = normalizePatch;
SA.extractPatch = extractPatch; SA.extractPatch = extractPatch;
})(); })();
+2 -1
View File
@@ -530,7 +530,8 @@ public sealed class AssistentConfig
} }
if (patch["prompt"] != null || patch["loras"] != null || patch["aspect"] != null if (patch["prompt"] != null || patch["loras"] != null || patch["aspect"] != null
|| patch["width"] != null || patch["height"] != null || patch["steps"] != null || patch["width"] != null || patch["height"] != null || patch["steps"] != null
|| patch["cfg"] != null || patch["seed"] != null) || patch["cfg"] != null || patch["seed"] != null
|| patch["variants"] != null)
{ {
return true; return true;
} }
+8
View File
@@ -166,6 +166,12 @@ public partial class SwarmAssistentExtension
{ {
int numCtx = Config.LoadAssistant(AssistentConfig.SafeId(personaId) ?? Config.DefaultPersonaId())["num_ctx"]?.Value<int?>() int numCtx = Config.LoadAssistant(AssistentConfig.SafeId(personaId) ?? Config.DefaultPersonaId())["num_ctx"]?.Value<int?>()
?? DefaultNumCtxFallback; ?? DefaultNumCtxFallback;
int numPredict = Config.LoadAssistant(AssistentConfig.SafeId(personaId) ?? Config.DefaultPersonaId())["num_predict"]?.Value<int?>()
?? 3072;
if (numPredict < 512)
{
numPredict = 512;
}
JObject payload = new() JObject payload = new()
{ {
["model"] = modelName, ["model"] = modelName,
@@ -174,6 +180,8 @@ public partial class SwarmAssistentExtension
["options"] = new JObject ["options"] = new JObject
{ {
["num_ctx"] = numCtx, ["num_ctx"] = numCtx,
// Without this, Ollama defaults can cut mid-prompt / mid-skill_load fence.
["num_predict"] = numPredict,
}, },
["keep_alive"] = "15m", ["keep_alive"] = "15m",
}; };
+77 -10
View File
@@ -21,6 +21,7 @@ public partial class SwarmAssistentExtension
"clear_prompt_images", "slot_to_prompt_image", "pack", "memories", "memory", "clear_prompt_images", "slot_to_prompt_image", "pack", "memories", "memory",
"memory_query", "memory_kind", "tag_query", "user_prefs", "memory_query", "memory_kind", "tag_query", "user_prefs",
"inventory_query", "skills", "persona_shelves", "controls", "inventory_query", "skills", "persona_shelves", "controls",
"variants",
]; ];
static bool HasValue(JObject obj, string key) static bool HasValue(JObject obj, string key)
@@ -107,8 +108,10 @@ public partial class SwarmAssistentExtension
} }
/// <summary> /// <summary>
/// If the reply already contains a closed fenced patch/card, cut everything after it. /// If the reply already contains a closed fenced patch/card that is "done enough" to act on,
/// Models often keep writing («Готово!», second aspect, …) and the stream never feels done. /// cut everything after it. Do NOT stop on weak fences (pack/creativity/notes-only) — models
/// often emit a tiny JSON first then the real prompt fence; aborting early cuts the prompt
/// and blocks skill_load / generate.
/// </summary> /// </summary>
static bool TryTruncateAtCompleteFence(string reply, out string truncated) static bool TryTruncateAtCompleteFence(string reply, out string truncated)
{ {
@@ -129,18 +132,11 @@ public partial class SwarmAssistentExtension
try try
{ {
JObject obj = JObject.Parse(raw); JObject obj = JObject.Parse(raw);
if (obj is null) if (obj is null || !FenceIsTerminalPatch(obj))
{
continue;
}
bool usable = LooksLikeCardObject(obj)
|| Array.Exists(PatchKeys, k => obj[k] is not null);
if (!usable)
{ {
continue; continue;
} }
truncated = reply.Substring(0, match.Index + match.Length).TrimEnd(); truncated = reply.Substring(0, match.Index + match.Length).TrimEnd();
// Only treat as complete if the fence actually closed (regex requires ```).
return true; return true;
} }
catch catch
@@ -151,6 +147,77 @@ public partial class SwarmAssistentExtension
return false; return false;
} }
/// <summary>
/// True when a closed fence is worth aborting the Ollama stream (real deliverable or tool hop).
/// </summary>
static bool FenceIsTerminalPatch(JObject obj)
{
if (obj is null)
{
return false;
}
if (LooksLikeCardObject(obj))
{
return true;
}
if (obj["variants"] is JArray variants && variants.Count > 0)
{
return true;
}
if (HasValue(obj, "look_at") || HasValue(obj, "vision_from") || HasValue(obj, "vision_slots"))
{
return true;
}
if (HasValue(obj, "search_query") || HasValue(obj, "civitai_query"))
{
return true;
}
if (HasValue(obj, "memory_query") || HasValue(obj, "tag_query") || HasValue(obj, "inventory_query"))
{
return true;
}
if (obj["actions"] is JArray acts)
{
foreach (JToken a in acts)
{
string s = a?.ToString() ?? "";
if (string.IsNullOrWhiteSpace(s))
{
continue;
}
if (s.Equals("skill_load", StringComparison.OrdinalIgnoreCase)
|| s.Equals("persona_read", StringComparison.OrdinalIgnoreCase)
|| s.Equals("memory_get", StringComparison.OrdinalIgnoreCase)
|| s.Equals("memory_search", StringComparison.OrdinalIgnoreCase)
|| s.Equals("lookup_tags", StringComparison.OrdinalIgnoreCase)
|| s.Equals("list_inventory", StringComparison.OrdinalIgnoreCase)
|| s.Equals("search_civitai", StringComparison.OrdinalIgnoreCase)
|| s.Equals("interrupt", StringComparison.OrdinalIgnoreCase)
|| s.Equals("generate", StringComparison.OrdinalIgnoreCase)
|| s.Equals("memory_upsert", StringComparison.OrdinalIgnoreCase)
|| s.Equals("user_pref_upsert", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
}
string prompt = obj["prompt"]?.ToString()?.Trim() ?? "";
if (prompt.Length >= 48)
{
return true;
}
// Real param change without prose notes
if (HasValue(obj, "loras") || HasValue(obj, "aspect") || HasValue(obj, "steps")
|| HasValue(obj, "width") || HasValue(obj, "height") || HasValue(obj, "cfg")
|| HasValue(obj, "seed") || HasValue(obj, "controls")
|| HasValue(obj, "memories") || HasValue(obj, "user_prefs"))
{
return true;
}
// Weak: pack / creativity / intensity / empty actions / notes-only → keep streaming
return false;
}
static string ExtractSearchQuery(JObject patch) static string ExtractSearchQuery(JObject patch)
{ {
if (patch is null) if (patch is null)
+2
View File
@@ -1,5 +1,6 @@
{ {
"num_ctx": 16384, "num_ctx": 16384,
"num_predict": 3072,
"max_civitai_hops": 2, "max_civitai_hops": 2,
"max_loras_inventory": 150, "max_loras_inventory": 150,
"max_checkpoints_inventory": 60, "max_checkpoints_inventory": 60,
@@ -9,6 +10,7 @@
"inventory_prompt_names": 24, "inventory_prompt_names": 24,
"inventory_hop_limit": 20, "inventory_hop_limit": 20,
"max_ref_slots": 4, "max_ref_slots": 4,
"max_gen_variants": 4,
"default_pack": "ordinary", "default_pack": "ordinary",
"default_persona": "neutral", "default_persona": "neutral",
"embed_model": "nomic-embed-text", "embed_model": "nomic-embed-text",
+92 -77
View File
@@ -1,77 +1,92 @@
# Swarm Assistent — core contract # Swarm Assistent — core contract
You are **Swarm Assistent**, a collaborative art director for image generation inside SwarmUI. You are **Swarm Assistent**, a collaborative art director for image generation inside SwarmUI.
When a `## Persona` block is present, **you speak as that character** — their title/name is *your* name, not the user's. Never call the user by the persona title unless `## About the user` says that is their name. When a `## Persona` block is present, **you speak as that character** — their title/name is *your* name, not the user's. Never call the user by the persona title unless `## About the user` says that is their name.
## Priority (mandatory) ## Priority (mandatory)
When instructions conflict, apply this order (highest wins): When instructions conflict, apply this order (highest wins):
1. **This core contract** — output format, never invent LoRA/checkpoint names or triggers, never use CFG 0, never depict or request anyone 17 or under (adults only). 1. **This core contract** — output format, never invent LoRA/checkpoint names or triggers, never use CFG 0, never depict or request anyone 17 or under (adults only).
2. **Current user message** — explicit “use steps 20 / aspect 16:9 now” wins for that turn. 2. **Current user message** — explicit “use steps 20 / aspect 16:9 now” wins for that turn.
3. **About the user** (`## About the user`) — durable preferences (global + this persona). Respect unless this turn overrides. 3. **About the user** (`## About the user`) — durable preferences (global + this persona). Respect unless this turn overrides.
4. **Live `session_exact`** — prior user overrides this chat (until persona change / clear chat). 4. **Live `session_exact`** — prior user overrides this chat (until persona change / clear chat).
5. **Exact memory** (`## Exact memory` JSON) — canonical defaults (steps/CFG/aspect/facts). Persona overlays are already merged. 5. **Exact memory** (`## Exact memory` JSON) — canonical defaults (steps/CFG/aspect/facts). Persona overlays are already merged.
6. **Filled live SwarmUI fields** — respect what is already set unless the user or pack asks to change. 6. **Filled live SwarmUI fields** — respect what is already set unless the user or pack asks to change.
7. **`memory_hits` (hybrid FTS + vector RAG)** — craft notes / LoRA blurbs (often truncated). Prefer over guesses; never override Exact, About the user, or the users param request. Full row → `memory_get`; more search → `memory_search`; Danbooru spelling → `lookup_tags` (no tag soup in Krea prompts). 7. **`memory_hits` (hybrid FTS + vector RAG)** — craft notes / LoRA blurbs (often truncated). Prefer over guesses; never override Exact, About the user, or the users param request. Full row → `memory_get`; more search → `memory_search`; Danbooru spelling → `lookup_tags` (no tag soup in Krea prompts).
8. Guesses — last resort only. 8. Guesses — last resort only.
Exact = defaults encyclopedia. About the user = human taste. RAG = soft craft notes. Do **not** re-emit `steps` / `cfg` / `sigma_shift` / `aspect` when they already match Exact (or `session_exact`) and the user did not ask to change them. Exact = defaults encyclopedia. About the user = human taste. RAG = soft craft notes. Do **not** re-emit `steps` / `cfg` / `sigma_shift` / `aspect` when they already match Exact (or `session_exact`) and the user did not ask to change them.
Never write a “JSON Patch” section in prose without an actual fenced ```json``` object. An empty `### JSON Patch` header is a failure — omit the section or emit a real fence. Keep prose short (a few lines). **After the closing ``` of the JSON fence, STOP** — no «Готово!», no second aspect, no “сейчас сгенерирую оба”. One turn = one patch (one aspect). Prompt prose structure lives in skill `prompting` — do not invent a second recipe here. Never write a “JSON Patch” section in prose without an actual fenced ```json``` object. An empty `### JSON Patch` header is a failure — omit the section or emit a real fence. If you described the next frame / prompt in prose, the fence **must** include that `prompt` and usually `actions: ["generate"]` in the **same** turn — never stop after the header. **`prompt` must be English** (Krea 2 / Qwen3-VL) — translate + structure per skill `prompting`; chat prose may stay RU. Keep prose short (a few lines). **After the closing ``` of the JSON fence, STOP** — no «Готово!», no second fenced patch, no “сейчас сгенерирую оба” in prose. **One turn = one patch.** When the user asks for several options (разный свет / оба / варианты), put 24 items in **`variants`** (partial patches with optional `label`); the UI runs them sequentially and shows a grid. Do not emit two fences. Ordinary single-image requests stay one patch without `variants`. Prompt prose structure lives in skill `prompting` — do not invent a second recipe here.
## Live context ## Live context
"Live SwarmUI context" JSON is ground truth for this turn: "Live SwarmUI context" JSON is ground truth for this turn:
- Use only LoRA/checkpoint **names** from `selected_loras` / `available_loras` (or Civitai hop results). `selected_loras` = currently enabled. Prefer listed `triggers` / `trigger_phrase` / `blurb` — **never invent**. - Use only LoRA/checkpoint **names** from `selected_loras` / `available_loras` (or Civitai hop results). `selected_loras` = currently enabled. Prefer listed `triggers` / `trigger_phrase` / `blurb` — **never invent**.
- Rich entries (blurbs/triggers) are selected + top krea-likely. Name-only rows need `list_inventory` + `inventory_query` before you rely on them. - Rich entries (blurbs/triggers) are selected + top krea-likely. Name-only rows need `list_inventory` + `inventory_query` before you rely on them.
- `memory_hits` may be truncated (`truncated: true`) — use `memory_get` for the full text. - `memory_hits` may be truncated (`truncated: true`) — use `memory_get` for the full text.
- `has_vision_image` true means a real board frame exists. `images_in_request` true means JPEG bytes are in **this** request. If you need to see a frame and `images_in_request` is false, emit `look_at` first — do not invent what the image looks like. - `has_vision_image` true means a real board frame exists. `images_in_request` true means JPEG bytes are in **this** request. If you need to see a frame and `images_in_request` is false, emit `look_at` first — do not invent what the image looks like.
- Prefer `krea_likely` / Krea architecture; ignore FLUX/SDXL. Respect current params unless asked or pack is `form_params`. - Prefer `krea_likely` / Krea architecture; ignore FLUX/SDXL. Respect current params unless asked or pack is `form_params`.
- Init/inpaint flags and `image_slots` are in the JSON. Extra pack fields are documented in the active pack. - Init/inpaint flags and `image_slots` are in the JSON. Extra pack fields are documented in the active pack.
## Memory (short) ## Memory (short)
- Craft RAG write: `memory_upsert` / `memory_forget` + `memories: [{kind,key,text,scope}]` (default personal). - Craft RAG write: `memory_upsert` / `memory_forget` + `memories: [{kind,key,text,scope}]` (default personal).
- About the user: `user_pref_upsert` / `user_pref_forget` + `user_prefs: [{key,text,scope}]`. Do **not** put human taste into craft `memories`. - About the user: `user_pref_upsert` / `user_pref_forget` + `user_prefs: [{key,text,scope}]`. Do **not** put human taste into craft `memories`.
- Fat memory skill text: `skill_load` + `skills: ["memory"]` when you need the full write/read playbook. - Fat memory skill text: `skill_load` + `skills: ["memory"]` when you need the full write/read playbook.
## Output contract (mandatory) ## Output contract (mandatory)
1. Short helpful reply in the user's language (RU or EN). 1. Short helpful reply in the user's language (RU or EN).
2. One fenced JSON patch with **only fields you want to change**: 2. One fenced JSON patch with **only fields you want to change**:
```json ```json
{ {
"prompt": "A fluffy red fox in fresh powder snow, soft morning light, 85mm f/2.8…", "prompt": "A fluffy red fox in fresh powder snow, soft morning light, 85mm f/2.8…",
"loras": [{"name": "exact_name_from_list", "weight": 0.8, "triggers": ["listed_trigger"]}], "loras": [{"name": "exact_name_from_list", "weight": 0.8, "triggers": ["listed_trigger"]}],
"aspect": "16:9", "aspect": "16:9",
"actions": ["generate"], "actions": ["generate"],
"notes": "one-line why" "notes": "one-line why"
} }
``` ```
### Patch rules Several options in one ask (still one fence):
- Omit unchanged keys. Prefer omitting Exact-matching `steps`/`cfg`/`sigma_shift`/`aspect`. ```json
- Prefer omitting Exact-matching **`controls`** (e.g. do not re-emit `"horny": 35` / `"preference_bias": 0.35` when unchanged) — echoing defaults in a Generate patch resets the UI sliders. {
- `loras` replaces the full intended set for Apply. Prefer `aspect` over raw width/height. "prompt": "same subject base…",
- Optional keys (seed, vary, init/mask, creativity/sliders, pack, **controls**, persona authoring, search/memory queries) — use when needed; packs list the ones for that mode. "aspect": "16:9",
- **`controls`** — only keys declared in this persona's `controls.json` (Exact). Clamp to min/max. Example: `"controls": { "horny": 55 }`. Do not invent control ids. "actions": ["generate"],
- Do not invent model or LoRA filenames. "variants": [
{ "label": "warm light", "prompt": "… warm window light …" },
### Actions / hops { "label": "cool light", "prompt": "… cool moonlight …" },
{ "label": "portrait 9:16", "aspect": "9:16" }
- `"generate"` — Apply + start generation when the user wants a new image. ]
- If the user only asks to **remember / save** a prompt as base/template («запомни», «как базовый промпт», «шаблон») and did **not** ask for a new image: **omit** `actions: ["generate"]`, do **not** `look_at`. Prefer `memory_upsert` (kind `note`, key like `base_prompt`) or a short ack; you may echo `prompt` in the patch only to sync the SwarmUI box — the UI will not Generate on remember turns. }
- `"search_civitai"` + **required** short `search_query` — Civitai hop (user Confirms downloads). Without `search_query` the hop is skipped (never search the whole user message). ```
- `"interrupt"` — stop generation.
- `"memory_get"` / `"memory_search"` / `"lookup_tags"` — read hops. ### Patch rules
- `"list_inventory"` + `inventory_query` — rich LoRA/checkpoint details beyond the slim list.
- `"skill_load"` + `skills: ["memory"]` — load fat skill text. - Omit unchanged keys. Prefer omitting Exact-matching `steps`/`cfg`/`sigma_shift`/`aspect`.
- `"persona_read"` — load lore shelves not in always-on identity (e.g. `roleplay` / `craft` / `humor` when NSFW tone or craft detail needs them). - Prefer omitting Exact-matching **`controls`** (e.g. do not re-emit `"horny": 35` / `"preference_bias": 0.35` when unchanged) — echoing defaults in a Generate patch resets the UI sliders.
- `"memory_upsert"` / `"memory_forget"` / `"user_pref_upsert"` / `"user_pref_forget"` — writes. - `loras` replaces the full intended set for Apply. Prefer `aspect` over raw width/height.
- `"persona_clone"` / `"persona_write"` / `"persona_switch"` — `author_persona` only. Never `"persona_delete"`. - Optional keys (seed, vary, init/mask, creativity/sliders, pack, **controls**, persona authoring, search/memory queries, **`variants`**) — use when needed; packs list the ones for that mode.
- `look_at: ["generate"|"ref1"|…]` — vision hop (JPEG arrives on the follow-up). - **`controls`** — only keys declared in this persona's `controls.json` (Exact). Clamp to min/max. Example: `"controls": { "horny": 55 }`. Do not invent control ids.
- Pure Q&A: omit the JSON patch. - Do not invent model or LoRA filenames.
### Actions / hops
- `"generate"` — Apply + start generation when the user wants a new image. Auto-Generate is a **UI checkbox** (Settings → Поведение); the model cannot toggle it — emit `actions:["generate"]` instead.
- If the user only asks to **remember / save** a prompt as base/template («запомни», «как базовый промпт», «шаблон») and did **not** ask for a new image: **omit** `actions: ["generate"]`, do **not** `look_at`. Prefer `memory_upsert` (kind `note`, key like `base_prompt`) or a short ack; you may echo `prompt` in the patch only to sync the SwarmUI box — the UI will not Generate on remember turns.
- `"search_civitai"` + **required** short `search_query` — Civitai hop (user Confirms downloads). Without `search_query` the hop is skipped (never search the whole user message).
- `"interrupt"` — stop generation.
- `"memory_get"` / `"memory_search"` / `"lookup_tags"` — read hops.
- `"list_inventory"` + `inventory_query` — rich LoRA/checkpoint details beyond the slim list.
- `"skill_load"` + `skills: ["memory"]` — load fat skill text.
- `"persona_read"` — load lore shelves not in always-on identity (e.g. `roleplay` / `craft` / `humor` when NSFW tone or craft detail needs them).
- `"memory_upsert"` / `"memory_forget"` / `"user_pref_upsert"` / `"user_pref_forget"` — writes.
- `"persona_clone"` / `"persona_write"` / `"persona_switch"` — `author_persona` only. Never `"persona_delete"`.
- `look_at: ["generate"|"ref1"|…]` — vision hop (JPEG arrives on the follow-up).
- Pure Q&A: omit the JSON patch.
+1
View File
@@ -30,6 +30,7 @@
}, },
"facts": { "facts": {
"architecture": "Krea 2 is a 12B DiT architecture. Not FLUX, not SDXL, not FLUX.1-Krea. Text encoder: Qwen3-VL 4B. VAE: Qwen Image VAE. Use only Krea2-trained LoRAs — never suggest FLUX/SDXL LoRAs.", "architecture": "Krea 2 is a 12B DiT architecture. Not FLUX, not SDXL, not FLUX.1-Krea. Text encoder: Qwen3-VL 4B. VAE: Qwen Image VAE. Use only Krea2-trained LoRAs — never suggest FLUX/SDXL LoRAs.",
"prompt_language": "Chat model always prep's Generate prompt for Krea: English natural prose for Qwen3-VL, structured (subject→pose→setting→camera→light). Chat may be RU; never leave Russian or thin drafts in patch.prompt.",
"negatives": "Negative prompts are nearly useless with Qwen3-VL. Prefer positives (sharp focus, empty street) over no blur / no people. Built-in NSFW text-refiner may strip risque words; LoRAs/finetunes may restore — stay practical.", "negatives": "Negative prompts are nearly useless with Qwen3-VL. Prefer positives (sharp focus, empty street) over no blur / no people. Built-in NSFW text-refiner may strip risque words; LoRAs/finetunes may restore — stay practical.",
"prompt_images": "Prompt Images (refs in the prompt box) often overpower text — use sparingly and warn. Init Image = structure (img2img). Mask = local fix. They are not interchangeable. Cloud-only features (moodboards, Generative Sliders) are not in Swarm — emulate with prompt language + board refs.", "prompt_images": "Prompt Images (refs in the prompt box) often overpower text — use sparingly and warn. Init Image = structure (img2img). Mask = local fix. They are not interchangeable. Cloud-only features (moodboards, Generative Sliders) are not in Swarm — emulate with prompt language + board refs.",
"turbo": "Krea 2 Turbo defaults live in exact.generation / exact.profiles.turbo. Never use CFG 0 — broken output. Side ~1024 (1284096 OK).", "turbo": "Krea 2 Turbo defaults live in exact.generation / exact.profiles.turbo. Never use CFG 0 — broken output. Side ~1024 (1284096 OK).",
+2
View File
@@ -27,3 +27,5 @@ Otherwise **stay in ordinary** and just do the work.
## Deliverable ## Deliverable
Same as write_prompt: short reply + one fenced JSON patch; `actions: ["generate"]` when they want an image. Same as write_prompt: short reply + one fenced JSON patch; `actions: ["generate"]` when they want an image.
«давай дальше» / next frame = new English `prompt` + `actions:["generate"]` in the **same** turn — never leave an empty `### JSON Patch` header. Chat may be RU; **Generate `prompt` is always EN** (skill `prompting`).
Several options in one ask → `variants` (24 partial patches with `label`); still one fence, still STOP after it.
+2 -1
View File
@@ -1,6 +1,6 @@
# Mode: write_prompt # Mode: write_prompt
Goal: craft or improve a **Krea 2** prompt that will generate well on Turbo (local Swarm). Prompt prose recipe is in skill `prompting` — follow it; do not invent a second structure. Goal: craft or improve a **Krea 2** prompt that will generate well on Turbo (local Swarm). Prompt prose recipe is in skill `prompting` — follow it; do not invent a second structure. The JSON **`prompt` field is always English** (translate + structure); user-facing notes may stay in the users language.
## Deliverable ## Deliverable
@@ -9,6 +9,7 @@ Goal: craft or improve a **Krea 2** prompt that will generate well on Turbo (loc
- `actions: ["generate"]` when the user wants a new image — UI applies + Generate without Apply buttons. - `actions: ["generate"]` when the user wants a new image — UI applies + Generate without Apply buttons.
- Prefer Exact Turbo defaults / `recommended_params`. Prefer `aspect` for framing; omit steps/cfg/sigma/aspect when they already match Exact and the user did not ask to change them. - Prefer Exact Turbo defaults / `recommended_params`. Prefer `aspect` for framing; omit steps/cfg/sigma/aspect when they already match Exact and the user did not ask to change them.
- Missing style LoRA → `actions: ["search_civitai"]` + short `search_query` (Krea-compatible). - Missing style LoRA → `actions: ["search_civitai"]` + short `search_query` (Krea-compatible).
- User wants several options (оба / варианты / разный свет) → `variants: [{label, prompt|aspect|…}, …]` (24). Base keys inherit; each item overrides only its diffs. Still one fence.
### Bad → good ### Bad → good
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"always": [ "always": [
"Match the user's language (RU or EN)", "Match the user's language (RU or EN) in chat — Generate prompt stays English",
"Use only inventory / memory_hits / cards for LoRA names and triggers", "Use only inventory / memory_hits / cards for LoRA names and triggers",
"Emit valid JSON patches when changing generation state" "Emit valid JSON patches when changing generation state"
], ],
+23 -5
View File
@@ -1,9 +1,27 @@
# Skill: prompting # Skill: prompting
Write **natural prose** for a photographer/director — not Danbooru tags, not `(word:1.5)`, not `masterpiece / best quality / 8k`. The **chat model** prepares the Generate-box text for **Krea 2** (Qwen3-VL). Do this prep as fully as you can in the same turn (or the dedicated prep hop). Chat with the user may be RU/EN; the JSON **`prompt` is always English**.
Order (front-load importance): **subject → pose/action → setting → materials → camera/framing → lighting → medium/mood**. ## Hard rules (Krea / Qwen3-VL)
- Short user ideas: expand. Finished Krea-style paragraphs: keep wording; only fix anti-patterns. 1. **`prompt` language = English only** — never put Russian into the JSON `prompt`. Translate the idea, then structure it.
- Put LoRA trigger phrases near the subject they affect. 2. **Natural photographer/director prose** — not Danbooru tag soup, not `(word:1.5)`, not `masterpiece / best quality / 8k`.
- Prefer positives over negatives. 3. **Order (front-load):**
**subject → pose/action → body/wardrobe → setting → materials/textures → camera/framing → lighting → medium/mood**.
4. Put **LoRA trigger phrases** (exact English spelling) near the subject they affect.
5. Prefer **positives** over negatives (Qwen negatives are weak).
6. Short ideas: expand into a full Krea paragraph. Thin or RU drafts: rewrite before Generate — do not hand Krea a half-ready line.
## Prep checklist (before `actions:["generate"]`)
- English only in `prompt`
- Subject and action clear in the first sentence
- Wardrobe / body / setting concrete
- Camera + lighting present
- One coherent scene; NSFW stated in plain English if needed
- Triggers placed next to what they modify
## Deliverable
User-facing prose: short, in the users language.
JSON `prompt`: English, structured as above — ready for Swarm Generate / Krea 2.
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"welcome_html": "<div class=\"sa-welcome-title\">Assistent · Krea 2</div><ul><li><strong>Generate</strong> слева — живой просмотр. В чат сам не уходит.</li><li><strong>Refs</strong> — референсы на отдельной вкладке: drop / paste / Снимок gen.</li><li>Галочка vision на окне — отправить кадр модели.</li><li>Чипсы aspect / seed / Vary / Turbo·RAW. В чате: <code>/help</code>.</li><li>Кнопки патча только у последнего предложения.</li></ul>Напиши, что сгенерировать — или кинь референс и попроси правку.", "welcome_html": "<div class=\"sa-welcome-title\">Assistent · Krea 2</div><ul><li><strong>Generate</strong> слева — живой просмотр. Несколько вариантов → сетка + клик для просмотра.</li><li><strong>Refs</strong> — референсы на отдельной вкладке: drop / paste / Снимок gen.</li><li>Галочка vision на окне — отправить кадр модели.</li><li>Чипсы aspect / seed / Vary / Turbo·RAW. В чате: <code>/help</code>.</li><li>Кнопки патча только у последнего предложения.</li></ul>Напиши, что сгенерировать — или кинь референс и попроси правку.",
"help_text": "Slash-команды (без LLM):\n/help — этот список\n/new — новый чат\n/history — список чатов\n/debug — сводка UI/Exact\n/debug ask · /why — сводка + короткий ответ модели\n/gen — Generate сейчас\n/look generate|refN — прикрепить окно к vision\n/init /mask /clear — Init / Mask / Clear Init\n/interrupt — остановить генерацию\n/aspect 16:9 — размер из таблицы 1K\n/seed lock|random — зафиксировать или рандомизировать seed\n/vary — новый seed, тот же промпт\n/pack write|ordinary|critique|compose|params|inpaint|describe|card|persona\n/persona new — интервью: клон текущей личности (overlay)\n/persona clone <id> — клон с указанной\n/persona save — записать согласованный черновик\n/civitai <query> — поиск LoRA (Confirm в чате)\n/inventory — rescan моделей + обновить список LoRA\n\nЧипсы над полем ввода делают то же для aspect / seed / vary / Turbo·RAW.\nПри старте всегда новый чат; смена чата восстанавливает параметры.\nOverlay-личности удаляет только кнопка ✕ рядом с селектом (не модель).", "help_text": "Slash-команды (без LLM):\n/help — этот список\n/new — новый чат\n/history — список чатов\n/debug — сводка UI/Exact\n/debug ask · /why — сводка + короткий ответ модели\n/gen — Generate сейчас\n/look generate|refN — прикрепить окно к vision\n/init /mask /clear — Init / Mask / Clear Init\n/interrupt — остановить генерацию\n/aspect 16:9 — размер из таблицы 1K\n/seed lock|random — зафиксировать или рандомизировать seed\n/vary — новый seed, тот же промпт\n/pack write|ordinary|critique|compose|params|inpaint|describe|card|persona\n/persona new — интервью: клон текущей личности (overlay)\n/persona clone <id> — клон с указанной\n/persona save — записать согласованный черновик\n/civitai <query> — поиск LoRA (Confirm в чате)\n/inventory — rescan моделей + обновить список LoRA\n\nНесколько вариантов в одном запросе («оба», разный свет) → патч с variants[] → сетка на Generate, клик / Открыть / Enter — просмотр.\nЧипсы над полем ввода делают то же для aspect / seed / vary / Turbo·RAW.\nПри старте всегда новый чат; смена чата восстанавливает параметры.\nOverlay-личности удаляет только кнопка ✕ рядом с селектом (не модель).",
"chips": [ "chips": [
{ "label": "1:1", "action": "aspect", "value": "1:1", "title": "1024×1024" }, { "label": "1:1", "action": "aspect", "value": "1:1", "title": "1024×1024" },
{ "label": "4:5", "action": "aspect", "value": "4:5", "title": "928×1152" }, { "label": "4:5", "action": "aspect", "value": "4:5", "title": "928×1152" },
+2 -1
View File
@@ -6,9 +6,10 @@
"detailed Krea prose" "detailed Krea prose"
], ],
"process": [ "process": [
"translate idea → English Krea prose before Generate",
"subject → pose → clothes/hair → setting → camera → light → mood", "subject → pose → clothes/hair → setting → camera → light → mood",
"front-load what matters", "front-load what matters",
"natural prose, not tag soup" "natural EN prose, not tag soup"
], ],
"defaults": [ "defaults": [
"photoreal / film unless user asks anime/hentai", "photoreal / film unless user asks anime/hentai",
+1 -1
View File
@@ -2,7 +2,7 @@
"always": [ "always": [
"You are Leonid; the user is not Leonid — never greet/address them as Леонид/Leonid unless About the user says their name is that", "You are Leonid; the user is not Leonid — never greet/address them as Леонид/Leonid unless About the user says their name is that",
"If asked your name, say you are Leonid (assistant)", "If asked your name, say you are Leonid (assistant)",
"Match user language (RU/EN)", "Match user language (RU/EN) in chat; Generate patch prompt is always English for Krea",
"Craft first: triggers, aspect, Turbo — horny never replaces technique", "Craft first: triggers, aspect, Turbo — horny never replaces technique",
"Scale appearance/outfits by controls.preference_bias (1…1)", "Scale appearance/outfits by controls.preference_bias (1…1)",
"Scale sexual tone + roleplay fetishes by controls.horny (0…100)", "Scale sexual tone + roleplay fetishes by controls.horny (0…100)",
+183 -173
View File
@@ -1,173 +1,183 @@
# Swarm Assistent # Swarm Assistent
SwarmUI extension for **collaborative Krea 2** prompting via **Ollama**: chat + board (Generate | Refs tabs), LoRA chips, **persona presets** (`Config/personas/`), **About the user** prefs + craft vector memory, model cards with Civitai fetch, img2img/inpaint, slash commands, auto Generate. SwarmUI extension for **collaborative Krea 2** prompting via **Ollama**: chat + board (Generate | Refs tabs), LoRA chips, **persona presets** (`Config/personas/`), **About the user** prefs + craft vector memory, model cards with Civitai fetch, img2img/inpaint, slash commands, auto Generate.
**Version 0.10.21**Board Generate spinner clears when Swarm finishes (`num_live_gens`), not only when the image URL string changes. Builds on 0.10.20 remember-without-gen. **Version 0.11.3**Stream no longer aborts on weak JSON fences (so prompts/skill_load can finish); `num_predict` 3072. Builds on 0.11.2 Krea prep.
**Version 0.10.20**«Запомни / базовый промпт» no longer triggers Auto-Generate or auto look_at (even if the model sneaks `actions:["generate"]`). Builds on 0.10.19 slider echo fix. **Version 0.11.2**Before Krea Generate, chat model maximally preps the prompt (EN + structure); skip prep hop only if already Krea-ready English. Builds on 0.11.1.
**Version 0.10.19**Вкус/Хорни no longer reset: Generate patches never apply `controls`; default-echo filtered always; bare «вкус» no longer disables the filter. Builds on 0.10.18 settings tab. **Version 0.11.1**Krea Generate `prompt` must be English (skill `prompting` + Exact + client rewrite hop if Cyrillic leaks). Builds on 0.11.0 variants.
**Version 0.10.18**Settings is a full subtab (Чат | Карточки | Настройки), not a header overlay. Builds on 0.10.17 post-Generate warm. **Version 0.11.0**Пакет вариантов: JSON `variants[]` (2–4) → последовательные Generate → сетка на вкладке Generate + lightbox. Builds on 0.10.22 empty-patch fix.
## Layout **Version 0.10.22** — Empty `### JSON Patch` no longer dead-ends: synthesize prompt from prose / retry; «давай дальше» counts as Generate. Builds on 0.10.21 spinner fix.
- **Left — Board tabs:** **Generate** (full-height live view) | **Refs** (reference grid + badge `N · vision M`); **Посмотри результат** attaches the finished frame and asks for a verdict **Version 0.10.21** — Board Generate spinner clears when Swarm finishes (`num_live_gens`), not only when the image URL string changes. Builds on 0.10.20 remember-without-gen.
- **Splitter:** drag to resize panes
- **Right:** Chat | Cards | Settings; persona / pack / Ollama chat model; **Ollama health** badge **Version 0.10.20** — «Запомни / базовый промпт» no longer triggers Auto-Generate or auto look_at (even if the model sneaks `actions:["generate"]`). Builds on 0.10.19 slider echo fix.
- **Chips / slash:** loaded from `Config/_base/ui.json` (persona can override)
**Version 0.10.19** — Вкус/Хорни no longer reset: Generate patches never apply `controls`; default-echo filtered always; bare «вкус» no longer disables the filter. Builds on 0.10.18 settings tab.
## Config (bundled + overlay)
**Version 0.10.18** — Settings is a full subtab (Чат | Карточки | Настройки), not a header overlay. Builds on 0.10.17 post-Generate warm.
```
Config/ ## Layout
_base/ # defaults (assistant, ui, models/krea2, exact.json, core, packs, skills, memory-seed, identity)
personas/<id>/ # sparse shelves: persona/bio/voice/humor/… + optional controls.json / exact.json / memory-seed - **Left — Board tabs:** **Generate** (live view, or a **variant grid** when the model emits `variants[]`) | **Refs** (reference grid + badge `N · vision M`); click a variant to select / open lightbox; **Посмотри результат** attaches the selected finished frame and asks for a verdict
``` - **Splitter:** drag to resize panes
- **Right:** Chat | Cards | Settings; persona / pack / Ollama chat model; **Ollama health** badge
Disk overlay (wins over bundled): `/mnt/swarm_data/Assistent/` — same folder layout as `Config/`. Drop `_base/…` and `personas/<id>/…` to override. Plus runtime state: - **Chips / slash:** loaded from `Config/_base/ui.json` (persona can override)
``` ## Config (bundled + overlay)
Assistent/
_base/ personas/<id>/ # overlay presets — same names as Config/, sparse ```
settings.json # embed_model, base_url, per-persona skills Config/
ollama-roles.json # chat vs memory model tags (gpu-rent writes this) _base/ # defaults (assistant, ui, models/krea2, exact.json, core, packs, skills, memory-seed, identity)
memory/assistent.sqlite # craft RAG + user_prefs + tags FTS + chats + ui_state + taste (legacy) personas/<id>/ # sparse shelves: persona/bio/voice/humor/… + optional controls.json / exact.json / memory-seed
_migrated_json/ # one-shot archive of old chats/*.json, ui-state.json, taste.json ```
```
Disk overlay (wins over bundled): `/mnt/swarm_data/Assistent/` — same folder layout as `Config/`. Drop `_base/…` and `personas/<id>/…` to override. Plus runtime state:
Copy `personas/leonid/` → new id, edit only differing JSON. See `Config/personas/README.md`.
```
**Controls:** optional `controls.json` schema + `exact.controls` values. UI auto-draws every slider (`order`, `display: percent`). LLM may patch `"controls": {…}`. Values persist in overlay Exact (DeepMerge partial saves). Leonid: **Вкус** + **Хорни**; `/остынь`, `/horny-game`. Assistent/
_base/ personas/<id>/ # overlay presets — same names as Config/, sparse
**Authoring:** pack `author_persona` + `/persona new` clones to overlay only. Delete overlay personas with the ✕ button or ⚙ → Личности (never from the model). Export/import `.assistent-persona.json` for sharing. settings.json # embed_model, base_url, per-persona skills
ollama-roles.json # chat vs memory model tags (gpu-rent writes this)
## Exact memory (KV) memory/assistent.sqlite # craft RAG + user_prefs + tags FTS + chats + ui_state + taste (legacy)
_migrated_json/ # one-shot archive of old chats/*.json, ui-state.json, taste.json
- `Config/_base/exact.json` — canonical generation defaults, profiles (turbo/raw), aspect table, short facts ```
- Persona / disk overlays merge via DeepMerge (matching keys overwrite)
- Always injected into the system prompt; UI fills **empty** SwarmUI fields from Exact (no LLM call) Copy `personas/leonid/` → new id, edit only differing JSON. See `Config/personas/README.md`.
- Chat-session overrides (`session_exact`) last until persona change or clear chat — not written to disk
- Priority: core → user message → **About the user** → session_exact → exact (+ persona) → live fields → craft `memory_hits` **Controls:** optional `controls.json` schema + `exact.controls` values. UI auto-draws every slider (`order`, `display: percent`). LLM may patch `"controls": {…}`. Values persist in overlay Exact (DeepMerge partial saves). Leonid: **Вкус** + **Хорни**; `/остынь`, `/horny-game`.
## About the user (UserPrefs) **Authoring:** pack `author_persona` + `/persona new` clones to overlay only. Delete overlay personas with the ✕ button or ⚙ → Личности (never from the model). Export/import `.assistent-persona.json` for sharing.
Separate sqlite table `user_prefs` (not craft RAG): ## Exact memory (KV)
- **Global** — every persona (e.g. “avoid blonde hair”) - `Config/_base/exact.json` — canonical generation defaults, profiles (turbo/raw), aspect table, short facts
- **Persona** — only the current agent - Persona / disk overlays merge via DeepMerge (matching keys overwrite)
- Injected as `## About the user`; strength via `user_prefs_weight` / `user_prefs_max` in `assistant.json` (⚙ → О пользователе) - Always injected into the system prompt; UI fills **empty** SwarmUI fields from Exact (no LLM call)
- Agent write: `actions: ["user_pref_upsert"]` + `user_prefs: [{key,text,scope}]` - Chat-session overrides (`session_exact`) last until persona change or clear chat — not written to disk
- Legacy `kv.taste` migrates once into global prefs - Priority: core → user message → **About the user** → session_exact → exact (+ persona) → live fields → craft `memory_hits`
## Craft vector memory ## About the user (UserPrefs)
Two layers in `memory/assistent.sqlite` (`persona` column; empty = shared): Separate sqlite table `user_prefs` (not craft RAG):
- **Shared** — `Config/_base/memory-seed/`, model cards, `scope: "shared"` upserts. Visible to every persona. - **Global** — every persona (e.g. “avoid blonde hair”)
- **Personal** — `Config/personas/<id>/memory-seed/` and chat upserts (default). Never copied into shared. Other personas do not retrieve it. - **Persona** — only the current agent
- Retrieve = shared this persona (and `extends` parents). Hybrid **FTS5 + cosine**, kind quotas, `min_score`. Same `kind`+`key`: personal overwrites parent overwrites shared. - Injected as `## About the user`; strength via `user_prefs_weight` / `user_prefs_max` in `assistant.json` (⚙ → О пользователе)
- Tools: `memory_get`, `memory_search`, `lookup_tags` (Danbooru csv in `Data/Autocompletions`, FTS, **no embeddings**). - Agent write: `actions: ["user_pref_upsert"]` + `user_prefs: [{key,text,scope}]`
- Soft craft notes only — Exact, About the user, and the user beat RAG for params - Legacy `kv.taste` migrates once into global prefs
- ⚙ → **Крафт** lists rows with filters + clear (bundled seed is read-only)
## Craft vector memory
## Chats and runtime KV
Two layers in `memory/assistent.sqlite` (`persona` column; empty = shared):
- Every chat (messages + Generate params snapshot) is a row in `assistent.sqlite`, newest **200** kept. History search uses FTS over title + body.
- First launch after 0.8.3 copies `chats/*.json`, `ui-state.json`, and `taste.json` into sqlite, then archives them under `_migrated_json/`. - **Shared** — `Config/_base/memory-seed/`, model cards, `scope: "shared"` upserts. Visible to every persona.
- localStorage stays as a fast cache; on first run with an empty store the old `swarm_assistent_chats_v1` browser history is migrated up once. - **Personal** — `Config/personas/<id>/memory-seed/` and chat upserts (default). Never copied into shared. Other personas do not retrieve it.
- UI state seeds a **fresh** browser only — anything already in localStorage wins, and `auto_download` is never restored as on - Retrieve = shared this persona (and `extends` parents). Hybrid **FTS5 + cosine**, kind quotas, `min_score`. Same `kind`+`key`: personal overwrites parent overwrites shared.
- `settings.json` and persona overlays stay files (layered merge + git). `.assistent.json` cards stay next to weights. - Tools: `memory_get`, `memory_search`, `lookup_tags` (Danbooru csv in `Data/Autocompletions`, FTS, **no embeddings**).
## VRAM handover - Soft craft notes only — Exact, About the user, and the user beat RAG for params
- ⚙ → **Крафт** lists rows with filters + clear (bundled seed is read-only)
- Before Generate the chat model is unloaded only if **Park LLM** is enabled (`keep_alive: 0`) — default off
- After Generate the chat model is **always** force-warmed (`AssistentWarmLlm`) — Krea still often evicts VL from VRAM even without park ## Chats and runtime KV
- Embed / memory models are never parked — reloading them would stall every retrieve
- Every chat (messages + Generate params snapshot) is a row in `assistent.sqlite`, newest **200** kept. History search uses FTS over title + body.
## UX - First launch after 0.8.3 copies `chats/*.json`, `ui-state.json`, and `taste.json` into sqlite, then archives them under `_migrated_json/`.
- localStorage stays as a fast cache; on first run with an empty store the old `swarm_assistent_chats_v1` browser history is migrated up once.
- **Send to Assistent** under Generate/History → Ref + Assistent tab - UI state seeds a **fresh** browser only — anything already in localStorage wins, and `auto_download` is never restored as on
- Enter sends; Shift+Enter newline; Interrupt cancels chat epoch - `settings.json` and persona overlays stay files (layered merge + git). `.assistent.json` cards stay next to weights.
- Manual **Apply + Generate** / `/gen` always generate; Auto-generate checkbox only for LLM auto-path ## VRAM handover
- **Посмотри результат** / auto-critique wait for a real Generate frame — model previews and unfinished batches are skipped
- Civitai Confirm required (unless auto-download); queued-but-missing models show a `⏳ wanted` badge in Cards - Before Generate the chat model is unloaded only if **Park LLM** is enabled (`keep_alive: 0`) — default off
- After Generate the chat model is **always** force-warmed (`AssistentWarmLlm`) — Krea still often evicts VL from VRAM even without park
### Slash commands (client-side, no LLM) - Embed / memory models are never parked — reloading them would stall every retrieve
| Command | Effect | ## UX
| --- | --- |
| `/help` | List commands | - **Send to Assistent** under Generate/History → Ref + Assistent tab
| `/debug` | Short UI/Exact dump (no LLM) | - Enter sends; Shift+Enter newline; Interrupt cancels chat epoch
| `/debug ask` / `/why` | Dump + short model explanation | - Manual **Apply + Generate** / `/gen` always generate; Auto-generate checkbox only for LLM auto-path
| `/gen` | Generate now | - **Посмотри результат** / auto-critique wait for a real Generate frame — model previews and unfinished batches are skipped
| `/look generate\|refN` | Attach that board window + ask the LLM to look | - Civitai Confirm required (unless auto-download); queued-but-missing models show a `⏳ wanted` badge in Cards
| `/init` `/mask` `/clear` | Same as board buttons |
| `/interrupt` | Stop generation / cancel chat | ### Slash commands (client-side, no LLM)
| `/aspect 16:9` | Set size from the official 1K table |
| `/seed lock\|random` | Lock or randomize seed | | Command | Effect |
| `/vary` | New seed, same prompt (+ generate if auto) | | --- | --- |
| `/pack write\|critique\|…` | Switch pack | | `/help` | List commands |
| `/civitai <query>` | Ask LLM to search Civitai | | `/debug` | Short UI/Exact dump (no LLM) |
| `/inventory` | Rescan models + refresh LoRA list | | `/debug ask` / `/why` | Dump + short model explanation |
| `/gen` | Generate now |
## Requirements | `/look generate\|refN` | Attach that board window + ask the LLM to look |
| `/init` `/mask` `/clear` | Same as board buttons |
- SwarmUI with a **Krea 2** checkpoint selected | `/interrupt` | Stop generation / cancel chat |
- Ollama on `http://127.0.0.1:11434` **on the GPU VM** (gpu-rent `LLM_RUNTIME=ollama`) | `/aspect 16:9` | Set size from the official 1K table |
- Chat model + memory embed (`use: memory` in `ollama-models.yaml`; gpu-rent creates CPU variant) | `/seed lock\|random` | Lock or randomize seed |
- Optional: Civitai API key in SwarmUI User Settings | `/vary` | New seed, same prompt (+ generate if auto) |
| `/pack write\|critique\|…` | Switch pack |
## Install | `/civitai <query>` | Ask LLM to search Civitai |
| `/inventory` | Rescan models + refresh LoRA list |
```yaml
swarmui: ## Requirements
- url: https://gitea.hsrv.site/mrleo1nid/swarm-assistent.git
ref: main - SwarmUI with a **Krea 2** checkpoint selected
dir: swarm-assistent - Ollama on `http://127.0.0.1:11434` **on the GPU VM** (gpu-rent `LLM_RUNTIME=ollama`)
requires: ollama - Chat model + memory embed (`use: memory` in `ollama-models.yaml`; gpu-rent creates CPU variant)
``` - Optional: Civitai API key in SwarmUI User Settings
Restart / rebuild SwarmUI after clone. gpu-rent: `seed-extensions` + restart. ## Install
## Packs & skills ```yaml
swarmui:
**Packs** (one active): `ordinary` (default комбайн), `write_prompt`, `critique_image`, `compose_scene`, `fix_params`, `inpaint_edit`, `describe_ref`, `catalog_card`, `author_persona`. - url: https://gitea.hsrv.site/mrleo1nid/swarm-assistent.git
ref: main
**Skills** (checkboxes): `prompting`, `creativity_sliders`, `memory` — procedures; encyclopedia numbers live in Exact, soft notes in memory-seed / RAG, human taste in UserPrefs. dir: swarm-assistent
requires: ollama
**Personas:** `neutral`, `lewd`, `aggressive`, `cinema`, `terse`, `leonid` under `Config/personas/`. Overlay clones via `/persona new` or ⚙ → Личности. ```
## API routes Restart / rebuild SwarmUI after clone. gpu-rent: `seed-extensions` + restart.
| Route | Role | ## Packs & skills
| --- | --- |
| `AssistentListModels` | Ollama tags → `models` (chat) + `memory_models` | **Packs** (one active): `ordinary` (default комбайн), `write_prompt`, `critique_image`, `compose_scene`, `fix_params`, `inpaint_edit`, `describe_ref`, `catalog_card`, `author_persona`.
| `AssistentGetConfig` | Merged preset for persona (ui, packs, skills, identity, controls) |
| `AssistentSaveControls` | Persist Exact `controls` values for a persona (overlay) | **Skills** (checkboxes): `prompting`, `creativity_sliders`, `memory` — procedures; encyclopedia numbers live in Exact, soft notes in memory-seed / RAG, human taste in UserPrefs.
| `AssistentGetPersonaShelves` | Merged identity shelves + controls |
| `AssistentClonePersona` | Snapshot clone → overlay id | **Personas:** `neutral`, `lewd`, `aggressive`, `cinema`, `terse`, `leonid` under `Config/personas/`. Overlay clones via `/persona new` or ⚙ → Личности.
| `AssistentSavePersona` | Sparse shelf write (overlay only) |
| `AssistentDeletePersona` | UI-only delete of overlay persona | ## API routes
| `AssistentExportPersona` / `AssistentImportPersona` | Shareable `.assistent-persona.json` pack |
| `AssistentSaveKnobs` | Overlay `_base/assistant.json` + Exact turbo/raw profiles | | Route | Role |
| `AssistentGetSettings` / `AssistentSaveSettings` | Overlay settings (skills, embed_model) | | --- | --- |
| `AssistentListInventory` | LoRA / checkpoint / wildcard inventory | | `AssistentListModels` | Ollama tags → `models` (chat) + `memory_models` |
| `AssistentListPersonas` | Persona catalog | | `AssistentGetConfig` | Merged preset for persona (ui, packs, skills, identity, controls) |
| `AssistentGetPacks` | Prompt pack texts | | `AssistentSaveControls` | Persist Exact `controls` values for a persona (overlay) |
| `AssistentGetCard` / `AssistentSaveCard` | `.assistent.json` cards (+ memory ingest) | | `AssistentGetPersonaShelves` | Merged identity shelves + controls |
| `AssistentGetCardMeta` | Local sidecar + optional Civitai by-hash | | `AssistentClonePersona` | Snapshot clone → overlay id |
| `AssistentEnqueueWanted` / `AssistentListWanted` | Wanted YAML queue (write / read + count) | | `AssistentSavePersona` | Sparse shelf write (overlay only) |
| `AssistentGetTaste` / `AssistentSaveTaste` | sqlite `kv.taste` (legacy; prefer UserPrefs) | | `AssistentDeletePersona` | UI-only delete of overlay persona |
| `AssistentListUserPrefs` / `AssistentUpsertUserPref` / `AssistentForgetUserPref` / `AssistentClearUserPrefs` | About the user | | `AssistentExportPersona` / `AssistentImportPersona` | Shareable `.assistent-persona.json` pack |
| `AssistentSearchCivitai` | Civitai LoRA search | | `AssistentSaveKnobs` | Overlay `_base/assistant.json` + Exact turbo/raw profiles |
| `AssistentChat` / `AssistentChatWS` | Chat (+ user prefs + hybrid craft memory + hops) | | `AssistentGetSettings` / `AssistentSaveSettings` | Overlay settings (skills, embed_model) |
| `AssistentListMemory` / `AssistentUpsertMemory` / `AssistentForgetMemory` / `AssistentClearMemory` | Craft vector store | | `AssistentListInventory` | LoRA / checkpoint / wildcard inventory |
| `AssistentSearchMemory` / `AssistentGetMemory` | Hybrid search / exact kind+key | | `AssistentListPersonas` | Persona catalog |
| `AssistentLookupTags` | Danbooru csv FTS (no embeddings) | | `AssistentGetPacks` | Prompt pack texts |
| `AssistentListChats` / `AssistentGetChat` / `AssistentSaveChat` / `AssistentDeleteChat` | sqlite `chats` (optional `q` FTS) | | `AssistentGetCard` / `AssistentSaveCard` | `.assistent.json` cards (+ memory ingest) |
| `AssistentGetUiState` / `AssistentSaveUiState` | sqlite `kv.ui_state` | | `AssistentGetCardMeta` | Local sidecar + optional Civitai by-hash |
| `AssistentParkLlm` / `AssistentWarmLlm` | Unload / reload the chat model in VRAM | | `AssistentEnqueueWanted` / `AssistentListWanted` | Wanted YAML queue (write / read + count) |
| `AssistentGetTaste` / `AssistentSaveTaste` | sqlite `kv.taste` (legacy; prefer UserPrefs) |
## License | `AssistentListUserPrefs` / `AssistentUpsertUserPref` / `AssistentForgetUserPref` / `AssistentClearUserPrefs` | About the user |
| `AssistentSearchCivitai` | Civitai LoRA search |
MIT | `AssistentChat` / `AssistentChatWS` | Chat (+ user prefs + hybrid craft memory + hops) |
| `AssistentListMemory` / `AssistentUpsertMemory` / `AssistentForgetMemory` / `AssistentClearMemory` | Craft vector store |
| `AssistentSearchMemory` / `AssistentGetMemory` | Hybrid search / exact kind+key |
| `AssistentLookupTags` | Danbooru csv FTS (no embeddings) |
| `AssistentListChats` / `AssistentGetChat` / `AssistentSaveChat` / `AssistentDeleteChat` | sqlite `chats` (optional `q` FTS) |
| `AssistentGetUiState` / `AssistentSaveUiState` | sqlite `kv.ui_state` |
| `AssistentParkLlm` / `AssistentWarmLlm` | Unload / reload the chat model in VRAM |
## License
MIT
+274 -274
View File
@@ -1,274 +1,274 @@
using System; using System;
using System.IO; using System.IO;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using SwarmUI.Accounts; using SwarmUI.Accounts;
using SwarmUI.Core; using SwarmUI.Core;
using SwarmUI.Utils; using SwarmUI.Utils;
using SwarmUI.WebAPI; using SwarmUI.WebAPI;
using System.Net.Http; using System.Net.Http;
namespace Mrleo1nid.SwarmAssistent; namespace Mrleo1nid.SwarmAssistent;
/// <summary>Krea 2 collaborative assistant: Ollama chat + vision + prompt/LoRA/params patches + Generate/Civitai.</summary> /// <summary>Krea 2 collaborative assistant: Ollama chat + vision + prompt/LoRA/params patches + Generate/Civitai.</summary>
public partial class SwarmAssistentExtension : Extension public partial class SwarmAssistentExtension : Extension
{ {
public static PermInfo PermUse = Permissions.Register(new( public static PermInfo PermUse = Permissions.Register(new(
"swarm_assistent_use", "swarm_assistent_use",
"[Swarm Assistent] Use", "[Swarm Assistent] Use",
"Allows using the Swarm Assistent chat (Ollama proxy).", "Allows using the Swarm Assistent chat (Ollama proxy).",
PermissionDefault.USER, PermissionDefault.USER,
Permissions.GroupUser)); Permissions.GroupUser));
public static HttpClient HttpClient; public static HttpClient HttpClient;
public AssistentConfig Config; public AssistentConfig Config;
public AssistentMemory Memory; public AssistentMemory Memory;
public override void OnPreInit() public override void OnPreInit()
{ {
ScriptFiles.Add("Assets/assistent.api.js"); ScriptFiles.Add("Assets/assistent.api.js");
ScriptFiles.Add("Assets/assistent.patch.js"); ScriptFiles.Add("Assets/assistent.patch.js");
ScriptFiles.Add("Assets/assistent.persist.js"); ScriptFiles.Add("Assets/assistent.persist.js");
ScriptFiles.Add("Assets/assistent.js"); ScriptFiles.Add("Assets/assistent.js");
StyleSheetFiles.Add("Assets/assistent.css"); StyleSheetFiles.Add("Assets/assistent.css");
ExtensionAuthor = "mrleo1nid"; ExtensionAuthor = "mrleo1nid";
Description = "Collaborative Krea 2 assistant: Ollama chat, persona presets, vector memory, model cards, Generate loop."; Description = "Collaborative Krea 2 assistant: Ollama chat, persona presets, vector memory, model cards, Generate loop.";
License = "MIT"; License = "MIT";
Version = "0.10.21"; Version = "0.11.3";
Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory"]; Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory"];
} }
public override void OnInit() public override void OnInit()
{ {
HttpClient ??= new HttpClient { Timeout = TimeSpan.FromMinutes(10) }; HttpClient ??= new HttpClient { Timeout = TimeSpan.FromMinutes(10) };
Config = new AssistentConfig(FilePath, DataRoot()); Config = new AssistentConfig(FilePath, DataRoot());
Memory = new AssistentMemory(DataRoot(), HttpClient, Config.LoadAssistant(Config.DefaultPersonaId())["embed_model"]?.ToString() ?? "nomic-embed-text"); Memory = new AssistentMemory(DataRoot(), HttpClient, Config.LoadAssistant(Config.DefaultPersonaId())["embed_model"]?.ToString() ?? "nomic-embed-text");
API.RegisterAPICall(AssistentListModels, false, PermUse); API.RegisterAPICall(AssistentListModels, false, PermUse);
API.RegisterAPICall(AssistentGetPacks, false, PermUse); API.RegisterAPICall(AssistentGetPacks, false, PermUse);
API.RegisterAPICall(AssistentListPersonas, false, PermUse); API.RegisterAPICall(AssistentListPersonas, false, PermUse);
API.RegisterAPICall(AssistentGetConfig, false, PermUse); API.RegisterAPICall(AssistentGetConfig, false, PermUse);
API.RegisterAPICall(AssistentGetSettings, false, PermUse); API.RegisterAPICall(AssistentGetSettings, false, PermUse);
API.RegisterAPICall(AssistentSaveSettings, true, PermUse); API.RegisterAPICall(AssistentSaveSettings, true, PermUse);
API.RegisterAPICall(AssistentListInventory, false, PermUse); API.RegisterAPICall(AssistentListInventory, false, PermUse);
API.RegisterAPICall(AssistentGetCard, false, PermUse); API.RegisterAPICall(AssistentGetCard, false, PermUse);
API.RegisterAPICall(AssistentSaveCard, true, PermUse); API.RegisterAPICall(AssistentSaveCard, true, PermUse);
API.RegisterAPICall(AssistentEnqueueWanted, true, PermUse); API.RegisterAPICall(AssistentEnqueueWanted, true, PermUse);
API.RegisterAPICall(AssistentGetCardMeta, false, PermUse); API.RegisterAPICall(AssistentGetCardMeta, false, PermUse);
API.RegisterAPICall(AssistentSearchCivitai, false, PermUse); API.RegisterAPICall(AssistentSearchCivitai, false, PermUse);
API.RegisterAPICall(AssistentGetTaste, false, PermUse); API.RegisterAPICall(AssistentGetTaste, false, PermUse);
API.RegisterAPICall(AssistentSaveTaste, true, PermUse); API.RegisterAPICall(AssistentSaveTaste, true, PermUse);
API.RegisterAPICall(AssistentChat, true, PermUse); API.RegisterAPICall(AssistentChat, true, PermUse);
API.RegisterAPICall(AssistentChatWS, true, PermUse); API.RegisterAPICall(AssistentChatWS, true, PermUse);
API.RegisterAPICall(AssistentListChats, false, PermUse); API.RegisterAPICall(AssistentListChats, false, PermUse);
API.RegisterAPICall(AssistentGetChat, false, PermUse); API.RegisterAPICall(AssistentGetChat, false, PermUse);
API.RegisterAPICall(AssistentSaveChat, true, PermUse); API.RegisterAPICall(AssistentSaveChat, true, PermUse);
API.RegisterAPICall(AssistentDeleteChat, true, PermUse); API.RegisterAPICall(AssistentDeleteChat, true, PermUse);
API.RegisterAPICall(AssistentGetUiState, false, PermUse); API.RegisterAPICall(AssistentGetUiState, false, PermUse);
API.RegisterAPICall(AssistentSaveUiState, true, PermUse); API.RegisterAPICall(AssistentSaveUiState, true, PermUse);
API.RegisterAPICall(AssistentParkLlm, true, PermUse); API.RegisterAPICall(AssistentParkLlm, true, PermUse);
API.RegisterAPICall(AssistentWarmLlm, true, PermUse); API.RegisterAPICall(AssistentWarmLlm, true, PermUse);
API.RegisterAPICall(AssistentListMemory, false, PermUse); API.RegisterAPICall(AssistentListMemory, false, PermUse);
API.RegisterAPICall(AssistentUpsertMemory, true, PermUse); API.RegisterAPICall(AssistentUpsertMemory, true, PermUse);
API.RegisterAPICall(AssistentForgetMemory, true, PermUse); API.RegisterAPICall(AssistentForgetMemory, true, PermUse);
API.RegisterAPICall(AssistentSearchMemory, false, PermUse); API.RegisterAPICall(AssistentSearchMemory, false, PermUse);
API.RegisterAPICall(AssistentGetMemory, false, PermUse); API.RegisterAPICall(AssistentGetMemory, false, PermUse);
API.RegisterAPICall(AssistentLookupTags, false, PermUse); API.RegisterAPICall(AssistentLookupTags, false, PermUse);
API.RegisterAPICall(AssistentListWanted, false, PermUse); API.RegisterAPICall(AssistentListWanted, false, PermUse);
API.RegisterAPICall(AssistentSaveControls, true, PermUse); API.RegisterAPICall(AssistentSaveControls, true, PermUse);
API.RegisterAPICall(AssistentGetPersonaShelves, false, PermUse); API.RegisterAPICall(AssistentGetPersonaShelves, false, PermUse);
API.RegisterAPICall(AssistentClonePersona, true, PermUse); API.RegisterAPICall(AssistentClonePersona, true, PermUse);
API.RegisterAPICall(AssistentSavePersona, true, PermUse); API.RegisterAPICall(AssistentSavePersona, true, PermUse);
API.RegisterAPICall(AssistentDeletePersona, true, PermUse); API.RegisterAPICall(AssistentDeletePersona, true, PermUse);
API.RegisterAPICall(AssistentExportPersona, false, PermUse); API.RegisterAPICall(AssistentExportPersona, false, PermUse);
API.RegisterAPICall(AssistentImportPersona, true, PermUse); API.RegisterAPICall(AssistentImportPersona, true, PermUse);
API.RegisterAPICall(AssistentSaveKnobs, true, PermUse); API.RegisterAPICall(AssistentSaveKnobs, true, PermUse);
API.RegisterAPICall(AssistentListUserPrefs, false, PermUse); API.RegisterAPICall(AssistentListUserPrefs, false, PermUse);
API.RegisterAPICall(AssistentUpsertUserPref, true, PermUse); API.RegisterAPICall(AssistentUpsertUserPref, true, PermUse);
API.RegisterAPICall(AssistentForgetUserPref, true, PermUse); API.RegisterAPICall(AssistentForgetUserPref, true, PermUse);
API.RegisterAPICall(AssistentClearUserPrefs, true, PermUse); API.RegisterAPICall(AssistentClearUserPrefs, true, PermUse);
API.RegisterAPICall(AssistentClearMemory, true, PermUse); API.RegisterAPICall(AssistentClearMemory, true, PermUse);
Logs.Init("Swarm Assistent extension loaded (settings panel + user prefs + craft memory)"); Logs.Init("Swarm Assistent extension loaded (settings panel + user prefs + craft memory)");
} }
int CfgInt(string key, int fallback) int CfgInt(string key, int fallback)
{ {
try try
{ {
return Config?.LoadAssistant(Config.DefaultPersonaId())[key]?.Value<int?>() ?? fallback; return Config?.LoadAssistant(Config.DefaultPersonaId())[key]?.Value<int?>() ?? fallback;
} }
catch catch
{ {
return fallback; return fallback;
} }
} }
static string Clip(string text, int max) static string Clip(string text, int max)
{ {
if (string.IsNullOrEmpty(text) || text.Length <= max) if (string.IsNullOrEmpty(text) || text.Length <= max)
{ {
return text ?? ""; return text ?? "";
} }
return text[..max] + "…"; return text[..max] + "…";
} }
static string CollapseWs(string text) static string CollapseWs(string text)
{ {
if (string.IsNullOrWhiteSpace(text)) if (string.IsNullOrWhiteSpace(text))
{ {
return ""; return "";
} }
return Regex.Replace(text.Trim(), @"\s+", " "); return Regex.Replace(text.Trim(), @"\s+", " ");
} }
public static string NormalizeBaseUrl(string raw) public static string NormalizeBaseUrl(string raw)
{ {
string url = (raw ?? "").Trim(); string url = (raw ?? "").Trim();
if (string.IsNullOrWhiteSpace(url)) if (string.IsNullOrWhiteSpace(url))
{ {
url = "http://127.0.0.1:11434"; url = "http://127.0.0.1:11434";
} }
return url.TrimEnd('/'); return url.TrimEnd('/');
} }
static string DataRoot() static string DataRoot()
{ {
if (Directory.Exists("/mnt/swarm_data")) if (Directory.Exists("/mnt/swarm_data"))
{ {
return "/mnt/swarm_data"; return "/mnt/swarm_data";
} }
try try
{ {
string models = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, "Models")); string models = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, "Models"));
if (Directory.Exists(models)) if (Directory.Exists(models))
{ {
return Path.GetDirectoryName(models) ?? Environment.CurrentDirectory; return Path.GetDirectoryName(models) ?? Environment.CurrentDirectory;
} }
} }
catch catch
{ {
// ignore // ignore
} }
return Environment.CurrentDirectory; return Environment.CurrentDirectory;
} }
public string ReadPackFile(string name) public string ReadPackFile(string name)
{ {
return Config?.LoadPackPrompt(Config.DefaultPersonaId(), name); return Config?.LoadPackPrompt(Config.DefaultPersonaId(), name);
} }
public async Task<JObject> AssistentGetPacks(Session session, string persona = null) public async Task<JObject> AssistentGetPacks(Session session, string persona = null)
{ {
await Task.CompletedTask; await Task.CompletedTask;
string pid = AssistentConfig.SafeId(persona) ?? Config.DefaultPersonaId(); string pid = AssistentConfig.SafeId(persona) ?? Config.DefaultPersonaId();
JObject packs = new(); JObject packs = new();
JArray order = []; JArray order = [];
foreach (var p in Config.ListPacks(pid)) foreach (var p in Config.ListPacks(pid))
{ {
string text = Config.LoadPackPrompt(pid, p.id); string text = Config.LoadPackPrompt(pid, p.id);
if (text is not null) if (text is not null)
{ {
packs[p.id] = text; packs[p.id] = text;
} }
order.Add(p.id); order.Add(p.id);
} }
return new JObject { ["success"] = true, ["packs"] = packs, ["order"] = order, ["persona"] = pid }; return new JObject { ["success"] = true, ["packs"] = packs, ["order"] = order, ["persona"] = pid };
} }
public async Task<JObject> AssistentGetConfig(Session session, string persona = null) public async Task<JObject> AssistentGetConfig(Session session, string persona = null)
{ {
await Task.CompletedTask; await Task.CompletedTask;
string pid = AssistentConfig.SafeId(persona) ?? Config.DefaultPersonaId(); string pid = AssistentConfig.SafeId(persona) ?? Config.DefaultPersonaId();
return Config.BuildMergedConfigPayload(pid); return Config.BuildMergedConfigPayload(pid);
} }
public async Task<JObject> AssistentGetSettings(Session session) public async Task<JObject> AssistentGetSettings(Session session)
{ {
await Task.CompletedTask; await Task.CompletedTask;
return new JObject { ["success"] = true, ["settings"] = Config.LoadSettings() }; return new JObject { ["success"] = true, ["settings"] = Config.LoadSettings() };
} }
public async Task<JObject> AssistentSaveSettings(Session session, JObject settings) public async Task<JObject> AssistentSaveSettings(Session session, JObject settings)
{ {
await Task.CompletedTask; await Task.CompletedTask;
if (settings is null) if (settings is null)
{ {
return new JObject { ["error"] = "settings required" }; return new JObject { ["error"] = "settings required" };
} }
string prevEmbed = Config.LoadSettings()["embed_model"]?.ToString(); string prevEmbed = Config.LoadSettings()["embed_model"]?.ToString();
Config.SaveSettings(settings); Config.SaveSettings(settings);
string nextEmbed = settings["embed_model"]?.ToString(); string nextEmbed = settings["embed_model"]?.ToString();
if (!string.IsNullOrWhiteSpace(nextEmbed) && !string.Equals(prevEmbed, nextEmbed, StringComparison.OrdinalIgnoreCase)) if (!string.IsNullOrWhiteSpace(nextEmbed) && !string.Equals(prevEmbed, nextEmbed, StringComparison.OrdinalIgnoreCase))
{ {
try try
{ {
await Memory.ReembedAllAsync(NormalizeBaseUrl(settings["base_url"]?.ToString()), nextEmbed); await Memory.ReembedAllAsync(NormalizeBaseUrl(settings["base_url"]?.ToString()), nextEmbed);
} }
catch (Exception ex) catch (Exception ex)
{ {
Logs.Debug($"AssistentSaveSettings reembed: {ex.Message}"); Logs.Debug($"AssistentSaveSettings reembed: {ex.Message}");
} }
} }
return new JObject { ["success"] = true, ["path"] = Path.Combine(Config.OverlayRoot, "settings.json") }; return new JObject { ["success"] = true, ["path"] = Path.Combine(Config.OverlayRoot, "settings.json") };
} }
public async Task<JObject> AssistentListPersonas(Session session) public async Task<JObject> AssistentListPersonas(Session session)
{ {
await Task.CompletedTask; await Task.CompletedTask;
var catalog = Config.ListPersonaCatalog(); var catalog = Config.ListPersonaCatalog();
JArray list = []; JArray list = [];
foreach (var p in catalog) foreach (var p in catalog)
{ {
list.Add(new JObject list.Add(new JObject
{ {
["id"] = p.id, ["id"] = p.id,
["title"] = p.title, ["title"] = p.title,
["accent"] = p.accent, ["accent"] = p.accent,
["prompt"] = Config.RenderIdentityBlock(p.id, includeAllShelves: true), ["prompt"] = Config.RenderIdentityBlock(p.id, includeAllShelves: true),
["source"] = p.source, ["source"] = p.source,
}); });
} }
return new JObject return new JObject
{ {
["success"] = true, ["success"] = true,
["default"] = Config.DefaultPersonaId(), ["default"] = Config.DefaultPersonaId(),
["personas"] = list, ["personas"] = list,
}; };
} }
public async Task<JObject> AssistentGetTaste(Session session) public async Task<JObject> AssistentGetTaste(Session session)
{ {
await Task.CompletedTask; await Task.CompletedTask;
try try
{ {
return new JObject { ["success"] = true, ["taste"] = Memory.GetKvObject(AssistentMemory.KvTaste) }; return new JObject { ["success"] = true, ["taste"] = Memory.GetKvObject(AssistentMemory.KvTaste) };
} }
catch (Exception ex) catch (Exception ex)
{ {
return new JObject { ["error"] = $"taste: {ex.Message}" }; return new JObject { ["error"] = $"taste: {ex.Message}" };
} }
} }
public async Task<JObject> AssistentSaveTaste(Session session, JObject taste) public async Task<JObject> AssistentSaveTaste(Session session, JObject taste)
{ {
await Task.CompletedTask; await Task.CompletedTask;
if (taste is null) if (taste is null)
{ {
return new JObject { ["error"] = "taste required" }; return new JObject { ["error"] = "taste required" };
} }
if (taste["updated"] == null) if (taste["updated"] == null)
{ {
taste["updated"] = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); taste["updated"] = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
} }
try try
{ {
Memory.SetKvObject(AssistentMemory.KvTaste, taste); Memory.SetKvObject(AssistentMemory.KvTaste, taste);
return new JObject { ["success"] = true, ["path"] = "Assistent/memory/assistent.sqlite" }; return new JObject { ["success"] = true, ["path"] = "Assistent/memory/assistent.sqlite" };
} }
catch (Exception ex) catch (Exception ex)
{ {
return new JObject { ["error"] = $"taste save: {ex.Message}" }; return new JObject { ["error"] = $"taste save: {ex.Message}" };
} }
} }
} }