From 9ac24a828b166b11f5ef04120a8cecf638521e3a Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Sun, 23 Aug 2026 06:19:04 +0300 Subject: [PATCH] Ship Assistent 0.14.0: chat sessions, ask-only hops, and context compression. Per-chat Generate session with sparse deltas; drop Cards/Civitai/wanted hops; rolling history summary via the same Ollama model with a budget chip and /compress. Co-authored-by: Cursor --- Assets/assistent.bundle.js | 3129 ++++++++++++---------- Assets/assistent.css | 514 +++- AssistentChatPipeline.cs | 2382 ++++++++-------- AssistentConfig.cs | 8 +- AssistentInventory.cs | 635 +---- AssistentMemoryApi.cs | 44 +- AssistentOllama.cs | 39 +- AssistentPatch.cs | 184 +- AssistentPersonaApi.cs | 5 +- AssistentWanted.cs | 189 -- Config/_base/assistant.json | 8 +- Config/_base/core/core.md | 71 +- Config/_base/packs/catalog_card.json | 7 - Config/_base/packs/catalog_card.md | 34 - Config/_base/packs/compose_scene.md | 2 +- Config/_base/packs/compress_history.json | 9 + Config/_base/packs/compress_history.md | 32 + Config/_base/packs/ordinary.md | 25 +- Config/_base/patch-keys.json | 8 +- Config/_base/skills/prompting.md | 5 +- Config/_base/ui.json | 11 +- README.md | 15 +- SwarmAssistentExtension.cs | 10 +- Tabs/Text2Image/Assistent.html | 90 +- package.json | 2 +- src/activity.js | 263 ++ src/app.js | 2591 ++++++++---------- src/context.js | 194 ++ src/intent.js | 161 +- src/main.js | 6 + src/patch.js | 58 +- src/session.js | 368 +++ test/context.test.js | 99 + test/intent.test.js | 26 +- test/patch.test.js | 68 +- 35 files changed, 5640 insertions(+), 5652 deletions(-) delete mode 100644 AssistentWanted.cs delete mode 100644 Config/_base/packs/catalog_card.json delete mode 100644 Config/_base/packs/catalog_card.md create mode 100644 Config/_base/packs/compress_history.json create mode 100644 Config/_base/packs/compress_history.md create mode 100644 src/activity.js create mode 100644 src/context.js create mode 100644 src/session.js create mode 100644 test/context.test.js diff --git a/Assets/assistent.bundle.js b/Assets/assistent.bundle.js index 1411677..2d54c5d 100644 --- a/Assets/assistent.bundle.js +++ b/Assets/assistent.bundle.js @@ -41,8 +41,8 @@ "sampler", "scheduler", "actions", - "search_query", - "civitai_query", + "generate", + "ask", "use_init_image", "clear_init_image", "init_creativity", @@ -70,18 +70,9 @@ "clear_prompt_images", "slot_to_prompt_image", "pack", - "memories", - "memory", - "memory_query", - "memory_kind", - "tag_query", - "user_prefs", - "inventory_query", - "skills", - "persona_shelves", - "persona_clone", "persona", "controls", + "inventory_query", "variants" ]; var PATCH_KEYS = DEFAULT_PATCH_KEYS.slice(); @@ -94,32 +85,22 @@ function has(obj, key) { return obj[key] !== void 0 && obj[key] !== null; } - function isCardObject(obj) { - if (!obj || typeof obj !== "object") { - return false; - } - const cardish = !!(obj.kind || obj.triggers || obj.when || obj.prompt_hint); - const genish = !!(obj.prompt != null || obj.negative != null || obj.loras || obj.actions || obj.width || obj.height || obj.steps != null || obj.cfg != null || obj.aspect || obj.seed != null || obj.search_query || obj.civitai_query || obj.look_at || obj.controls); - if (cardish && !genish && (obj.name || obj.triggers || obj.when)) { - return true; - } - return !!(obj.kind && obj.name && (obj.triggers || obj.when || obj.prompt_hint || obj.notes != null)); - } function isPatchObject2(obj) { if (!obj || typeof obj !== "object") { return false; } - if (isCardObject(obj)) { - return false; - } return PATCH_KEYS.some((k) => has(obj, k)); } function normalizePatch(patch) { if (!patch || typeof patch !== "object") { return patch; } - if (!has(patch, "search_query") && has(patch, "civitai_query")) { - patch.search_query = patch.civitai_query; + const acts = Array.isArray(patch.actions) ? patch.actions.map(String) : []; + if (patch.generate === true || acts.includes("generate")) { + patch.generate = true; + } + if (typeof patch.ask === "string") { + patch.ask = [patch.ask]; } if (!has(patch, "init_creativity") && has(patch, "denoise")) { patch.init_creativity = patch.denoise; @@ -157,7 +138,13 @@ if (!obj || typeof obj !== "object") { return false; } - if (isCardObject(obj)) { + if (obj.generate === true) { + return true; + } + if (Array.isArray(obj.ask) && obj.ask.length) { + return true; + } + if (typeof obj.ask === "string" && obj.ask) { return true; } if (Array.isArray(obj.variants) && obj.variants.length) { @@ -166,30 +153,14 @@ 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))) { + if (acts.includes("generate")) { 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) { + 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) { return true; } return false; @@ -197,7 +168,6 @@ function attachPatch(SA2) { SA2.PATCH_KEYS = PATCH_KEYS; SA2.setPatchKeys = setPatchKeys; - SA2.isCardObject = isCardObject; SA2.isPatchObject = isPatchObject2; SA2.isTerminalStreamPatch = isTerminalStreamPatch; SA2.normalizePatch = normalizePatch; @@ -333,6 +303,741 @@ }; } + // src/session.js + var MAX_DATA_URL_CHARS = 35e4; + var GEN_KEYS = [ + "prompt", + "negative", + "width", + "height", + "aspect", + "steps", + "cfg", + "sigma_shift", + "seed", + "sampler", + "scheduler", + "batch", + "checkpoint", + "loras", + "controls", + "use_init_image", + "clear_init_image", + "init_creativity", + "denoise", + "use_mask_image", + "clear_mask_image", + "mask_blur", + "mask_grow" + ]; + function emptySession() { + return { + gen: { + prompt: "", + negative: "", + width: null, + height: null, + aspect: null, + steps: null, + cfg: null, + sigma_shift: null, + seed: null, + sampler: null, + scheduler: null, + batch: null, + checkpoint: null, + loras: [], + controls: {}, + use_init_image: false, + clear_init_image: false, + init_creativity: null, + denoise: null, + use_mask_image: false, + clear_mask_image: false, + mask_blur: null, + mask_grow: null + }, + board: { + slots: [], + selectedSlotId: "ref1", + genResults: [], + selectedGenResultId: null, + refSeq: 1 + }, + persona: "neutral", + pack: "ordinary", + context_memory: null + }; + } + function normalizeDelta(raw) { + if (!raw || typeof raw !== "object") { + return null; + } + const delta = { ...raw }; + const acts = Array.isArray(delta.actions) ? delta.actions.map(String) : []; + if (delta.generate === true || acts.includes("generate")) { + delta.generate = true; + } + if (typeof delta.ask === "string") { + delta.ask = [delta.ask]; + } + if (!Array.isArray(delta.ask)) { + delete delta.ask; + } else { + delta.ask = delta.ask.map(String).filter(Boolean); + } + return delta; + } + function patchWantsGenerate(patch) { + if (!patch || typeof patch !== "object") { + return false; + } + if (patch.generate === true) { + return true; + } + const acts = Array.isArray(patch.actions) ? patch.actions.map(String) : []; + return acts.includes("generate"); + } + function patchAskList(patch) { + const n = normalizeDelta(patch); + return Array.isArray(n?.ask) ? n.ask : []; + } + function mergeDelta(session, rawDelta) { + const base = session && typeof session === "object" ? structuredCloneSession(session) : emptySession(); + const delta = normalizeDelta(rawDelta); + if (!delta) { + return base; + } + if (!base.gen) { + base.gen = emptySession().gen; + } + for (const key of GEN_KEYS) { + if (delta[key] === void 0 || delta[key] === null) { + continue; + } + if (key === "loras" && Array.isArray(delta.loras)) { + base.gen.loras = delta.loras.map((l) => ({ + name: l?.name || l, + weight: l?.weight != null ? Number(l.weight) : 1, + triggers: Array.isArray(l?.triggers) ? l.triggers : void 0, + trigger_phrase: l?.trigger_phrase || void 0 + })).filter((l) => l.name); + continue; + } + if (key === "controls" && typeof delta.controls === "object") { + base.gen.controls = { ...base.gen.controls || {}, ...delta.controls }; + continue; + } + if (key === "checkpoint") { + base.gen.checkpoint = typeof delta.checkpoint === "object" ? { ...delta.checkpoint } : { name: String(delta.checkpoint) }; + continue; + } + base.gen[key] = delta[key]; + } + if (delta.images != null && delta.batch == null) { + base.gen.batch = delta.images; + } + if (delta.pack) { + base.pack = String(delta.pack); + } + if (delta.persona) { + base.persona = String(delta.persona); + } + return base; + } + function structuredCloneSession(session) { + try { + return JSON.parse(JSON.stringify(session)); + } catch { + return emptySession(); + } + } + function slimSrc(src) { + if (!src || typeof src !== "string") { + return null; + } + const s = src.trim(); + if (!s || s.startsWith("#")) { + return null; + } + if (s.startsWith("data:") && s.length > MAX_DATA_URL_CHARS) { + return null; + } + return s; + } + function snapshotFromLive({ + genFields, + board, + persona, + pack, + context_memory + }) { + const session = emptySession(); + if (genFields && typeof genFields === "object") { + for (const key of GEN_KEYS) { + if (genFields[key] !== void 0) { + session.gen[key] = genFields[key]; + } + } + } + session.persona = persona || "neutral"; + session.pack = pack || "ordinary"; + if (context_memory && typeof context_memory === "object") { + session.context_memory = context_memory; + } + if (board && typeof board === "object") { + session.board = { + slots: (board.slots || []).map((s) => ({ + id: s.id, + type: s.type, + label: s.label, + src: slimSrc(s.src), + attach: !!s.attach, + note: s.note || null + })), + selectedSlotId: board.selectedSlotId || "ref1", + genResults: (board.genResults || []).map((r) => ({ + id: r.id, + label: r.label, + src: slimSrc(r.src), + patch: r.patch || null + })), + selectedGenResultId: board.selectedGenResultId || null, + refSeq: board.refSeq || 1 + }; + } + return session; + } + function sessionFromLegacyParams(params) { + if (!params || typeof params !== "object") { + return emptySession(); + } + if (params.gen && typeof params.gen === "object") { + const s2 = emptySession(); + s2.gen = { ...s2.gen, ...params.gen }; + if (params.board && typeof params.board === "object") { + s2.board = { ...s2.board, ...params.board }; + } + s2.persona = params.persona || s2.persona; + s2.pack = params.pack || s2.pack; + if (params.context_memory && typeof params.context_memory === "object") { + s2.context_memory = params.context_memory; + } + return s2; + } + const s = emptySession(); + for (const key of GEN_KEYS) { + if (params[key] !== void 0 && params[key] !== null) { + s.gen[key] = params[key]; + } + } + if (Array.isArray(params.loras)) { + s.gen.loras = params.loras; + } + if (params.checkpoint) { + s.gen.checkpoint = typeof params.checkpoint === "object" ? params.checkpoint : { name: String(params.checkpoint) }; + } + s.persona = params.persona || "neutral"; + s.pack = params.pack || "ordinary"; + if (params.context_memory && typeof params.context_memory === "object") { + s.context_memory = params.context_memory; + } + s.board.genResults = Array.isArray(params.genResults) ? params.genResults : []; + s.board.selectedGenResultId = params.selectedGenResultId || null; + if (Array.isArray(params.slots)) { + s.board.slots = params.slots; + } + if (params.selectedSlotId) { + s.board.selectedSlotId = params.selectedSlotId; + } + if (params.refSeq) { + s.board.refSeq = params.refSeq; + } + return s; + } + function toPersistParams(session) { + const s = session && typeof session === "object" ? session : emptySession(); + const out = { + gen: s.gen || emptySession().gen, + board: s.board || emptySession().board, + persona: s.persona || "neutral", + pack: s.pack || "ordinary" + }; + if (s.context_memory && typeof s.context_memory === "object") { + out.context_memory = s.context_memory; + } + return out; + } + function slimText(t, max) { + const s = String(t || ""); + if (s.length <= max) { + return s; + } + return `${s.slice(0, max)}\u2026`; + } + function compactContext(session, extras = {}) { + const s = session && typeof session === "object" ? session : emptySession(); + const g = s.gen || {}; + const board = s.board || {}; + const slots = board.slots || []; + const genSlot = slots.find((x) => x.type === "generate" || x.id === "generate"); + const refs = slots.filter((x) => x.type === "ref" || String(x.id || "").startsWith("ref")); + return { + session: true, + prompt: slimText(g.prompt, extras.promptMax || 2e3), + negative: slimText(g.negative, 500), + aspect: g.aspect || null, + width: g.width ?? null, + height: g.height ?? null, + steps: g.steps ?? null, + cfg: g.cfg ?? null, + seed: g.seed ?? null, + sigma_shift: g.sigma_shift ?? null, + sampler: g.sampler || null, + scheduler: g.scheduler || null, + batch: g.batch ?? null, + checkpoint: g.checkpoint?.name || g.checkpoint || null, + selected_loras: (g.loras || []).map((l) => ({ + name: l.name || l, + weight: l.weight != null ? l.weight : 1 + })), + persona: s.persona || "neutral", + pack: s.pack || "ordinary", + board: { + has_generate: !!(genSlot?.src || (board.genResults || []).some((r) => r.src)), + refs: refs.map((r) => ({ id: r.id, has_image: !!r.src, attach: !!r.attach })), + gen_results: (board.genResults || []).map((r) => ({ + id: r.id, + label: r.label, + has_image: !!r.src, + selected: r.id === board.selectedGenResultId + })), + selected_slot: board.selectedSlotId || null + }, + architecture_ok: extras.architecture_ok !== false, + ...extras.extra + }; + } + function fullSettingsDump(session, extras = {}) { + const compact = compactContext(session, extras); + const s = session && typeof session === "object" ? session : emptySession(); + return { + ...compact, + detail: "settings", + gen: { ...s.gen || {} }, + controls: s.gen?.controls || {}, + exact: extras.exact || null, + krea_profiles: extras.kreaProfiles || null, + session_exact: extras.sessionExact || null + }; + } + function resolveTurnIntent(patch, userText, { vetoFn } = {}) { + const delta = normalizeDelta(patch) || {}; + const vetoed = typeof vetoFn === "function" ? !!vetoFn(userText) : false; + const generate = !vetoed && patchWantsGenerate(delta); + const hasLook = delta.look_at != null || delta.vision_from != null || delta.vision_slots != null; + const look = !!(hasLook && !generate && !vetoed); + const ask = patchAskList(delta); + return { generate, look, vetoed, ask }; + } + function attachSession(SA2) { + SA2.session = { + emptySession, + normalizeDelta, + mergeDelta, + patchWantsGenerate, + patchAskList, + snapshotFromLive, + sessionFromLegacyParams, + toPersistParams, + compactContext, + fullSettingsDump, + resolveTurnIntent, + GEN_KEYS + }; + } + + // src/context.js + function emptyContextMemory() { + return { + summary: "", + untilCount: 0, + foldedTurns: 0, + at: 0, + uiCollapsed: false, + promptEvalCount: null + }; + } + function normalizeContextMemory(raw) { + if (!raw || typeof raw !== "object") { + return emptyContextMemory(); + } + const summary = String(raw.summary || "").trim(); + return { + summary, + untilCount: Math.max(0, Number(raw.untilCount) || 0), + foldedTurns: Math.max(0, Number(raw.foldedTurns) || 0), + at: Number(raw.at) || 0, + uiCollapsed: !!raw.uiCollapsed && !!summary, + promptEvalCount: raw.promptEvalCount != null ? Number(raw.promptEvalCount) || null : null + }; + } + function charsToTokens(chars, charsPerToken = 3.2) { + const cpt = Math.max(1.5, Number(charsPerToken) || 3.2); + const n = Math.max(0, Number(chars) || 0); + return Math.ceil(n / cpt); + } + function estimateBudget(opts = {}) { + const numCtx = Math.max(1024, Number(opts.numCtx) || 16384); + const numPredict = Math.max(256, Number(opts.numPredict) || 3072); + const charsPerToken = Math.max(1.5, Number(opts.charsPerToken) || 3.2); + const compressAt = Math.min(0.95, Math.max(0.4, Number(opts.compressAt) || 0.7)); + const systemChars = Math.max(0, Number(opts.systemChars) || 0); + const historyChars = Math.max(0, Number(opts.historyChars) || 0); + const memoryChars = Math.max(0, Number(opts.memoryChars) || 0); + const inputChars = systemChars + historyChars + memoryChars; + const estimated = charsToTokens(inputChars, charsPerToken); + const used = opts.promptEvalCount != null && Number(opts.promptEvalCount) > 0 ? Number(opts.promptEvalCount) : estimated; + const headroom = Math.max(1024, numCtx - numPredict); + const threshold = Math.floor(headroom * compressAt); + const ratio = numCtx > 0 ? used / numCtx : 0; + let level = "ok"; + if (ratio >= 0.85 || used >= threshold) { + level = "hot"; + } else if (ratio >= 0.65 || used >= threshold * 0.85) { + level = "warn"; + } + return { + numCtx, + numPredict, + headroom, + threshold, + systemChars, + historyChars, + memoryChars, + inputChars, + estimated, + used, + fromEval: opts.promptEvalCount != null && Number(opts.promptEvalCount) > 0, + ratio, + level, + charsPerToken, + compressAt + }; + } + function shouldCompress(budget, memory, historyLen, opts = {}) { + const keep = Math.max(2, Number(opts.keepMessages) || 8); + const len = Math.max(0, Number(historyLen) || 0); + const until = Math.max(0, Number(memory?.untilCount) || 0); + const uncovered = Math.max(0, len - until); + if (uncovered <= keep) { + return false; + } + const used = budget?.used ?? 0; + const threshold = budget?.threshold ?? Infinity; + return used >= threshold; + } + function assembleModelMessages(history, memory, keepTurns) { + const keep = Math.max(1, Number(keepTurns) || 4) * 2; + const until = Math.max(0, Number(memory?.untilCount) || 0); + const list = (history || []).filter((m) => m && (m.role === "user" || m.role === "assistant") && !m.systemish); + const afterSummary = until > 0 ? list.slice(until) : list; + const window2 = afterSummary.length > keep ? afterSummary.slice(-keep) : afterSummary; + return window2.map((m) => ({ + role: m.role, + content: String(m.content || "").slice(0, 4e3) + })); + } + function messagesToFold(history, memory, keepTurns) { + const keep = Math.max(1, Number(keepTurns) || 4) * 2; + const list = (history || []).filter((m) => m && (m.role === "user" || m.role === "assistant") && !m.systemish); + const until = Math.max(0, Number(memory?.untilCount) || 0); + const foldEnd = Math.max(until, list.length - keep); + if (foldEnd <= until) { + return []; + } + return list.slice(until, foldEnd); + } + function mergeSummary(oldSummary, incoming) { + const next = String(incoming || "").trim(); + if (!next) { + return String(oldSummary || "").trim(); + } + const prev = String(oldSummary || "").trim(); + if (!prev) { + return next; + } + return next; + } + function formatTokenShort(n) { + const v = Math.max(0, Number(n) || 0); + if (v >= 1e4) { + return `${(v / 1e3).toFixed(1)}k`; + } + if (v >= 1e3) { + return `${(v / 1e3).toFixed(1)}k`; + } + return String(Math.round(v)); + } + function conversationMemoryBlock(memory, maxChars = 2400) { + const m = normalizeContextMemory(memory); + if (!m.summary) { + return null; + } + let text = m.summary; + if (text.length > maxChars) { + text = `${text.slice(0, maxChars)}\u2026`; + } + return { + summary: text, + until_count: m.untilCount, + folded_turns: m.foldedTurns + }; + } + function attachContext(SA2) { + SA2.context = { + emptyContextMemory, + normalizeContextMemory, + charsToTokens, + estimateBudget, + shouldCompress, + assembleModelMessages, + messagesToFold, + mergeSummary, + formatTokenShort, + conversationMemoryBlock + }; + } + + // src/activity.js + var STEP_ICONS = { + think: "\u25C7", + stream: "\u270E", + delta: "\u21E2", + ask: "?", + look: "\u25CE", + prep: "\u21BB", + generate: "\u25B7", + merge: "\u2295", + warm: "\u25B2", + park: "\u25BC", + inventory: "\u25A4", + compress: "\u25A4", + done: "\u2713", + skip: "\u2013", + error: "!" + }; + function createActivityController(opts = {}) { + const { + getMessagesEl, + scrollToBottom, + hideEmpty + } = opts; + let card2 = null; + let listEl = null; + let titleEl = null; + let steps = []; + let open = true; + function ensureCard() { + const box = typeof getMessagesEl === "function" ? getMessagesEl() : null; + if (!box) { + return null; + } + if (card2 && card2.isConnected) { + return card2; + } + if (typeof hideEmpty === "function") { + hideEmpty(); + } + card2 = document.createElement("div"); + card2.className = "sa-activity sa-activity-live"; + card2.setAttribute("role", "status"); + card2.setAttribute("aria-live", "polite"); + const head = document.createElement("button"); + head.type = "button"; + head.className = "sa-activity-head"; + head.setAttribute("aria-expanded", "true"); + const spin = document.createElement("span"); + spin.className = "sa-activity-spin"; + spin.setAttribute("aria-hidden", "true"); + titleEl = document.createElement("span"); + titleEl.className = "sa-activity-title"; + titleEl.textContent = "Assistent"; + const chev = document.createElement("span"); + chev.className = "sa-activity-chev"; + chev.setAttribute("aria-hidden", "true"); + chev.textContent = "\u25BE"; + head.appendChild(spin); + head.appendChild(titleEl); + head.appendChild(chev); + head.addEventListener("click", () => { + open = !open; + card2.classList.toggle("sa-activity-collapsed", !open); + head.setAttribute("aria-expanded", open ? "true" : "false"); + }); + listEl = document.createElement("div"); + listEl.className = "sa-activity-steps"; + card2.appendChild(head); + card2.appendChild(listEl); + box.appendChild(card2); + if (typeof scrollToBottom === "function") { + scrollToBottom(); + } + return card2; + } + function renderStep(step) { + const row = document.createElement("div"); + row.className = `sa-activity-step sa-activity-${step.status || "running"}`; + row.dataset.id = step.id; + const icon = document.createElement("span"); + icon.className = "sa-activity-icon"; + icon.setAttribute("aria-hidden", "true"); + icon.textContent = STEP_ICONS[step.kind] || STEP_ICONS.think; + const body = document.createElement("div"); + body.className = "sa-activity-body"; + const label = document.createElement("div"); + label.className = "sa-activity-label"; + label.textContent = step.label || step.id; + body.appendChild(label); + if (step.detail) { + const detail = document.createElement("div"); + detail.className = "sa-activity-detail"; + detail.textContent = step.detail; + body.appendChild(detail); + } + row.appendChild(icon); + row.appendChild(body); + return row; + } + function paint() { + if (!ensureCard() || !listEl) { + return; + } + listEl.replaceChildren(...steps.map(renderStep)); + const running = steps.find((s) => s.status === "running"); + const last = steps[steps.length - 1]; + if (titleEl) { + titleEl.textContent = running ? running.label : last?.label || "Assistent"; + } + card2.classList.toggle("sa-activity-live", steps.some((s) => s.status === "running")); + card2.classList.toggle("sa-activity-done", steps.length > 0 && steps.every((s) => s.status === "done" || s.status === "skip")); + if (typeof scrollToBottom === "function") { + scrollToBottom(); + } + } + function begin(title) { + steps = []; + card2 = null; + listEl = null; + titleEl = null; + open = true; + ensureCard(); + if (titleEl && title) { + titleEl.textContent = title; + } + paint(); + } + function upsert(id, patch) { + ensureCard(); + let step = steps.find((s) => s.id === id); + if (!step) { + step = { id, kind: "think", label: id, status: "running", detail: "" }; + steps.push(step); + } + Object.assign(step, patch); + if (!step.status) { + step.status = "running"; + } + paint(); + return step; + } + function done(id, patch = {}) { + return upsert(id, { ...patch, status: "done" }); + } + function skip(id, patch = {}) { + return upsert(id, { ...patch, status: "skip" }); + } + function fail(id, patch = {}) { + return upsert(id, { ...patch, status: "error" }); + } + function finish(summary) { + steps.forEach((s) => { + if (s.status === "running") { + s.status = "done"; + } + }); + if (summary && titleEl) { + titleEl.textContent = summary; + } + paint(); + if (card2) { + card2.classList.remove("sa-activity-live"); + card2.classList.add("sa-activity-done"); + } + } + function noteModelCommands(patch) { + if (!patch || typeof patch !== "object") { + return; + } + const keys = Object.keys(patch).filter((k) => patch[k] != null && !["actions", "generate", "ask", "look_at", "vision_from", "vision_slots", "notes", "variants"].includes(k)); + if (keys.length) { + done("delta", { + kind: "delta", + label: "\u041E\u0431\u043D\u043E\u0432\u0438\u043B \u0441\u0435\u0441\u0441\u0438\u044E", + detail: keys.slice(0, 10).join(", ") + }); + } + const ask = Array.isArray(patch.ask) ? patch.ask.map(String) : patch.ask ? [String(patch.ask)] : []; + if (ask.length) { + upsert("ask", { + kind: "ask", + label: `\u0417\u0430\u043F\u0440\u043E\u0441\u0438\u043B ${ask.join(", ")}`, + detail: "\u043F\u043E\u0434\u0433\u0440\u0443\u0436\u0430\u044E \u0434\u0435\u0442\u0430\u043B\u0438\u2026", + status: "running" + }); + } + if (patch.look_at != null || patch.vision_from != null || patch.vision_slots != null) { + const slots = [].concat(patch.look_at || patch.vision_from || patch.vision_slots || []); + upsert("look", { + kind: "look", + label: "\u0421\u043C\u043E\u0442\u0440\u0438\u0442 \u043D\u0430 \u043A\u0430\u0434\u0440", + detail: slots.map(String).slice(0, 4).join(", "), + status: "running" + }); + } + if (patch.generate === true || Array.isArray(patch.actions) && patch.actions.map(String).includes("generate")) { + upsert("generate", { + kind: "generate", + label: "Generate", + detail: "\u0436\u0434\u0451\u0442 \u043F\u0430\u0439\u043F\u043B\u0430\u0439\u043D\u2026", + status: "running" + }); + } + if (Array.isArray(patch.variants) && patch.variants.length) { + upsert("variants", { + kind: "generate", + label: `\u0412\u0430\u0440\u0438\u0430\u043D\u0442\u044B \xD7${patch.variants.length}`, + status: "running" + }); + } + } + return { + begin, + upsert, + done, + skip, + fail, + finish, + noteModelCommands, + get steps() { + return steps.slice(); + } + }; + } + function attachActivity(SA2) { + SA2.createActivityController = createActivityController; + } + // src/app.js (function() { const LS_BASE = "swarm_assistent_base_url"; @@ -362,6 +1067,9 @@ let HISTORY_KEEP_TURNS = 4; let INVENTORY_PROMPT_RICH = 12; let INVENTORY_PROMPT_NAMES = 24; + let COMPRESS_AT = 0.7; + let CHARS_PER_TOKEN = 3.2; + let COMPRESS_AUTO = true; let ASPECT_TABLE = { "1:1": [1024, 1024], "4:3": [1184, 896], @@ -389,10 +1097,7 @@ inpaint: "inpaint_edit", inpaint_edit: "inpaint_edit", describe: "describe_ref", - describe_ref: "describe_ref", - card: "catalog_card", - catalog: "catalog_card", - catalog_card: "catalog_card" + describe_ref: "describe_ref" }; let WELCOME_HTML = `
Assistent \xB7 Krea 2
@@ -408,6 +1113,7 @@ /help \u2014 \u044D\u0442\u043E\u0442 \u0441\u043F\u0438\u0441\u043E\u043A /new \u2014 \u043D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442 (\u0442\u0435\u043A\u0443\u0449\u0438\u0439 \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u0441\u044F \u0432 \u0418\u0441\u0442\u043E\u0440\u0438\u044E) /history \u2014 \u043E\u0442\u043A\u0440\u044B\u0442\u044C \u0441\u043F\u0438\u0441\u043E\u043A \u0447\u0430\u0442\u043E\u0432 +/compress \u2014 \u0441\u0436\u0430\u0442\u044C \u0441\u0442\u0430\u0440\u044B\u0435 \u0445\u043E\u0434\u044B \u0432 \u0441\u0430\u043C\u043C\u0430\u0440\u0438 /debug \u2014 \u0441\u0432\u043E\u0434\u043A\u0430 UI/Exact (\u0431\u0435\u0437 LLM) /debug ask \u2014 \u0442\u043E \u0436\u0435 + \u043A\u043E\u0440\u043E\u0442\u043A\u0438\u0439 \u043E\u0442\u0432\u0435\u0442 \u043C\u043E\u0434\u0435\u043B\u0438 /why \u2014 \u0441\u0440\u0430\u0437\u0443 /debug ask @@ -418,8 +1124,7 @@ /aspect 16:9 \u2014 \u0440\u0430\u0437\u043C\u0435\u0440 \u0438\u0437 \u0442\u0430\u0431\u043B\u0438\u0446\u044B 1K /seed lock|random \u2014 \u0437\u0430\u0444\u0438\u043A\u0441\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0438\u043B\u0438 \u0440\u0430\u043D\u0434\u043E\u043C\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u0442\u044C seed /vary \u2014 \u043D\u043E\u0432\u044B\u0439 seed, \u0442\u043E\u0442 \u0436\u0435 \u043F\u0440\u043E\u043C\u043F\u0442 -/pack write|critique|compose|params|inpaint|describe|card -/civitai \u2014 \u043F\u043E\u0438\u0441\u043A LoRA (Confirm \u0432 \u0447\u0430\u0442\u0435) +/pack write|critique|compose|params|inpaint|describe /inventory \u2014 rescan \u043C\u043E\u0434\u0435\u043B\u0435\u0439 + \u043E\u0431\u043D\u043E\u0432\u0438\u0442\u044C \u0441\u043F\u0438\u0441\u043E\u043A LoRA \u0427\u0438\u043F\u0441\u044B \u043D\u0430\u0434 \u043F\u043E\u043B\u0435\u043C \u0432\u0432\u043E\u0434\u0430 \u0434\u0435\u043B\u0430\u044E\u0442 \u0442\u043E \u0436\u0435 \u0434\u043B\u044F aspect / seed / vary / Turbo\xB7RAW. @@ -428,6 +1133,7 @@ { cmd: "/help", hint: "\u0441\u043F\u0438\u0441\u043E\u043A \u043A\u043E\u043C\u0430\u043D\u0434" }, { cmd: "/new", hint: "\u043D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442" }, { cmd: "/history", hint: "\u0438\u0441\u0442\u043E\u0440\u0438\u044F \u0447\u0430\u0442\u043E\u0432" }, + { cmd: "/compress", hint: "\u0441\u0436\u0430\u0442\u044C \u0441\u0442\u0430\u0440\u044B\u0435 \u0445\u043E\u0434\u044B" }, { cmd: "/debug", hint: "\u0441\u0432\u043E\u0434\u043A\u0430 \xB7 ask = \u0441 LLM" }, { cmd: "/why", hint: "debug + \u043F\u043E\u044F\u0441\u043D\u0435\u043D\u0438\u0435 LLM" }, { cmd: "/gen", hint: "Generate \u0441\u0435\u0439\u0447\u0430\u0441" }, @@ -440,7 +1146,6 @@ { cmd: "/seed ", hint: "lock|random" }, { cmd: "/vary", hint: "\u043D\u043E\u0432\u044B\u0439 seed" }, { cmd: "/pack ", hint: "write|critique|\u2026" }, - { cmd: "/civitai ", hint: "\u0437\u0430\u043F\u0440\u043E\u0441 LoRA" }, { cmd: "/inventory", hint: "rescan \u043C\u043E\u0434\u0435\u043B\u0435\u0439" } ]; const state = { @@ -472,6 +1177,10 @@ lastSystemChars: 0, lastSystemLayers: null, lastContextChars: 0, + lastPromptEvalCount: null, + contextMemory: null, + ctxPanelOpen: false, + compressing: false, busyPhase: "idle", busyStarted: 0, gotDelta: false, @@ -486,8 +1195,7 @@ view: "chat", boardTab: "generate", personas: [], - modelCards: {}, - cardsSelection: null, + chatSession: null, pendingPersonaNote: null, chats: [], activeChatId: null, @@ -503,17 +1211,53 @@ userPrefs: [], settingsTab: "behavior", settingsPersonaId: null, - wanted: { count: 0, items: [] }, - wantedKeys: /* @__PURE__ */ new Set(), ollamaHealth: "unknown", - trainingLock: false + trainingLock: false, + activity: null }; + function getActivity() { + if (state.activity) { + return state.activity; + } + if (window.SA && typeof SA.createActivityController === "function") { + state.activity = SA.createActivityController({ + getMessagesEl: () => $2("sa_messages"), + scrollToBottom: () => scrollMessagesToBottom(), + hideEmpty: () => hideChatEmpty() + }); + } + return state.activity; + } + function activityBegin(title) { + const a = getActivity(); + if (a) { + a.begin(title || "Assistent"); + } + } + function activityStep(id, patch) { + const a = getActivity(); + if (a) { + a.upsert(id, patch); + } + } + function activityDone(id, patch) { + const a = getActivity(); + if (a) { + a.done(id, patch); + } + } + function activityFinish(summary) { + const a = getActivity(); + if (a) { + a.finish(summary); + } + } const HOP_BUDGET = 4; function isContinuationTurn(opts) { - return !!(opts && (opts.fromVisionHop || opts.fromAutoCritique || opts.fromPromptEnRetry || opts.fromEmptyPatchRetry)); + return !!(opts && (opts.fromVisionHop || opts.fromAutoCritique || opts.fromPromptEnRetry || opts.fromAskHop)); } function isMachineTurn(opts) { - return isContinuationTurn(opts) || !!(opts && (opts.fromCards || opts.fromDownload || opts.fromDebug)); + return isContinuationTurn(opts) || !!(opts && opts.fromDebug); } function resetTurnHops() { state.turnHops = []; @@ -618,9 +1362,37 @@ parking: "\u041E\u0441\u0432\u043E\u0431\u043E\u0436\u0434\u0430\u044E VRAM (park LLM)\u2026", applying: "Applying patch\u2026", silent_gen: "\u041F\u0440\u0438\u043C\u0435\u043D\u044F\u044E \u043F\u0430\u0442\u0447 \u2192 Generate\u2026", - refining: "Civitai search done \u2014 refining\u2026" + refining: "\u0423\u0442\u043E\u0447\u043D\u044F\u044E \u043E\u0442\u0432\u0435\u0442\u2026", + compressing: "\u0421\u0436\u0438\u043C\u0430\u044E \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442\u2026" }; const text = labels[state.busyPhase] || "Working\u2026"; + const phaseKind = { + thinking: "think", + streaming: "stream", + waiting: "think", + loading: "warm", + warming: "warm", + parking: "park", + encoding: "look", + generating: "generate", + applying: "merge", + silent_gen: "generate", + refining: "prep", + compressing: "compress" + }; + activityStep(`phase:${state.busyPhase}`, { + kind: phaseKind[state.busyPhase] || "think", + label: text, + status: "running" + }); + const a = getActivity(); + if (a && Array.isArray(a.steps)) { + for (const s of a.steps) { + if (s.id.startsWith("phase:") && s.id !== `phase:${state.busyPhase}` && s.status === "running") { + a.done(s.id); + } + } + } const barText = $2("sa_livebar_text"); if (barText) { barText.textContent = text; @@ -672,6 +1444,7 @@ } const elapsed = Date.now() - (state.busyStarted || Date.now()); state.busyPhase = "idle"; + activityFinish(finalStatus || "\u0413\u043E\u0442\u043E\u0432\u043E"); $2("swarm_assistent_root")?.classList.remove("sa-is-busy"); $2("sa_composer")?.classList.remove("sa-composer-busy"); const send = $2("sa_btn_send"); @@ -1102,23 +1875,6 @@ } return /###\s*JSON\s*Patch\b/i.test(t) || /JSON\s*Patch\s*:?\s*$/im.test(t); } - function userAsksGenerate(text) { - const t = String(text || "").trim(); - if (!t) { - return false; - } - if (/^(gen|generate|go|рисуй|нарисуй)([!.…\s]|$)/i.test(t)) { - return true; - } - if (/^(ещё|еще)(\s+раз)?([!.…\s]|$)/i.test(t)) { - return true; - } - const letter = "[0-9A-Za-z_\u0410-\u042F\u0430-\u044F\u0401\u0451]"; - const stem = `${letter}*`; - return cyrTokenRe( - `\u0441\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0439|\u0441\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C|\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0439|generate|\u043D\u0430\u0440\u0438\u0441\u0443\u0439|\u043F\u0435\u0440\u0435\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0439|\u043F\u0435\u0440\u0435\u0440\u0438\u0441\u0443\u0439|\u0441\u0434\u0435\u043B\u0430\u0439\\s+(\u043A\u0430\u0440\u0442\u0438\u043D\u043A${stem}|\u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D${stem}|\u0444\u043E\u0442\u043E${stem})|\u0445\u043E\u0447\u0443\\s+(\u043A\u0430\u0440\u0442\u0438\u043D\u043A${stem}|\u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D${stem}|\u0444\u043E\u0442\u043E${stem})|run\\s+generat|/gen` - ).test(t); - } function userAsksContinue(text) { const t = String(text || "").trim(); if (!t) { @@ -1162,24 +1918,6 @@ } return null; } - function synthesizePatchAfterEmptyFence(reply, userText, opts = {}) { - const wantsFrame = !!opts.userWantsGenerate || !isMachineTurn(opts) && userImpliesGenerate(userText); - if (!wantsFrame && !replyMissingJsonPatch(reply)) { - return null; - } - const prompt = extractPromptFromProse(reply) || state.lastPatch?.prompt || null; - if (!prompt) { - return null; - } - const patch = wantsFrame ? { prompt, actions: ["generate"] } : { prompt }; - if (Array.isArray(state.lastPatch?.loras) && state.lastPatch.loras.length) { - patch.loras = state.lastPatch.loras; - } - if (state.lastPatch?.aspect) { - patch.aspect = state.lastPatch.aspect; - } - return patch; - } function promptLooksKreaReady(prompt) { const t = String(prompt || "").trim(); if (t.length < 80) { @@ -1258,23 +1996,16 @@ ${patch.prompt}`; } function userAsksNoGenerate(text) { const t = String(text || "").trim(); - if (!t || userAsksGenerate(t)) { + if (!t) { return false; } if (/\b(remember|save\s+(this\s+)?(as\s+)?(the\s+)?(base\s+)?(prompt|template)|don'?t\s+generat|do\s+not\s+generat|no\s+generat|without\s+generat)\b/i.test(t)) { return true; } return cyrTokenRe( - "\u0437\u0430\u043F\u043E\u043C\u043D|\u0437\u0430\u043F\u043E\u043C\u043D\u0438|\u0437\u0430\u043F\u043E\u043C\u043D\u0438\u043C|\u0441\u043E\u0445\u0440\u0430\u043D\u0438|\u0441\u043E\u0445\u0440\u0430\u043D\u0438\u043C|\u0448\u0430\u0431\u043B\u043E\u043D|\u0431\u0430\u0437\u043E\u0432(\u044B\u0439|\u043E\u0433\u043E|\u043E\u043C\u0443|\u044B\u043C|\u0430\u044F|\u0443\u044E|\u043E\u0435)?\\s+\u043F\u0440\u043E\u043C\u043F\u0442|\u043D\u0435\\s+\u0433\u0435\u043D\u0435\u0440\u0438\u0440|\u0431\u0435\u0437\\s+\u0433\u0435\u043D\u0435\u0440\u0430\u0446|\u043D\u0435\\s+\u043D\u0430\u0434\u043E\\s+\u0433\u0435\u043D\u0435\u0440|\u0442\u043E\u043B\u044C\u043A\u043E\\s+\u0437\u0430\u043F\u043E\u043C\u043D|\u043F\u043E\u043A\u0430\\s+\u0437\u0430\u043F\u043E\u043C\u043D|\u043D\u0435\\s+\u0440\u0438\u0441\u0443\u0439|\u043D\u0435\\s+\u0437\u0430\u043F\u0443\u0441\u043A\u0430\u0439\\s+\u0433\u0435\u043D\u0435\u0440" + "\u0437\u0430\u043F\u043E\u043C\u043D|\u0437\u0430\u043F\u043E\u043C\u043D\u0438|\u0437\u0430\u043F\u043E\u043C\u043D\u0438\u043C|\u0441\u043E\u0445\u0440\u0430\u043D\u0438|\u0441\u043E\u0445\u0440\u0430\u043D\u0438\u043C|\u0448\u0430\u0431\u043B\u043E\u043D|\u0431\u0430\u0437\u043E\u0432(\u044B\u0439|\u043E\u0433\u043E|\u043E\u043C\u0443|\u044B\u043C|\u0430\u044F|\u0443\u044E|\u043E\u0435)?\\s+\u043F\u0440\u043E\u043C\u043F\u0442|\u043D\u0435\\s+\u0433\u0435\u043D\u0435\u0440\u0438\u0440[\u0430-\u044F\u0451]*|\u0431\u0435\u0437\\s+\u0433\u0435\u043D\u0435\u0440\u0430\u0446[\u0430-\u044F\u0451]*|\u043D\u0435\\s+\u043D\u0430\u0434\u043E\\s+\u0433\u0435\u043D\u0435\u0440[\u0430-\u044F\u0451]*|\u0442\u043E\u043B\u044C\u043A\u043E\\s+\u0437\u0430\u043F\u043E\u043C\u043D[\u0430-\u044F\u0451]*|\u043F\u043E\u043A\u0430\\s+\u0437\u0430\u043F\u043E\u043C\u043D[\u0430-\u044F\u0451]*|\u043D\u0435\\s+\u0440\u0438\u0441\u0443\u0439|\u043D\u0435\\s+\u0437\u0430\u043F\u0443\u0441\u043A\u0430\u0439\\s+\u0433\u0435\u043D\u0435\u0440[\u0430-\u044F\u0451]*" ).test(t); } - function userCommandsGenerate(text) { - const t = String(text || "").trim(); - if (!t || userAsksNoGenerate(t)) { - return false; - } - return userAsksGenerate(t) || userAsksContinue(t) || isSameButAspectRequest(t); - } function userAsksLook(text) { const t = String(text || "").trim(); if (!t) { @@ -1291,79 +2022,22 @@ ${patch.prompt}`; } return /(?:^|[^а-яёa-z0-9_])(посмотри|смотри|глянь)\s+(на\s+)?(результат|картинк[а-яё]*|изображен[а-яё]*|кадр|ген|реф)/i.test(t); } - function userIsChatNotFrame(text) { - const t = String(text || "").trim(); - if (!t) { - return false; - } - if (/^(ок|окей|ok|okay|ладно|хорошо|понял|ясно|спасибо|thanks)([!.…\s]*)$/i.test(t)) { - return true; - } - if (cyrTokenRe( - "\u0447\u0442\u043E\\s+\u0442\u0430\u043A\u043E\u0435|\u043A\u0430\u043A\\s+\u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442|\u0437\u0430\u0447\u0435\u043C\\s+|\u043A\u0430\u043A\u0438\u0435\\s+(\u043B\u043E\u0440|\u043C\u043E\u0434\u0435\u043B|\u0447\u0435\u043A\u043F\u043E\u0438\u043D\u0442)|\u0441\u043F\u0438\u0441\u043E\u043A\\s+\u043B\u043E\u0440|\u0433\u0434\u0435\\s+\u043D\u0430\u0441\u0442\u0440\u043E\u0439|\u0447\u0442\u043E\\s+\u0437\u043D\u0430\u0447\u0438\u0442|\u043D\u0440\u0430\u0432\u0438\u0442|\u0441\u043F\u0430\u0441\u0438\u0431\u043E|\u0431\u043B\u0430\u0433\u043E\u0434\u0430\u0440|\u043F\u043E\u0447\u0435\u043C\u0443\\s+\u0442\u0430\u043A|\u0447\u0442\u043E\\s+\u0442\u044B\\s+(\u0441\u0434\u0435\u043B\u0430\u043B|\u0438\u0437\u043C\u0435\u043D\u0438\u043B)|\u0442\u043E\u043B\u044C\u043A\u043E\\s+(\u043E\u0442\u0432\u0435\u0442\u044C|\u0441\u043A\u0430\u0436\u0438|\u043E\u0431\u044A\u044F\u0441\u043D\u0438)|\u0431\u0435\u0437\\s+(\u043A\u0430\u0434\u0440|\u0433\u0435\u043D\u0435\u0440\u0430\u0446)|\u043D\u0435\\s+\u043D\u0430\u0434\u043E\\s+\u043A\u0430\u0434\u0440" - ).test(t) && !userAsksGenerate(t) && !userAsksContinue(t)) { - return true; - } - return false; - } - function userImpliesGenerate(text) { - const t = String(text || "").trim(); - if (!t || userAsksNoGenerate(t) || userIsChatNotFrame(t)) { - return false; - } - if (userCommandsGenerate(t)) { - return true; - } - if (t.length < 8) { - return false; - } - const wantsLook = userAsksLook(t); - const wantsRedraw = cyrTokenRe("\u043F\u043E\u043F\u0440\u0430\u0432\u044C|\u0438\u0441\u043F\u0440\u0430\u0432\u044C|\u043F\u0435\u0440\u0435\u0433\u0435\u043D\u0435\u0440\u0438\u0440|\u043F\u0435\u0440\u0435\u0440\u0438\u0441\u0443\u0439|\u0443\u043B\u0443\u0447\u0448\u0438|\u043F\u0435\u0440\u0435\u0434\u0435\u043B\u0430\u0439").test(t) || /\b(fix|redo|redraw|improve)\b/i.test(t); - if (wantsLook && !wantsRedraw) { - return false; - } - if (cyrTokenRe( - "\u043D\u0430\u0440\u0438\u0441\u0443|\u0441\u0433\u0435\u043D\u0435\u0440|\u043F\u0435\u0440\u0435\u0440\u0438\u0441\u0443|\u0441\u0434\u0435\u043B\u0430\u0439\\s+(\u043A\u0430\u0440\u0442\u0438\u043D\u043A|\u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D|\u0444\u043E\u0442\u043E|\u043A\u0430\u0434\u0440)|\u0445\u043E\u0447\u0443\\s+(\u043A\u0430\u0440\u0442\u0438\u043D\u043A|\u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D|\u0444\u043E\u0442\u043E|\u0443\u0432\u0438\u0434\u0435\u0442\u044C|\u0432\u0438\u0434\u0435\u0442\u044C)|\u043F\u043E\u043A\u0430\u0436\u0438\\s+\u043A\u0430\u043A\\s+(\u043E\u043D\u0430|\u043E\u043D|\u044D\u0442\u043E)|\u0441\u0434\u0435\u043B\u0430\u0439\\s+(\u0435\u0451|\u0435\u0435|\u0435\u0433\u043E|\u043C\u043D\u0435)\\s|\u043F\u0443\u0441\u0442\u044C\\s+\u0431\u0443\u0434\u0435\u0442|\u0434\u0440\u0443\u0433\u043E\u0439\\s+(\u0440\u0430\u043A\u0443\u0440\u0441|\u0441\u0432\u0435\u0442|\u043D\u0430\u0440\u044F\u0434|\u043F\u043E\u0437\u0430)|\u043F\u043E\u043C\u0435\u043D\u044F\u0439\\s+(\u043F\u043E\u0437\u0443|\u0441\u0432\u0435\u0442|\u043E\u0434\u0435\u0436\u0434|\u0444\u043E\u043D)|\u0434\u043E\u0431\u0430\u0432\u044C\\s+(\u0441\u0432\u0435\u0442|\u0434\u0435\u0442\u0430\u043B)|\u0435\u0449\u0451\\s+\u043E\u0434\u043D|\u0435\u0449\u0435\\s+\u043E\u0434\u043D" - ).test(t)) { - return true; - } - if (/\b(draw|paint|render|make her|make him|another one|new frame)\b/i.test(t)) { - return true; - } - const isQuestion = /[??]\s*$/.test(t); - if (isQuestion) { - return cyrTokenRe("\u043D\u0430\u0440\u0438\u0441\u0443|\u0441\u0433\u0435\u043D\u0435\u0440|\u043C\u043E\u0436\u0435\u0448\u044C\\s+(\u0441\u0434\u0435\u043B\u0430\u0442\u044C|\u043D\u0430\u0440\u0438\u0441\u043E\u0432\u0430\u0442\u044C)|\u043C\u043E\u0436\u043D\u043E\\s+(\u043A\u0430\u0440\u0442\u0438\u043D\u043A|\u0441\u0433\u0435\u043D\u0435\u0440)").test(t); - } - return false; - } - function packBlocksAutoGenerate(pack) { - const p = String(pack || ""); - return p === "describe_ref" || p === "catalog_card" || p === "author_persona" || p === "debug_explain"; - } function packWantsVision(pack) { const p = String(pack || ""); return p === "critique_image" || p === "describe_ref" || p === "compose_scene" || p === "inpaint_edit"; } - function resolveTurnIntent(patch, userText, opts = {}) { - const machine = isMachineTurn(opts); - const vetoed = !machine && userAsksNoGenerate(userText); - const commanded = !!opts.userWantsGenerate || !machine && userCommandsGenerate(userText); - const implied = !machine && userImpliesGenerate(userText); - const modelAsked = Array.isArray(patch?.actions) && patch.actions.map(String).includes("generate"); - let generate; - if (vetoed || opts.fromAutoCritique) { - generate = false; - } else if (commanded) { - generate = true; - } else if (packBlocksAutoGenerate($2("sa_pack")?.value || "")) { - generate = false; - } else { - generate = modelAsked || implied; + function resolveTurnIntent2(patch, userText, opts = {}) { + const S = window.SA && window.SA.session; + if (S && typeof S.resolveTurnIntent === "function") { + return S.resolveTurnIntent(patch, userText, { vetoFn: userAsksNoGenerate }); } + const vetoed = !isMachineTurn(opts) && userAsksNoGenerate(userText); + const modelAsked = !!(patch && (patch.generate === true || Array.isArray(patch.actions) && patch.actions.map(String).includes("generate"))); + const generate = !vetoed && !opts.fromAutoCritique && modelAsked; const hasLook = !!patch && (patch.look_at != null || patch.vision_from != null || patch.vision_slots != null); - const honorLook = opts.fromAutoCritique || opts.fromVisionHop || !machine && userAsksLook(userText) || packWantsVision($2("sa_pack")?.value); - const look = !!(hasLook && !vetoed && !generate && honorLook); - return { generate, look, vetoed }; + const look = !!(hasLook && !vetoed && !generate); + const ask = Array.isArray(patch?.ask) ? patch.ask.map(String) : typeof patch?.ask === "string" && patch.ask ? [patch.ask] : []; + return { generate, look, vetoed, ask }; } function stripGenerateAction(patch) { if (!patch || typeof patch !== "object") { @@ -1398,7 +2072,7 @@ ${patch.prompt}`; return out; } function rememberLastPatch(patch) { - if (patch && typeof patch === "object" && !isCardObject2(patch)) { + if (patch && typeof patch === "object") { state.lastPatch = patch; syncBuildGenButton(); } @@ -1434,7 +2108,6 @@ ${patch.prompt}`; fix_params: "params", inpaint_edit: "inpaint", describe_ref: "describe", - catalog_card: "card", author_persona: "persona" }; const short = shortMap[pack] || pack.replace(/_/g, " ").slice(0, 12); @@ -1624,6 +2297,391 @@ ${patch.prompt}`; const turns = Math.max(1, Number(HISTORY_KEEP_TURNS) || 4); return turns * 2; } + function ctxApi() { + return window.SA?.context || null; + } + function getContextMemory() { + const C = ctxApi(); + if (C?.normalizeContextMemory) { + return C.normalizeContextMemory(state.contextMemory); + } + return state.contextMemory && typeof state.contextMemory === "object" ? state.contextMemory : { summary: "", untilCount: 0, foldedTurns: 0, at: 0, uiCollapsed: false, promptEvalCount: null }; + } + function setContextMemory(raw, { persist = true } = {}) { + const C = ctxApi(); + state.contextMemory = C?.normalizeContextMemory ? C.normalizeContextMemory(raw) : raw && typeof raw === "object" ? raw : null; + if (state.chatSession && typeof state.chatSession === "object") { + state.chatSession.context_memory = state.contextMemory?.summary ? state.contextMemory : null; + } + if (persist && !state.restoringChat) { + persistHistory(); + } + updateCtxChip(); + if (state.ctxPanelOpen) { + renderCtxPanel(); + } + } + function resetContextMemory({ persist = true } = {}) { + setContextMemory(ctxApi()?.emptyContextMemory?.() || { + summary: "", + untilCount: 0, + foldedTurns: 0, + at: 0, + uiCollapsed: false, + promptEvalCount: null + }, { persist }); + } + function historyCharsForBudget(messages) { + return (messages || []).reduce((n, m) => n + String(m?.content || "").length, 0); + } + function currentBudgetEstimate(modelMessages) { + const C = ctxApi(); + const mem = getContextMemory(); + const memChars = mem.summary ? mem.summary.length : 0; + const hist = modelMessages || assembleOutgoingMessages(); + const numCtx = Number($2("sa_num_ctx")?.value) || state.config?.assistant?.num_ctx || 16384; + const numPredict = Number(state.config?.assistant?.num_predict) || 3072; + if (!C?.estimateBudget) { + return { + used: 0, + numCtx, + level: "ok", + estimated: 0, + fromEval: false, + systemChars: state.lastSystemChars || 0, + historyChars: historyCharsForBudget(hist), + memoryChars: memChars, + threshold: Math.floor((numCtx - numPredict) * COMPRESS_AT) + }; + } + return C.estimateBudget({ + systemChars: state.lastSystemChars || 0, + historyChars: historyCharsForBudget(hist), + memoryChars: memChars, + numCtx, + numPredict, + charsPerToken: CHARS_PER_TOKEN, + compressAt: COMPRESS_AT, + promptEvalCount: state.lastPromptEvalCount ?? mem.promptEvalCount + }); + } + function assembleOutgoingMessages({ includePendingUser } = {}) { + const C = ctxApi(); + const mem = getContextMemory(); + let msgs; + if (C?.assembleModelMessages) { + msgs = C.assembleModelMessages(state.history, mem, HISTORY_KEEP_TURNS).map((m) => { + let content = String(m.content || ""); + if (m.role === "assistant") { + content = stripJsonFencesForHistory(content); + } + return { role: m.role, content: content.slice(0, 4e3) }; + }); + } else { + msgs = state.history.slice(-historyMessageLimit()).map((m) => { + let content = String(m.content || ""); + if (m.role === "assistant") { + content = stripJsonFencesForHistory(content); + } + return { role: m.role, content: content.slice(0, 4e3) }; + }); + } + if (includePendingUser) { + msgs.push({ role: "user", content: String(includePendingUser) }); + } + return msgs; + } + function buildCompressUserPrompt() { + const C = ctxApi(); + const mem = getContextMemory(); + const fold = C?.messagesToFold ? C.messagesToFold(state.history, mem, HISTORY_KEEP_TURNS) : []; + const lines = []; + if (mem.summary) { + lines.push("## Previous conversation memory"); + lines.push(mem.summary); + lines.push(""); + } + lines.push("## Dialogue chunk to fold"); + for (const m of fold) { + const role = m.role === "assistant" ? "Assistant" : "User"; + let content = String(m.content || ""); + if (m.role === "assistant") { + content = stripJsonFencesForHistory(content); + } + content = content.slice(0, 1500); + lines.push(`### ${role}`); + lines.push(content || "(empty)"); + lines.push(""); + } + lines.push("Compress the chunk into the required heading format. Merge with previous memory when present."); + return { prompt: lines.join("\n"), foldCount: fold.length, fold }; + } + function applyCompressResult(summaryText, foldCount, { uiCollapsed = false } = {}) { + const C = ctxApi(); + const prev = getContextMemory(); + const merged = C?.mergeSummary ? C.mergeSummary(prev.summary, summaryText) : String(summaryText || "").trim() || prev.summary; + const nextUntil = prev.untilCount + Math.max(0, foldCount); + setContextMemory({ + summary: merged, + untilCount: nextUntil, + foldedTurns: Math.floor(nextUntil / 2), + at: Date.now(), + uiCollapsed: uiCollapsed || prev.uiCollapsed, + promptEvalCount: state.lastPromptEvalCount + }); + } + function callOllamaOnce(payload) { + return new Promise((resolve, reject) => { + const fail = (err) => reject(new Error(String(err || "Chat failed"))); + const ok = (data) => { + if (data?.error) { + fail(data.error); + return; + } + resolve(data || {}); + }; + if (typeof makeWSRequest === "function") { + let settled = false; + makeWSRequest( + "AssistentChatWS", + payload, + (data) => { + if (settled) { + return; + } + if (data?.error) { + settled = true; + fail(data.error); + return; + } + if (data?.done || data?.reply != null) { + settled = true; + ok(data); + } + }, + 0, + (err) => { + if (settled) { + return; + } + genericRequest("AssistentChat", payload, (data) => { + settled = true; + ok(data); + }, 0, (err2) => { + settled = true; + fail(err2 || err); + }); + } + ); + return; + } + genericRequest("AssistentChat", payload, ok, 0, fail); + }); + } + async function runCompressTurn({ uiCollapsed = false, chatEpoch = state.chatEpoch } = {}) { + const C = ctxApi(); + const mem = getContextMemory(); + const fold = C?.messagesToFold ? C.messagesToFold(state.history, mem, HISTORY_KEEP_TURNS) : []; + if (!fold.length) { + return false; + } + const model = $2("sa_model")?.value; + if (!model) { + setStatus("\u0412\u044B\u0431\u0435\u0440\u0438 \u043C\u043E\u0434\u0435\u043B\u044C Ollama \u0432 \u2699"); + return false; + } + const { prompt, foldCount } = buildCompressUserPrompt(); + if (!foldCount) { + return false; + } + state.compressing = true; + updateCtxChip(); + setBusyPhase("compressing"); + setStatus("\u0421\u0436\u0438\u043C\u0430\u044E \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442\u2026"); + const persona = $2("sa_persona")?.value || "neutral"; + const baseUrl = $2("sa_base_url")?.value || "http://127.0.0.1:11434"; + const payload = { + baseUrl, + model, + pack: "compress_history", + persona, + includeBase: false, + messages: [{ role: "user", content: prompt }], + context_json: JSON.stringify({ + compress: true, + previous_memory: mem.summary || null, + fold_count: foldCount + }), + skills: [], + embed_model: $2("sa_embed_model")?.value || state.preferredEmbed || "" + }; + try { + const data = await callOllamaOnce(payload); + if (chatEpoch !== state.chatEpoch) { + return false; + } + if (data.prompt_eval_count != null) { + state.lastPromptEvalCount = Number(data.prompt_eval_count) || null; + } else if (data.raw?.prompt_eval_count != null) { + state.lastPromptEvalCount = Number(data.raw.prompt_eval_count) || null; + } + const reply = String(data.reply || "").trim(); + if (!reply) { + setStatus("\u0421\u0436\u0430\u0442\u0438\u0435: \u043F\u0443\u0441\u0442\u043E\u0439 \u043E\u0442\u0432\u0435\u0442 \u043C\u043E\u0434\u0435\u043B\u0438"); + return false; + } + applyCompressResult(reply, foldCount, { uiCollapsed }); + if (uiCollapsed) { + renderHistoryIntoUi(state.history); + } + setStatus("\u041A\u043E\u043D\u0442\u0435\u043A\u0441\u0442 \u0441\u0436\u0430\u0442"); + return true; + } catch (e) { + console.warn("Assistent compress failed", e); + setStatus(`\u0421\u0436\u0430\u0442\u0438\u0435 \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C: ${e.message || e}`); + return false; + } finally { + state.compressing = false; + updateCtxChip(); + } + } + function maybeAutoCompressBeforeSend(chatEpoch) { + if (!COMPRESS_AUTO) { + return Promise.resolve(false); + } + const C = ctxApi(); + const mem = getContextMemory(); + const msgs = assembleOutgoingMessages(); + const budget = currentBudgetEstimate(msgs); + const should = C?.shouldCompress ? C.shouldCompress(budget, mem, (state.history || []).filter((m) => m && !m.systemish).length, { + keepMessages: historyMessageLimit() + }) : false; + if (!should) { + return Promise.resolve(false); + } + return runCompressTurn({ uiCollapsed: false, chatEpoch }); + } + function formatCtxChipLabel(budget) { + const C = ctxApi(); + const used = C?.formatTokenShort ? C.formatTokenShort(budget.used) : String(budget.used || 0); + const cap = C?.formatTokenShort ? C.formatTokenShort(budget.numCtx) : String(budget.numCtx || 0); + return `${used} / ${cap}`; + } + function updateCtxChip() { + const chip = $2("sa_ctx_chip"); + if (!chip) { + return; + } + const budget = currentBudgetEstimate(); + const mem = getContextMemory(); + const label = formatCtxChipLabel(budget); + const textEl = chip.querySelector(".sa-ctx-chip-text"); + if (textEl) { + textEl.textContent = label; + } else { + chip.textContent = label; + } + chip.classList.remove("sa-ctx-ok", "sa-ctx-warn", "sa-ctx-hot", "sa-ctx-compressing", "sa-ctx-has-mem"); + if (state.compressing) { + chip.classList.add("sa-ctx-compressing"); + } else { + chip.classList.add(`sa-ctx-${budget.level || "ok"}`); + } + if (mem.summary) { + chip.classList.add("sa-ctx-has-mem"); + } + const src = budget.fromEval ? "\u0444\u0430\u043A\u0442 Ollama" : "\u043E\u0446\u0435\u043D\u043A\u0430"; + chip.title = `\u041A\u043E\u043D\u0442\u0435\u043A\u0441\u0442 \u043C\u043E\u0434\u0435\u043B\u0438 \xB7 ${src}${mem.summary ? " \xB7 \u0435\u0441\u0442\u044C \u0441\u0430\u043C\u043C\u0430\u0440\u0438" : ""}`; + const dot = chip.querySelector(".sa-ctx-dot"); + if (dot) { + dot.hidden = !mem.summary; + } + } + function toggleCtxPanel(force) { + const panel = $2("sa_ctx_panel"); + const chip = $2("sa_ctx_chip"); + if (!panel || !chip) { + return; + } + const open = force != null ? !!force : !state.ctxPanelOpen; + state.ctxPanelOpen = open; + panel.hidden = !open; + chip.setAttribute("aria-expanded", open ? "true" : "false"); + if (open) { + renderCtxPanel(); + } + } + function renderCtxPanel() { + const body = $2("sa_ctx_panel_body"); + const bar = $2("sa_ctx_bar_fill"); + const auto = $2("sa_ctx_auto"); + if (!body) { + return; + } + const budget = currentBudgetEstimate(); + const mem = getContextMemory(); + const layers = state.lastSystemLayers || {}; + const layerRows = Object.entries(layers).filter(([k]) => k !== "total").map(([k, v]) => `
${escapeHtml2(k)}${Number(v) || 0}
`).join(""); + const keep = HISTORY_KEEP_TURNS; + const uncovered = Math.max(0, (state.history || []).filter((m) => m && !m.systemish).length - (mem.untilCount || 0)); + body.innerHTML = ` +
${budget.fromEval ? "\u0422\u043E\u043A\u0435\u043D\u044B (prompt_eval)" : "\u041E\u0446\u0435\u043D\u043A\u0430 \u0442\u043E\u043A\u0435\u043D\u043E\u0432"} \xB7 \u043F\u043E\u0440\u043E\u0433 ${budget.threshold || "\u2014"}
+
system${budget.systemChars || 0}
+
history${budget.historyChars || 0}
+
memory${budget.memoryChars || 0}
+ ${layerRows ? `
system_layers
${layerRows}` : ""} +
\u041C\u043E\u0434\u0435\u043B\u044C \u0432\u0438\u0434\u0438\u0442: ${mem.summary ? `\u0441\u0430\u043C\u043C\u0430\u0440\u0438 (${mem.foldedTurns || 0} \u0445\u043E\u0434\u043E\u0432) +` : ""} \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0435 ${keep} \u0445\u043E\u0434\u043E\u0432 \xB7 \u0441\u044B\u0440\u044B\u0445 \u0432 \u043E\u043A\u043D\u0435 \u2248 ${Math.min(uncovered, historyMessageLimit())}
+ ${mem.summary ? `
${escapeHtml2(mem.summary.slice(0, 800))}${mem.summary.length > 800 ? "\u2026" : ""}
` : '
\u0421\u0430\u043C\u043C\u0430\u0440\u0438 \u0435\u0449\u0451 \u043D\u0435\u0442 \u2014 \u0441\u0442\u0430\u0440\u044B\u0435 \u0445\u043E\u0434\u044B \u043F\u0440\u043E\u0441\u0442\u043E \u043E\u0442\u0431\u0440\u0430\u0441\u044B\u0432\u0430\u044E\u0442\u0441\u044F.
'} + `; + if (bar) { + const pct = Math.max(0, Math.min(100, budget.used / (budget.numCtx || 1) * 100)); + bar.style.width = `${pct}%`; + bar.dataset.level = budget.level || "ok"; + } + if (auto) { + auto.checked = !!COMPRESS_AUTO; + } + updateCtxChip(); + } + async function compressNowFromUi() { + if (state.busy || state.generating || state.compressing) { + setStatus("\u0417\u0430\u043D\u044F\u0442\u043E \u2014 \u0434\u043E\u0436\u0434\u0438\u0441\u044C \u043A\u043E\u043D\u0446\u0430 \u043E\u0442\u0432\u0435\u0442\u0430"); + return; + } + const model = $2("sa_model")?.value; + if (!model) { + setStatus("\u0412\u044B\u0431\u0435\u0440\u0438 \u043C\u043E\u0434\u0435\u043B\u044C Ollama \u0432 \u2699"); + return; + } + state.busy = true; + setInterruptVisible(true); + startBusyUi("compressing"); + const epoch = state.chatEpoch; + try { + const ok = await runCompressTurn({ uiCollapsed: true, chatEpoch: epoch }); + if (!ok) { + setStatus("\u041D\u0435\u0447\u0435\u0433\u043E \u0441\u0436\u0438\u043C\u0430\u0442\u044C (\u0445\u0432\u043E\u0441\u0442 \u2264 keep)"); + } + } finally { + if (epoch === state.chatEpoch) { + state.busy = false; + setInterruptVisible(state.generating); + stopBusyUi(getContextMemory().summary ? "\u041A\u043E\u043D\u0442\u0435\u043A\u0441\u0442 \u0441\u0436\u0430\u0442" : "\u0413\u043E\u0442\u043E\u0432\u043E"); + } + updateCtxChip(); + if (state.ctxPanelOpen) { + renderCtxPanel(); + } + } + } + function resetCompressionFromUi() { + resetContextMemory(); + renderHistoryIntoUi(state.history); + setStatus("\u0421\u0436\u0430\u0442\u0438\u0435 \u0441\u0431\u0440\u043E\u0448\u0435\u043D\u043E \u2014 \u043C\u043E\u0434\u0435\u043B\u044C \u0441\u043D\u043E\u0432\u0430 \u0432\u0438\u0434\u0438\u0442 \u0442\u043E\u043B\u044C\u043A\u043E last-K"); + if (state.ctxPanelOpen) { + renderCtxPanel(); + } + } function flashImagePane(slotId) { const el = document.querySelector(`.sa-slot[data-id="${slotId || state.selectedSlotId}"]`); if (!el) { @@ -1978,8 +3036,8 @@ ${patch.prompt}`; if (silent) { const note = document.createElement("div"); note.className = "sa-patch-actions sa-patch-silent sa-patch-current"; - const willGen = Array.isArray(patch.actions) && patch.actions.map(String).includes("generate") || !!state.pendingSilentGen; - note.textContent = willGen ? "\u041F\u0440\u0438\u043C\u0435\u043D\u0435\u043D\u043E \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \xB7 Generate\u2026" : "\u041F\u0440\u0438\u043C\u0435\u043D\u0435\u043D\u043E \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438"; + const willGen = Array.isArray(patch.actions) && patch.actions.map(String).includes("generate") || !!patch.generate || !!state.pendingSilentGen; + note.textContent = willGen ? "\u0412 \u0441\u0435\u0441\u0441\u0438\u044E \xB7 Generate\u2026" : "\u0412 \u0441\u0435\u0441\u0441\u0438\u044E"; host.appendChild(note); return; } @@ -1998,17 +3056,43 @@ ${patch.prompt}`; btn.addEventListener("click", () => applyPatch(patch, which)); actions.appendChild(btn); } + const toSession = document.createElement("button"); + toSession.type = "button"; + toSession.className = "basic-button"; + toSession.textContent = "\u0412 \u0441\u0435\u0441\u0441\u0438\u044E"; + toSession.addEventListener("click", async () => { + const S = window.SA && window.SA.session; + if (S) { + state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), patch); + } + await pushSessionToSwarm(state.chatSession); + rememberLastPatch(patch); + try { + const chat = findChat(state.activeChatId); + if (chat) { + chat.params = snapshotChatParams(); + persistChatsStore(); + } + } catch (e) { + } + setStatus("\u041F\u0430\u0442\u0447 \u0432 \u0441\u0435\u0441\u0441\u0438\u0438"); + }); + actions.appendChild(toSession); const genBtn = document.createElement("button"); genBtn.type = "button"; genBtn.className = "basic-button sa-btn-gen"; - genBtn.textContent = "\u041F\u0440\u0438\u043C\u0435\u043D\u0438\u0442\u044C + Generate"; + genBtn.textContent = "\u0421\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C"; genBtn.addEventListener("click", async () => { if (isGenerateUnavailable()) { return; } startBusyUi("silent_gen"); - await applyPatch(patch, "all"); - await runGenerateFromPatch({ ...patch, actions: ["generate"] }, { force: true }); + const S = window.SA && window.SA.session; + if (S) { + state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), patch); + } + await pushSessionToSwarm(state.chatSession); + await runGenerateFromPatch({ ...patch, actions: ["generate"] }, { force: true, fromSession: true }); }); actions.appendChild(genBtn); host.appendChild(actions); @@ -2019,22 +3103,19 @@ ${patch.prompt}`; setStatus("\u0417\u0430\u043D\u044F\u0442\u043E \u2014 \u043F\u043E\u0434\u043E\u0436\u0434\u0438 \u0438\u043B\u0438 \u043D\u0430\u0436\u043C\u0438 \u0421\u0442\u043E\u043F"); return; } - if (isGenerateUnavailable()) { + if (typeof isGenerateUnavailable === "function" && isGenerateUnavailable()) { setStatus("Generate \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u2014 \u0434\u043E\u0436\u0434\u0438\u0441\u044C SwarmUI"); return; } - const patch = state.lastPatch; - if (patch) { - startBusyUi("silent_gen"); - setStatus("\u0421\u043E\u0431\u0438\u0440\u0430\u044E \u043F\u0430\u0442\u0447 \u2192 Generate\u2026"); - await applyPatch(patch, "all"); - syncLiveParamsBar(); - await runGenerateFromPatch({ ...patch, actions: ["generate"] }, { force: true }); - return; + pullLiveIntoSession(); + const S = window.SA && window.SA.session; + if (state.lastPatch && S) { + state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), state.lastPatch); } - startBusyUi("generating"); - setStatus("Generate \u0441 \u0442\u0435\u043A\u0443\u0449\u0438\u043C \u043F\u0440\u043E\u043C\u043F\u0442\u043E\u043C\u2026"); - await runGenerateFromPatch({ actions: ["generate"] }, { force: true }); + if (typeof startBusyUi === "function") startBusyUi(state.lastPatch ? "silent_gen" : "generating"); + setStatus(state.lastPatch ? "\u0421\u0435\u0441\u0441\u0438\u044F \u2192 Generate\u2026" : "Generate \u0441 \u0442\u0435\u043A\u0443\u0449\u0435\u0439 \u0441\u0435\u0441\u0441\u0438\u0435\u0439\u2026"); + await pushSessionToSwarm(state.chatSession); + await runGenerateFromPatch({ actions: ["generate"] }, { force: true, fromSession: true }); } function renderBoard() { const board = $2("sa_board"); @@ -2495,7 +3576,7 @@ ${patch.prompt}`; const t = String(u?.content || "").replace(/\s+/g, " ").trim(); return t ? t.slice(0, 52) : "\u041D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442"; } - function snapshotChatParams() { + function readLiveGenFields() { let loras = []; try { if (typeof loraHelper !== "undefined" && loraHelper && Array.isArray(loraHelper.selected)) { @@ -2506,17 +3587,19 @@ ${patch.prompt}`; } } catch (e) { } - let lastPatch = null; + let checkpoint = null; try { - lastPatch = state.lastPatch ? JSON.parse(JSON.stringify(state.lastPatch)) : null; + if (typeof resolveCurrentCheckpoint === "function") { + const m = resolveCurrentCheckpoint(); + if (m && (m.name || m.title)) { + checkpoint = { + name: m.name || m.title || null, + architecture: m.architecture || m.compat_class || m.class || null, + title: m.title || null + }; + } + } } catch (e) { - lastPatch = null; - } - let sessionExact = {}; - try { - sessionExact = state.sessionExact && typeof state.sessionExact === "object" ? JSON.parse(JSON.stringify(state.sessionExact)) : {}; - } catch (e) { - sessionExact = {}; } return { prompt: val("alt_prompt_textbox") || val("input_prompt") || "", @@ -2531,110 +3614,108 @@ ${patch.prompt}`; scheduler: val("input_scheduler") || null, batch: parseInt(val("input_images") || val("input_batchsize") || "0", 10) || null, loras, - persona: $2("sa_persona")?.value || "neutral", - pack: $2("sa_pack")?.value || defaultPackId(), - sessionExact, - lastPatch, + checkpoint + }; + } + function boardSnapshotForSession() { + if (typeof ensureBoard === "function") ensureBoard(); + return { + slots: (state.slots || []).map((s) => ({ + id: s.id, + type: s.type, + label: s.label, + src: s.src || null, + attach: !!s.attach, + note: s.note || null + })), + selectedSlotId: state.selectedSlotId || "ref1", genResults: Array.isArray(state.genResults) ? state.genResults.map((r) => ({ id: r.id, label: r.label, src: r.src || null, patch: r.patch || null })) : [], - selectedGenResultId: state.selectedGenResultId || null + selectedGenResultId: state.selectedGenResultId || null, + refSeq: state.refSeq || 1 }; } + function pullLiveIntoSession() { + const S = window.SA && window.SA.session; + if (!S || typeof S.snapshotFromLive !== "function") return state.chatSession; + state.chatSession = S.snapshotFromLive({ + genFields: readLiveGenFields(), + board: boardSnapshotForSession(), + persona: $2("sa_persona")?.value || "neutral", + pack: $2("sa_pack")?.value || "ordinary", + context_memory: getContextMemory() + }); + return state.chatSession; + } + async function pushSessionToSwarm(session) { + const S = window.SA && window.SA.session; + const sess = session || state.chatSession || S && S.emptySession && S.emptySession(); + if (!sess || !sess.gen) return; + if (typeof applyPatch === "function") await applyPatch({ ...sess.gen }, "all"); + try { + const name = sess.gen.checkpoint?.name || (typeof sess.gen.checkpoint === "string" ? sess.gen.checkpoint : null); + if (name && typeof currentModelHelper !== "undefined" && currentModelHelper?.setModel) { + currentModelHelper.setModel(name); + } + } catch (e) { + } + if (sess.pack && typeof setPackValue === "function") setPackValue(sess.pack, { flash: false }); + if (sess.persona && typeof applyPersonaForChat === "function") { + await applyPersonaForChat(sess.persona, { quiet: true }); + } + state.chatSession = sess; + } + function snapshotChatParams() { + const S = window.SA && window.SA.session; + const session = pullLiveIntoSession(); + if (S && typeof S.toPersistParams === "function") return S.toPersistParams(session); + return session || {}; + } async function restoreChatParams(params) { state.restoringChat = true; try { state.sessionExact = {}; state.lastPatch = null; state.lastUserParamIntent = false; + const S = window.SA && window.SA.session; + state.chatSession = S && typeof S.sessionFromLegacyParams === "function" ? S.sessionFromLegacyParams(params) : params && params.gen ? params : S && S.emptySession ? S.emptySession() : { gen: {}, board: {} }; if (!params || typeof params !== "object") { - clearGenResults(); - syncBuildGenButton(); - syncLiveParamsBar(); - syncModeBadge(); - renderBoard(); + if (typeof clearGenResults === "function") clearGenResults(); + if (typeof renderBoard === "function") renderBoard(); + setContextMemory(null, { persist: false }); return { restored: false }; } - const promptBox = document.getElementById("alt_prompt_textbox") || document.getElementById("input_prompt"); - if (promptBox) { - promptBox.value = params.prompt != null ? String(params.prompt) : ""; - promptBox.dispatchEvent(new Event("input", { bubbles: true })); - promptBox.dispatchEvent(new Event("change", { bubbles: true })); + await pushSessionToSwarm(state.chatSession); + const board = state.chatSession.board || {}; + if (Array.isArray(board.slots) && board.slots.length) { + state.slots = board.slots.map((s) => ({ ...s })); + if (board.selectedSlotId) state.selectedSlotId = board.selectedSlotId; + if (board.refSeq) state.refSeq = board.refSeq; } - setVal("input_negativeprompt", params.negative != null ? String(params.negative) : ""); - if (document.getElementById("alt_negativeprompt_textbox")) { - setVal("alt_negativeprompt_textbox", params.negative != null ? String(params.negative) : ""); - } - if (params.width != null) { - setVal("input_width", String(params.width)); - } - if (params.height != null) { - setVal("input_height", String(params.height)); - } - if (params.steps != null) { - setVal("input_steps", String(params.steps)); - } - if (params.cfg != null) { - if (document.getElementById("input_cfgscale")) { - setVal("input_cfgscale", String(params.cfg)); - } else { - setVal("input_cfg", String(params.cfg)); - } - } - if (params.sigma_shift != null) { - setVal("input_sigmashift", String(params.sigma_shift)); - } - if (params.seed != null && params.seed !== "") { - setVal("input_seed", String(params.seed)); - } - if (params.sampler) { - setVal("input_sampler", String(params.sampler)); - } - if (params.scheduler) { - setVal("input_scheduler", String(params.scheduler)); - } - if (params.batch != null) { - if (document.getElementById("input_images")) { - setVal("input_images", String(params.batch)); - } else if (document.getElementById("input_batchsize")) { - setVal("input_batchsize", String(params.batch)); - } - } - const loras = Array.isArray(params.loras) ? params.loras : []; - await applyPatch({ loras }, "loras"); - if (params.pack) { - setPackValue(params.pack, { flash: false }); - } - if (params.persona) { - await applyPersonaForChat(params.persona, { quiet: true }); - } - state.sessionExact = params.sessionExact && typeof params.sessionExact === "object" ? { ...params.sessionExact } : {}; - state.lastPatch = params.lastPatch || null; - if (Array.isArray(params.genResults) && params.genResults.length) { - state.genResults = params.genResults.map((r, i) => ({ - id: r.id || `var${i + 1}`, - label: r.label || `\u0412\u0430\u0440\u0438\u0430\u043D\u0442 ${i + 1}`, + if (Array.isArray(board.genResults) && board.genResults.length) { + state.genResults = board.genResults.map((r, i) => ({ + id: r.id || "var" + (i + 1), + label: r.label || "\u0412\u0430\u0440\u0438\u0430\u043D\u0442 " + (i + 1), src: r.src || null, patch: r.patch || null })); - state.selectedGenResultId = params.selectedGenResultId || state.genResults.find((r) => r.src)?.id || state.genResults[0]?.id || null; - const selected = state.genResults.find((r) => r.id === state.selectedGenResultId); - const gen = generateSlot(); - if (gen && selected?.src) { - gen.src = selected.src; - } - } else { + state.selectedGenResultId = board.selectedGenResultId || state.genResults.find((x) => x.src)?.id || state.genResults[0]?.id || null; + const selected = state.genResults.find((x) => x.id === state.selectedGenResultId); + const gen = typeof generateSlot === "function" ? generateSlot() : null; + if (gen && selected?.src) gen.src = selected.src; + } else if (typeof clearGenResults === "function") { clearGenResults(); } - syncBuildGenButton(); - syncLiveParamsBar(); - syncModeBadge(); - renderLoraChips(); - syncChipHighlight(); - renderBoard(); + if (typeof syncBuildGenButton === "function") syncBuildGenButton(); + if (typeof syncLiveParamsBar === "function") syncLiveParamsBar(); + if (typeof syncModeBadge === "function") syncModeBadge(); + if (typeof renderLoraChips === "function") renderLoraChips(); + if (typeof renderBoard === "function") renderBoard(); + setContextMemory(state.chatSession?.context_memory || null); return { restored: true }; } finally { state.restoringChat = false; @@ -2812,9 +3893,36 @@ ${patch.prompt}`; const list = slimHistoryMessages(messages); if (!list.length) { resetMessagesUi(); + updateCtxChip(); return; } - for (const m of list) { + const mem = getContextMemory(); + let start = 0; + if (mem.uiCollapsed && mem.summary && mem.untilCount > 0) { + const folded = list.slice(0, Math.min(mem.untilCount, list.length)); + start = folded.length; + const details = document.createElement("details"); + details.className = "sa-msg sa-msg-compress"; + const summary = document.createElement("summary"); + summary.textContent = `\u0421\u0436\u0430\u0442\u044B\u0439 \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442 \xB7 ${mem.foldedTurns || Math.floor(folded.length / 2)} \u0445\u043E\u0434\u043E\u0432`; + details.appendChild(summary); + const body = document.createElement("div"); + body.className = "sa-msg-compress-body"; + const pre = document.createElement("pre"); + pre.className = "sa-ctx-summary"; + pre.textContent = mem.summary; + body.appendChild(pre); + for (const m of folded) { + const row = document.createElement("div"); + row.className = `sa-msg-compress-row sa-msg-compress-${m.role}`; + row.textContent = `${m.role === "assistant" ? "\u0410\u0441\u0441\u0438\u0441\u0442\u0435\u043D\u0442" : "\u0412\u044B"}: ${String(m.content || "").slice(0, 500)}`; + body.appendChild(row); + } + details.appendChild(body); + box.appendChild(details); + } + for (let i = start; i < list.length; i++) { + const m = list[i]; if (m.role === "user") { appendMessage("user", m.content, null, null, { historical: true }); } else { @@ -2825,6 +3933,7 @@ ${patch.prompt}`; }); } } + updateCtxChip(); } function updateSessionLabel() { const el = $2("sa_session_label"); @@ -2976,6 +4085,7 @@ ${patch.prompt}`; } state.streamEl = null; } + resetContextMemory({ persist: false }); syncBuildGenButton(); resetMessagesUi(); persistChatsStore(); @@ -2983,6 +4093,7 @@ ${patch.prompt}`; renderBoard(); syncHistoryBadge(); renderChatsList(); + updateCtxChip(); setStatus("\u041D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442 \u2014 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B Generate \u043A\u0430\u043A \u0441\u0435\u0439\u0447\u0430\u0441"); maybeWelcome(); } @@ -3030,6 +4141,7 @@ ${patch.prompt}`; } state.streamEl = null; } + setContextMemory(chat.params?.context_memory || null, { persist: false }); renderHistoryIntoUi(state.history); const result = await restoreChatParams(chat.params); updateSessionLabel(); @@ -3122,11 +4234,13 @@ ${patch.prompt}`; clearGenResults(); syncBuildGenButton(); clearPersistedHistory(); + resetContextMemory({ persist: false }); resetMessagesUi('
\u0427\u0430\u0442 \u043E\u0447\u0438\u0449\u0435\u043D
\u0421\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F \u0441\u0431\u0440\u043E\u0448\u0435\u043D\u044B. \u041F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B Generate \u043D\u0430 \u043C\u0435\u0441\u0442\u0435. + \u2014 \u043D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442 \u0432 \u0418\u0441\u0442\u043E\u0440\u0438\u044E, \u0418\u0441\u0442\u043E\u0440\u0438\u044F \u2014 \u043F\u0440\u043E\u0448\u043B\u044B\u0435 \u0434\u0438\u0430\u043B\u043E\u0433\u0438.
'); setStatus("\u0427\u0430\u0442 \u043E\u0447\u0438\u0449\u0435\u043D"); updateSessionLabel(); syncHistoryBadge(); renderBoard(); + updateCtxChip(); } function hideSlashMenu() { const menu = $2("sa_slash_menu"); @@ -3228,6 +4342,7 @@ ${patch.prompt}`; } if (state.settingsTab === "more") { fillKnobsFromConfig(data); + updateCtxChip(); } } }); @@ -3407,213 +4522,36 @@ ${patch.prompt}`; return s; } function collectLiveContext() { - const inv = state.inventory || {}; - const initCtx = readInitContext(); - const ctx = { - architecture_ok: isKreaSelected(), - checkpoint: null, - prompt: slimPromptForContext(val("alt_prompt_textbox") || val("input_prompt") || ""), - negative: slimPromptForContext(val("input_negativeprompt") || val("alt_negativeprompt_textbox") || ""), - width: parseInt(val("input_width") || "0", 10) || null, - height: parseInt(val("input_height") || "0", 10) || null, - steps: parseInt(val("input_steps") || "0", 10) || null, - cfg: parseFloat(val("input_cfgscale") || val("input_cfg") || "") || null, - sigma_shift: parseFloat(val("input_sigmashift") || "") || null, - seed: val("input_seed") || null, - sampler: val("input_sampler") || val("input_samplerate") || null, - scheduler: val("input_scheduler") || null, - batch: parseInt(val("input_images") || val("input_batchsize") || "0", 10) || null, - prompt_image_count: countPromptImages(), - selected_loras: [], - available_loras: [], - available_checkpoints: slimInventoryCheckpoints(inv.checkpoints || [], 8), - wildcards: (inv.wildcards || []).map((w) => w.name || w).slice(0, 20), - inventory_at: inv.inventory_at || null, - has_vision_image: visionReadySlots().length > 0, - image_slots: slotCatalog(), - attached_slot_ids: attachableSlots().map((s) => s.id), - gen_results: (state.genResults || []).map((r) => ({ - id: r.id, - label: r.label, - has_image: !!r.src, - selected: r.id === state.selectedGenResultId - })), - selected_gen_result: state.selectedGenResultId || null, - has_civitai_key: !!inv.has_civitai_key, + pullLiveIntoSession(); + const S = window.SA && window.SA.session; + let initCtx = {}; + try { + if (typeof readInitContext === "function") initCtx = readInitContext(); + } catch (e) { + } + const extra = { + prompt_image_count: typeof countPromptImages === "function" ? countPromptImages() : 0, + has_vision_image: typeof visionReadySlots === "function" ? visionReadySlots().length > 0 : false, + image_slots: typeof slotCatalog === "function" ? slotCatalog() : [], + attached_slot_ids: typeof attachableSlots === "function" ? attachableSlots().map((s) => s.id) : [], auto_apply: !!$2("sa_auto_apply")?.checked, - auto_generate: !!$2("sa_auto_generate")?.checked, - persona: $2("sa_persona")?.value || localStorage.getItem(LS_PERSONA) || "neutral", - model_cards: [], - user_prefs_count: 0, + auto_generate: true, ...initCtx }; - { - const slim = slimInventoryLoras(inv.loras || [], INVENTORY_PROMPT_NAMES); - ctx.available_loras = slim; - if ((inv.loras || []).length > slim.length) { - ctx.available_loras_truncated = true; - ctx.available_loras_total = (inv.loras || []).length; + if (S && typeof S.compactContext === "function") { + const ctx = S.compactContext(state.chatSession, { + architecture_ok: typeof isKreaSelected === "function" ? isKreaSelected() : true, + promptMax: typeof CONTEXT_PROMPT_MAX !== "undefined" ? CONTEXT_PROMPT_MAX : 2e3, + extra + }); + const block = ctxApi()?.conversationMemoryBlock?.(getContextMemory()); + if (block) { + ctx.conversation_memory = block; } + return ctx; } - try { - const model = resolveCurrentCheckpoint(); - if (model.name || model.architecture) { - ctx.checkpoint = { - name: model.name || model.title || null, - architecture: model.architecture || model.compat_class || model.class || null, - title: model.title || null - }; - } - } catch (e) { - } - try { - if (typeof loraHelper !== "undefined" && loraHelper && Array.isArray(loraHelper.selected)) { - const byName = /* @__PURE__ */ new Map(); - for (const l of inv.loras || []) { - if (l?.name) { - byName.set(String(l.name).toLowerCase(), l); - } - } - ctx.selected_loras = loraHelper.selected.map((l) => { - const name = l.name || l; - const invRow = byName.get(String(name).toLowerCase()) || {}; - const out = { - name, - weight: loraHelper.loraWeightPref && loraHelper.loraWeightPref[name] || invRow.default_weight || 1 - }; - if (invRow.trigger_phrase) { - out.trigger_phrase = invRow.trigger_phrase; - } - if (Array.isArray(invRow.triggers) && invRow.triggers.length) { - out.triggers = invRow.triggers.slice(0, 8); - } - if (invRow.blurb) { - out.blurb = invRow.blurb; - } - return out; - }); - } - } catch (e) { - } - const cardKeys = []; - const seenCard = /* @__PURE__ */ new Set(); - const addKey = (kind, name) => { - if (!kind || !name) { - return; - } - const key = `${kind}:${name}`; - if (seenCard.has(key)) { - return; - } - seenCard.add(key); - cardKeys.push({ kind, name }); - }; - if (ctx.checkpoint?.name) { - addKey("checkpoint", ctx.checkpoint.name); - } - for (const l of ctx.selected_loras || []) { - if (l?.name) { - addKey("lora", l.name); - } - } - for (const k of cardKeys) { - const cached = state.modelCards[`${k.kind}:${k.name}`]; - if (!cached) { - continue; - } - const slim = slimCardForContext(cached); - if (!slim) { - continue; - } - if (k.kind === "lora") { - const sel = (ctx.selected_loras || []).find( - (l) => String(l.name || "").toLowerCase() === String(k.name).toLowerCase() - ); - const inventoryRich = !!(sel && (sel.triggers?.length || sel.trigger_phrase || sel.blurb)); - const cardExtra = !!(slim.when || slim.avoid || slim.prompt_hint || slim.notes); - if (inventoryRich && !cardExtra) { - continue; - } - } - ctx.model_cards.push(slim); - } - if (!ctx.available_loras.length) { - try { - const models = typeof allModels !== "undefined" && allModels || typeof model_list !== "undefined" && model_list || []; - const list = Array.isArray(models) ? models : Object.values(models || {}); - for (const m of list) { - if (!m) { - continue; - } - const folder = `${m.folder || m.path || ""}`; - const isLora = /lora/i.test(m.category || m.type || "") || m.name && String(m.name).toLowerCase().includes("lora"); - const inLoraFolder = /lora/i.test(folder); - if (!(isLora || inLoraFolder)) { - continue; - } - ctx.available_loras.push({ - name: m.name || m.title, - title: m.title || m.name, - trigger_phrase: m.trigger_phrase || m.trigger || m.metadata && (m.metadata.trigger_phrase || m.metadata.trigger) || null, - architecture: m.architecture || null - }); - } - if (ctx.available_loras.length > INVENTORY_PROMPT_NAMES) { - ctx.available_loras = ctx.available_loras.slice(0, INVENTORY_PROMPT_NAMES); - } - } catch (e) { - } - } - try { - const model = ctx.checkpoint || {}; - const blob = `${model.name || ""} ${model.title || ""}`.toLowerCase(); - const hasRaw = /\braw\b/.test(blob); - const hasTurbo = /\bturbo\b/.test(blob); - const profile = hasRaw && !hasTurbo ? "raw" : "turbo"; - ctx.krea_profile = profile; - const defaults = mergedGenerationDefaults(profile); - const rec = { - steps: defaults.steps ?? 8, - cfg: defaults.cfg ?? 1, - sigma_shift: defaults.sigma_shift ?? 1.15 - }; - if (defaults.aspect) { - rec.aspect = defaults.aspect; - } - const liveDiffers = ctx.steps != null && ctx.steps !== rec.steps || ctx.cfg != null && ctx.cfg !== rec.cfg || ctx.sigma_shift != null && ctx.sigma_shift !== rec.sigma_shift; - if (liveDiffers) { - ctx.recommended_params = rec; - } - } catch (e) { - ctx.krea_profile = "turbo"; - } - ctx.session_exact = state.sessionExact && Object.keys(state.sessionExact).length ? { ...state.sessionExact } : void 0; - if (!ctx.session_exact) { - delete ctx.session_exact; - } - return ctx; - } - function slimCardForContext(card) { - if (!card || typeof card !== "object") { - return null; - } - const out = { - kind: card.kind || null, - name: card.name || null, - triggers: Array.isArray(card.triggers) ? card.triggers.slice(0, 8) : void 0, - weight: card.weight != null ? card.weight : void 0, - when: card.when ? String(card.when).slice(0, 160) : void 0, - avoid: card.avoid ? String(card.avoid).slice(0, 120) : void 0, - prompt_hint: card.prompt_hint ? String(card.prompt_hint).slice(0, 160) : void 0, - notes: card.notes ? String(card.notes).slice(0, 200) : void 0 - }; - const clean = {}; - for (const [k, v] of Object.entries(out)) { - if (v != null && v !== "") { - clean[k] = v; - } - } - return clean; + const g = state.chatSession && state.chatSession.gen || {}; + return { session: true, prompt: g.prompt || "", negative: g.negative || "", ...extra }; } function slimInventoryLoras(list, limit) { const selected = /* @__PURE__ */ new Set(); @@ -3726,46 +4664,6 @@ ${patch.prompt}`; next.unshift(v.slice(0, 80)); return next.slice(0, max); } - function isCardObject2(obj) { - if (window.SA && typeof SA.isCardObject === "function") { - return SA.isCardObject(obj); - } - if (!obj || typeof obj !== "object") { - return false; - } - const cardish = !!(obj.kind || obj.triggers || obj.when || obj.prompt_hint); - const genish = !!(obj.prompt != null || obj.loras || obj.actions || obj.width || obj.height || obj.steps != null || obj.cfg != null || obj.aspect || obj.seed != null || obj.search_query || obj.civitai_query || obj.look_at || obj.controls); - if (cardish && !genish && (obj.name || obj.triggers || obj.when)) { - return true; - } - return !!(obj.kind && obj.name && (obj.triggers || obj.when || obj.prompt_hint || obj.notes != null)); - } - function extractCardJson(text) { - if (!text) { - return null; - } - const re = /```(?:json)?\s*([\s\S]*?)```/gi; - let match; - let last = null; - while ((match = re.exec(text)) !== null) { - try { - const obj = JSON.parse(match[1].trim()); - if (isCardObject2(obj)) { - last = obj; - } - } catch (e) { - } - } - if (last) { - return last; - } - try { - const obj = JSON.parse(text.trim()); - return isCardObject2(obj) ? obj : null; - } catch (e) { - return null; - } - } function extractPatch2(text) { if (window.SA && typeof SA.extractPatch === "function") { return SA.extractPatch(text); @@ -4519,8 +5417,19 @@ ${patch.prompt}`; return true; } async function runGenerateFromPatch(patch, opts = {}) { + if (typeof pullLiveIntoSession === "function") pullLiveIntoSession(); + if (state.chatSession && state.chatSession.gen && (opts.fromSession || opts.force)) { + const fromSess = { ...state.chatSession.gen, generate: true, actions: ["generate"] }; + if (patch && typeof patch === "object") { + for (const k of Object.keys(patch)) { + if (patch[k] != null) fromSess[k] = patch[k]; + } + } + patch = fromSess; + if (typeof pushSessionToSwarm === "function") await pushSessionToSwarm(state.chatSession); + } const force = !!opts.force; - if (!force && !$2("sa_auto_generate")?.checked || !patchHasGenTrigger(patch)) { + if (!force && false || !patchHasGenTrigger(patch)) { return null; } const variantItems = normalizeVariantList(patch); @@ -4806,9 +5715,6 @@ ${patch.prompt}`; const silent = !!(meta && meta.silentPatch); mountPatchBlock(div, finalPatch, { silent }); } - if (civitaiResults && civitaiResults.length) { - div.appendChild(buildCivitaiCards(civitaiResults)); - } if (role === "assistant" && !(meta && meta.historical)) { mountCurateButtons(div, meta); } @@ -4846,7 +5752,7 @@ ${patch.prompt}`; while ((match = re.exec(t)) !== null) { try { const obj = JSON.parse(match[1].trim()); - const terminal = window.SA && typeof SA.isTerminalStreamPatch === "function" ? SA.isTerminalStreamPatch(obj) : isPatchObject(obj) || isCardObject2(obj); + const terminal = window.SA && typeof SA.isTerminalStreamPatch === "function" ? SA.isTerminalStreamPatch(obj) : typeof isPatchObject === "function" && isPatchObject(obj); if (terminal) { return true; } @@ -4863,7 +5769,7 @@ ${patch.prompt}`; while ((match = re.exec(t)) !== null) { try { const obj = JSON.parse(match[1].trim()); - const terminal = window.SA && typeof SA.isTerminalStreamPatch === "function" ? SA.isTerminalStreamPatch(obj) : isPatchObject(obj) || isCardObject2(obj); + const terminal = window.SA && typeof SA.isTerminalStreamPatch === "function" ? SA.isTerminalStreamPatch(obj) : typeof isPatchObject === "function" && isPatchObject(obj); if (terminal) { lastEnd = match.index + match[0].length; } @@ -4912,11 +5818,10 @@ ${patch.prompt}`; } el.classList.remove("sa-streaming", "sa-typing"); mountAssistantMeta(el, meta || void 0); - const card = extractCardJson(fullReply); const { prose, patch } = extractPatch2(fullReply); setAssistantBody(el, prose || fullReply || ""); el.querySelectorAll(".sa-patch, .sa-civitai-list").forEach((n) => n.remove()); - if (patch && !isCardObject2(patch) && !(card && !patch.prompt && !patch.actions && !patch.loras)) { + if (patch && !(card && !patch.prompt && !patch.actions && !patch.loras)) { const silent = !!(meta && meta.silentPatch) || !!state.pendingSilentGen; mountPatchBlock(el, patch, { silent }); } else if (card) { @@ -4927,9 +5832,6 @@ ${patch.prompt}`; wrap.appendChild(pre); el.appendChild(wrap); } - if (civitaiResults && civitaiResults.length) { - el.appendChild(buildCivitaiCards(civitaiResults)); - } if (!(meta && meta.historical)) { mountCurateButtons(el, meta); } @@ -4998,134 +5900,6 @@ ${patch.prompt}`; function isTrainingLocked() { return !!state.trainingLock || document.getElementById("swarm_assistent_root")?.classList.contains("sa-root-training-lock"); } - function buildCivitaiCards(results) { - const list = document.createElement("div"); - list.className = "sa-civitai-list"; - for (const r of results) { - const card = document.createElement("div"); - card.className = "sa-civitai-card" + (r.already_installed ? " sa-installed" : ""); - const title = document.createElement("div"); - title.className = "sa-civitai-title"; - title.textContent = r.name || r.file_name || "LoRA"; - card.appendChild(title); - const meta = document.createElement("div"); - meta.className = "sa-civitai-meta"; - const bits = [ - r.base_model || "?", - r.krea_likely ? "Krea?" : null, - r.already_installed ? "already installed" : null, - (r.triggers || []).slice(0, 3).join(", ") || null - ].filter(Boolean); - meta.textContent = bits.join(" \xB7 "); - card.appendChild(meta); - const actions = document.createElement("div"); - actions.className = "sa-civitai-actions"; - if (r.already_installed) { - const note = document.createElement("span"); - note.textContent = "\u0423\u0436\u0435 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430"; - actions.appendChild(note); - } else if (r.download_url) { - const btn = document.createElement("button"); - btn.type = "button"; - btn.className = "basic-button sa-primary"; - btn.textContent = "\u041F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044C \u0441\u043A\u0430\u0447\u0438\u0432\u0430\u043D\u0438\u0435"; - btn.addEventListener("click", () => downloadCivitaiLoRA(r, btn)); - actions.appendChild(btn); - } else { - const note = document.createElement("span"); - note.textContent = "\u041D\u0435\u0442 URL \u0441\u043A\u0430\u0447\u0438\u0432\u0430\u043D\u0438\u044F"; - actions.appendChild(note); - } - card.appendChild(actions); - list.appendChild(card); - } - return list; - } - function downloadCivitaiLoRA(card, btn) { - if (!card.download_url) { - return; - } - if (btn) { - btn.disabled = true; - btn.textContent = "\u0421\u043A\u0430\u0447\u0438\u0432\u0430\u044E\u2026"; - } - setStatus(`\u0421\u043A\u0430\u0447\u0438\u0432\u0430\u044E ${card.file_name || card.name}\u2026`); - setInterruptVisible(true); - const payload = { - url: card.download_url, - type: "LoRA", - name: card.file_name || card.name || "lora" - }; - const onDone = (ok, msg) => { - setInterruptVisible(state.busy || state.generating); - if (ok) { - setStatus(`\u0421\u043A\u0430\u0447\u0430\u043D\u043E ${payload.name}`); - if (btn) { - btn.textContent = "\u0421\u043A\u0430\u0447\u0430\u043D\u043E"; - } - refreshInventory(async () => { - await maybeWriteCardAfterDownload({ - kind: "lora", - name: payload.name, - civitai: card - }); - }, { rescan: true }); - } else { - setStatus(msg || "\u041E\u0448\u0438\u0431\u043A\u0430 \u0441\u043A\u0430\u0447\u0438\u0432\u0430\u043D\u0438\u044F"); - if (btn) { - btn.disabled = false; - btn.textContent = "\u041F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044C \u0441\u043A\u0430\u0447\u0438\u0432\u0430\u043D\u0438\u0435"; - } - appendMessage("error", msg || "\u041E\u0448\u0438\u0431\u043A\u0430 \u0441\u043A\u0430\u0447\u0438\u0432\u0430\u043D\u0438\u044F"); - } - }; - if (typeof makeWSRequest === "function") { - makeWSRequest( - "DoModelDownloadWS", - payload, - (data) => { - if (data.error) { - onDone(false, String(data.error)); - return; - } - if (data.success || data.overall_percent >= 1 || data.current_percent >= 1) { - if (data.success || data.overall_percent >= 0.99) { - triggerSwarmModelRefresh(() => onDone(true)); - } else if (data.current_percent != null) { - setStatus(`Download ${(data.current_percent * 100).toFixed(0)}%`); - } - } - }, - 0, - (err) => onDone(false, String(err || "Download failed")) - ); - } else { - onDone(false, "makeWSRequest unavailable"); - } - } - async function maybeWriteCardAfterDownload({ kind, name, civitai }) { - const display = name || civitai?.file_name || civitai?.name || "model"; - appendSystemNote(`Downloaded ${display}. Writing a recommendation card\u2026`); - setPackValue("catalog_card", { flash: true }); - const meta = { - triggers: civitai?.triggers || [], - base_model: civitai?.base_model, - civitai_url: civitai?.url || civitai?.civitai_url, - version_id: civitai?.version_id || civitai?.modelVersionId, - name: display - }; - if ($2("sa_input")) { - $2("sa_input").value = ""; - } - await sendChat({ - forcedUserText: `LoRA "${display}" is now installed. Write a recommendation card (JSON) using its triggers/metadata. Then briefly suggest how to enable it in the next generate.`, - skipSlash: true, - skipAutoPack: true, - fromDownload: true, - fromCards: true, - cardTarget: { kind: kind || "lora", name: display, meta } - }); - } function wantsAutoVision() { return !!$2("sa_auto_vision")?.checked; } @@ -5306,7 +6080,7 @@ ${patch.prompt}`; persona = "aggressive"; localStorage.setItem(LS_PERSONA, persona); } - const view = localStorage.getItem(LS_VIEW); + let view = localStorage.getItem(LS_VIEW); const auto = localStorage.getItem(LS_AUTO_VISION); const autoApply = localStorage.getItem(LS_AUTO_APPLY); const autoGen = localStorage.getItem(LS_AUTO_GENERATE); @@ -5351,7 +6125,10 @@ ${patch.prompt}`; if (paneW) { document.documentElement.style.setProperty("--sa-image-width", paneW); } - if (view === "cards" || view === "chat" || view === "settings" || view === "train") { + if (view === "cards") { + view = "chat"; + } + if (view === "chat" || view === "settings" || view === "train") { state.view = view; } const drawer = localStorage.getItem(LS_CHATS_DRAWER); @@ -5370,7 +6147,7 @@ ${patch.prompt}`; persona: $2("sa_persona")?.value || "neutral", auto_vision: !!$2("sa_auto_vision")?.checked, auto_apply: !!$2("sa_auto_apply")?.checked, - auto_generate: !!$2("sa_auto_generate")?.checked, + auto_generate: true, auto_critique: !!$2("sa_auto_critique")?.checked, auto_download: !!$2("sa_auto_download")?.checked, park_llm: !!$2("sa_park_llm")?.checked, @@ -5426,7 +6203,10 @@ ${patch.prompt}`; } }); fill(LS_PANE_WIDTH, ui.pane_width, (v) => document.documentElement.style.setProperty("--sa-image-width", v)); - if (ui.view === "cards" || ui.view === "chat" || ui.view === "settings" || ui.view === "train") { + if (ui.view === "cards") { + ui.view = "chat"; + } + if (ui.view === "chat" || ui.view === "settings" || ui.view === "train") { fill(LS_VIEW, ui.view, (v) => { state.view = v; }); @@ -5574,6 +6354,15 @@ ${data.ui.help_extra}`.trim(); if (asst.history_keep_turns != null) { HISTORY_KEEP_TURNS = Math.max(1, Number(asst.history_keep_turns) || 4); } + if (asst.compress_at != null) { + COMPRESS_AT = Math.min(0.95, Math.max(0.4, Number(asst.compress_at) || 0.7)); + } + if (asst.chars_per_token != null) { + CHARS_PER_TOKEN = Math.max(1.5, Number(asst.chars_per_token) || 3.2); + } + if (asst.compress_auto != null) { + COMPRESS_AUTO = !!asst.compress_auto; + } if (asst.max_ref_slots != null) { MAX_REF_SLOTS = Math.max(1, Number(asst.max_ref_slots) || 4); } @@ -5590,6 +6379,7 @@ ${data.ui.help_extra}`.trim(); INVENTORY_PROMPT_NAMES = Math.max(INVENTORY_PROMPT_RICH, Number(asst.inventory_prompt_names) || 24); } fillKnobsFromConfig(data); + updateCtxChip(); if (applyDefaults || data.exact) { fillEmptyParamsFromExact(); } @@ -6173,9 +6963,7 @@ ${data.ui.help_extra}`.trim(); const n = state.inventory.loras.length; const ck = state.inventory.checkpoints.length; setStatus(`Inventory: ${n} LoRAs, ${ck} ckpts${rescan ? " (rescanned)" : ""}`); - prefetchActiveModelCards(); if (state.view === "cards") { - renderCardsList(); } if (done) { done(state.inventory); @@ -6366,7 +7154,6 @@ ${data.ui.help_extra}`.trim(); }); if (state.settingsTab === "craft") { refreshMemoryList(); - refreshWantedQueue(); } if (state.settingsTab === "user") { refreshUserPrefs(); @@ -6396,6 +7183,12 @@ ${data.ui.help_extra}`.trim(); }; setNum("sa_num_ctx", asst.num_ctx); setNum("sa_history_keep", asst.history_keep_turns); + setNum("sa_compress_at", asst.compress_at != null ? asst.compress_at : COMPRESS_AT); + setNum("sa_chars_per_token", asst.chars_per_token != null ? asst.chars_per_token : CHARS_PER_TOKEN); + const autoEl = $2("sa_compress_auto"); + if (autoEl) { + autoEl.checked = asst.compress_auto != null ? !!asst.compress_auto : COMPRESS_AUTO; + } setNum("sa_memory_top_k", asst.memory_top_k); const w = asst.user_prefs_weight != null ? Number(asst.user_prefs_weight) : 1; const weightEl = $2("sa_user_prefs_weight"); @@ -6426,6 +7219,9 @@ ${data.ui.help_extra}`.trim(); const assistant = { num_ctx: num("sa_num_ctx"), history_keep_turns: num("sa_history_keep"), + compress_at: num("sa_compress_at"), + chars_per_token: num("sa_chars_per_token"), + compress_auto: $2("sa_compress_auto") ? !!$2("sa_compress_auto").checked : null, memory_top_k: num("sa_memory_top_k"), user_prefs_weight: num("sa_user_prefs_weight") }; @@ -6459,6 +7255,16 @@ ${data.ui.help_extra}`.trim(); exact: data.exact || state.config?.exact }); } + if (assistant.compress_at != null) { + COMPRESS_AT = Math.min(0.95, Math.max(0.4, Number(assistant.compress_at) || 0.7)); + } + if (assistant.chars_per_token != null) { + CHARS_PER_TOKEN = Math.max(1.5, Number(assistant.chars_per_token) || 3.2); + } + if (assistant.compress_auto != null) { + COMPRESS_AUTO = !!assistant.compress_auto; + } + updateCtxChip(); setStatus("Knobs \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u044B \u0432 overlay"); }, 0, @@ -6822,58 +7628,6 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; function modelKeyLeaf(name) { return String(name || "").replace(/\\/g, "/").split("/").pop().replace(/\.(safetensors|ckpt|pt|pth|gguf|bin)$/i, "").trim().toLowerCase(); } - function refreshWantedQueue() { - if (typeof genericRequest !== "function") { - return; - } - genericRequest( - "AssistentListWanted", - {}, - (data) => { - const items = Array.isArray(data?.items) ? data.items : []; - state.wanted = { count: data?.count ?? items.length, items }; - const keys = /* @__PURE__ */ new Set(); - for (const item of items) { - const leaf = modelKeyLeaf(item?.title); - if (leaf) { - keys.add(leaf); - } - if (item?.version_id) { - keys.add(`v${item.version_id}`); - } - } - state.wantedKeys = keys; - const el = $2("sa_mem_wanted"); - if (el) { - el.textContent = state.wanted.count ? `\u041E\u0447\u0435\u0440\u0435\u0434\u044C wanted: ${state.wanted.count} (\u0441\u043A\u0430\u0447\u0430\u0435\u0442\u0441\u044F \u043D\u0430 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u043C up)` : "\u041E\u0447\u0435\u0440\u0435\u0434\u044C wanted: \u043F\u0443\u0441\u0442\u043E"; - el.title = items.slice(0, 12).map((i) => `${i.kind}: ${i.title || i.url}`).join("\n"); - } - if (state.view === "cards") { - renderCardsList(); - } - }, - 0, - () => { - const el = $2("sa_mem_wanted"); - if (el) { - el.textContent = "\u041E\u0447\u0435\u0440\u0435\u0434\u044C wanted: \u2014"; - } - } - ); - } - function isWantedModel(row) { - const keys = state.wantedKeys; - if (!keys || !keys.size) { - return false; - } - for (const candidate of [row?.name, row?.title]) { - const leaf = modelKeyLeaf(candidate); - if (leaf && keys.has(leaf)) { - return true; - } - } - return false; - } function setOllamaHealth(level, text, title) { state.ollamaHealth = level; const el = $2("sa_ollama_health"); @@ -6912,16 +7666,11 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; (err) => setOllamaHealth("down", "Ollama \u2715", `\u041D\u0435\u0442 \u0441\u0432\u044F\u0437\u0438: ${String(err || "")} \xB7 ${baseUrl}`) ); } - function setCardStatus(msg) { - const el = $2("sa_card_status"); - if (el) { - el.textContent = msg || ""; - } - } function setView(view) { if (view === "cards") { - state.view = "cards"; - } else if (view === "settings") { + view = "chat"; + } + if (view === "settings") { state.view = "settings"; } else if (view === "train") { state.view = "train"; @@ -6929,15 +7678,11 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; state.view = "chat"; } const chat = $2("sa_view_chat"); - const cards = $2("sa_view_cards"); const settings = $2("sa_view_settings"); const train = $2("sa_view_train"); if (chat) { chat.hidden = state.view !== "chat"; } - if (cards) { - cards.hidden = state.view !== "cards"; - } if (settings) { settings.hidden = state.view !== "settings"; } @@ -6950,13 +7695,10 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; $2(id)?.setAttribute("aria-selected", on ? "true" : "false"); }; tabActive("sa_tab_chat", state.view === "chat"); - tabActive("sa_tab_cards", state.view === "cards"); tabActive("sa_tab_settings", state.view === "settings"); tabActive("sa_tab_train", state.view === "train"); saveSettings(); - if (state.view === "cards") { - renderCardsList(); - } else if (state.view === "settings") { + if (state.view === "settings") { setSettingsTab(state.settingsTab || "behavior"); } else if (state.view === "train") { window.SA?.training?.render?.(); @@ -6973,479 +7715,12 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; function closeSettings() { setView("chat"); } - function prefetchCard(kind, name) { - return new Promise((resolve) => { - if (!kind || !name || typeof genericRequest !== "function") { - resolve(null); - return; - } - const key = `${kind}:${name}`; - genericRequest( - "AssistentGetCard", - { kind, name }, - (data) => { - if (data?.card) { - state.modelCards[key] = data.card; - } - resolve(data?.card || null); - }, - 0, - () => resolve(null) - ); - }); - } - async function prefetchActiveModelCards() { - const keys = []; - const seen = /* @__PURE__ */ new Set(); - const add = (kind, name) => { - if (!kind || !name) { - return; - } - const key = `${kind}:${name}`; - if (seen.has(key)) { - return; - } - seen.add(key); - keys.push({ kind, name }); - }; - try { - const ck = resolveCurrentCheckpoint(); - if (ck?.name) { - add("checkpoint", ck.name); - } - } catch (e) { - } - try { - if (typeof loraHelper !== "undefined" && Array.isArray(loraHelper?.selected)) { - for (const l of loraHelper.selected) { - add("lora", l?.name || l); - } - } - } catch (e) { - } - for (const l of state.inventory?.loras || []) { - if (l?.has_card) { - add("lora", l.name); - } - if (keys.length >= 14) { - break; - } - } - for (const c of state.inventory?.checkpoints || []) { - if (c?.has_card) { - add("checkpoint", c.name); - } - if (keys.length >= 16) { - break; - } - } - await Promise.all(keys.map((k) => prefetchCard(k.kind, k.name))); - } - function cardsCatalog() { - const kind = $2("sa_cards_kind")?.value || "all"; - const inv = state.inventory || {}; - const rows = []; - if (kind === "all" || kind === "checkpoint") { - for (const c of inv.checkpoints || []) { - rows.push({ - kind: "checkpoint", - name: c.name, - title: c.title || c.name, - has_card: !!c.has_card, - hash: c.hash || "", - preview_url: c.preview_url || null, - has_sidecar: !!c.has_sidecar - }); - } - } - if (kind === "all" || kind === "lora") { - for (const l of inv.loras || []) { - rows.push({ - kind: "lora", - name: l.name, - title: l.title || l.name, - has_card: !!l.has_card, - trigger: l.trigger_phrase, - hash: l.hash || "", - preview_url: l.preview_url || null, - has_sidecar: !!l.has_sidecar - }); - } - } - return rows; - } - function renderCardsList() { - const root = $2("sa_cards_list"); - if (!root) { - return; - } - root.innerHTML = ""; - const rows = cardsCatalog(); - if (!rows.length) { - root.innerHTML = '
Inventory \u043F\u0443\u0441\u0442 \u2014 \u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C.
'; - return; - } - for (const row of rows) { - const btn = document.createElement("button"); - btn.type = "button"; - btn.className = "sa-card-row"; - if (state.cardsSelection && state.cardsSelection.kind === row.kind && state.cardsSelection.name === row.name) { - btn.classList.add("sa-selected"); - } - const thumb = row.preview_url ? `` : '
'; - const metaBits = []; - metaBits.push(row.has_card ? "card \u2713" : "\u043D\u0435\u0442 card"); - if (row.has_sidecar) { - metaBits.push("sidecar"); - } - if (isWantedModel(row)) { - metaBits.push("\u23F3 wanted"); - btn.classList.add("sa-card-row-wanted"); - } - if (row.trigger) { - metaBits.push(String(row.trigger).slice(0, 40)); - } - btn.innerHTML = `${thumb}
${escapeHtml2(row.kind)}
${escapeHtml2(row.title || row.name)}
${escapeHtml2(metaBits.join(" \xB7 "))}
`; - btn.addEventListener("click", (e) => { - if (e.target?.closest?.("[data-chat]")) { - e.preventDefault(); - e.stopPropagation(); - sendCardToChat(row); - return; - } - selectCardModel(row); - }); - root.appendChild(btn); - } - } - function sendCardToChat(row) { - if (!row?.name) { - return; - } - selectCardModel(row); - setView("chat"); - const kind = row.kind === "checkpoint" ? "checkpoint" : "LoRA"; - const triggers = row.trigger ? ` Triggers: ${row.trigger}.` : ""; - if ($2("sa_input")) { - $2("sa_input").value = `\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439 ${kind} \xAB${row.name}\xBB.${triggers} \u0423\u0447\u0442\u0438 \u043A\u0430\u0440\u0442\u043E\u0447\u043A\u0443/triggers \u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0438 \u043F\u0430\u0442\u0447.`; - $2("sa_input").focus(); - } - setStatus(`\u0412 \u0447\u0430\u0442 \u2192 ${row.name}`); - } - function wireCardForm() { - const sync = () => { - if ($2("sa_card_show_json")?.checked) { - syncCardJsonFromForm(); - } - }; - ["sa_card_triggers", "sa_card_weight", "sa_card_when", "sa_card_avoid", "sa_card_hint", "sa_card_notes", "sa_card_url"].forEach((id) => $2(id)?.addEventListener("change", sync)); - $2("sa_card_show_json")?.addEventListener("change", () => { - const on = !!$2("sa_card_show_json")?.checked; - const ta = $2("sa_card_json"); - if (ta) { - ta.hidden = !on; - if (on) { - syncCardJsonFromForm(); - } - } - }); - $2("sa_card_json")?.addEventListener("change", () => { - if ($2("sa_card_show_json")?.checked) { - applyCardToForm(readCardDraft() || {}); - } - }); - } - function applyCardToForm(card) { - card = card || {}; - const triggers = Array.isArray(card.triggers) ? card.triggers.join(", ") : card.triggers || ""; - if ($2("sa_card_triggers")) { - $2("sa_card_triggers").value = triggers; - } - if ($2("sa_card_weight")) { - $2("sa_card_weight").value = card.weight != null ? card.weight : state.cardsSelection?.kind === "lora" ? 0.8 : 1; - } - if ($2("sa_card_when")) { - $2("sa_card_when").value = card.when || ""; - } - if ($2("sa_card_avoid")) { - $2("sa_card_avoid").value = card.avoid || ""; - } - if ($2("sa_card_hint")) { - $2("sa_card_hint").value = card.prompt_hint || ""; - } - if ($2("sa_card_notes")) { - $2("sa_card_notes").value = card.notes || ""; - } - if ($2("sa_card_url")) { - $2("sa_card_url").value = card.civitai_url || ""; - } - if ($2("sa_card_json")) { - $2("sa_card_json").value = JSON.stringify(card, null, 2); - } - } - function syncCardJsonFromForm() { - const sel = state.cardsSelection || {}; - let base = {}; - try { - base = JSON.parse($2("sa_card_json")?.value || "{}"); - } catch (e) { - base = {}; - } - const triggers = String($2("sa_card_triggers")?.value || "").split(/[,;]/).map((s) => s.trim()).filter(Boolean); - const card = { - ...base, - kind: sel.kind || base.kind || "lora", - name: sel.name || base.name || "", - triggers, - weight: parseFloat($2("sa_card_weight")?.value || "0.8") || 0.8, - when: $2("sa_card_when")?.value || "", - avoid: $2("sa_card_avoid")?.value || "", - prompt_hint: $2("sa_card_hint")?.value || "", - notes: $2("sa_card_notes")?.value || "", - civitai_url: $2("sa_card_url")?.value || "", - version_id: base.version_id != null ? base.version_id : null - }; - if ($2("sa_card_json")) { - $2("sa_card_json").value = JSON.stringify(card, null, 2); - } - return card; - } - function renderCardPreviews(urls) { - const root = $2("sa_card_previews"); - if (!root) { - return; - } - const list = (urls || []).filter(Boolean).slice(0, 6); - root.innerHTML = ""; - if (!list.length) { - root.hidden = true; - return; - } - root.hidden = false; - for (const url of list) { - const img = document.createElement("img"); - img.className = "sa-card-thumb"; - img.src = url; - img.alt = "preview"; - img.title = "\u041A\u043B\u0438\u043A \u2014 \u043D\u0430 \u0432\u043A\u043B\u0430\u0434\u043A\u0443 Refs"; - img.addEventListener("click", () => { - setBoardTab("refs"); - addRefFromUrl(url); - setStatus("\u041F\u0440\u0435\u0432\u044C\u044E \u2192 Refs"); - }); - root.appendChild(img); - } - } - function mergeCivitaiIntoCard(card, data) { - const out = { ...card || {} }; - const civ = data?.civitai; - if (!out.triggers?.length && data?.trigger_phrase) { - out.triggers = [data.trigger_phrase]; - } - if (civ) { - const trained = civ.trainedWords || civ.trained_words; - if ((!out.triggers || !out.triggers.length) && Array.isArray(trained) && trained.length) { - out.triggers = trained.slice(0, 12); - } - if (!out.civitai_url) { - const mid = civ.modelId || civ.model?.id || civ.model?.modelId; - const vid = civ.id || data.version_id; - if (mid && vid) { - out.civitai_url = `https://civitai.red/models/${mid}?modelVersionId=${vid}`; - } else if (vid) { - out.civitai_url = `https://civitai.red/models/0?modelVersionId=${vid}`; - } - } - if (out.version_id == null && (civ.id || data.version_id)) { - out.version_id = civ.id || data.version_id; - } - if (!out.notes && civ.description) { - out.notes = String(civ.description).replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim().slice(0, 400); - } - } - if (out.version_id == null && data?.version_id) { - out.version_id = data.version_id; - } - return out; - } - function formatMetaStatus(data) { - if (!data) { - return "\u041D\u0435\u0442 \u043E\u0442\u0432\u0435\u0442\u0430"; - } - if (data.error) { - return String(data.error); - } - const parts = []; - if (data.has_sidecar) { - parts.push(`\u0421\u0438\u0434\u0438\u043A\u0430\u0440\u044C \u2713 \xB7 version ${data.version_id || "?"}`); - } else if (data.fetched) { - parts.push(`Civitai \u2713 \xB7 version ${data.version_id || "?"}`); - } else { - parts.push("\u0421\u0438\u0434\u0438\u043A\u0430\u0440\u044F \u043D\u0435\u0442"); - } - const n = (data.example_urls || data.preview_urls || []).length; - if (n) { - parts.push(`${n} \u043A\u0430\u0434\u0440${n === 1 ? "" : "\u0430"}`); - } - if (data.has_card) { - parts.push("\u043A\u0430\u0440\u0442\u043E\u0447\u043A\u0430 Assistent \u2713"); - } else { - parts.push("\u043A\u0430\u0440\u0442\u043E\u0447\u043A\u0438 Assistent \u043D\u0435\u0442"); - } - if (data.fetch_error) { - parts.push(String(data.fetch_error)); - } - return parts.join(" \xB7 "); - } - function applyCardMetaResponse(row, data, { preserveUser } = {}) { - let card = data.card || { - kind: row.kind, - name: row.name, - triggers: data.trigger_phrase ? [data.trigger_phrase] : [], - weight: row.kind === "lora" ? 0.8 : 1, - when: "", - avoid: "", - prompt_hint: "", - notes: "", - civitai_url: "", - version_id: data.version_id || null - }; - if (preserveUser) { - const current = syncCardJsonFromForm(); - card = { - ...mergeCivitaiIntoCard(card, data), - when: current.when || card.when || "", - avoid: current.avoid || card.avoid || "", - prompt_hint: current.prompt_hint || card.prompt_hint || "", - notes: current.notes || card.notes || "" - }; - } else { - card = mergeCivitaiIntoCard(card, data); - } - applyCardToForm(card); - state.modelCards[`${row.kind}:${row.name}`] = card; - const urls = [ - ...data.preview_urls || [], - ...data.example_urls || [] - ]; - renderCardPreviews(urls); - const badge = $2("sa_card_badge"); - if (badge) { - badge.hidden = false; - badge.textContent = data.has_card ? "card \u2713" : data.has_sidecar || data.fetched ? "meta \u2713" : "\u043D\u0435\u0442 \u043C\u0435\u0442\u044B"; - } - setCardStatus(formatMetaStatus(data)); - } - function selectCardModel(row) { - state.cardsSelection = row; - renderCardsList(); - if ($2("sa_card_title")) { - $2("sa_card_title").textContent = row.title || row.name; - } - const badge = $2("sa_card_badge"); - if (badge) { - badge.hidden = false; - badge.textContent = "\u2026"; - } - setCardStatus("\u0427\u0438\u0442\u0430\u044E \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u0443\u044E \u043C\u0435\u0442\u0443\u2026"); - genericRequest( - "AssistentGetCardMeta", - { kind: row.kind, name: row.name, fetch: false }, - (data) => applyCardMetaResponse(row, data || {}), - 0, - (err) => setCardStatus(String(err || "\u041E\u0448\u0438\u0431\u043A\u0430 \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0438")) - ); - } - function fetchCardMetaLive() { - const row = state.cardsSelection; - if (!row) { - setCardStatus("\u0412\u044B\u0431\u0435\u0440\u0438 \u043C\u043E\u0434\u0435\u043B\u044C"); - return; - } - setCardStatus("\u0421\u0438\u0434\u0438\u043A\u0430\u0440\u044F \u043D\u0435\u0442 \xB7 \u0438\u0449\u0443 \u043F\u043E SHA\u2026"); - genericRequest( - "AssistentGetCardMeta", - { kind: row.kind, name: row.name, fetch: true }, - (data) => applyCardMetaResponse(row, data || {}, { preserveUser: true }), - 0, - (err) => setCardStatus(String(err || "Civitai: \u043E\u0448\u0438\u0431\u043A\u0430 \u0437\u0430\u043F\u0440\u043E\u0441\u0430")) - ); - } async function addRefFromUrl(url) { if (!url) { return; } addRefSlot({ src: url, select: false }); } - function readCardDraft() { - const fromForm = syncCardJsonFromForm(); - if ($2("sa_card_show_json")?.checked) { - const raw = $2("sa_card_json")?.value || ""; - try { - return JSON.parse(raw); - } catch (e) { - setCardStatus("\u041D\u0435\u0432\u0430\u043B\u0438\u0434\u043D\u044B\u0439 JSON"); - return null; - } - } - return fromForm; - } - function saveCurrentCard({ enqueue } = {}) { - const sel = state.cardsSelection; - if (!sel) { - setCardStatus("\u0412\u044B\u0431\u0435\u0440\u0438 \u043C\u043E\u0434\u0435\u043B\u044C"); - return; - } - const card = readCardDraft(); - if (!card) { - return; - } - card.kind = card.kind || sel.kind; - card.name = card.name || sel.name; - setCardStatus("\u0421\u043E\u0445\u0440\u0430\u043D\u044F\u044E\u2026"); - genericRequest( - "AssistentSaveCard", - { kind: sel.kind, name: sel.name, card, enqueue_wanted: !!enqueue }, - (data) => { - if (data.error) { - setCardStatus(data.error); - return; - } - state.modelCards[`${sel.kind}:${sel.name}`] = card; - setCardStatus(data.installed ? `\u041A\u0430\u0440\u0442\u043E\u0447\u043A\u0430 Assistent \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0430 \xB7 ${data.path}` : `\u0427\u0435\u0440\u043D\u043E\u0432\u0438\u043A + wanted \xB7 ${data.path}`); - refreshInventory(() => renderCardsList()); - if (enqueue || !data.installed) { - refreshWantedQueue(); - } - }, - 0, - (err) => setCardStatus(String(err || "\u041E\u0448\u0438\u0431\u043A\u0430 \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u044F")) - ); - } - function enqueueWantedOnly() { - const sel = state.cardsSelection; - const card = readCardDraft() || {}; - if (!sel && !card.civitai_url) { - setCardStatus("\u041D\u0443\u0436\u043D\u0430 \u043C\u043E\u0434\u0435\u043B\u044C \u0438\u043B\u0438 civitai_url"); - return; - } - genericRequest( - "AssistentEnqueueWanted", - { - kind: card.kind || sel?.kind || "lora", - url: card.civitai_url || "", - version_id: card.version_id || 0, - title: card.name || sel?.name || "", - card - }, - (data) => { - setCardStatus(data.already ? "\u0423\u0436\u0435 \u0432 wanted" : `Wanted \u2192 ${data.path}`); - refreshWantedQueue(); - }, - 0, - (err) => setCardStatus(String(err || "\u041E\u0448\u0438\u0431\u043A\u0430 enqueue")) - ); - } function shortLoraName(name) { const s = String(name || ""); const base = s.split(/[/\\]/).pop() || s; @@ -7567,40 +7842,6 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; setTimeout(() => document.addEventListener("mousedown", onDoc), 0); filter.focus(); } - async function generateCardWithAssistent() { - const sel = state.cardsSelection; - if (!sel) { - setCardStatus("\u0412\u044B\u0431\u0435\u0440\u0438 \u043C\u043E\u0434\u0435\u043B\u044C"); - return; - } - if (state.busy) { - setCardStatus("\u0427\u0430\u0442 \u0437\u0430\u043D\u044F\u0442"); - return; - } - setPackValue("catalog_card", { flash: true }); - setView("chat"); - const meta = await new Promise((resolve) => { - genericRequest( - "AssistentGetCardMeta", - { kind: sel.kind, name: sel.name }, - (data) => resolve(data), - 0, - () => resolve(null) - ); - }); - const forced = `Write a recommendation card for this ${sel.kind}: ${sel.name}. Use metadata/triggers only; output one JSON card.`; - await sendChat({ - forcedUserText: forced, - skipSlash: true, - skipAutoPack: true, - fromCards: true, - cardTarget: { - kind: sel.kind, - name: sel.name, - meta - } - }); - } function inventoryIsStale(maxAgeMs = 2e4) { if (!state.inventoryFetchedAt) { return true; @@ -7635,151 +7876,159 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; ); } async function handleReplySideEffects(reply, civitaiResults, opts = {}) { - const { fromAutoCritique, fromVisionHop, fromCards, fromDebug } = opts; - if (fromCards) { - const card = extractCardJson(reply); - if (card) { - if ($2("sa_card_json")) { - $2("sa_card_json").value = JSON.stringify(card, null, 2); - } - if (opts.fromDownload || opts.cardTarget) { - const kind = card.kind || opts.cardTarget?.kind || "lora"; - const name = card.name || opts.cardTarget?.name; - if (name && typeof genericRequest === "function") { - genericRequest( - "AssistentSaveCard", - { kind, name, card, enqueue_wanted: false }, - (data) => { - if (data?.path) { - state.modelCards[`${kind}:${name}`] = card; - setCardStatus(data.installed ? `Card saved \u2192 ${data.path}` : `Card draft \u2192 ${data.path}`); - setStatus(`Card saved for ${name}`); - } - }, - 0, - () => setCardStatus("Card draft ready \u2014 Save manually") - ); - } - } else { - setView("cards"); - setCardStatus("Draft from Assistent \u2014 review & Save"); - } - } - return; - } - const { patch } = extractPatch2(reply); + const { fromAutoCritique, fromVisionHop, fromDebug } = opts; + void civitaiResults; if (fromDebug) { state.pendingSilentGen = false; return; } - const commanded = !!opts.userWantsGenerate || !isMachineTurn(opts) && userImpliesGenerate(opts.userText || ""); - let effective = patch; - if (!effective && !fromVisionHop && !fromAutoCritique) { - const aspect = parseAspectFromUserText(opts.userText || ""); - if (aspect && (replyMissingJsonPatch(reply) || isSameButAspectRequest(opts.userText || ""))) { - effective = { aspect, actions: ["generate"] }; - if (state.lastPatch?.prompt) { - effective.prompt = state.lastPatch.prompt; - } - appendSystemNote(`\u041F\u0430\u0442\u0447 \u043F\u0443\u0441\u0442\u043E\u0439 \u2014 \u043F\u0440\u0438\u043C\u0435\u043D\u0438\u043B aspect ${aspect} \u0441\u0430\u043C.`); - } - } - if (!effective && !fromVisionHop && !fromAutoCritique) { - const synthesized = synthesizePatchAfterEmptyFence(reply, opts.userText || "", opts); - if (synthesized) { - effective = synthesized; - appendSystemNote(synthesized.actions ? "\u041F\u0430\u0442\u0447 \u043F\u0443\u0441\u0442\u043E\u0439 \u2014 \u0441\u043E\u0431\u0440\u0430\u043B prompt \u0438\u0437 \u043E\u0442\u0432\u0435\u0442\u0430 \u0438 \u0437\u0430\u043F\u0443\u0441\u0442\u0438\u043B Generate." : "\u041F\u0430\u0442\u0447 \u043F\u0443\u0441\u0442\u043E\u0439 \u2014 \u0441\u043E\u0431\u0440\u0430\u043B prompt \u0438\u0437 \u043E\u0442\u0432\u0435\u0442\u0430."); - } - } - if (!effective && !fromVisionHop && !fromAutoCritique && commanded && claimTurnHop("empty_patch")) { - appendSystemNote("\u041D\u0443\u0436\u0435\u043D \u043A\u0430\u0434\u0440 \u2014 \u043F\u0440\u043E\u0448\u0443 JSON \u0441 prompt + generate."); - await sendChat({ - skipSlash: true, - skipAutoPack: true, - fromEmptyPatchRetry: true, - userWantsGenerate: true, - forcedUserText: '\u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C \u0443\u0436\u0435 \u043F\u0440\u043E\u0441\u0438\u0442 \u043A\u0430\u0434\u0440 (\u044D\u0442\u043E \u0441\u043B\u0435\u0434\u0443\u0435\u0442 \u0438\u0437 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F, \u0434\u0430\u0436\u0435 \u0431\u0435\u0437 \u0441\u043B\u043E\u0432\u0430 \xAB\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0439\xBB). \u041E\u0442\u0432\u0435\u0442\u044C \u0422\u041E\u041B\u042C\u041A\u041E \u043E\u0434\u043D\u0438\u043C fenced JSON: {"prompt":"","negative":"","actions":["generate"]}. prompt \u2014 \u0430\u043D\u0433\u043B\u0438\u0439\u0441\u043A\u0438\u0439. \u0411\u0435\u0437 \u043F\u0440\u043E\u0437\u044B, \u0431\u0435\u0437 \xAB\u0441\u043A\u0430\u0436\u0438 \u0441\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0439\xBB.' - }); - return; - } - if (opts.fromPromptEnRetry && effective) { + const extracted = typeof extractPatch2 === "function" ? extractPatch2(reply) : { patch: null }; + let effective = extracted && extracted.patch ? extracted.patch : null; + if (opts.fromPromptEnRetry && effective && typeof mergePromptEnRewrite === "function") { effective = mergePromptEnRewrite(effective); } - if (effective) { - rememberLastPatch(effective); + const S = window.SA && window.SA.session; + const act = getActivity(); + if (effective && act && typeof act.noteModelCommands === "function") { + act.noteModelCommands(effective); + } + if (effective && S) { + state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective); + activityDone("delta", { + kind: "delta", + label: "\u041E\u0431\u043D\u043E\u0432\u0438\u043B \u0441\u0435\u0441\u0441\u0438\u044E", + detail: Object.keys(effective).filter((k) => effective[k] != null && !["actions", "notes"].includes(k)).slice(0, 10).join(", ") + }); + try { + const chat = typeof findChat === "function" ? findChat(state.activeChatId) : null; + if (chat && typeof snapshotChatParams === "function") { + chat.params = snapshotChatParams(); + if (typeof persistChatsStore === "function") persistChatsStore(); + } + } catch (e) { + } + if (typeof rememberLastPatch === "function") rememberLastPatch(effective); } if (Array.isArray(effective?.actions) && effective.actions.map(String).includes("interrupt")) { - doInterruptNow(); + if (typeof doInterruptNow === "function") doInterruptNow(); } - if (civitaiResults && civitaiResults.length && $2("sa_auto_download")?.checked) { - const pick = civitaiResults.find((r) => !r.already_installed && r.download_url && r.krea_likely) || civitaiResults.find((r) => !r.already_installed && r.download_url); - if (pick) { - downloadCivitaiLoRA(pick, null); - } - } - const intent = resolveTurnIntent(effective, opts.userText || "", opts); + const intent = resolveTurnIntent2(effective, opts.userText || "", opts); if (effective) { if (intent.generate) { const acts = Array.isArray(effective.actions) ? effective.actions.map(String) : []; - if (!acts.includes("generate")) { - effective = { ...effective, actions: acts.concat("generate") }; - } - } else { + effective = { ...effective, actions: acts.includes("generate") ? acts : acts.concat("generate"), generate: true }; + } else if (typeof stripGenerateAction === "function") { effective = stripGenerateAction(effective); + if (effective && effective.generate) { + effective = { ...effective }; + delete effective.generate; + } } - if (!intent.look) { - effective = stripLookAt(effective); - } - rememberLastPatch(effective); + if (!intent.look && typeof stripLookAt === "function") effective = stripLookAt(effective); + if (typeof rememberLastPatch === "function") rememberLastPatch(effective); } if (intent.vetoed) { state.pendingSilentGen = false; + const a = getActivity(); + if (a) { + a.skip("generate", { + kind: "generate", + label: "Generate \u043E\u0442\u043C\u0435\u043D\u0451\u043D", + detail: "\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C \u043F\u043E\u043F\u0440\u043E\u0441\u0438\u043B \u043D\u0435 \u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C" + }); + } } - if (effective && intent.look && !fromVisionHop && !fromAutoCritique) { - const hopped = await maybeVisionHop(effective, opts.attachedSlotIds || []); - if (hopped) { + const askList = Array.isArray(intent.ask) ? intent.ask : []; + if (askList.length && typeof claimTurnHop === "function" && claimTurnHop("ask")) { + activityStep("ask", { + kind: "ask", + label: `\u0417\u0430\u043F\u0440\u043E\u0441\u0438\u043B ${askList.join(", ")}`, + detail: "\u043F\u043E\u0434\u0433\u0440\u0443\u0436\u0430\u044E \u0434\u0435\u0442\u0430\u043B\u0438\u2026", + status: "running" + }); + pullLiveIntoSession(); + const bits = []; + if (askList.some((a) => /settings/i.test(String(a)))) { + const dump = S && typeof S.fullSettingsDump === "function" ? S.fullSettingsDump(state.chatSession, { + exact: state.exact, + kreaProfiles: state.kreaProfiles, + sessionExact: state.sessionExact + }) : collectLiveContext(); + bits.push("SETTINGS_JSON:\n" + JSON.stringify(dump)); + } + if (askList.some((a) => /inventory/i.test(String(a)))) { + const inv = state.inventory || {}; + bits.push("INVENTORY_JSON:\n" + JSON.stringify({ + loras: (inv.loras || []).slice(0, 40).map((l) => ({ name: l.name || l, trigger_phrase: l.trigger_phrase || null })), + checkpoints: (inv.checkpoints || []).slice(0, 16).map((c) => c.name || c), + wildcards: (inv.wildcards || []).slice(0, 20).map((w) => w.name || w) + })); + } + if (bits.length) { + activityDone("ask", { detail: "\u0434\u0435\u0442\u0430\u043B\u0438 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u044B \u043C\u043E\u0434\u0435\u043B\u0438" }); + await sendChat({ skipSlash: true, skipAutoPack: true, fromAskHop: true, forcedUserText: bits.join("\n\n") }); return; } } - if (intent.generate && effective?.prompt && promptNeedsKreaPrep(effective.prompt) && !fromVisionHop && claimTurnHop("krea_prep")) { + if (effective && intent.look && !fromVisionHop && !fromAutoCritique) { + activityStep("look", { + kind: "look", + label: "\u0421\u043C\u043E\u0442\u0440\u0438\u0442 \u043D\u0430 \u043A\u0430\u0434\u0440", + status: "running" + }); + if (typeof maybeVisionHop === "function") { + const hopped = await maybeVisionHop(effective, opts.attachedSlotIds || []); + if (hopped) { + activityDone("look", { detail: "vision hop" }); + return; + } + activityDone("look", { detail: "\u043A\u0430\u0434\u0440 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D" }); + } + } + if (intent.generate && effective?.prompt && typeof promptNeedsKreaPrep === "function" && promptNeedsKreaPrep(effective.prompt) && !fromVisionHop && typeof claimTurnHop === "function" && claimTurnHop("krea_prep")) { state.pendingPromptEnMerge = { ...effective }; - appendSystemNote("\u0413\u043E\u0442\u043E\u0432\u043B\u044E \u043F\u0440\u043E\u043C\u043F\u0442 \u0434\u043B\u044F Krea 2 \u0447\u0430\u0442-\u043C\u043E\u0434\u0435\u043B\u044C\u044E (EN + \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0430)\u2026"); - setBusyPhase("refining"); + activityStep("prep", { + kind: "prep", + label: "\u0413\u043E\u0442\u043E\u0432\u043B\u044E \u043F\u0440\u043E\u043C\u043F\u0442 \u0434\u043B\u044F Krea", + detail: "EN + \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0430", + status: "running" + }); + if (typeof appendSystemNote === "function") appendSystemNote("\u0413\u043E\u0442\u043E\u0432\u043B\u044E \u043F\u0440\u043E\u043C\u043F\u0442 \u0434\u043B\u044F Krea\u2026"); await sendChat({ skipSlash: true, skipAutoPack: true, fromPromptEnRetry: true, userWantsGenerate: true, - forcedUserText: buildKreaPromptPrepRequest(effective) + forcedUserText: typeof buildKreaPromptPrepRequest === "function" ? buildKreaPromptPrepRequest(effective) : String(effective.prompt || "") }); return; } - const doApply = !!(effective && (intent.generate || $2("sa_auto_apply")?.checked)); - if (doApply) { - if (intent.generate) { - startBusyUi("silent_gen"); - } else { - setBusyPhase("applying"); + if (intent.generate) { + activityStep("generate", { + kind: "generate", + label: intent.vetoed ? "Generate \u043E\u0442\u043C\u0435\u043D\u0451\u043D (\u0432\u0435\u0442\u043E)" : "Generate", + status: intent.vetoed ? "skip" : "running" + }); + if (typeof startBusyUi === "function") startBusyUi("silent_gen"); + if (S && effective) state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective); + await pushSessionToSwarm(state.chatSession); + if (typeof syncLiveParamsBar === "function") syncLiveParamsBar(); + const srcOut = await runGenerateFromPatch( + { ...effective || {}, actions: ["generate"], generate: true }, + { force: true, fromSession: true } + ); + activityDone("generate", { detail: srcOut ? "\u043A\u0430\u0434\u0440 \u0433\u043E\u0442\u043E\u0432" : "\u0431\u0435\u0437 \u043A\u0430\u0434\u0440\u0430" }); + if (srcOut) { + if (typeof maybeAutoCritique === "function") await maybeAutoCritique(srcOut); + if (typeof maybeAutoVisionLook === "function") await maybeAutoVisionLook(srcOut); } - await applyPatch(effective, "all"); - syncLiveParamsBar(); - if (intent.generate) { - if (effective?.prompt && promptNeedsKreaPrep(effective.prompt)) { - setStatus("\u041F\u0440\u043E\u043C\u043F\u0442 \u0432\u0441\u0451 \u0435\u0449\u0451 \u043D\u0435 EN/Krea-ready \u2014 Generate \u0441 \u0442\u0435\u043C \u0447\u0442\u043E \u0435\u0441\u0442\u044C"); - } - const src = await runGenerateFromPatch( - { ...effective, actions: Array.isArray(effective.actions) ? effective.actions : ["generate"] }, - { force: true } - ); - if (src) { - await maybeAutoCritique(src); - await maybeAutoVisionLook(src); - } - } else if (!state.generating) { + } else if (effective && $2("sa_auto_apply")?.checked) { + if (S) state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective); + await pushSessionToSwarm(state.chatSession); + if (typeof syncLiveParamsBar === "function") syncLiveParamsBar(); + if (!state.generating && typeof stopBusyUi === "function") { stopBusyUi(intent.vetoed ? "\u0417\u0430\u043F\u043E\u043C\u043D\u0438\u043B \xB7 \u0431\u0435\u0437 Generate" : ""); } - } else if (replyMissingJsonPatch(reply) && !fromAutoCritique && !fromVisionHop) { - setStatus("\u041E\u0442\u0432\u0435\u0442 \u0431\u0435\u0437 JSON-\u043F\u0430\u0442\u0447\u0430 \u2014 \u043D\u0438\u0447\u0435\u0433\u043E \u043D\u0435 \u043F\u0440\u0438\u043C\u0435\u043D\u0435\u043D\u043E"); } state.pendingSilentGen = false; } @@ -7790,11 +8039,15 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; } const prevIntent = state.lastUserParamIntent; state.lastUserParamIntent = true; - await applyPatch(withActions, "all"); + const S = window.SA && window.SA.session; + if (S) { + state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), withActions); + } + await pushSessionToSwarm(state.chatSession); state.lastUserParamIntent = prevIntent; setStatus(note || "Applied"); - if ($2("sa_auto_generate")?.checked) { - await runGenerateFromPatch(withActions); + if (patchHasGenTrigger(withActions)) { + await runGenerateFromPatch(withActions, { force: true, fromSession: true }); } syncChipHighlight(); } @@ -7882,7 +8135,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; `persona=${persona} \xB7 pack=${pack}`, `chat=${chatModel} \xB7 embed=${embed}`, `skills=${(state.enabledSkills || []).join(",") || "\u2014"}`, - `auto: apply=${!!$2("sa_auto_apply")?.checked} gen=${!!$2("sa_auto_generate")?.checked} vision=${!!$2("sa_auto_vision")?.checked} critique=${!!$2("sa_auto_critique")?.checked}`, + `auto: apply=${!!$2("sa_auto_apply")?.checked} gen=${true} vision=${!!$2("sa_auto_vision")?.checked} critique=${!!$2("sa_auto_critique")?.checked}`, "", "Live SwarmUI:", ` ckpt=${ctx.checkpoint?.name || "\u2014"} \xB7 krea_profile=${ctx.krea_profile || profile}`, @@ -7895,6 +8148,11 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; ` has_vision_image=${!!ctx.has_vision_image} \xB7 images_in_request=${!!ctx.images_in_request} \xB7 vision_ready=${visionReadySlots().length}`, ` context_json_chars\u2248${JSON.stringify(ctx).length} \xB7 last_system_chars=${state.lastSystemChars || "\u2014"} \xB7 last_context_chars=${state.lastContextChars || "\u2014"}`, state.lastSystemLayers ? ` system_layers: ${Object.entries(state.lastSystemLayers).map(([k, v]) => `${k}=${v}`).join(" \xB7 ")}` : " system_layers: \u2014 (\u043E\u0442\u043F\u0440\u0430\u0432\u044C \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435, \u0447\u0442\u043E\u0431\u044B \u0437\u0430\u043F\u043E\u043B\u043D\u0438\u0442\u044C)", + (() => { + const b = currentBudgetEstimate(); + const mem = getContextMemory(); + return ` budget\u2248${b.used}/${b.numCtx} (${b.fromEval ? "eval" : "est"}) \xB7 level=${b.level} \xB7 memory_until=${mem.untilCount || 0} \xB7 auto_compress=${COMPRESS_AUTO}`; + })(), "", "Exact defaults (merged):", ` generation=${JSON.stringify(exactGen)}`, @@ -7938,6 +8196,10 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; setStatus("/history"); return true; } + if (cmd === "compress" || cmd === "compact" || cmd === "\u0441\u0436\u0430\u0442\u044C") { + await compressNowFromUi(); + return true; + } if (cmd === "debug" || cmd === "dbg" || cmd === "why") { const dump = buildDebugSummary(); appendSystemNote(dump); @@ -8077,7 +8339,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; } if (cmd === "pack") { if (!setPackValue(arg, { flash: true, user: true })) { - setStatus("Pack: write|ordinary|critique|compose|params|inpaint|describe|card|persona"); + setStatus("Pack: write|ordinary|critique|compose|params|inpaint|describe|persona"); } else { setStatus(`Pack \u2192 ${$2("sa_pack")?.value}`); } @@ -8091,21 +8353,6 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; await startHornyGame(); return true; } - if (cmd === "civitai") { - if (!arg) { - setStatus("/civitai "); - return true; - } - if ($2("sa_input")) { - $2("sa_input").value = `Find a Krea 2 LoRA for: ${arg}`; - } - setPackValue(defaultPackId(), { flash: true }); - await sendChat({ - skipAutoPack: true, - forcedUserText: `Search Civitai for Krea-compatible LoRA: ${arg}. Prefer actions search_civitai.` - }); - return true; - } if (cmd === "persona") { const sub = (parts[1] || "new").toLowerCase(); const rest = parts.slice(2).join(" ").trim(); @@ -8186,7 +8433,7 @@ ${HELP_TEXT}`); if (!isMachineTurn(opts)) { state.lastUserParamIntent = userTextMentionsParams(text); state.lastUserControlIntent = userTextMentionsControls(text); - state.pendingSilentGen = userImpliesGenerate(text); + state.pendingSilentGen = false; } if (!isMachineTurn(opts) && !opts.skipSlash) { if (rawInput.startsWith("/")) { @@ -8231,8 +8478,8 @@ ${HELP_TEXT}`); setPackValue(guessed, { flash: true }); } } - if (!opts.fromDebug && (opts.fromCards || state.view === "cards")) { - setPackValue("catalog_card", { flash: false }); + if (!opts.fromDebug && false) { + setPackValue("ordinary", { flash: false }); } const pack = opts.fromDebug ? "debug_explain" : $2("sa_pack")?.value || defaultPackId(); const persona = $2("sa_persona")?.value || localStorage.getItem(LS_PERSONA) || "neutral"; @@ -8246,6 +8493,16 @@ ${HELP_TEXT}`); state.busy = true; state.llmParked = false; setInterruptVisible(true); + if (!isContinuationTurn(opts) && !opts.fromAskHop) { + activityBegin(opts.fromDebug ? "Debug" : "\u0425\u043E\u0434 Assistent"); + activityStep("think", { kind: "think", label: "\u0414\u0443\u043C\u0430\u044E\u2026", status: "running" }); + } else if (opts.fromAskHop) { + activityStep("think", { kind: "think", label: "\u041E\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0441 \u0434\u0435\u0442\u0430\u043B\u044F\u043C\u0438\u2026", status: "running" }); + } else if (opts.fromVisionHop) { + activityStep("look", { kind: "look", label: "\u0421\u043C\u043E\u0442\u0440\u0438\u0442 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435\u2026", status: "running" }); + } else if (opts.fromPromptEnRetry) { + activityStep("prep", { kind: "prep", label: "\u0414\u043E\u043F\u0438\u0441\u044B\u0432\u0430\u044E EN-\u043F\u0440\u043E\u043C\u043F\u0442\u2026", status: "running" }); + } if (state.expectColdLoad && !isContinuationTurn(opts)) { startBusyUi("warming"); setStatus("\u0412\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u044E LLM \u0432 GPU\u2026"); @@ -8263,7 +8520,6 @@ ${HELP_TEXT}`); setStatus("\u041E\u0431\u043D\u043E\u0432\u043B\u044F\u044E inventory\u2026"); try { await ensureFreshInventory({ forceRescan: !!opts.fromDownload }); - await prefetchActiveModelCards(); } catch (e) { console.warn("Assistent inventory refresh", e); } @@ -8323,34 +8579,31 @@ ${HELP_TEXT}`); persistHistory(); } } + if (!opts.fromDebug && !opts.fromCompress && !opts.fromAskHop && !opts.fromVisionHop && !opts.fromAutoCritique && !isContinuationTurn(opts)) { + try { + await maybeAutoCompressBeforeSend(chatEpoch); + } catch (e) { + console.warn("Assistent auto-compress", e); + } + if (chatEpoch !== state.chatEpoch) { + return; + } + startBusyUi(state.expectColdLoad ? "loading" : "thinking"); + } const context = collectLiveContext(); context.has_vision_image = visionReadySlots().length > 0; context.images_in_request = !!(images && images.length); context.attached_slot_ids = attachableSlots().map((s) => s.id); context.vision_slot_ids = visionSlots.map((s) => s.id); context.persona = persona; - if (opts.cardTarget) { - context.card_target = opts.cardTarget; - } - if (opts.fromCards || pack === "catalog_card") { - context.auto_apply = false; - context.auto_generate = false; - } - await prefetchActiveModelCards(); if (chatEpoch !== state.chatEpoch) { return; } - const refreshed = collectLiveContext(); - context.model_cards = refreshed.model_cards; - const messages = state.history.slice(-historyMessageLimit()).map((m) => { - let content = String(m.content || ""); - if (m.role === "assistant") { - content = stripJsonFencesForHistory(content); - } - return { role: m.role, content: content.slice(0, 4e3) }; - }); - if (opts.skipAppendUser) { - messages.push({ role: "user", content: text }); + let messages = assembleOutgoingMessages( + opts.skipAppendUser ? { includePendingUser: text } : void 0 + ); + if (!messages.length && text) { + messages = [{ role: "user", content: text }]; } if (images && messages.length) { messages[messages.length - 1].images = images; @@ -8378,11 +8631,19 @@ ${HELP_TEXT}`); if (meta.system_layers && typeof meta.system_layers === "object") { state.lastSystemLayers = meta.system_layers; } + if (meta.prompt_eval_count != null) { + state.lastPromptEvalCount = Number(meta.prompt_eval_count) || null; + const mem = getContextMemory(); + if (mem.summary) { + setContextMemory({ ...mem, promptEvalCount: state.lastPromptEvalCount }, { persist: true }); + } + } try { state.lastContextChars = context && JSON.stringify(context).length || 0; } catch (e) { state.lastContextChars = 0; } + updateCtxChip(); const prose = extractPatch2(reply).prose || reply; state.history.push({ role: "assistant", content: prose, persona, pack }); persistHistory(); @@ -8406,6 +8667,7 @@ ${HELP_TEXT}`); state.busy = false; setInterruptVisible(true); } + updateCtxChip(); } }; const finishErr = (msg) => { @@ -8463,7 +8725,11 @@ ${HELP_TEXT}`); const reply = data.reply || state.streamEl?.querySelector(".sa-msg-body")?.textContent || ""; const civitai = data.civitai_results || []; finalizeStreamMessage(reply, civitai); - finishOk(reply, civitai, { system_chars: data.system_chars, system_layers: data.system_layers }); + finishOk(reply, civitai, { + system_chars: data.system_chars, + system_layers: data.system_layers, + prompt_eval_count: data.prompt_eval_count ?? data.raw?.prompt_eval_count + }); } }, 0, @@ -8490,7 +8756,11 @@ ${HELP_TEXT}`); } const reply = data.reply || ""; appendMessage("assistant", reply, null, data.civitai_results || [], msgMeta); - finishOk(reply, data.civitai_results || [], { system_chars: data.system_chars, system_layers: data.system_layers }); + finishOk(reply, data.civitai_results || [], { + system_chars: data.system_chars, + system_layers: data.system_layers, + prompt_eval_count: data.prompt_eval_count ?? data.raw?.prompt_eval_count + }); }, 0, (err2) => finishErr(String(err2 || err || "Chat failed")) @@ -8512,7 +8782,11 @@ ${HELP_TEXT}`); } const reply = data.reply || ""; appendMessage("assistant", reply, null, data.civitai_results || [], msgMeta); - finishOk(reply, data.civitai_results || [], { system_chars: data.system_chars, system_layers: data.system_layers }); + finishOk(reply, data.civitai_results || [], { + system_chars: data.system_chars, + system_layers: data.system_layers, + prompt_eval_count: data.prompt_eval_count ?? data.raw?.prompt_eval_count + }); }, 0, (err) => finishErr(String(err || "Chat failed")) @@ -8680,12 +8954,10 @@ ${HELP_TEXT}`); loadConfig(localStorage.getItem(LS_PERSONA) || "neutral", () => { refreshModels(); refreshInventory(() => { - renderCardsList(); renderLoraChips(); }); }); probeOllamaHealth(); - refreshWantedQueue(); } function wire() { if (!$2("swarm_assistent_root")) { @@ -8714,8 +8986,36 @@ ${HELP_TEXT}`); wireSplitter(); registerSendButton(); wireSlashInput(); - wireCardForm(); $2("sa_btn_new_chat")?.addEventListener("click", () => startNewChat({ saveCurrent: true })); + $2("sa_ctx_chip")?.addEventListener("click", (e) => { + e.stopPropagation(); + toggleCtxPanel(); + }); + $2("sa_ctx_close")?.addEventListener("click", (e) => { + e.stopPropagation(); + toggleCtxPanel(false); + }); + $2("sa_ctx_panel")?.addEventListener("click", (e) => e.stopPropagation()); + $2("sa_ctx_compress")?.addEventListener("click", () => compressNowFromUi()); + $2("sa_ctx_reset")?.addEventListener("click", () => resetCompressionFromUi()); + $2("sa_ctx_auto")?.addEventListener("change", () => { + COMPRESS_AUTO = !!$2("sa_ctx_auto")?.checked; + const settingsAuto = $2("sa_compress_auto"); + if (settingsAuto) { + settingsAuto.checked = COMPRESS_AUTO; + } + if (state.config?.assistant) { + state.config.assistant.compress_auto = COMPRESS_AUTO; + } + setStatus(COMPRESS_AUTO ? "\u0410\u0432\u0442\u043E\u0441\u0436\u0430\u0442\u0438\u0435 \u0432\u043A\u043B\u044E\u0447\u0435\u043D\u043E" : "\u0410\u0432\u0442\u043E\u0441\u0436\u0430\u0442\u0438\u0435 \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D\u043E"); + }); + $2("sa_compress_auto")?.addEventListener("change", () => { + COMPRESS_AUTO = !!$2("sa_compress_auto")?.checked; + const panelAuto = $2("sa_ctx_auto"); + if (panelAuto) { + panelAuto.checked = COMPRESS_AUTO; + } + }); $2("sa_btn_chats")?.addEventListener("click", (e) => { e.stopPropagation(); setChatsPanelOpen(!state.chatsPanelOpen); @@ -8773,25 +9073,17 @@ ${HELP_TEXT}`); }, 220); }); $2("sa_tab_chat")?.addEventListener("click", () => setView("chat")); - $2("sa_tab_cards")?.addEventListener("click", () => setView("cards")); $2("sa_tab_train")?.addEventListener("click", () => setView("train")); $2("sa_tab_settings")?.addEventListener("click", () => openSettings(state.settingsTab || "behavior")); $2("sa_board_tab_gen")?.addEventListener("click", () => setBoardTab("generate")); $2("sa_board_tab_refs")?.addEventListener("click", () => setBoardTab("refs")); $2("sa_persona")?.addEventListener("change", onPersonaChanged); $2("sa_persona_delete")?.addEventListener("click", () => deleteCurrentOverlayPersona()); - $2("sa_cards_kind")?.addEventListener("change", renderCardsList); - $2("sa_btn_cards_refresh")?.addEventListener("click", () => refreshInventory(() => renderCardsList(), { rescan: true })); - $2("sa_btn_card_meta")?.addEventListener("click", () => fetchCardMetaLive()); - $2("sa_btn_card_generate")?.addEventListener("click", () => generateCardWithAssistent()); - $2("sa_btn_card_save")?.addEventListener("click", () => saveCurrentCard()); - $2("sa_btn_card_wanted")?.addEventListener("click", () => enqueueWantedOnly()); document.querySelectorAll("#sa_settings .sa-stab").forEach((btn) => { btn.addEventListener("click", () => setSettingsTab(btn.getAttribute("data-stab"))); }); $2("sa_btn_mem_refresh")?.addEventListener("click", () => { refreshMemoryList(); - refreshWantedQueue(); }); $2("sa_mem_kind")?.addEventListener("change", renderMemoryList); $2("sa_mem_scope")?.addEventListener("change", renderMemoryList); @@ -8898,7 +9190,6 @@ ${HELP_TEXT}`); probeOllamaHealth(); }); $2("sa_btn_refresh_inventory")?.addEventListener("click", () => refreshInventory(() => { - renderCardsList(); renderLoraChips(); }, { rescan: true })); $2("sa_btn_add_ref")?.addEventListener("click", () => { @@ -8965,17 +9256,13 @@ ${HELP_TEXT}`); closeAllMoreMenus(); clearPatchBlocksOnly(); }); - $2("sa_btn_card_to_chat")?.addEventListener("click", () => { - if (state.cardsSelection) { - sendCardToChat(state.cardsSelection); - } else { - setCardStatus("\u0421\u043D\u0430\u0447\u0430\u043B\u0430 \u0432\u044B\u0431\u0435\u0440\u0438 \u043C\u043E\u0434\u0435\u043B\u044C \u0432 \u0441\u043F\u0438\u0441\u043A\u0435"); - } - }); document.addEventListener("click", () => { if (state.chatsPanelOpen) { setChatsPanelOpen(false); } + if (state.ctxPanelOpen) { + toggleCtxPanel(false); + } closeAllMoreMenus(); }); $2("sa_board_more_menu")?.addEventListener("click", (e) => e.stopPropagation()); @@ -9052,7 +9339,6 @@ ${HELP_TEXT}`); }, 45e3); setInterval(() => { if (!state.busy && !state.generating) { - refreshWantedQueue(); } }, 12e4); window.addEventListener("beforeunload", () => { @@ -9781,6 +10067,9 @@ ${HELP_TEXT}`); attachApi(window.SA); attachPatch(window.SA); attachPersist(window.SA); + attachSession(window.SA); + attachContext(window.SA); + attachActivity(window.SA); window.SA.applyConfigPatchKeys = function(config) { const keys = config?.patch_keys; if (Array.isArray(keys) && keys.length) { diff --git a/Assets/assistent.css b/Assets/assistent.css index 858c162..8c6f635 100644 --- a/Assets/assistent.css +++ b/Assets/assistent.css @@ -118,15 +118,15 @@ } .sa-chats-drawer { - flex: 0 0 var(--sa-chats-drawer-width, 16rem); - width: var(--sa-chats-drawer-width, 16rem); + flex: 0 0 var(--sa-chats-drawer-width, 17.5rem); + width: var(--sa-chats-drawer-width, 17.5rem); display: flex; flex-direction: column; - gap: 0.35rem; + gap: 0.45rem; min-height: 0; - border-left: 1px solid color-mix(in srgb, currentColor 18%, transparent); - background: color-mix(in srgb, currentColor 4%, transparent); - padding: 0.45rem 0.5rem; + border-left: 1px solid color-mix(in srgb, currentColor 12%, transparent); + background: color-mix(in srgb, #000 22%, transparent); + padding: 0.55rem 0.4rem 0.55rem 0.45rem; overflow: hidden; transition: width 0.2s ease, flex-basis 0.2s ease, opacity 0.2s ease; } @@ -140,12 +140,54 @@ align-items: center; justify-content: space-between; gap: 0.35rem; - font-size: 0.82rem; + padding: 0.15rem 0.35rem 0.1rem; +} + +.sa-chats-drawer-label { + font-size: 0.72rem; + font-weight: 650; + letter-spacing: 0.06em; + text-transform: uppercase; + opacity: 0.55; } .sa-chats-drawer-actions { display: inline-flex; - gap: 0.2rem; + gap: 0.1rem; +} + +.sa-chats-icon-btn { + appearance: none; + border: 0; + background: transparent; + color: inherit; + width: 1.65rem; + height: 1.65rem; + border-radius: 0.4rem; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + opacity: 0.55; +} + +.sa-chats-icon-btn:hover { + opacity: 1; + background: color-mix(in srgb, currentColor 10%, transparent); +} + +.sa-chats-search-wrap { + position: relative; + margin: 0 0.2rem; +} + +.sa-chats-search-ico { + position: absolute; + left: 0.55rem; + top: 50%; + transform: translateY(-50%); + opacity: 0.4; + pointer-events: none; } .sa-layout { @@ -555,8 +597,8 @@ box-shadow: none; border-radius: 0; border: none; - border-left: 1px solid color-mix(in srgb, currentColor 18%, transparent); - background: color-mix(in srgb, currentColor 4%, transparent); + border-left: 1px solid color-mix(in srgb, currentColor 12%, transparent); + background: color-mix(in srgb, #000 22%, transparent); } .sa-chats-panel-head { @@ -570,51 +612,67 @@ appearance: none; width: 100%; box-sizing: border-box; - border: 1px solid color-mix(in srgb, currentColor 22%, transparent); - background: color-mix(in srgb, #000 28%, transparent); + border: 1px solid transparent; + background: color-mix(in srgb, currentColor 7%, transparent); color: inherit; - border-radius: 0.35rem; - padding: 0.35rem 0.5rem; + border-radius: 0.5rem; + padding: 0.42rem 0.55rem 0.42rem 1.7rem; font: inherit; - font-size: 0.82rem; + font-size: 0.8rem; } .sa-chats-search:focus { - outline: 1px solid color-mix(in srgb, #6cf 55%, currentColor); + outline: none; + border-color: color-mix(in srgb, currentColor 22%, transparent); + background: color-mix(in srgb, currentColor 10%, transparent); } .sa-chats-panel-hint { - font-size: 0.72rem; - opacity: 0.65; - line-height: 1.35; + display: none; } .sa-chats-list { overflow: auto; display: flex; flex-direction: column; - gap: 0.25rem; + gap: 0.1rem; min-height: 0; + padding: 0.15rem 0.15rem 0.4rem; + flex: 1; } .sa-chats-empty { - font-size: 0.82rem; - opacity: 0.65; - padding: 0.5rem 0.25rem; + font-size: 0.8rem; + opacity: 0.5; + padding: 0.85rem 0.55rem; + line-height: 1.4; } .sa-chat-row { display: flex; - align-items: stretch; - gap: 0.2rem; - border-radius: 0.4rem; - border: 1px solid transparent; - background: color-mix(in srgb, currentColor 5%, transparent); + align-items: center; + gap: 0; + border-radius: 0.5rem; + border: 0; + background: transparent; + position: relative; } .sa-chat-row-active { - border-color: color-mix(in srgb, #6cf 45%, currentColor); - background: color-mix(in srgb, #6cf 12%, transparent); + background: color-mix(in srgb, currentColor 11%, transparent); +} + +.sa-chat-row-active::before { + content: ''; + position: absolute; + left: 0.35rem; + top: 50%; + transform: translateY(-50%); + width: 0.35rem; + height: 0.35rem; + border-radius: 50%; + background: #f59e0b; + box-shadow: 0 0 0 2px color-mix(in srgb, #f59e0b 25%, transparent); } .sa-chat-row-main { @@ -625,27 +683,60 @@ text-align: left; flex: 1; min-width: 0; - padding: 0.4rem 0.5rem; + padding: 0.48rem 0.45rem 0.48rem 0.55rem; cursor: pointer; - display: flex; - flex-direction: column; - gap: 0.12rem; + display: grid; + grid-template-columns: 1.1rem 1fr auto; + align-items: center; + gap: 0.45rem; + border-radius: 0.5rem; +} + +.sa-chat-row-active .sa-chat-row-main { + padding-left: 1.05rem; +} + +.sa-chat-row-main:hover { + background: color-mix(in srgb, currentColor 7%, transparent); +} + +.sa-chat-row-active .sa-chat-row-main:hover { + background: transparent; +} + +.sa-chat-row-ico { + width: 1.05rem; + height: 1.05rem; + opacity: 0.45; + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.sa-chat-row-active .sa-chat-row-ico { + opacity: 0.75; } .sa-chat-row-title { - font-size: 0.85rem; - font-weight: 600; + font-size: 0.84rem; + font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + min-width: 0; } -.sa-chat-row-meta { +.sa-chat-row-active .sa-chat-row-title { + font-weight: 600; +} + +.sa-chat-row-when { font-size: 0.72rem; - opacity: 0.7; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + opacity: 0.42; + font-variant-numeric: tabular-nums; + flex-shrink: 0; + padding-right: 0.15rem; } .sa-chat-row-del { @@ -653,16 +744,28 @@ border: 0; background: transparent; color: inherit; - opacity: 0.55; + opacity: 0; cursor: pointer; - padding: 0 0.55rem; - font-size: 1.1rem; + width: 1.55rem; + height: 1.55rem; + border-radius: 0.35rem; + font-size: 0.95rem; line-height: 1; + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + margin-right: 0.15rem; +} + +.sa-chat-row:hover .sa-chat-row-del { + opacity: 0.45; } .sa-chat-row-del:hover { - opacity: 1; + opacity: 1 !important; color: #f66; + background: color-mix(in srgb, #f66 12%, transparent); } .sa-live-dot { @@ -1213,6 +1316,180 @@ border-color: color-mix(in srgb, #f2777a 45%, transparent); } +.sa-ctx-wrap { + position: relative; + display: inline-flex; + align-items: center; +} + +.sa-ctx-chip { + display: inline-flex; + align-items: center; + gap: 0.3rem; + font-size: 0.72rem; + font-variant-numeric: tabular-nums; + padding: 0.15rem 0.5rem; + border-radius: 0.75rem; + border: 1px solid color-mix(in srgb, currentColor 28%, transparent); + background: color-mix(in srgb, currentColor 6%, transparent); + cursor: pointer; + white-space: nowrap; + color: inherit; + line-height: 1.2; +} + +.sa-ctx-chip:hover { + border-color: color-mix(in srgb, currentColor 45%, transparent); +} + +.sa-ctx-dot { + width: 0.4rem; + height: 0.4rem; + border-radius: 50%; + background: color-mix(in srgb, #6ea8fe 80%, currentColor); + flex-shrink: 0; +} + +.sa-ctx-ok { + border-color: color-mix(in srgb, #6ee7a8 35%, transparent); +} + +.sa-ctx-warn { + color: color-mix(in srgb, #e3b341 85%, currentColor); + border-color: color-mix(in srgb, #e3b341 45%, transparent); +} + +.sa-ctx-hot { + color: color-mix(in srgb, #f2777a 85%, currentColor); + border-color: color-mix(in srgb, #f2777a 50%, transparent); +} + +.sa-ctx-compressing { + color: color-mix(in srgb, #6ea8fe 85%, currentColor); + border-color: color-mix(in srgb, #6ea8fe 50%, transparent); +} + +.sa-ctx-panel { + position: absolute; + top: calc(100% + 0.35rem); + right: 0; + z-index: 40; + width: min(22rem, 78vw); + padding: 0.65rem 0.75rem 0.75rem; + border-radius: 0.55rem; + border: 1px solid color-mix(in srgb, currentColor 22%, transparent); + background: color-mix(in srgb, #1a1d24 92%, transparent); + box-shadow: 0 0.6rem 1.4rem color-mix(in srgb, #000 45%, transparent); + display: flex; + flex-direction: column; + gap: 0.45rem; +} + +.sa-ctx-panel-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.4rem; + font-size: 0.85rem; +} + +.sa-ctx-bar { + height: 0.35rem; + border-radius: 0.25rem; + background: color-mix(in srgb, currentColor 12%, transparent); + overflow: hidden; +} + +.sa-ctx-bar-fill { + height: 100%; + width: 0%; + background: color-mix(in srgb, #6ee7a8 70%, currentColor); + transition: width 0.2s ease; +} + +.sa-ctx-bar-fill[data-level="warn"] { + background: color-mix(in srgb, #e3b341 75%, currentColor); +} + +.sa-ctx-bar-fill[data-level="hot"] { + background: color-mix(in srgb, #f2777a 75%, currentColor); +} + +.sa-ctx-panel-body { + font-size: 0.75rem; + display: flex; + flex-direction: column; + gap: 0.25rem; + max-height: 14rem; + overflow: auto; +} + +.sa-ctx-layer { + display: flex; + justify-content: space-between; + gap: 0.5rem; + opacity: 0.85; +} + +.sa-ctx-layers-label, +.sa-ctx-meta, +.sa-ctx-see { + opacity: 0.65; + font-size: 0.7rem; + margin-top: 0.15rem; +} + +.sa-ctx-summary { + margin: 0.2rem 0 0; + padding: 0.4rem 0.45rem; + font-size: 0.7rem; + white-space: pre-wrap; + word-break: break-word; + max-height: 7rem; + overflow: auto; + border-radius: 0.35rem; + background: color-mix(in srgb, currentColor 8%, transparent); + border: 1px solid color-mix(in srgb, currentColor 12%, transparent); +} + +.sa-ctx-actions { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; +} + +.sa-ctx-auto { + font-size: 0.75rem; +} + +.sa-msg-compress { + margin: 0.35rem 0.55rem; + padding: 0.35rem 0.55rem; + border-radius: 0.45rem; + border: 1px dashed color-mix(in srgb, #6ea8fe 40%, transparent); + background: color-mix(in srgb, #6ea8fe 8%, transparent); + font-size: 0.8rem; +} + +.sa-msg-compress > summary { + cursor: pointer; + font-weight: 600; +} + +.sa-msg-compress-body { + margin-top: 0.4rem; + display: flex; + flex-direction: column; + gap: 0.3rem; +} + +.sa-msg-compress-row { + opacity: 0.8; + font-size: 0.72rem; + white-space: pre-wrap; + word-break: break-word; +} + .sa-card-row-wanted { border-color: color-mix(in srgb, #e3b341 40%, transparent); } @@ -2481,3 +2758,150 @@ pointer-events: none; opacity: 0.45; } + +/* Turn activity timeline (model commands + pipeline) */ +.sa-activity { + align-self: stretch; + margin: 0.35rem 0 0.55rem; + border: 1px solid color-mix(in srgb, currentColor 14%, transparent); + border-radius: 0.65rem; + background: + linear-gradient(180deg, + color-mix(in srgb, currentColor 7%, transparent), + color-mix(in srgb, currentColor 3%, transparent)); + overflow: hidden; + box-shadow: 0 1px 0 color-mix(in srgb, #fff 4%, transparent) inset; +} + +.sa-activity-live { + border-color: color-mix(in srgb, #6af 35%, transparent); +} + +.sa-activity-done { + opacity: 0.92; +} + +.sa-activity-head { + display: flex; + align-items: center; + gap: 0.55rem; + width: 100%; + padding: 0.55rem 0.75rem; + border: 0; + background: transparent; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} + +.sa-activity-head:hover { + background: color-mix(in srgb, currentColor 5%, transparent); +} + +.sa-activity-spin { + width: 0.7rem; + height: 0.7rem; + border-radius: 50%; + border: 1.5px solid color-mix(in srgb, currentColor 25%, transparent); + border-top-color: color-mix(in srgb, #8cf 90%, currentColor); + flex-shrink: 0; +} + +.sa-activity-live .sa-activity-spin { + animation: sa-spin 0.7s linear infinite; +} + +.sa-activity-done .sa-activity-spin { + border-color: color-mix(in srgb, #6c6 55%, transparent); + border-top-color: #6c6; + animation: none; + background: color-mix(in srgb, #6c6 35%, transparent); +} + +.sa-activity-title { + flex: 1; + min-width: 0; + font-size: 0.9rem; + font-weight: 600; + letter-spacing: 0.01em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.sa-activity-chev { + opacity: 0.45; + font-size: 0.75rem; + transition: transform 0.15s ease; +} + +.sa-activity-collapsed .sa-activity-chev { + transform: rotate(-90deg); +} + +.sa-activity-collapsed .sa-activity-steps { + display: none; +} + +.sa-activity-steps { + display: flex; + flex-direction: column; + gap: 0.15rem; + padding: 0 0.55rem 0.6rem 0.75rem; +} + +.sa-activity-step { + display: grid; + grid-template-columns: 1.1rem 1fr; + gap: 0.45rem; + align-items: start; + padding: 0.28rem 0.35rem; + border-radius: 0.4rem; +} + +.sa-activity-step.sa-activity-running { + background: color-mix(in srgb, #6af 10%, transparent); +} + +.sa-activity-step.sa-activity-done { + opacity: 0.85; +} + +.sa-activity-step.sa-activity-skip { + opacity: 0.45; +} + +.sa-activity-step.sa-activity-error { + background: color-mix(in srgb, #c44 12%, transparent); +} + +.sa-activity-icon { + font-size: 0.78rem; + line-height: 1.35; + opacity: 0.7; + text-align: center; +} + +.sa-activity-running .sa-activity-icon { + opacity: 1; + color: color-mix(in srgb, #8cf 80%, currentColor); +} + +.sa-activity-label { + font-size: 0.86rem; + line-height: 1.35; +} + +.sa-activity-detail { + margin-top: 0.1rem; + font-size: 0.78rem; + line-height: 1.35; + opacity: 0.62; + word-break: break-word; +} + +@keyframes sa-spin { + to { transform: rotate(360deg); } +} + diff --git a/AssistentChatPipeline.cs b/AssistentChatPipeline.cs index b84bc41..a6f1418 100644 --- a/AssistentChatPipeline.cs +++ b/AssistentChatPipeline.cs @@ -1,1262 +1,1120 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Newtonsoft.Json.Linq; -using SwarmUI.Accounts; -using SwarmUI.Core; -using SwarmUI.Text2Image; -using SwarmUI.Utils; - -namespace Mrleo1nid.SwarmAssistent; - -/// Prompt assembly, memory retrieval/writeback and the Civitai search hop loop. -public partial class SwarmAssistentExtension -{ - const int MaxCivitaiHopsFallback = 2; - const int MaxToolHopsFallback = 4; - - static bool IsSlimDebugPack(string packName) => - string.Equals(packName, "debug_explain", StringComparison.OrdinalIgnoreCase); - - (List messages, JObject systemLayers) BuildOllamaMessages(string packName, bool includeBase, string contextJson, JArray userMessages, string extraSystem = null, string personaId = null, IEnumerable skillIds = null) - { - List ollamaMessages = []; - StringBuilder system = new(); - JObject layers = new(); - string pid = AssistentConfig.SafeId(personaId) ?? Config.DefaultPersonaId(); - - void AddLayer(string name, string block) - { - if (string.IsNullOrWhiteSpace(block)) - { - return; - } - if (system.Length > 0) - { - system.AppendLine(); - } - int before = system.Length; - system.AppendLine(block.TrimEnd()); - layers[name] = system.Length - before; - } - - if (includeBase) - { - AddLayer("core", Config.LoadCorePrompt(pid)); - } - - bool slimDebug = IsSlimDebugPack(packName); - - if (Memory is not null && !slimDebug) - { - try - { - JObject asst = Config.LoadAssistant(pid); - double weight = asst["user_prefs_weight"]?.Value() ?? 1.0; - int maxPrefs = asst["user_prefs_max"]?.Value() ?? 16; - AddLayer("prefs", Memory.FormatUserPrefsBlock(pid, weight, maxPrefs)); - } - catch (Exception ex) - { - Logs.Debug($"BuildOllamaMessages user prefs: {ex.Message}"); - } - } - - JObject exact = Config.LoadExactForPrompt(pid); - if (exact is not null && exact.Count > 0 && LiveContextHasSize(contextJson)) - { - exact.Remove("aspect_table"); - } - if (exact is not null && exact.Count > 0) - { - AddLayer("exact", - "## Exact memory (canonical KV defaults — prefer over RAG for numbers)\n```json\n" - + exact.ToString(Newtonsoft.Json.Formatting.None) + "\n```"); - } - - if (!slimDebug) - { - StringBuilder skillsBlock = new(); - foreach (string skillId in skillIds ?? Config.ResolveEnabledSkills(pid, null)) - { - string skillText = Config.LoadSkillPrompt(pid, skillId); - if (!string.IsNullOrWhiteSpace(skillText)) - { - if (skillsBlock.Length > 0) - { - skillsBlock.AppendLine(); - } - skillsBlock.AppendLine($"## Skill: {skillId}"); - skillsBlock.AppendLine(skillText.TrimEnd()); - } - } - AddLayer("skills", skillsBlock.ToString()); - AddLayer("identity", Config.RenderIdentityBlock(pid)); - } - - if (!string.IsNullOrWhiteSpace(packName)) - { - string situational = Config.LoadPackPrompt(pid, packName); - if (!string.IsNullOrWhiteSpace(situational)) - { - AddLayer("pack", $"## Active mode: {packName}\n{situational.TrimEnd()}"); - } - } - if (!string.IsNullOrWhiteSpace(contextJson)) - { - AddLayer("live", - "## Live SwarmUI context (JSON — trust this over guesses)\n```json\n" - + contextJson + "\n```"); - } - if (!string.IsNullOrWhiteSpace(extraSystem)) - { - AddLayer("extra", extraSystem); - } - - layers["total"] = system.Length; - if (system.Length > 0) - { - ollamaMessages.Add(new JObject - { - ["role"] = "system", - ["content"] = system.ToString(), - }); - } - foreach (JToken msg in userMessages ?? []) - { - if (msg is not JObject mo) - { - continue; - } - JObject copy = new() - { - ["role"] = mo["role"]?.ToString() ?? "user", - ["content"] = mo["content"]?.ToString() ?? "", - }; - if (mo["images"] is JArray images && images.Count > 0) - { - copy["images"] = images; - } - ollamaMessages.Add(copy); - } - return (ollamaMessages, layers); - } - - async Task<(string reply, JObject raw, JArray civitaiResults, int systemChars, JObject systemLayers)> RunChatWithHops( - Session session, - string root, - string modelName, - string packName, - bool includeBase, - string contextJson, - JArray userMessages, - Func onDelta = null, - Func onHopStart = null, - string personaId = null, - JArray skillIds = null, - string embedModel = null) - { - string pid = AssistentConfig.SafeId(personaId) ?? Config.DefaultPersonaId(); - List skills = Config.ResolveEnabledSkills(pid, skillIds); - string embed = string.IsNullOrWhiteSpace(embedModel) - ? (Config.LoadSettings()["embed_model"]?.ToString() - ?? Config.LoadAssistant(pid)["embed_model"]?.ToString() - ?? "nomic-embed-text") - : embedModel; - - try - { - await Memory.EnsureSeedAsync(root, Config, embed); - } - catch (Exception ex) - { - Logs.Debug($"Assistent memory seed: {ex.Message}"); - } - - bool slimDebug = IsSlimDebugPack(packName); - JArray hits = []; - if (!slimDebug) - { - string retrieveQuery = BuildRetrieveQuery(userMessages, contextJson, packName); - try - { - AssistentMemory.RetrieveOptions opt = MemoryRetrieveOptions(pid); - hits = await Memory.RetrieveAsync(root, retrieveQuery, opt.TopK, embed, Config.PersonaExtendsChain(pid), opt); - hits = FilterHeardHitsIfDisabled(hits); - } - catch (Exception ex) - { - Logs.Debug($"Assistent memory retrieve: {ex.Message}"); - } - } - - string enrichedContext = InjectMemoryHits(contextJson, hits, pid); - if (!slimDebug) - { - enrichedContext = EnrichPersonaContext(enrichedContext, pid, packName); - } - (List messages, JObject systemLayers) = BuildOllamaMessages(packName, includeBase, enrichedContext, userMessages, personaId: pid, skillIds: skills); - int systemChars = systemLayers["total"]?.Value() - ?? messages.FirstOrDefault(m => string.Equals(m["role"]?.ToString(), "system", StringComparison.OrdinalIgnoreCase))?["content"]?.ToString()?.Length - ?? 0; - JArray civitaiResults = []; - string reply = ""; - JObject lastRaw = null; - int maxHops = slimDebug - ? 1 - : Math.Max(CfgInt("max_civitai_hops", MaxCivitaiHopsFallback), CfgInt("max_tool_hops", MaxToolHopsFallback)); - HashSet hopDone = new(StringComparer.OrdinalIgnoreCase); - var chain = Config.PersonaExtendsChain(pid); - for (int hop = 0; hop < maxHops; hop++) - { - if (onHopStart is not null) - { - await onHopStart(hop); - } - (reply, lastRaw) = await CallOllamaChat(root, modelName, messages, stream: onDelta is not null, onDelta, pid); - if (slimDebug) - { - break; - } - JObject patch = TryParsePatch(reply); - await ApplyMemoryActions(root, patch, embed, pid); - ApplyUserPrefActions(patch, pid); - ApplyPersonaActions(patch, ref pid); - if (hop + 1 >= maxHops) - { - break; - } - string follow = null; - JArray civitaiHop = null; - HashSet hopSkip = new(StringComparer.OrdinalIgnoreCase); - while (true) - { - string tool = NextToolHop(patch, hopSkip); - if (string.IsNullOrWhiteSpace(tool)) - { - follow = null; - break; - } - (follow, civitaiHop) = await RunToolHop(session, root, embed, pid, chain, patch, tool, hopDone); - if (follow is not null) - { - break; - } - hopSkip.Add(tool); - } - if (follow is null) - { - break; - } - if (civitaiHop is { Count: > 0 }) - { - civitaiResults = civitaiHop; - } - // Re-feed only the parsed patch JSON (not full prose) to save hop tokens. - string assistantContent = patch is not null - ? patch.ToString(Newtonsoft.Json.Formatting.None) - : reply; - messages.Add(new JObject { ["role"] = "assistant", ["content"] = assistantContent }); - messages.Add(new JObject { ["role"] = "user", ["content"] = follow }); - } - return (reply, lastRaw, civitaiResults, systemChars, systemLayers); - } - - static string BuildRetrieveQuery(JArray userMessages, string contextJson, string packName = null) - { - StringBuilder sb = new(); - if (!string.IsNullOrWhiteSpace(packName)) - { - sb.Append(packName).Append(' '); - } - if (!string.IsNullOrWhiteSpace(contextJson)) - { - try - { - JObject ctx = JObject.Parse(contextJson); - string ckpt = ctx["checkpoint"]?.ToString() ?? ctx["current_model"]?.ToString(); - if (!string.IsNullOrWhiteSpace(ckpt)) - { - sb.Append(ckpt).Append(' '); - } - if (ctx["selected_loras"] is JArray selLoras) - { - foreach (JToken t in selLoras.Take(12)) - { - string n = t?["name"]?.ToString() ?? t?.ToString(); - if (!string.IsNullOrWhiteSpace(n)) - { - sb.Append(n).Append(' '); - } - } - } - else if (ctx["enabled_loras"] is JArray en) - { - foreach (JToken t in en.Take(12)) - { - string n = t?["name"]?.ToString() ?? t?.ToString(); - if (!string.IsNullOrWhiteSpace(n)) - { - sb.Append(n).Append(' '); - } - } - } - if (ctx["krea_profile"] != null) - { - sb.Append("krea ").Append(ctx["krea_profile"]).Append(' '); - } - string aspect = ctx["aspect"]?.ToString(); - if (!string.IsNullOrWhiteSpace(aspect)) - { - sb.Append(aspect).Append(' '); - } - string prompt = ctx["prompt"]?.ToString(); - if (!string.IsNullOrWhiteSpace(prompt)) - { - sb.Append(prompt.Length > 400 ? prompt[..400] : prompt).Append(' '); - } - } - catch - { - // ignore - } - } - foreach (JToken msg in (userMessages ?? []).Reverse().Take(3)) - { - if (msg is JObject mo && string.Equals(mo["role"]?.ToString(), "user", StringComparison.OrdinalIgnoreCase)) - { - string c = mo["content"]?.ToString() ?? ""; - sb.Append(c.Length > 500 ? c[..500] : c).Append(' '); - } - } - string q = CollapseWs(sb.ToString()); - return string.IsNullOrWhiteSpace(q) ? "krea2 prompting" : q; - } - - AssistentMemory.RetrieveOptions MemoryRetrieveOptions(string pid) - { - JObject a = Config.LoadAssistant(pid) ?? new JObject(); - JObject agent = Config.LoadTrainingAgent(); - AssistentMemory.RetrieveOptions opt = new() - { - TopK = a["memory_top_k"]?.Value() ?? 8, - MinScore = a["memory_min_score"]?.Value() ?? 0.32f, - ApplyQuotas = true, - }; - Dictionary quotas = AssistentMemory.CopyDefaultQuotas(); - if (a["memory_quotas"] is JObject qOverrides) - { - foreach (JProperty p in qOverrides.Properties()) - { - quotas[p.Name] = p.Value?.Value() ?? 2; - } - } - if (agent["enabled"]?.Value() != false) - { - quotas["heard"] = agent["heard_quota"]?.Value() ?? 3; - } - else - { - quotas.Remove("heard"); - } - opt.Quotas = quotas; - return opt; - } - - JArray FilterHeardHitsIfDisabled(JArray hits) - { - if (Config.LoadTrainingAgent()["enabled"]?.Value() != false) - { - return hits; - } - JArray filtered = []; - foreach (JToken t in hits ?? []) - { - if (t is JObject ho && string.Equals(ho["kind"]?.ToString(), AssistentMemory.HeardKind, StringComparison.OrdinalIgnoreCase)) - { - continue; - } - filtered.Add(t); - } - return filtered; - } - - async Task<(string follow, JArray civitai)> RunToolHop( - Session session, - string root, - string embed, - string pid, - IEnumerable chain, - JObject patch, - string tool, - HashSet hopDone) - { - if (tool == "memory_get") - { - JArray got = []; - foreach (JToken t in patch["memories"] as JArray ?? []) - { - if (t is not JObject mo) - { - continue; - } - string kind = mo["kind"]?.ToString() ?? "note"; - string key = mo["key"]?.ToString() ?? ""; - if (string.IsNullOrWhiteSpace(key)) - { - continue; - } - string sig = $"get:{kind}:{key}"; - if (!hopDone.Add(sig)) - { - continue; - } - JObject row = Memory.Get(kind, key, chain); - got.Add(row ?? new JObject { ["kind"] = kind, ["key"] = key, ["missing"] = true }); - } - if (got.Count == 0) - { - return (null, null); - } - return ( - "memory_get results (JSON). Use these facts; omit memory_get unless you need a different key.\n```json\n" - + got.ToString(Newtonsoft.Json.Formatting.None) + "\n```", - null); - } - if (tool == "memory_search") - { - string q = ExtractMemoryQuery(patch); - if (string.IsNullOrWhiteSpace(q) || !hopDone.Add("search:" + q)) - { - return (null, null); - } - string kind = patch["memory_kind"]?.ToString(); - int topK = Config.LoadAssistant(pid)["memory_top_k"]?.Value() ?? 8; - JArray rows = await Memory.SearchAsync(root, q, kind, topK, embed, chain); - return ( - "memory_search results (JSON, hybrid FTS+vector). Omit memory_search unless you need a different query.\n```json\n" - + rows.ToString(Newtonsoft.Json.Formatting.None) + "\n```", - null); - } - if (tool == "heard_search") - { - if (Config.LoadTrainingAgent()["enabled"]?.Value() == false) - { - return ("heard_search disabled in training-agent settings.", null); - } - string q = patch["memory_query"]?.ToString()?.Trim() - ?? patch["search_query"]?.ToString()?.Trim() - ?? ExtractMemoryQuery(patch); - if (string.IsNullOrWhiteSpace(q) || !hopDone.Add("heard:" + q)) - { - return (null, null); - } - int topK = Config.LoadTrainingAgent()["heard_quota"]?.Value() ?? 3; - JArray rows = await Memory.SearchAsync(root, q, AssistentMemory.HeardKind, topK, embed, chain); - JArray examples = []; - foreach (JToken t in rows) - { - if (t is JObject ho) - { - JObject ex = Memory.BuildHeardExampleFromHit(ho, chain); - if (ex is not null) - { - examples.Add(ex); - } - } - } - return ( - "heard_search — curated dialogue examples the assistant learned (style/reference, not hard rules). Use tone and structure; omit heard_search unless you need more examples.\n```json\n" - + examples.ToString(Newtonsoft.Json.Formatting.None) + "\n```", - null); - } - if (tool == "lookup_tags") - { - string q = ExtractTagQuery(patch); - if (string.IsNullOrWhiteSpace(q) || !hopDone.Add("tags:" + q)) - { - return (null, null); - } - int lim = Config.LoadAssistant(pid)["tag_lookup_limit"]?.Value() ?? 20; - JArray tags = Memory.LookupTags(q, lim); - return ( - "lookup_tags results from Danbooru csv (canonical name, aliases, post_count). Krea prompts stay natural prose — use this to check spelling/aliases, do not dump tag soup.\n```json\n" - + tags.ToString(Newtonsoft.Json.Formatting.None) + "\n```", - null); - } - if (tool == "list_inventory") - { - string q = patch["inventory_query"]?.ToString()?.Trim() ?? ""; - string sig = "inv:" + q.ToLowerInvariant(); - if (!hopDone.Add(sig)) - { - return (null, null); - } - int lim = Config.LoadAssistant(pid)["inventory_hop_limit"]?.Value() ?? 20; - JArray rows = SearchInventoryForHop(q, lim); - return ( - "list_inventory results (rich LoRA/checkpoint rows). Use exact names + listed triggers; omit list_inventory unless you need a different query.\n```json\n" - + rows.ToString(Newtonsoft.Json.Formatting.None) + "\n```", - null); - } - if (tool == "skill_load") - { - List ids = []; - if (patch["skills"] is JArray skArr) - { - foreach (JToken t in skArr) - { - string sid = AssistentConfig.SafeId(t?.ToString()); - if (!string.IsNullOrWhiteSpace(sid)) - { - ids.Add(sid); - } - } - } - if (ids.Count == 0) - { - ids.Add("memory"); - } - StringBuilder sb = new(); - foreach (string sid in ids) - { - string sig = "skill:" + sid; - if (!hopDone.Add(sig)) - { - continue; - } - string text = Config.LoadSkillPrompt(pid, sid); - if (string.IsNullOrWhiteSpace(text)) - { - continue; - } - sb.AppendLine($"## Skill: {sid}"); - sb.AppendLine(text); - sb.AppendLine(); - } - if (sb.Length == 0) - { - return (null, null); - } - return ( - "skill_load results. Follow these skill rules on the next reply; omit skill_load unless you need another skill.\n\n" - + sb.ToString().TrimEnd(), - null); - } - if (tool == "persona_read") - { - List shelves = []; - if (patch["persona_shelves"] is JArray shArr) - { - foreach (JToken t in shArr) - { - string name = t?.ToString()?.Trim(); - if (!string.IsNullOrWhiteSpace(name)) - { - shelves.Add(name); - } - } - } - else if (patch["persona_shelves"] is JObject shObj) - { - // Model sometimes echoes shelf objects; treat keys as names. - foreach (JProperty p in shObj.Properties()) - { - shelves.Add(p.Name); - } - } - string sig = "persona_read:" + string.Join(",", shelves); - if (!hopDone.Add(sig)) - { - return (null, null); - } - string body = Config.RenderPersonaReadBlock(pid, shelves.Count > 0 ? shelves : null); - if (string.IsNullOrWhiteSpace(body)) - { - return ("persona_read: no additional lore shelves for this persona.", null); - } - return ( - "persona_read results (lore shelves). Use for roleplay/appearance/outfit detail; omit persona_read unless you need different shelves.\n\n" - + body, - null); - } - if (tool == "civitai") - { - string query = ExtractSearchQuery(patch); - if (string.IsNullOrWhiteSpace(query)) - { - if (!hopDone.Add("civitai:missing_query")) - { - return (null, null); - } - return ( - "search_civitai skipped: provide a short search_query (LoRA keywords only). " - + "Never search with the whole user message. Then retry with actions:[\"search_civitai\"] + search_query, " - + "or continue using available_loras only.", - null); - } - if (!hopDone.Add("civitai:" + query)) - { - return (null, null); - } - JObject search = await AssistentSearchCivitai(session, query, 8); - if (search["error"] is not null) - { - return ($"Civitai search failed: {search["error"]}. Continue without download — use only available_loras from context.", null); - } - JArray civitaiResults = search["results"] as JArray ?? []; - return ( - "Civitai search results (JSON). Prefer `krea_likely: true`. Do NOT download yourself — the UI shows Confirm cards. " + - "Pick useful LoRAs from results or available_loras, emit a normal patch (prompt/loras). " + - "Omit search_civitai from actions unless you need a different query.\n```json\n" + - civitaiResults.ToString(Newtonsoft.Json.Formatting.None) + "\n```", - civitaiResults); - } - return (null, null); - } - - /// Substring filter over current LoRA/checkpoint inventory for list_inventory hop. - JArray SearchInventoryForHop(string query, int limit) - { - int lim = Math.Max(1, Math.Min(limit, 40)); - string q = (query ?? "").Trim().ToLowerInvariant(); - JArray outRows = []; - void AddFromHandler(string setName, string kind) - { - if (!Program.T2IModelSets.TryGetValue(setName, out T2IModelHandler handler)) - { - return; - } - IEnumerable models = handler.Models.Values - .OrderByDescending(LooksLikeKreaArch) - .ThenBy(m => m.Name); - foreach (T2IModel model in models) - { - if (outRows.Count >= lim) - { - return; - } - string name = model.Name ?? ""; - if (!string.IsNullOrEmpty(q)) - { - string blob = $"{name} {model.Metadata?.UsageHint} {model.Metadata?.Description}".ToLowerInvariant(); - if (!blob.Contains(q, StringComparison.Ordinal)) - { - continue; - } - } - outRows.Add(BuildInventoryModelEntry(model, kind)); - } - } - AddFromHandler("LoRA", "lora"); - if (outRows.Count < lim) - { - AddFromHandler("Stable-Diffusion", "checkpoint"); - } - return outRows; - } - - string InjectMemoryHits(string contextJson, JArray hits, string personaId = null) - { - JObject ctx; - try - { - ctx = string.IsNullOrWhiteSpace(contextJson) ? new JObject() : JObject.Parse(contextJson); - } - catch - { - ctx = new JObject { ["_raw_context"] = contextJson }; - } - - int hitChars = 240; - try - { - string pidHit = AssistentConfig.SafeId(ctx["persona"]?.ToString()) ?? personaId ?? Config?.DefaultPersonaId() ?? "neutral"; - hitChars = Config?.LoadAssistant(pidHit)?["memory_hit_chars"]?.Value() ?? 240; - } - catch - { - hitChars = 240; - } - hitChars = Math.Max(80, Math.Min(hitChars, 800)); - - string pid = AssistentConfig.SafeId(ctx["persona"]?.ToString()) ?? personaId ?? Config?.DefaultPersonaId() ?? "neutral"; - IEnumerable chain = Config?.PersonaExtendsChain(pid) ?? []; - - JArray clippedHits = []; - JArray heardExamples = []; - foreach (JToken t in hits ?? []) - { - if (t is not JObject ho) - { - continue; - } - if (IsExactPointerHit(ho)) - { - continue; - } - if (string.Equals(ho["kind"]?.ToString(), AssistentMemory.HeardKind, StringComparison.OrdinalIgnoreCase)) - { - JObject ex = Memory?.BuildHeardExampleFromHit(ho, chain); - if (ex is not null) - { - heardExamples.Add(ex); - } - continue; - } - JObject copy = (JObject)ho.DeepClone(); - string text = copy["text"]?.ToString() ?? ""; - if (text.Length > hitChars) - { - copy["text"] = text[..hitChars] + "…"; - copy["truncated"] = true; - } - clippedHits.Add(copy); - } - ctx["memory_hits"] = clippedHits; - if (heardExamples.Count > 0) - { - ctx["heard_examples"] = heardExamples; - } - else - { - ctx.Remove("heard_examples"); - } - ctx.Remove("taste_profile"); - ctx.Remove("enabled_loras"); // alias of selected_loras — do not double-feed - try - { - JObject asst = Config?.LoadAssistant(pid) ?? new JObject(); - double weight = asst["user_prefs_weight"]?.Value() ?? 1.0; - int maxPrefs = asst["user_prefs_max"]?.Value() ?? 16; - ctx["user_prefs_count"] = Memory?.SelectUserPrefsForPrompt(pid, weight, maxPrefs).Count ?? 0; - } - catch - { - ctx["user_prefs_count"] = 0; - } - // Never re-inject full Exact into live context (already in system prompt). - ctx.Remove("exact"); - if (ctx["session_exact"] is JObject se && !se.Properties().Any()) - { - ctx.Remove("session_exact"); - } - - // Always normalize inventory: keep enabled + rich top-N; name-only for the rest. - SlimAvailableLorasInContext(ctx, hits); - DropNullOrEmpty(ctx); - return ctx.ToString(Newtonsoft.Json.Formatting.None); - } - - static bool LiveContextHasSize(string contextJson) - { - if (string.IsNullOrWhiteSpace(contextJson)) - { - return false; - } - try - { - JObject ctx = JObject.Parse(contextJson); - int? w = ctx["width"]?.Value(); - int? h = ctx["height"]?.Value(); - return w is > 0 && h is > 0; - } - catch - { - return false; - } - } - - /// Drop RAG rows that only point at Exact (legacy krea_facts seed / "see Exact memory…"). - static bool IsExactPointerHit(JObject ho) - { - if (ho is null) - { - return false; - } - string key = (ho["key"]?.ToString() ?? "").Trim().ToLowerInvariant(); - if (key.StartsWith("krea2_", StringComparison.Ordinal)) - { - return true; - } - string text = (ho["text"]?.ToString() ?? "").ToLowerInvariant(); - if (string.IsNullOrWhiteSpace(text)) - { - return false; - } - return text.Contains("live in exact memory", StringComparison.Ordinal) - || text.Contains("see exact memory", StringComparison.Ordinal) - || text.Contains("prefer exact kv", StringComparison.Ordinal) - || text.Contains("exact.facts.", StringComparison.Ordinal) - || text.Contains("exact memory profiles.", StringComparison.Ordinal); - } - - void SlimAvailableLorasInContext(JObject ctx, JArray hits) - { - if (ctx["available_loras"] is not JArray allLoras || allLoras.Count == 0) - { - return; - } - int richCap = 12; - int namesCap = 24; - try - { - string pid = AssistentConfig.SafeId(ctx["persona"]?.ToString()) ?? Config?.DefaultPersonaId() ?? "neutral"; - JObject asst = Config?.LoadAssistant(pid) ?? new JObject(); - richCap = asst["inventory_prompt_rich"]?.Value() ?? 12; - namesCap = asst["inventory_prompt_names"]?.Value() ?? 24; - } - catch { /* defaults */ } - richCap = Math.Max(4, Math.Min(richCap, 40)); - namesCap = Math.Max(richCap, Math.Min(namesCap, 80)); - - HashSet keepRich = new(StringComparer.OrdinalIgnoreCase); - if (ctx["enabled_loras"] is JArray en) - { - foreach (JToken t in en) - { - string n = t?["name"]?.ToString() ?? t?.ToString(); - if (!string.IsNullOrWhiteSpace(n)) - { - keepRich.Add(n); - } - } - } - if (ctx["selected_loras"] is JArray sel) - { - foreach (JToken t in sel) - { - string n = t?["name"]?.ToString() ?? t?.ToString(); - if (!string.IsNullOrWhiteSpace(n)) - { - keepRich.Add(n); - } - } - } - foreach (JToken hit in hits ?? []) - { - if (string.Equals(hit?["kind"]?.ToString(), "lora", StringComparison.OrdinalIgnoreCase) - || string.Equals(hit?["kind"]?.ToString(), "card", StringComparison.OrdinalIgnoreCase)) - { - string k = hit?["key"]?.ToString(); - if (!string.IsNullOrWhiteSpace(k)) - { - keepRich.Add(k); - } - } - } - - List ordered = allLoras - .OrderByDescending(t => keepRich.Contains(t?["name"]?.ToString() ?? "") ? 1000 : 0) - .ThenByDescending(t => t?["krea_likely"]?.Value() == true ? 50 : 0) - .ThenBy(t => t?["name"]?.ToString() ?? "", StringComparer.OrdinalIgnoreCase) - .ToList(); - - JArray slim = []; - int richCount = 0; - foreach (JToken t in ordered) - { - if (slim.Count >= namesCap) - { - break; - } - string n = t?["name"]?.ToString(); - if (string.IsNullOrWhiteSpace(n)) - { - continue; - } - bool wantRich = keepRich.Contains(n) || (t?["krea_likely"]?.Value() == true && richCount < richCap); - if (wantRich) - { - JObject rich = EnrichLoraRowForPrompt(t as JObject ?? new JObject { ["name"] = n }); - slim.Add(rich); - if (!keepRich.Contains(n)) - { - richCount++; - } - } - else - { - JObject nameOnly = new() { ["name"] = n }; - if (t?["krea_likely"]?.Value() == true) - { - nameOnly["krea_likely"] = true; - } - slim.Add(nameOnly); - } - } - ctx["available_loras"] = slim; - if (allLoras.Count > slim.Count) - { - ctx["available_loras_truncated"] = true; - // Prefer client total if already set (full disk inventory count). - if (ctx["available_loras_total"] is null) - { - ctx["available_loras_total"] = allLoras.Count; - } - } - } - - /// Ensure rich LoRA rows have triggers/blurb from Swarm inventory when the client sent name-only. - JObject EnrichLoraRowForPrompt(JObject row) - { - if (row is null) - { - return new JObject(); - } - JObject outRow = (JObject)row.DeepClone(); - string name = outRow["name"]?.ToString(); - bool needsTriggers = string.IsNullOrWhiteSpace(outRow["trigger_phrase"]?.ToString()) - && (outRow["triggers"] is not JArray tr || tr.Count == 0); - bool needsBlurb = string.IsNullOrWhiteSpace(outRow["blurb"]?.ToString()); - if (!needsTriggers && !needsBlurb) - { - return outRow; - } - JObject fromDisk = FindInventoryLoraByName(name); - if (fromDisk is null) - { - return outRow; - } - if (needsTriggers) - { - if (!string.IsNullOrWhiteSpace(fromDisk["trigger_phrase"]?.ToString())) - { - outRow["trigger_phrase"] = fromDisk["trigger_phrase"]; - } - if (fromDisk["triggers"] is JArray ft && ft.Count > 0) - { - outRow["triggers"] = ft.DeepClone(); - } - } - if (needsBlurb && !string.IsNullOrWhiteSpace(fromDisk["blurb"]?.ToString())) - { - outRow["blurb"] = fromDisk["blurb"]; - } - if (outRow["default_weight"] is null && fromDisk["default_weight"] is not null) - { - outRow["default_weight"] = fromDisk["default_weight"]; - } - if (fromDisk["krea_likely"]?.Value() == true) - { - outRow["krea_likely"] = true; - } - if (fromDisk["has_card"]?.Value() == true) - { - outRow["has_card"] = true; - } - return outRow; - } - - JObject FindInventoryLoraByName(string name) - { - if (string.IsNullOrWhiteSpace(name) || !Program.T2IModelSets.TryGetValue("LoRA", out T2IModelHandler handler)) - { - return null; - } - T2IModel model = handler.Models.Values.FirstOrDefault(m => - string.Equals(m.Name, name, StringComparison.OrdinalIgnoreCase) - || string.Equals(Path.GetFileNameWithoutExtension(m.Name), Path.GetFileNameWithoutExtension(name), StringComparison.OrdinalIgnoreCase) - || (m.Name?.EndsWith("/" + name, StringComparison.OrdinalIgnoreCase) ?? false)); - return model is null ? null : BuildInventoryModelEntry(model, "lora"); - } - - static void DropNullOrEmpty(JObject ctx) - { - List remove = []; - foreach (JProperty p in ctx.Properties()) - { - if (p.Value is null || p.Value.Type == JTokenType.Null) - { - remove.Add(p.Name); - } - else if (p.Value is JObject jo && !jo.Properties().Any()) - { - remove.Add(p.Name); - } - else if (p.Value is JArray ja && ja.Count == 0 && p.Name is not "memory_hits") - { - remove.Add(p.Name); - } - } - foreach (string k in remove) - { - ctx.Remove(k); - } - } - - string EnrichPersonaContext(string contextJson, string personaId, string packName) - { - JObject ctx; - try - { - ctx = string.IsNullOrWhiteSpace(contextJson) ? new JObject() : JObject.Parse(contextJson); - } - catch - { - ctx = new JObject { ["_raw_context"] = contextJson }; - } - string pid = AssistentConfig.SafeId(personaId) ?? Config.DefaultPersonaId(); - ctx["persona_source"] = Config.PersonaSource(pid); - JObject schema = Config.LoadControlsSchema(pid); - JObject values = Config.LoadControlValues(pid); - bool authorPack = string.Equals(packName, "author_persona", StringComparison.OrdinalIgnoreCase) - || string.Equals(packName, "persona", StringComparison.OrdinalIgnoreCase); - // Values only outside author pack (schema is fat). Author pack gets full schema. - if (values.Properties().Any()) - { - if (authorPack && schema.Properties().Any()) - { - ctx["persona_controls"] = new JObject - { - ["schema"] = schema, - ["values"] = values, - }; - } - else - { - ctx["persona_controls"] = new JObject { ["values"] = values }; - } - } - JArray catalog = []; - if (authorPack) - { - foreach (var p in Config.ListPersonaCatalog()) - { - catalog.Add(new JObject - { - ["id"] = p.id, - ["title"] = p.title, - }); - } - ctx["personas"] = catalog; - JObject shelves = Config.LoadIdentityParts(pid); - shelves.Remove("extra"); - ctx["persona_shelves"] = shelves; - if (schema.Properties().Any()) - { - ctx["persona_controls_schema"] = schema; - } - } - DropNullOrEmpty(ctx); - return ctx.ToString(Newtonsoft.Json.Formatting.None); - } - - static string MemoryWritePersona(JObject mo, string currentPersonaId) - { - string scope = (mo?["scope"]?.ToString() ?? "").Trim().ToLowerInvariant(); - if (scope is "shared" or "common" or "global") - { - return AssistentMemory.SharedPersona; - } - // Personal only — never let the model write into another personality's store. - return AssistentConfig.SafeId(currentPersonaId) ?? AssistentMemory.SharedPersona; - } - - /// Apply overlay persona clone/write from patch. Ignores persona_delete. Updates pid ref after switch. - void ApplyPersonaActions(JObject patch, ref string personaId) - { - if (patch is null || Config is null) - { - return; - } - // Never honor delete from the model. - bool wantClone = false, wantWrite = false; - if (patch["actions"] is JArray acts) - { - foreach (JToken a in acts) - { - string s = a?.ToString() ?? ""; - if (string.Equals(s, "persona_clone", StringComparison.OrdinalIgnoreCase)) - { - wantClone = true; - } - if (string.Equals(s, "persona_write", StringComparison.OrdinalIgnoreCase)) - { - wantWrite = true; - } - } - } - if (patch["persona_clone"] is JObject) - { - wantClone = true; - } - // persona_shelves as object of content = write; as array of names = persona_read hop (ignore here). - if (patch["persona_shelves"] is JObject && !ActionsContain(patch, "persona_read")) - { - wantWrite = true; - } - try - { - if (wantClone && patch["persona_clone"] is JObject clone) - { - string from = AssistentConfig.SafeId(clone["from"]?.ToString()) ?? personaId; - string to = AssistentConfig.SafeId(clone["to"]?.ToString()); - string title = clone["title"]?.ToString(); - bool overwrite = clone["overwrite"]?.Value() == true; - if (to is not null) - { - Config.ClonePersonaToOverlay(from, to, title, overwrite); - personaId = to; - patch["_persona_cloned"] = to; - } - } - if (wantWrite && patch["persona_shelves"] is JObject shelves) - { - string target = AssistentConfig.SafeId(patch["persona"]?.ToString()) - ?? AssistentConfig.SafeId(patch["persona_clone"]?["to"]?.ToString()) - ?? personaId; - if (target is not null) - { - Config.SavePersonaShelves(target, shelves); - patch["_persona_written"] = target; - } - } - // Control values from model patch (Exact). Generate patches never touch sliders. - if (patch["controls"] is JObject ctrlVals) - { - if (AssistentConfig.PatchLooksLikeGeneration(patch)) - { - patch.Remove("controls"); - } - else - { - string ctrlPid = AssistentConfig.SafeId(patch["persona"]?.ToString()) ?? personaId; - JObject schema = Config.LoadControlsSchema(ctrlPid); - JObject current = Config.LoadControlValues(ctrlPid); - JObject filtered = AssistentConfig.FilterEchoedControlDefaults( - schema, current, ctrlVals, patchLooksLikeGen: false); - if (filtered.Count > 0) - { - Config.SaveControlValues(ctrlPid, filtered); - patch["controls"] = filtered; - patch["_controls_saved"] = true; - } - else - { - patch.Remove("controls"); - } - } - } - } - catch (Exception ex) - { - Logs.Warning($"Assistent persona actions: {ex.Message}"); - patch["_persona_error"] = ex.Message; - } - } - - async Task ApplyMemoryActions(string root, JObject patch, string embedModel, string personaId) - { - if (patch is null || Memory is null) - { - return; - } - bool upsert = false, forget = false; - if (patch["actions"] is JArray acts) - { - foreach (JToken a in acts) - { - string s = a?.ToString() ?? ""; - if (string.Equals(s, "memory_upsert", StringComparison.OrdinalIgnoreCase)) - { - upsert = true; - } - if (string.Equals(s, "memory_forget", StringComparison.OrdinalIgnoreCase)) - { - forget = true; - } - } - } - JArray memories = patch["memories"] as JArray; - if (memories is null || memories.Count == 0) - { - return; - } - foreach (JToken t in memories) - { - if (t is not JObject mo) - { - continue; - } - string kind = mo["kind"]?.ToString() ?? "note"; - string key = mo["key"]?.ToString() ?? ""; - string text = mo["text"]?.ToString() ?? ""; - string target = MemoryWritePersona(mo, personaId); - try - { - if (forget && string.IsNullOrWhiteSpace(text)) - { - Memory.Forget(kind, key, persona: target); - } - else if (upsert || !string.IsNullOrWhiteSpace(text)) - { - await Memory.UpsertTextAsync(root, kind, key, text, "user", mo, embedModel, target); - } - } - catch (Exception ex) - { - Logs.Debug($"ApplyMemoryActions: {ex.Message}"); - } - } - } - - void ApplyUserPrefActions(JObject patch, string personaId) - { - if (patch is null || Memory is null) - { - return; - } - bool upsert = false, forget = false; - if (patch["actions"] is JArray acts) - { - foreach (JToken a in acts) - { - string s = a?.ToString() ?? ""; - if (string.Equals(s, "user_pref_upsert", StringComparison.OrdinalIgnoreCase)) - { - upsert = true; - } - if (string.Equals(s, "user_pref_forget", StringComparison.OrdinalIgnoreCase)) - { - forget = true; - } - } - } - JArray prefs = patch["user_prefs"] as JArray; - if (prefs is null || prefs.Count == 0) - { - return; - } - string pid = AssistentConfig.SafeId(personaId) ?? Config.DefaultPersonaId(); - foreach (JToken t in prefs) - { - if (t is not JObject mo) - { - continue; - } - string key = mo["key"]?.ToString() ?? ""; - string text = mo["text"]?.ToString() ?? ""; - string scope = mo["scope"]?.ToString() ?? "global"; - bool pinned = mo["pinned"]?.Value() == true; - try - { - if (forget && string.IsNullOrWhiteSpace(text)) - { - Memory.ForgetUserPref(key, scope, pid); - } - else if (upsert || !string.IsNullOrWhiteSpace(text)) - { - Memory.UpsertUserPref(key, text, scope, pid, "agent", pinned); - } - } - catch (Exception ex) - { - Logs.Debug($"ApplyUserPrefActions: {ex.Message}"); - } - } - } -} +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using SwarmUI.Accounts; +using SwarmUI.Core; +using SwarmUI.Text2Image; +using SwarmUI.Utils; + +namespace Mrleo1nid.SwarmAssistent; + +/// Prompt assembly, memory retrieval, and ask-only server hop loop (settings / inventory). +public partial class SwarmAssistentExtension +{ + const int MaxToolHopsFallback = 2; + + static bool IsSlimUtilityPack(string packName) => + string.Equals(packName, "debug_explain", StringComparison.OrdinalIgnoreCase) + || string.Equals(packName, "compress_history", StringComparison.OrdinalIgnoreCase); + + [Obsolete("Use IsSlimUtilityPack")] + static bool IsSlimDebugPack(string packName) => IsSlimUtilityPack(packName); + + (List messages, JObject systemLayers) BuildOllamaMessages(string packName, bool includeBase, string contextJson, JArray userMessages, string extraSystem = null, string personaId = null, IEnumerable skillIds = null) + { + List ollamaMessages = []; + StringBuilder system = new(); + JObject layers = new(); + string pid = AssistentConfig.SafeId(personaId) ?? Config.DefaultPersonaId(); + + void AddLayer(string name, string block) + { + if (string.IsNullOrWhiteSpace(block)) + { + return; + } + if (system.Length > 0) + { + system.AppendLine(); + } + int before = system.Length; + system.AppendLine(block.TrimEnd()); + layers[name] = system.Length - before; + } + + if (includeBase) + { + AddLayer("core", Config.LoadCorePrompt(pid)); + } + + bool slimUtility = IsSlimUtilityPack(packName); + + if (Memory is not null && !slimUtility) + { + try + { + JObject asst = Config.LoadAssistant(pid); + double weight = asst["user_prefs_weight"]?.Value() ?? 1.0; + int maxPrefs = asst["user_prefs_max"]?.Value() ?? 16; + AddLayer("prefs", Memory.FormatUserPrefsBlock(pid, weight, maxPrefs)); + } + catch (Exception ex) + { + Logs.Debug($"BuildOllamaMessages user prefs: {ex.Message}"); + } + } + + JObject exact = Config.LoadExactForPrompt(pid); + if (exact is not null && exact.Count > 0 && LiveContextHasSize(contextJson)) + { + exact.Remove("aspect_table"); + } + if (exact is not null && exact.Count > 0) + { + AddLayer("exact", + "## Exact memory (canonical KV defaults — prefer over RAG for numbers)\n```json\n" + + exact.ToString(Newtonsoft.Json.Formatting.None) + "\n```"); + } + + if (!slimUtility) + { + StringBuilder skillsBlock = new(); + foreach (string skillId in skillIds ?? Config.ResolveEnabledSkills(pid, null)) + { + string skillText = Config.LoadSkillPrompt(pid, skillId); + if (!string.IsNullOrWhiteSpace(skillText)) + { + if (skillsBlock.Length > 0) + { + skillsBlock.AppendLine(); + } + skillsBlock.AppendLine($"## Skill: {skillId}"); + skillsBlock.AppendLine(skillText.TrimEnd()); + } + } + AddLayer("skills", skillsBlock.ToString()); + AddLayer("identity", Config.RenderIdentityBlock(pid)); + } + + if (!string.IsNullOrWhiteSpace(packName)) + { + string situational = Config.LoadPackPrompt(pid, packName); + if (!string.IsNullOrWhiteSpace(situational)) + { + AddLayer("pack", $"## Active mode: {packName}\n{situational.TrimEnd()}"); + } + } + if (!string.IsNullOrWhiteSpace(contextJson)) + { + AddLayer("live", + "## Live SwarmUI context (JSON — trust this over guesses)\n```json\n" + + contextJson + "\n```"); + } + if (!string.IsNullOrWhiteSpace(extraSystem)) + { + AddLayer("extra", extraSystem); + } + + layers["total"] = system.Length; + if (system.Length > 0) + { + ollamaMessages.Add(new JObject + { + ["role"] = "system", + ["content"] = system.ToString(), + }); + } + foreach (JToken msg in userMessages ?? []) + { + if (msg is not JObject mo) + { + continue; + } + JObject copy = new() + { + ["role"] = mo["role"]?.ToString() ?? "user", + ["content"] = mo["content"]?.ToString() ?? "", + }; + if (mo["images"] is JArray images && images.Count > 0) + { + copy["images"] = images; + } + ollamaMessages.Add(copy); + } + return (ollamaMessages, layers); + } + + async Task<(string reply, JObject raw, JArray civitaiResults, int systemChars, JObject systemLayers)> RunChatWithHops( + Session session, + string root, + string modelName, + string packName, + bool includeBase, + string contextJson, + JArray userMessages, + Func onDelta = null, + Func onHopStart = null, + string personaId = null, + JArray skillIds = null, + string embedModel = null) + { + string pid = AssistentConfig.SafeId(personaId) ?? Config.DefaultPersonaId(); + List skills = Config.ResolveEnabledSkills(pid, skillIds); + string embed = string.IsNullOrWhiteSpace(embedModel) + ? (Config.LoadSettings()["embed_model"]?.ToString() + ?? Config.LoadAssistant(pid)["embed_model"]?.ToString() + ?? "nomic-embed-text") + : embedModel; + + try + { + await Memory.EnsureSeedAsync(root, Config, embed); + } + catch (Exception ex) + { + Logs.Debug($"Assistent memory seed: {ex.Message}"); + } + + bool slimUtility = IsSlimUtilityPack(packName); + JArray hits = []; + if (!slimUtility) + { + string retrieveQuery = BuildRetrieveQuery(userMessages, contextJson, packName); + try + { + AssistentMemory.RetrieveOptions opt = MemoryRetrieveOptions(pid); + hits = await Memory.RetrieveAsync(root, retrieveQuery, opt.TopK, embed, Config.PersonaExtendsChain(pid), opt); + hits = FilterHeardHitsIfDisabled(hits); + } + catch (Exception ex) + { + Logs.Debug($"Assistent memory retrieve: {ex.Message}"); + } + } + + string enrichedContext = InjectMemoryHits(contextJson, hits, pid); + if (!slimUtility) + { + enrichedContext = EnrichPersonaContext(enrichedContext, pid, packName); + } + (List messages, JObject systemLayers) = BuildOllamaMessages(packName, includeBase, enrichedContext, userMessages, personaId: pid, skillIds: skills); + int systemChars = systemLayers["total"]?.Value() + ?? messages.FirstOrDefault(m => string.Equals(m["role"]?.ToString(), "system", StringComparison.OrdinalIgnoreCase))?["content"]?.ToString()?.Length + ?? 0; + string reply = ""; + JObject lastRaw = null; + int maxHops = slimUtility ? 1 : CfgInt("max_tool_hops", MaxToolHopsFallback); + HashSet hopDone = new(StringComparer.OrdinalIgnoreCase); + for (int hop = 0; hop < maxHops; hop++) + { + if (onHopStart is not null) + { + await onHopStart(hop); + } + (reply, lastRaw) = await CallOllamaChat(root, modelName, messages, stream: onDelta is not null, onDelta, pid); + if (slimUtility) + { + break; + } + JObject patch = TryParsePatch(reply); + // 0.14: no ApplyMemoryActions / ApplyUserPrefActions / ApplyPersonaActions / Civitai from server hop loop. + if (hop + 1 >= maxHops) + { + break; + } + string follow = null; + HashSet hopSkip = new(StringComparer.OrdinalIgnoreCase); + while (true) + { + string tool = NextToolHop(patch, hopSkip); + if (string.IsNullOrWhiteSpace(tool)) + { + follow = null; + break; + } + follow = await RunToolHop(session, pid, patch, tool, hopDone); + if (follow is not null) + { + break; + } + hopSkip.Add(tool); + } + if (follow is null) + { + break; + } + // Re-feed only the parsed patch JSON (not full prose) to save hop tokens. + string assistantContent = patch is not null + ? patch.ToString(Newtonsoft.Json.Formatting.None) + : reply; + messages.Add(new JObject { ["role"] = "assistant", ["content"] = assistantContent }); + messages.Add(new JObject { ["role"] = "user", ["content"] = follow }); + } + return (reply, lastRaw, [], systemChars, systemLayers); + } + + static string BuildRetrieveQuery(JArray userMessages, string contextJson, string packName = null) + { + StringBuilder sb = new(); + if (!string.IsNullOrWhiteSpace(packName)) + { + sb.Append(packName).Append(' '); + } + if (!string.IsNullOrWhiteSpace(contextJson)) + { + try + { + JObject ctx = JObject.Parse(contextJson); + string ckpt = ctx["checkpoint"]?.ToString() ?? ctx["current_model"]?.ToString(); + if (!string.IsNullOrWhiteSpace(ckpt)) + { + sb.Append(ckpt).Append(' '); + } + if (ctx["selected_loras"] is JArray selLoras) + { + foreach (JToken t in selLoras.Take(12)) + { + string n = t?["name"]?.ToString() ?? t?.ToString(); + if (!string.IsNullOrWhiteSpace(n)) + { + sb.Append(n).Append(' '); + } + } + } + else if (ctx["enabled_loras"] is JArray en) + { + foreach (JToken t in en.Take(12)) + { + string n = t?["name"]?.ToString() ?? t?.ToString(); + if (!string.IsNullOrWhiteSpace(n)) + { + sb.Append(n).Append(' '); + } + } + } + if (ctx["krea_profile"] != null) + { + sb.Append("krea ").Append(ctx["krea_profile"]).Append(' '); + } + string aspect = ctx["aspect"]?.ToString(); + if (!string.IsNullOrWhiteSpace(aspect)) + { + sb.Append(aspect).Append(' '); + } + string prompt = ctx["prompt"]?.ToString(); + if (!string.IsNullOrWhiteSpace(prompt)) + { + sb.Append(prompt.Length > 400 ? prompt[..400] : prompt).Append(' '); + } + } + catch + { + // ignore + } + } + foreach (JToken msg in (userMessages ?? []).Reverse().Take(3)) + { + if (msg is JObject mo && string.Equals(mo["role"]?.ToString(), "user", StringComparison.OrdinalIgnoreCase)) + { + string c = mo["content"]?.ToString() ?? ""; + sb.Append(c.Length > 500 ? c[..500] : c).Append(' '); + } + } + string q = CollapseWs(sb.ToString()); + return string.IsNullOrWhiteSpace(q) ? "krea2 prompting" : q; + } + + AssistentMemory.RetrieveOptions MemoryRetrieveOptions(string pid) + { + JObject a = Config.LoadAssistant(pid) ?? new JObject(); + JObject agent = Config.LoadTrainingAgent(); + AssistentMemory.RetrieveOptions opt = new() + { + TopK = a["memory_top_k"]?.Value() ?? 8, + MinScore = a["memory_min_score"]?.Value() ?? 0.32f, + ApplyQuotas = true, + }; + Dictionary quotas = AssistentMemory.CopyDefaultQuotas(); + if (a["memory_quotas"] is JObject qOverrides) + { + foreach (JProperty p in qOverrides.Properties()) + { + quotas[p.Name] = p.Value?.Value() ?? 2; + } + } + if (agent["enabled"]?.Value() != false) + { + quotas["heard"] = agent["heard_quota"]?.Value() ?? 3; + } + else + { + quotas.Remove("heard"); + } + opt.Quotas = quotas; + return opt; + } + + JArray FilterHeardHitsIfDisabled(JArray hits) + { + if (Config.LoadTrainingAgent()["enabled"]?.Value() != false) + { + return hits; + } + JArray filtered = []; + foreach (JToken t in hits ?? []) + { + if (t is JObject ho && string.Equals(ho["kind"]?.ToString(), AssistentMemory.HeardKind, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + filtered.Add(t); + } + return filtered; + } + + /// Ask-only server hops: Exact/assistant settings dump or truncated inventory. + async Task RunToolHop( + Session session, + string pid, + JObject patch, + string tool, + HashSet hopDone) + { + if (tool == "ask_settings") + { + if (!hopDone.Add("ask_settings")) + { + return null; + } + JObject dump = BuildAskSettingsDump(pid); + return + "ask:settings dump (Exact + assistant knobs from server). " + + "Live session fields arrive via client compact context. " + + "Reply with a sparse delta JSON only if needed; omit ask:settings unless you need a refresh.\n```json\n" + + dump.ToString(Newtonsoft.Json.Formatting.None) + "\n```"; + } + if (tool == "ask_inventory") + { + string q = patch?["inventory_query"]?.ToString()?.Trim() ?? ""; + string sig = "ask_inventory:" + q.ToLowerInvariant(); + if (!hopDone.Add(sig)) + { + return null; + } + int lim = Config.LoadAssistant(pid)["inventory_hop_limit"]?.Value() ?? 20; + JArray rows; + if (string.IsNullOrWhiteSpace(q)) + { + JObject inv = await AssistentListInventory(session, rescan: false); + rows = TruncateInventoryForAsk(inv, lim); + } + else + { + rows = SearchInventoryForHop(q, lim); + } + return + "ask:inventory truncated LoRA/checkpoint list. Use exact names + listed triggers; " + + "omit ask:inventory unless you need a different query.\n```json\n" + + rows.ToString(Newtonsoft.Json.Formatting.None) + "\n```"; + } + return null; + } + + static JArray TruncateInventoryForAsk(JObject inv, int limit) + { + int lim = Math.Max(1, Math.Min(limit, 40)); + JArray outRows = []; + foreach (JToken t in inv?["loras"] as JArray ?? []) + { + if (outRows.Count >= lim) + { + break; + } + outRows.Add(t); + } + foreach (JToken t in inv?["checkpoints"] as JArray ?? []) + { + if (outRows.Count >= lim) + { + break; + } + outRows.Add(t); + } + return outRows; + } + + JObject BuildAskSettingsDump(string pid) + { + JObject exact = Config.LoadExact(pid) ?? new JObject(); + JObject asst = Config.LoadAssistant(pid) ?? new JObject(); + // Knobs the model may need — not the full assistant.json blob (quotas/seed noise). + JObject knobs = new() + { + ["num_ctx"] = asst["num_ctx"], + ["num_predict"] = asst["num_predict"], + ["max_tool_hops"] = asst["max_tool_hops"], + ["inventory_hop_limit"] = asst["inventory_hop_limit"], + ["max_loras_inventory"] = asst["max_loras_inventory"], + ["max_checkpoints_inventory"] = asst["max_checkpoints_inventory"], + ["max_gen_variants"] = asst["max_gen_variants"], + ["max_ref_slots"] = asst["max_ref_slots"], + ["default_pack"] = asst["default_pack"], + ["default_persona"] = asst["default_persona"], + ["gate"] = asst["gate"], + ["context_prompt_max"] = asst["context_prompt_max"], + ["history_keep_turns"] = asst["history_keep_turns"], + ["compress_at"] = asst["compress_at"], + ["chars_per_token"] = asst["chars_per_token"], + ["compress_auto"] = asst["compress_auto"], + }; + return new JObject + { + ["detail"] = "settings", + ["exact"] = exact, + ["assistant"] = knobs, + ["persona"] = pid, + }; + } + + /// Substring filter over current LoRA/checkpoint inventory for ask:inventory hop. + JArray SearchInventoryForHop(string query, int limit) + { + int lim = Math.Max(1, Math.Min(limit, 40)); + string q = (query ?? "").Trim().ToLowerInvariant(); + JArray outRows = []; + void AddFromHandler(string setName, string kind) + { + if (!Program.T2IModelSets.TryGetValue(setName, out T2IModelHandler handler)) + { + return; + } + IEnumerable models = handler.Models.Values + .OrderByDescending(LooksLikeKreaArch) + .ThenBy(m => m.Name); + foreach (T2IModel model in models) + { + if (outRows.Count >= lim) + { + return; + } + string name = model.Name ?? ""; + if (!string.IsNullOrEmpty(q)) + { + string blob = $"{name} {model.Metadata?.UsageHint} {model.Metadata?.Description}".ToLowerInvariant(); + if (!blob.Contains(q, StringComparison.Ordinal)) + { + continue; + } + } + outRows.Add(BuildInventoryModelEntry(model, kind)); + } + } + AddFromHandler("LoRA", "lora"); + if (outRows.Count < lim) + { + AddFromHandler("Stable-Diffusion", "checkpoint"); + } + return outRows; + } + + string InjectMemoryHits(string contextJson, JArray hits, string personaId = null) + { + JObject ctx; + try + { + ctx = string.IsNullOrWhiteSpace(contextJson) ? new JObject() : JObject.Parse(contextJson); + } + catch + { + ctx = new JObject { ["_raw_context"] = contextJson }; + } + + int hitChars = 240; + try + { + string pidHit = AssistentConfig.SafeId(ctx["persona"]?.ToString()) ?? personaId ?? Config?.DefaultPersonaId() ?? "neutral"; + hitChars = Config?.LoadAssistant(pidHit)?["memory_hit_chars"]?.Value() ?? 240; + } + catch + { + hitChars = 240; + } + hitChars = Math.Max(80, Math.Min(hitChars, 800)); + + string pid = AssistentConfig.SafeId(ctx["persona"]?.ToString()) ?? personaId ?? Config?.DefaultPersonaId() ?? "neutral"; + IEnumerable chain = Config?.PersonaExtendsChain(pid) ?? []; + + JArray clippedHits = []; + JArray heardExamples = []; + foreach (JToken t in hits ?? []) + { + if (t is not JObject ho) + { + continue; + } + if (IsExactPointerHit(ho)) + { + continue; + } + if (string.Equals(ho["kind"]?.ToString(), AssistentMemory.HeardKind, StringComparison.OrdinalIgnoreCase)) + { + JObject ex = Memory?.BuildHeardExampleFromHit(ho, chain); + if (ex is not null) + { + heardExamples.Add(ex); + } + continue; + } + JObject copy = (JObject)ho.DeepClone(); + string text = copy["text"]?.ToString() ?? ""; + if (text.Length > hitChars) + { + copy["text"] = text[..hitChars] + "…"; + copy["truncated"] = true; + } + clippedHits.Add(copy); + } + ctx["memory_hits"] = clippedHits; + if (heardExamples.Count > 0) + { + ctx["heard_examples"] = heardExamples; + } + else + { + ctx.Remove("heard_examples"); + } + ctx.Remove("taste_profile"); + ctx.Remove("enabled_loras"); // alias of selected_loras — do not double-feed + try + { + JObject asst = Config?.LoadAssistant(pid) ?? new JObject(); + double weight = asst["user_prefs_weight"]?.Value() ?? 1.0; + int maxPrefs = asst["user_prefs_max"]?.Value() ?? 16; + ctx["user_prefs_count"] = Memory?.SelectUserPrefsForPrompt(pid, weight, maxPrefs).Count ?? 0; + } + catch + { + ctx["user_prefs_count"] = 0; + } + // Never re-inject full Exact into live context (already in system prompt). + ctx.Remove("exact"); + if (ctx["session_exact"] is JObject se && !se.Properties().Any()) + { + ctx.Remove("session_exact"); + } + + // Always normalize inventory: keep enabled + rich top-N; name-only for the rest. + SlimAvailableLorasInContext(ctx, hits); + DropNullOrEmpty(ctx); + return ctx.ToString(Newtonsoft.Json.Formatting.None); + } + + static bool LiveContextHasSize(string contextJson) + { + if (string.IsNullOrWhiteSpace(contextJson)) + { + return false; + } + try + { + JObject ctx = JObject.Parse(contextJson); + int? w = ctx["width"]?.Value(); + int? h = ctx["height"]?.Value(); + return w is > 0 && h is > 0; + } + catch + { + return false; + } + } + + /// Drop RAG rows that only point at Exact (legacy krea_facts seed / "see Exact memory…"). + static bool IsExactPointerHit(JObject ho) + { + if (ho is null) + { + return false; + } + string key = (ho["key"]?.ToString() ?? "").Trim().ToLowerInvariant(); + if (key.StartsWith("krea2_", StringComparison.Ordinal)) + { + return true; + } + string text = (ho["text"]?.ToString() ?? "").ToLowerInvariant(); + if (string.IsNullOrWhiteSpace(text)) + { + return false; + } + return text.Contains("live in exact memory", StringComparison.Ordinal) + || text.Contains("see exact memory", StringComparison.Ordinal) + || text.Contains("prefer exact kv", StringComparison.Ordinal) + || text.Contains("exact.facts.", StringComparison.Ordinal) + || text.Contains("exact memory profiles.", StringComparison.Ordinal); + } + + void SlimAvailableLorasInContext(JObject ctx, JArray hits) + { + if (ctx["available_loras"] is not JArray allLoras || allLoras.Count == 0) + { + return; + } + int richCap = 12; + int namesCap = 24; + try + { + string pid = AssistentConfig.SafeId(ctx["persona"]?.ToString()) ?? Config?.DefaultPersonaId() ?? "neutral"; + JObject asst = Config?.LoadAssistant(pid) ?? new JObject(); + richCap = asst["inventory_prompt_rich"]?.Value() ?? 12; + namesCap = asst["inventory_prompt_names"]?.Value() ?? 24; + } + catch { /* defaults */ } + richCap = Math.Max(4, Math.Min(richCap, 40)); + namesCap = Math.Max(richCap, Math.Min(namesCap, 80)); + + HashSet keepRich = new(StringComparer.OrdinalIgnoreCase); + if (ctx["enabled_loras"] is JArray en) + { + foreach (JToken t in en) + { + string n = t?["name"]?.ToString() ?? t?.ToString(); + if (!string.IsNullOrWhiteSpace(n)) + { + keepRich.Add(n); + } + } + } + if (ctx["selected_loras"] is JArray sel) + { + foreach (JToken t in sel) + { + string n = t?["name"]?.ToString() ?? t?.ToString(); + if (!string.IsNullOrWhiteSpace(n)) + { + keepRich.Add(n); + } + } + } + foreach (JToken hit in hits ?? []) + { + if (string.Equals(hit?["kind"]?.ToString(), "lora", StringComparison.OrdinalIgnoreCase) + || string.Equals(hit?["kind"]?.ToString(), "card", StringComparison.OrdinalIgnoreCase)) + { + string k = hit?["key"]?.ToString(); + if (!string.IsNullOrWhiteSpace(k)) + { + keepRich.Add(k); + } + } + } + + List ordered = allLoras + .OrderByDescending(t => keepRich.Contains(t?["name"]?.ToString() ?? "") ? 1000 : 0) + .ThenByDescending(t => t?["krea_likely"]?.Value() == true ? 50 : 0) + .ThenBy(t => t?["name"]?.ToString() ?? "", StringComparer.OrdinalIgnoreCase) + .ToList(); + + JArray slim = []; + int richCount = 0; + foreach (JToken t in ordered) + { + if (slim.Count >= namesCap) + { + break; + } + string n = t?["name"]?.ToString(); + if (string.IsNullOrWhiteSpace(n)) + { + continue; + } + bool wantRich = keepRich.Contains(n) || (t?["krea_likely"]?.Value() == true && richCount < richCap); + if (wantRich) + { + JObject rich = EnrichLoraRowForPrompt(t as JObject ?? new JObject { ["name"] = n }); + slim.Add(rich); + if (!keepRich.Contains(n)) + { + richCount++; + } + } + else + { + JObject nameOnly = new() { ["name"] = n }; + if (t?["krea_likely"]?.Value() == true) + { + nameOnly["krea_likely"] = true; + } + slim.Add(nameOnly); + } + } + ctx["available_loras"] = slim; + if (allLoras.Count > slim.Count) + { + ctx["available_loras_truncated"] = true; + // Prefer client total if already set (full disk inventory count). + if (ctx["available_loras_total"] is null) + { + ctx["available_loras_total"] = allLoras.Count; + } + } + } + + /// Ensure rich LoRA rows have triggers/blurb from Swarm inventory when the client sent name-only. + JObject EnrichLoraRowForPrompt(JObject row) + { + if (row is null) + { + return new JObject(); + } + JObject outRow = (JObject)row.DeepClone(); + string name = outRow["name"]?.ToString(); + bool needsTriggers = string.IsNullOrWhiteSpace(outRow["trigger_phrase"]?.ToString()) + && (outRow["triggers"] is not JArray tr || tr.Count == 0); + bool needsBlurb = string.IsNullOrWhiteSpace(outRow["blurb"]?.ToString()); + if (!needsTriggers && !needsBlurb) + { + return outRow; + } + JObject fromDisk = FindInventoryLoraByName(name); + if (fromDisk is null) + { + return outRow; + } + if (needsTriggers) + { + if (!string.IsNullOrWhiteSpace(fromDisk["trigger_phrase"]?.ToString())) + { + outRow["trigger_phrase"] = fromDisk["trigger_phrase"]; + } + if (fromDisk["triggers"] is JArray ft && ft.Count > 0) + { + outRow["triggers"] = ft.DeepClone(); + } + } + if (needsBlurb && !string.IsNullOrWhiteSpace(fromDisk["blurb"]?.ToString())) + { + outRow["blurb"] = fromDisk["blurb"]; + } + if (outRow["default_weight"] is null && fromDisk["default_weight"] is not null) + { + outRow["default_weight"] = fromDisk["default_weight"]; + } + if (fromDisk["krea_likely"]?.Value() == true) + { + outRow["krea_likely"] = true; + } + return outRow; + } + + JObject FindInventoryLoraByName(string name) + { + if (string.IsNullOrWhiteSpace(name) || !Program.T2IModelSets.TryGetValue("LoRA", out T2IModelHandler handler)) + { + return null; + } + T2IModel model = handler.Models.Values.FirstOrDefault(m => + string.Equals(m.Name, name, StringComparison.OrdinalIgnoreCase) + || string.Equals(Path.GetFileNameWithoutExtension(m.Name), Path.GetFileNameWithoutExtension(name), StringComparison.OrdinalIgnoreCase) + || (m.Name?.EndsWith("/" + name, StringComparison.OrdinalIgnoreCase) ?? false)); + return model is null ? null : BuildInventoryModelEntry(model, "lora"); + } + + static void DropNullOrEmpty(JObject ctx) + { + List remove = []; + foreach (JProperty p in ctx.Properties()) + { + if (p.Value is null || p.Value.Type == JTokenType.Null) + { + remove.Add(p.Name); + } + else if (p.Value is JObject jo && !jo.Properties().Any()) + { + remove.Add(p.Name); + } + else if (p.Value is JArray ja && ja.Count == 0 && p.Name is not "memory_hits") + { + remove.Add(p.Name); + } + } + foreach (string k in remove) + { + ctx.Remove(k); + } + } + + string EnrichPersonaContext(string contextJson, string personaId, string packName) + { + JObject ctx; + try + { + ctx = string.IsNullOrWhiteSpace(contextJson) ? new JObject() : JObject.Parse(contextJson); + } + catch + { + ctx = new JObject { ["_raw_context"] = contextJson }; + } + string pid = AssistentConfig.SafeId(personaId) ?? Config.DefaultPersonaId(); + ctx["persona_source"] = Config.PersonaSource(pid); + JObject schema = Config.LoadControlsSchema(pid); + JObject values = Config.LoadControlValues(pid); + bool authorPack = string.Equals(packName, "author_persona", StringComparison.OrdinalIgnoreCase) + || string.Equals(packName, "persona", StringComparison.OrdinalIgnoreCase); + // Values only outside author pack (schema is fat). Author pack gets full schema. + if (values.Properties().Any()) + { + if (authorPack && schema.Properties().Any()) + { + ctx["persona_controls"] = new JObject + { + ["schema"] = schema, + ["values"] = values, + }; + } + else + { + ctx["persona_controls"] = new JObject { ["values"] = values }; + } + } + JArray catalog = []; + if (authorPack) + { + foreach (var p in Config.ListPersonaCatalog()) + { + catalog.Add(new JObject + { + ["id"] = p.id, + ["title"] = p.title, + }); + } + ctx["personas"] = catalog; + JObject shelves = Config.LoadIdentityParts(pid); + shelves.Remove("extra"); + ctx["persona_shelves"] = shelves; + if (schema.Properties().Any()) + { + ctx["persona_controls_schema"] = schema; + } + } + DropNullOrEmpty(ctx); + return ctx.ToString(Newtonsoft.Json.Formatting.None); + } + + static string MemoryWritePersona(JObject mo, string currentPersonaId) + { + string scope = (mo?["scope"]?.ToString() ?? "").Trim().ToLowerInvariant(); + if (scope is "shared" or "common" or "global") + { + return AssistentMemory.SharedPersona; + } + // Personal only — never let the model write into another personality's store. + return AssistentConfig.SafeId(currentPersonaId) ?? AssistentMemory.SharedPersona; + } + + /// Apply overlay persona clone/write from patch. Ignores persona_delete. Updates pid ref after switch. + void ApplyPersonaActions(JObject patch, ref string personaId) + { + if (patch is null || Config is null) + { + return; + } + // Never honor delete from the model. + bool wantClone = false, wantWrite = false; + if (patch["actions"] is JArray acts) + { + foreach (JToken a in acts) + { + string s = a?.ToString() ?? ""; + if (string.Equals(s, "persona_clone", StringComparison.OrdinalIgnoreCase)) + { + wantClone = true; + } + if (string.Equals(s, "persona_write", StringComparison.OrdinalIgnoreCase)) + { + wantWrite = true; + } + } + } + if (patch["persona_clone"] is JObject) + { + wantClone = true; + } + // persona_shelves as object of content = write; as array of names = persona_read hop (ignore here). + if (patch["persona_shelves"] is JObject && !ActionsContain(patch, "persona_read")) + { + wantWrite = true; + } + try + { + if (wantClone && patch["persona_clone"] is JObject clone) + { + string from = AssistentConfig.SafeId(clone["from"]?.ToString()) ?? personaId; + string to = AssistentConfig.SafeId(clone["to"]?.ToString()); + string title = clone["title"]?.ToString(); + bool overwrite = clone["overwrite"]?.Value() == true; + if (to is not null) + { + Config.ClonePersonaToOverlay(from, to, title, overwrite); + personaId = to; + patch["_persona_cloned"] = to; + } + } + if (wantWrite && patch["persona_shelves"] is JObject shelves) + { + string target = AssistentConfig.SafeId(patch["persona"]?.ToString()) + ?? AssistentConfig.SafeId(patch["persona_clone"]?["to"]?.ToString()) + ?? personaId; + if (target is not null) + { + Config.SavePersonaShelves(target, shelves); + patch["_persona_written"] = target; + } + } + // Control values from model patch (Exact). Generate patches never touch sliders. + if (patch["controls"] is JObject ctrlVals) + { + if (AssistentConfig.PatchLooksLikeGeneration(patch)) + { + patch.Remove("controls"); + } + else + { + string ctrlPid = AssistentConfig.SafeId(patch["persona"]?.ToString()) ?? personaId; + JObject schema = Config.LoadControlsSchema(ctrlPid); + JObject current = Config.LoadControlValues(ctrlPid); + JObject filtered = AssistentConfig.FilterEchoedControlDefaults( + schema, current, ctrlVals, patchLooksLikeGen: false); + if (filtered.Count > 0) + { + Config.SaveControlValues(ctrlPid, filtered); + patch["controls"] = filtered; + patch["_controls_saved"] = true; + } + else + { + patch.Remove("controls"); + } + } + } + } + catch (Exception ex) + { + Logs.Warning($"Assistent persona actions: {ex.Message}"); + patch["_persona_error"] = ex.Message; + } + } + + async Task ApplyMemoryActions(string root, JObject patch, string embedModel, string personaId) + { + if (patch is null || Memory is null) + { + return; + } + bool upsert = false, forget = false; + if (patch["actions"] is JArray acts) + { + foreach (JToken a in acts) + { + string s = a?.ToString() ?? ""; + if (string.Equals(s, "memory_upsert", StringComparison.OrdinalIgnoreCase)) + { + upsert = true; + } + if (string.Equals(s, "memory_forget", StringComparison.OrdinalIgnoreCase)) + { + forget = true; + } + } + } + JArray memories = patch["memories"] as JArray; + if (memories is null || memories.Count == 0) + { + return; + } + foreach (JToken t in memories) + { + if (t is not JObject mo) + { + continue; + } + string kind = mo["kind"]?.ToString() ?? "note"; + string key = mo["key"]?.ToString() ?? ""; + string text = mo["text"]?.ToString() ?? ""; + string target = MemoryWritePersona(mo, personaId); + try + { + if (forget && string.IsNullOrWhiteSpace(text)) + { + Memory.Forget(kind, key, persona: target); + } + else if (upsert || !string.IsNullOrWhiteSpace(text)) + { + await Memory.UpsertTextAsync(root, kind, key, text, "user", mo, embedModel, target); + } + } + catch (Exception ex) + { + Logs.Debug($"ApplyMemoryActions: {ex.Message}"); + } + } + } + + void ApplyUserPrefActions(JObject patch, string personaId) + { + if (patch is null || Memory is null) + { + return; + } + bool upsert = false, forget = false; + if (patch["actions"] is JArray acts) + { + foreach (JToken a in acts) + { + string s = a?.ToString() ?? ""; + if (string.Equals(s, "user_pref_upsert", StringComparison.OrdinalIgnoreCase)) + { + upsert = true; + } + if (string.Equals(s, "user_pref_forget", StringComparison.OrdinalIgnoreCase)) + { + forget = true; + } + } + } + JArray prefs = patch["user_prefs"] as JArray; + if (prefs is null || prefs.Count == 0) + { + return; + } + string pid = AssistentConfig.SafeId(personaId) ?? Config.DefaultPersonaId(); + foreach (JToken t in prefs) + { + if (t is not JObject mo) + { + continue; + } + string key = mo["key"]?.ToString() ?? ""; + string text = mo["text"]?.ToString() ?? ""; + string scope = mo["scope"]?.ToString() ?? "global"; + bool pinned = mo["pinned"]?.Value() == true; + try + { + if (forget && string.IsNullOrWhiteSpace(text)) + { + Memory.ForgetUserPref(key, scope, pid); + } + else if (upsert || !string.IsNullOrWhiteSpace(text)) + { + Memory.UpsertUserPref(key, text, scope, pid, "agent", pinned); + } + } + catch (Exception ex) + { + Logs.Debug($"ApplyUserPrefActions: {ex.Message}"); + } + } + } +} diff --git a/AssistentConfig.cs b/AssistentConfig.cs index cce693e..9994e40 100644 --- a/AssistentConfig.cs +++ b/AssistentConfig.cs @@ -138,16 +138,14 @@ public sealed class AssistentConfig return [ "prompt", "negative", "loras", "width", "height", "steps", "cfg", "seed", "sigma_shift", "sampler", "scheduler", - "actions", "search_query", "civitai_query", + "actions", "generate", "ask", "use_init_image", "clear_init_image", "init_creativity", "denoise", "use_mask_image", "clear_mask_image", "mask_blur", "mask_grow", "look_at", "vision_from", "vision_slots", "slot_to_init", "slot_to_mask", "snapshot_generate", "select_slot", "aspect", "images", "batch", "vary", "lock_seed", "creativity", "intensity", "complexity", "movement", - "clear_prompt_images", "slot_to_prompt_image", "pack", "memories", "memory", - "memory_query", "memory_kind", "tag_query", "user_prefs", - "inventory_query", "skills", "persona_shelves", "persona_clone", "persona", "controls", - "variants", + "clear_prompt_images", "slot_to_prompt_image", "pack", "persona", "controls", + "inventory_query", "variants", ]; } diff --git a/AssistentInventory.cs b/AssistentInventory.cs index d334c5e..eb6fc16 100644 --- a/AssistentInventory.cs +++ b/AssistentInventory.cs @@ -2,22 +2,16 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; -using System.Text.RegularExpressions; using System.Threading.Tasks; -using FreneticUtilities.FreneticExtensions; using Newtonsoft.Json.Linq; using SwarmUI.Accounts; using SwarmUI.Core; using SwarmUI.Text2Image; using SwarmUI.Utils; -using SwarmUI.WebAPI; namespace Mrleo1nid.SwarmAssistent; -/// Server-side model inventory, assistant cards and Civitai lookups. +/// Server-side model inventory (LoRA / checkpoint / wildcard lists). public partial class SwarmAssistentExtension { const int MaxLorasInInventoryFallback = 150; @@ -25,444 +19,6 @@ public partial class SwarmAssistentExtension const int MaxCheckpointsInInventoryFallback = 60; const int InventoryBlurbMaxFallback = 140; - 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 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 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); - _ = IngestCardToMemory(card, name); - 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 rawVid = card["version_id"]?.ToString() ?? "draft"; - string vid = Regex.IsMatch(rawVid, @"^\d+$") ? rawVid : "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() ?? 0, card["title"]?.ToString() ?? name, card); - } - _ = IngestCardToMemory(card, name); - return new JObject { ["success"] = true, ["path"] = draft, ["installed"] = false, ["wanted"] = true }; - } - - async Task IngestCardToMemory(JObject card, string name) - { - if (Memory is null || card is null) - { - return; - } - try - { - string kind = (card["kind"]?.ToString() ?? "lora").Trim().ToLowerInvariant(); - string key = (card["name"]?.ToString() ?? name ?? "").Trim(); - List bits = []; - foreach (string field in new[] { "when", "avoid", "prompt_hint", "notes" }) - { - string v = card[field]?.ToString(); - if (!string.IsNullOrWhiteSpace(v)) - { - bits.Add($"{field}: {v.Trim()}"); - } - } - if (card["triggers"] is JArray tr) - { - string joined = string.Join(", ", tr.Select(t => t?.ToString()).Where(s => !string.IsNullOrWhiteSpace(s))); - if (!string.IsNullOrWhiteSpace(joined)) - { - bits.Add("triggers: " + joined); - } - } - if (bits.Count == 0 || string.IsNullOrWhiteSpace(key)) - { - return; - } - string text = $"{kind} {key}. " + string.Join(" ", bits); - string baseUrl = NormalizeBaseUrl(Config.LoadSettings()["base_url"]?.ToString()); - string embedModel = Config.LoadSettings()["embed_model"]?.ToString() - ?? Config.LoadAssistant(Config.DefaultPersonaId())["embed_model"]?.ToString(); - await Memory.UpsertTextAsync(baseUrl, "card", key, text, "user", card, embedModel, AssistentMemory.SharedPersona); - } - catch (Exception ex) - { - Logs.Debug($"IngestCardToMemory: {ex.Message}"); - } - } - - public async Task AssistentGetCardMeta(Session session, string kind, string name, int version_id = 0, bool fetch = false) - { - string set = SetNameForKind(kind); - string weight = ModelWeightPath(set, name); - JObject civitai = null; - JArray exampleUrls = []; - JArray previewUrls = []; - bool hasSidecar = false; - string fetchError = null; - bool fetched = false; - - 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)) - { - hasSidecar = true; - try - { - civitai = JObject.Parse(File.ReadAllText(side, Encoding.UTF8)); - } - catch - { - // ignore - } - } - foreach (string suffix in new[] { ".preview.jpg", ".preview.png", ".preview.jpeg", ".jpg", ".png", ".webp" }) - { - string prev = Path.Combine(dir ?? "", stem + suffix); - if (File.Exists(prev)) - { - // Swarm View path — relative URL works in the same origin browser session. - previewUrls.Add($"View/Models/{(kind == "lora" ? "Lora" : "Stable-Diffusion")}/{Path.GetFileName(prev)}"); - break; - } - } - } - - if (civitai is not null) - { - if (version_id <= 0) - { - version_id = civitai["id"]?.Value() ?? 0; - } - CollectExampleUrls(civitai, exampleUrls); - } - - string hash = null; - string trigger = null; - try - { - if (Program.T2IModelSets.TryGetValue(set, out T2IModelHandler h) - && (h.Models.TryGetValue(name, out T2IModel m) - || h.Models.TryGetValue(name.Replace('\\', '/'), out m))) - { - trigger = m.Metadata?.TriggerPhrase; - hash = m.Metadata?.Hash; - } - } - catch - { - // ignore - } - - if (fetch && civitai is null) - { - string apiKey = session.User.GetGenericData("civitai_api", "key") ?? ""; - if (string.IsNullOrWhiteSpace(apiKey)) - { - fetchError = "Civitai: нет ключа в User Settings"; - } - else - { - try - { - JObject remote = null; - if (version_id > 0) - { - remote = await FetchCivitaiModelVersion(apiKey, version_id); - } - if (remote is null && !string.IsNullOrWhiteSpace(hash)) - { - string sha = hash.Trim().ToLowerInvariant(); - if (sha.StartsWith("sha256:")) - { - sha = sha["sha256:".Length..]; - } - if (sha.Length == 64) - { - remote = await FetchCivitaiByHash(apiKey, sha); - } - else - { - fetchError ??= "Civitai: хеш модели не SHA256"; - } - } - if (remote is not null) - { - civitai = remote; - fetched = true; - version_id = remote["id"]?.Value() ?? version_id; - CollectExampleUrls(remote, exampleUrls); - if (!string.IsNullOrWhiteSpace(weight)) - { - try - { - string stem = Path.GetFileNameWithoutExtension(weight); - string dir = Path.GetDirectoryName(weight); - string side = Path.Combine(dir ?? "", $"{stem}.civitai.json"); - File.WriteAllText(side, remote.ToString(Newtonsoft.Json.Formatting.Indented), Encoding.UTF8); - hasSidecar = true; - } - catch (Exception ex) - { - Logs.Debug($"AssistentGetCardMeta write sidecar: {ex.Message}"); - } - } - } - else if (fetchError is null) - { - fetchError = string.IsNullOrWhiteSpace(hash) - ? "Civitai: нет hash и version_id" - : "Хеш не найден на Civitai"; - } - } - catch (Exception ex) - { - fetchError = $"Civitai: {ex.Message}"; - } - } - } - - JObject card = ReadCardObject(kind, name); - return new JObject - { - ["success"] = true, - ["kind"] = kind, - ["name"] = name, - ["version_id"] = version_id, - ["trigger_phrase"] = trigger, - ["has_card"] = card is not null, - ["has_sidecar"] = hasSidecar, - ["fetched"] = fetched, - ["fetch_error"] = fetchError, - ["card"] = card, - ["civitai"] = civitai, - ["example_urls"] = exampleUrls, - ["preview_urls"] = previewUrls, - ["weight_path"] = weight, - ["hash"] = hash, - }; - } - - static void CollectExampleUrls(JObject civitai, JArray exampleUrls) - { - if (civitai?["images"] is not JArray imgs) - { - return; - } - foreach (JToken img in imgs.Take(6)) - { - string u = img?["url"]?.ToString(); - if (!string.IsNullOrWhiteSpace(u)) - { - exampleUrls.Add(u); - } - } - } - - async Task FetchCivitaiByHash(string apiKey, string sha) - { - string[] hosts = ["civitai.red", "civitai.com"]; - Exception last = null; - foreach (string host in hosts) - { - try - { - string url = $"https://{host}/api/v1/model-versions/by-hash/{sha}"; - using HttpRequestMessage req = new(HttpMethod.Get, url); - if (!string.IsNullOrWhiteSpace(apiKey)) - { - req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey.Trim()); - } - using HttpResponseMessage resp = await HttpClient.SendAsync(req); - string body = await resp.Content.ReadAsStringAsync(); - if (resp.StatusCode == System.Net.HttpStatusCode.NotFound) - { - continue; - } - if (!resp.IsSuccessStatusCode) - { - last = new Exception($"HTTP {(int)resp.StatusCode}: {Clip(body, 160)}"); - if ((int)resp.StatusCode is 401 or 403) - { - throw last; - } - continue; - } - return JObject.Parse(body); - } - catch (Exception ex) when (ex is not HttpRequestException && ex.Message.Contains("401")) - { - throw; - } - catch (Exception ex) - { - last = ex; - } - } - if (last is not null) - { - throw last; - } - return null; - } - - async Task FetchCivitaiModelVersion(string apiKey, int versionId) - { - string[] hosts = ["civitai.red", "civitai.com"]; - Exception last = null; - foreach (string host in hosts) - { - try - { - string url = $"https://{host}/api/v1/model-versions/{versionId}"; - using HttpRequestMessage req = new(HttpMethod.Get, url); - if (!string.IsNullOrWhiteSpace(apiKey)) - { - req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey.Trim()); - } - using HttpResponseMessage resp = await HttpClient.SendAsync(req); - string body = await resp.Content.ReadAsStringAsync(); - if (!resp.IsSuccessStatusCode) - { - last = new Exception($"HTTP {(int)resp.StatusCode}: {Clip(body, 160)}"); - if ((int)resp.StatusCode is 401 or 403) - { - throw last; - } - continue; - } - return JObject.Parse(body); - } - catch (Exception ex) - { - last = ex; - if (ex.Message.Contains("401") || ex.Message.Contains("403")) - { - throw; - } - } - } - if (last is not null) - { - throw last; - } - return null; - } - /// Server-side LoRA / checkpoint / wildcard inventory (not DOM scrape). /// Pass rescan=true after downloads so new files appear (calls Program.RefreshAllModelSets). public async Task AssistentListInventory(Session session, bool rescan = false) @@ -553,8 +109,6 @@ public partial class SwarmAssistentExtension { 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; @@ -571,29 +125,10 @@ public partial class SwarmAssistentExtension } string blurb = null; - if (hasCard) + string raw = !string.IsNullOrWhiteSpace(usage) ? usage : desc; + if (!string.IsNullOrWhiteSpace(raw)) { - 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(), CfgInt("inventory_blurb_max", InventoryBlurbMaxFallback)); - } - } - catch - { - // ignore bad card json - } - } - if (string.IsNullOrWhiteSpace(blurb)) - { - string raw = !string.IsNullOrWhiteSpace(usage) ? usage : desc; - if (!string.IsNullOrWhiteSpace(raw)) - { - blurb = Clip(CollapseWs(raw), CfgInt("inventory_blurb_max", InventoryBlurbMaxFallback)); - } + blurb = Clip(CollapseWs(raw), CfgInt("inventory_blurb_max", InventoryBlurbMaxFallback)); } JArray tags = null; @@ -618,7 +153,6 @@ public partial class SwarmAssistentExtension ["architecture"] = model.ModelClass?.ID, ["compat_class"] = model.ModelClass?.CompatClass?.ID, ["hash"] = model.Metadata?.Hash ?? "", - ["has_card"] = hasCard, ["krea_likely"] = LooksLikeKreaArch(model), }; if (!string.IsNullOrWhiteSpace(weight)) @@ -665,165 +199,4 @@ public partial class SwarmAssistentExtension } return entry; } - - /// Search Civitai for LoRAs (prefers Krea 2 base). Uses Swarm-stored civitai_api key. - public async Task AssistentSearchCivitai(Session session, string query, int limit = 8) - { - string q = (query ?? "").Trim(); - if (string.IsNullOrWhiteSpace(q)) - { - return new JObject { ["error"] = "query is required" }; - } - limit = Math.Clamp(limit, 1, 20); - string apiKey = session.User.GetGenericData("civitai_api", "key") ?? ""; - HashSet installedNames = CollectInstalledLoraNames(); - HashSet installedHashes = CollectInstalledLoraHashes(); - - string[] hosts = ["civitai.red", "civitai.com"]; - Exception lastEx = null; - foreach (string host in hosts) - { - try - { - string url = $"https://{host}/api/v1/models?limit={limit}&types=LORA&query={Uri.EscapeDataString(q)}"; - using HttpRequestMessage req = new(HttpMethod.Get, url); - if (!string.IsNullOrWhiteSpace(apiKey)) - { - req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey.Trim()); - } - using HttpResponseMessage resp = await HttpClient.SendAsync(req); - string body = await resp.Content.ReadAsStringAsync(); - if (!resp.IsSuccessStatusCode) - { - lastEx = new Exception($"HTTP {(int)resp.StatusCode}: {Clip(body, 200)}"); - continue; - } - JObject parsed = JObject.Parse(body); - JArray items = parsed["items"] as JArray ?? []; - JArray results = []; - foreach (JToken item in items) - { - if (item is not JObject mo) - { - continue; - } - JObject card = BuildCivitaiCard(mo, installedNames, installedHashes); - if (card is not null) - { - results.Add(card); - } - } - // Prefer Krea-compatible first - JArray sorted = new(results.OrderByDescending(t => LooksLikeKrea(t["base_model"]?.ToString())).ThenBy(t => t["name"]?.ToString())); - return new JObject - { - ["success"] = true, - ["query"] = q, - ["host"] = host, - ["results"] = sorted, - ["has_civitai_key"] = !string.IsNullOrWhiteSpace(apiKey), - }; - } - catch (Exception ex) - { - lastEx = ex; - } - } - return new JObject { ["error"] = $"Civitai search failed: {lastEx?.Message ?? "unknown"}" }; - } - - static bool LooksLikeKrea(string text) => !string.IsNullOrEmpty(text) && Regex.IsMatch(text, @"krea", RegexOptions.IgnoreCase); - - static HashSet CollectInstalledLoraNames() - { - HashSet names = new(StringComparer.OrdinalIgnoreCase); - if (!Program.T2IModelSets.TryGetValue("LoRA", out T2IModelHandler handler)) - { - return names; - } - foreach (T2IModel m in handler.Models.Values) - { - names.Add(m.Name); - string leaf = m.Name.Replace('\\', '/').AfterLast('/'); - if (!string.IsNullOrEmpty(leaf)) - { - names.Add(leaf); - names.Add(Path.GetFileNameWithoutExtension(leaf)); - } - } - return names; - } - - static HashSet CollectInstalledLoraHashes() - { - HashSet hashes = new(StringComparer.OrdinalIgnoreCase); - if (!Program.T2IModelSets.TryGetValue("LoRA", out T2IModelHandler handler)) - { - return hashes; - } - foreach (T2IModel m in handler.Models.Values) - { - string h = m.Metadata?.Hash; - if (!string.IsNullOrWhiteSpace(h)) - { - hashes.Add(h.Trim().ToLowerInvariant()); - } - } - return hashes; - } - - static JObject BuildCivitaiCard(JObject model, HashSet installedNames, HashSet installedHashes) - { - string name = model["name"]?.ToString() ?? ""; - JArray versions = model["modelVersions"] as JArray; - JObject ver = versions?.FirstOrDefault() as JObject; - if (ver is null) - { - return null; - } - string baseModel = ver["baseModel"]?.ToString() ?? ""; - JArray trained = ver["trainedWords"] as JArray ?? []; - List triggers = trained.Select(t => t?.ToString()).Where(s => !string.IsNullOrWhiteSpace(s)).Take(8).ToList(); - JObject file = null; - foreach (JToken f in ver["files"] as JArray ?? []) - { - if (f is JObject fo && (fo["primary"]?.Value() == true || (fo["name"]?.ToString() ?? "").EndsWith(".safetensors", StringComparison.OrdinalIgnoreCase))) - { - file = fo; - break; - } - } - file ??= (ver["files"] as JArray)?.FirstOrDefault() as JObject; - string downloadUrl = file?["downloadUrl"]?.ToString() ?? ver["downloadUrl"]?.ToString() ?? ""; - string fileName = file?["name"]?.ToString() ?? ""; - string sha = file?["hashes"]?["SHA256"]?.ToString() ?? file?["hashes"]?["AutoV2"]?.ToString() ?? ""; - string saveName = string.IsNullOrWhiteSpace(fileName) - ? Regex.Replace(name, @"[^\w\-.]+", "_").Trim('_') - : Path.GetFileNameWithoutExtension(fileName); - - bool already = false; - if (!string.IsNullOrWhiteSpace(sha) && installedHashes.Contains(sha.Trim().ToLowerInvariant())) - { - already = true; - } - else if (installedNames.Contains(saveName) || installedNames.Contains(name) || installedNames.Contains(fileName)) - { - already = true; - } - - return new JObject - { - ["id"] = model["id"], - ["version_id"] = ver["id"], - ["name"] = name, - ["base_model"] = baseModel, - ["krea_likely"] = LooksLikeKrea(baseModel), - ["triggers"] = new JArray(triggers), - ["download_url"] = downloadUrl, - ["file_name"] = saveName, - ["sha256"] = sha, - ["already_installed"] = already, - ["n_sfw"] = model["nsfw"]?.Value() ?? false, - }; - } } diff --git a/AssistentMemoryApi.cs b/AssistentMemoryApi.cs index 7ad1f69..3e8761f 100644 --- a/AssistentMemoryApi.cs +++ b/AssistentMemoryApi.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Text; using System.Threading.Tasks; using Newtonsoft.Json.Linq; using SwarmUI.Accounts; @@ -10,7 +8,7 @@ using SwarmUI.Utils; namespace Mrleo1nid.SwarmAssistent; -/// Read/write routes for the vector memory list in ⚙ and the gpu-rent wanted queue badge. +/// Read/write routes for the vector memory list in ⚙. public partial class SwarmAssistentExtension { /// Embed model the UI should use: settings overlay wins, then persona assistant.json. @@ -274,44 +272,4 @@ public partial class SwarmAssistentExtension return new JObject { ["error"] = $"memory clear: {ex.Message}" }; } } - - /// The gpu-rent wanted queue (models pending the next up) — count + entries. - public async Task AssistentListWanted(Session session) - { - await Task.CompletedTask; - string path = WantedModelsPath(); - JArray items = []; - if (!File.Exists(path)) - { - return new JObject { ["success"] = true, ["count"] = 0, ["items"] = items, ["path"] = path }; - } - try - { - Dictionary> sections = LoadWantedYaml(File.ReadAllText(path, Encoding.UTF8)); - foreach ((string kind, List list) in sections.OrderBy(p => p.Key, StringComparer.OrdinalIgnoreCase)) - { - foreach (WantedEntry entry in list) - { - items.Add(new JObject - { - ["kind"] = kind, - ["url"] = entry.Url, - ["title"] = entry.Title, - ["version_id"] = entry.VersionId, - }); - } - } - return new JObject - { - ["success"] = true, - ["count"] = items.Count, - ["items"] = items, - ["path"] = path, - }; - } - catch (Exception ex) - { - return new JObject { ["error"] = $"wanted queue: {ex.Message}" }; - } - } } diff --git a/AssistentOllama.cs b/AssistentOllama.cs index 0d5600c..35f4f4c 100644 --- a/AssistentOllama.cs +++ b/AssistentOllama.cs @@ -297,7 +297,22 @@ public partial class SwarmAssistentExtension return null; } - /// Proxy to Ollama /api/chat (non-stream), with optional Civitai search hop. + /// Extract Ollama prompt token count when present. + static int? ReadPromptEvalCount(JObject raw) + { + if (raw is null) + { + return null; + } + int? n = raw["prompt_eval_count"]?.Value(); + if (n is null || n <= 0) + { + n = raw["promptEvalCount"]?.Value(); + } + return n is > 0 ? n : null; + } + + /// Proxy to Ollama /api/chat (non-stream), with optional ask hops. public async Task AssistentChat(Session session, string baseUrl, string model, string pack, bool includeBase, JObject raw) { ExtractChatPayload(raw, ref baseUrl, ref model, ref pack, ref includeBase, out JArray userMessages, out string contextJson, out string persona, out JArray skills); @@ -314,7 +329,7 @@ public partial class SwarmAssistentExtension { (string reply, JObject parsed, JArray civitai, int systemChars, JObject systemLayers) = await RunChatWithHops( session, root, modelName, packName, includeBase, contextJson, userMessages, personaId: persona, skillIds: skills, embedModel: embedModel); - return new JObject + JObject result = new() { ["success"] = true, ["reply"] = reply, @@ -326,6 +341,12 @@ public partial class SwarmAssistentExtension ["system_chars"] = systemChars, ["system_layers"] = systemLayers, }; + int? promptEval = ReadPromptEvalCount(parsed); + if (promptEval is not null) + { + result["prompt_eval_count"] = promptEval.Value; + } + return result; } catch (Exception ex) { @@ -333,7 +354,7 @@ public partial class SwarmAssistentExtension } } - /// WebSocket streaming chat (Ollama stream:true) + Civitai hops. + /// WebSocket streaming chat (Ollama stream:true) + ask hops. public async Task AssistentChatWS(Session session, WebSocket ws, string baseUrl, string model, string pack, bool includeBase, JObject raw) { ExtractChatPayload(raw, ref baseUrl, ref model, ref pack, ref includeBase, out JArray userMessages, out string contextJson, out string persona, out JArray skills); @@ -372,13 +393,13 @@ public partial class SwarmAssistentExtension { ["clear_stream"] = true, ["hop"] = hop + 1, - ["notice"] = "Civitai search done — refining…", + ["notice"] = "Ask hop — refining…", }, API.WebsocketTimeout); } } (string reply, JObject parsed, JArray civitai, int systemChars, JObject systemLayers) = await RunChatWithHops( session, root, modelName, packName, includeBase, contextJson, userMessages, OnDelta, OnHopStart, persona, skills, embedModel); - await ws.SendJson(new JObject + JObject done = new() { ["success"] = true, ["done"] = true, @@ -390,7 +411,13 @@ public partial class SwarmAssistentExtension ["civitai_results"] = civitai, ["system_chars"] = systemChars, ["system_layers"] = systemLayers, - }, API.WebsocketTimeout); + }; + int? promptEval = ReadPromptEvalCount(parsed); + if (promptEval is not null) + { + done["prompt_eval_count"] = promptEval.Value; + } + await ws.SendJson(done, API.WebsocketTimeout); } catch (Exception ex) { diff --git a/AssistentPatch.cs b/AssistentPatch.cs index f975ae9..2beb7ae 100644 --- a/AssistentPatch.cs +++ b/AssistentPatch.cs @@ -46,28 +46,25 @@ public partial class SwarmAssistentExtension patch["look_at"] = patch["vision_slots"]; } } + if (ActionsContain(patch, "generate")) + { + patch["generate"] = true; + } + if (patch["ask"] is JValue askVal && askVal.Type == JTokenType.String) + { + string one = askVal.ToString()?.Trim(); + if (!string.IsNullOrWhiteSpace(one)) + { + patch["ask"] = new JArray(one); + } + else + { + patch.Remove("ask"); + } + } return patch; } - static bool LooksLikeCardObject(JObject obj) - { - if (obj is null) - { - return false; - } - bool cardish = HasValue(obj, "kind") || HasValue(obj, "triggers") || HasValue(obj, "when") || HasValue(obj, "prompt_hint"); - bool genish = HasValue(obj, "prompt") || HasValue(obj, "negative") || HasValue(obj, "loras") || HasValue(obj, "actions") - || HasValue(obj, "width") || HasValue(obj, "height") || HasValue(obj, "steps") || HasValue(obj, "cfg") - || HasValue(obj, "aspect") || HasValue(obj, "seed") || HasValue(obj, "search_query") - || HasValue(obj, "civitai_query") || HasValue(obj, "look_at") || HasValue(obj, "controls"); - if (cardish && !genish && (HasValue(obj, "name") || HasValue(obj, "triggers") || HasValue(obj, "when"))) - { - return true; - } - return HasValue(obj, "kind") && HasValue(obj, "name") - && (HasValue(obj, "triggers") || HasValue(obj, "when") || HasValue(obj, "prompt_hint") || HasValue(obj, "notes")); - } - JObject TryParsePatch(string reply) { if (string.IsNullOrWhiteSpace(reply)) @@ -82,7 +79,7 @@ public partial class SwarmAssistentExtension try { JObject obj = JObject.Parse(raw); - if (obj is null || LooksLikeCardObject(obj)) + if (obj is null) { continue; } @@ -90,7 +87,7 @@ public partial class SwarmAssistentExtension { JObject normalized = NormalizePatch(obj); lastAny = normalized; - if (FenceIsTerminalPatch(obj)) + if (FenceIsTerminalPatch(normalized)) { lastTerminal = normalized; } @@ -105,10 +102,9 @@ public partial class SwarmAssistentExtension } /// - /// If the reply already contains a closed fenced patch/card that is "done enough" to act on, + /// If the reply already contains a closed fenced patch that is "done enough" to act on, /// 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. + /// often emit a tiny JSON first then the real prompt fence; aborting early cuts the prompt. /// static bool TryTruncateAtCompleteFence(string reply, out string truncated) { @@ -128,7 +124,7 @@ public partial class SwarmAssistentExtension string raw = match.Groups[1].Value.Trim(); try { - JObject obj = JObject.Parse(raw); + JObject obj = NormalizePatch(JObject.Parse(raw)); if (obj is null || !FenceIsTerminalPatch(obj)) { continue; @@ -145,7 +141,7 @@ public partial class SwarmAssistentExtension } /// - /// True when a closed fence is worth aborting the Ollama stream (real deliverable or tool hop). + /// True when a closed fence is worth aborting the Ollama stream (real deliverable or ask hop). /// static bool FenceIsTerminalPatch(JObject obj) { @@ -153,7 +149,11 @@ public partial class SwarmAssistentExtension { return false; } - if (LooksLikeCardObject(obj)) + if (obj["generate"]?.Type == JTokenType.Boolean && obj["generate"].Value()) + { + return true; + } + if (HasAsk(obj)) { return true; } @@ -165,50 +165,18 @@ public partial class SwarmAssistentExtension { return true; } - if (HasValue(obj, "search_query") || HasValue(obj, "civitai_query")) + if (ActionsContain(obj, "generate")) { 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("heard_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")) + || HasValue(obj, "seed") || HasValue(obj, "controls")) { return true; } @@ -216,14 +184,40 @@ public partial class SwarmAssistentExtension return false; } - static string ExtractSearchQuery(JObject patch) + static bool HasAsk(JObject patch) { - if (patch is null) + if (patch?["ask"] is JArray asks) { - return null; + foreach (JToken t in asks) + { + if (!string.IsNullOrWhiteSpace(t?.ToString())) + { + return true; + } + } + return false; } - string q = (patch["search_query"] ?? patch["civitai_query"])?.ToString()?.Trim(); - return string.IsNullOrWhiteSpace(q) ? null : q; + return !string.IsNullOrWhiteSpace(patch?["ask"]?.ToString()); + } + + static bool AskContains(JObject patch, string name) + { + if (patch is null || string.IsNullOrWhiteSpace(name)) + { + return false; + } + if (patch["ask"] is JArray asks) + { + foreach (JToken t in asks) + { + if (string.Equals(t?.ToString()?.Trim(), name, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + return false; + } + return string.Equals(patch["ask"]?.ToString()?.Trim(), name, StringComparison.OrdinalIgnoreCase); } static bool ActionsContain(JObject patch, string action) @@ -242,26 +236,7 @@ public partial class SwarmAssistentExtension return false; } - static string ExtractMemoryQuery(JObject patch) - { - string q = patch?["memory_query"]?.ToString()?.Trim(); - if (!string.IsNullOrWhiteSpace(q)) - { - return q; - } - return ActionsContain(patch, "memory_search") ? ExtractSearchQuery(patch) : null; - } - - static string ExtractTagQuery(JObject patch) - { - string q = patch?["tag_query"]?.ToString()?.Trim(); - if (!string.IsNullOrWhiteSpace(q)) - { - return q; - } - return ActionsContain(patch, "lookup_tags") ? ExtractSearchQuery(patch) : null; - } - + /// Server tool hops are ask-only: settings dump or truncated inventory. static string NextToolHop(JObject patch, HashSet skip = null) { if (patch is null) @@ -269,42 +244,13 @@ public partial class SwarmAssistentExtension return null; } bool Skip(string tool) => skip is not null && skip.Contains(tool); - if (ActionsContain(patch, "memory_get") && !Skip("memory_get")) + if (AskContains(patch, "settings") && !Skip("ask_settings")) { - return "memory_get"; + return "ask_settings"; } - if ((ActionsContain(patch, "memory_search") || !string.IsNullOrWhiteSpace(patch["memory_query"]?.ToString())) - && !Skip("memory_search")) + if (AskContains(patch, "inventory") && !Skip("ask_inventory")) { - return "memory_search"; - } - if ((ActionsContain(patch, "heard_search") || string.Equals(patch["heard_query"]?.ToString(), "1", StringComparison.Ordinal)) - && !Skip("heard_search")) - { - return "heard_search"; - } - if ((ActionsContain(patch, "lookup_tags") || !string.IsNullOrWhiteSpace(patch["tag_query"]?.ToString())) - && !Skip("lookup_tags")) - { - return "lookup_tags"; - } - if ((ActionsContain(patch, "list_inventory") || !string.IsNullOrWhiteSpace(patch["inventory_query"]?.ToString())) - && !Skip("list_inventory")) - { - return "list_inventory"; - } - if (ActionsContain(patch, "skill_load") && !Skip("skill_load")) - { - return "skill_load"; - } - if (ActionsContain(patch, "persona_read") && !Skip("persona_read")) - { - return "persona_read"; - } - if ((ActionsContain(patch, "search_civitai") || !string.IsNullOrWhiteSpace(ExtractSearchQuery(patch))) - && !Skip("civitai")) - { - return "civitai"; + return "ask_inventory"; } return null; } diff --git a/AssistentPersonaApi.cs b/AssistentPersonaApi.cs index d199ce2..70d4c97 100644 --- a/AssistentPersonaApi.cs +++ b/AssistentPersonaApi.cs @@ -190,7 +190,10 @@ public partial class SwarmAssistentExtension if (assistant is not null && assistant.Count > 0) { JObject sparse = new(); - foreach (string key in new[] { "num_ctx", "history_keep_turns", "memory_top_k", "user_prefs_weight", "user_prefs_max" }) + foreach (string key in new[] { + "num_ctx", "history_keep_turns", "memory_top_k", "user_prefs_weight", "user_prefs_max", + "compress_at", "chars_per_token", "compress_auto", "num_predict", + }) { if (assistant[key] is not null) { diff --git a/AssistentWanted.cs b/AssistentWanted.cs deleted file mode 100644 index d7940a0..0000000 --- a/AssistentWanted.cs +++ /dev/null @@ -1,189 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Newtonsoft.Json.Linq; -using SwarmUI.Accounts; - -namespace Mrleo1nid.SwarmAssistent; - -/// Queue of models the assistant wants downloaded (merged into gpu-rent models.yaml on next up/capture). -public partial class SwarmAssistentExtension -{ - static readonly object WantedFileLock = new(); - - string WantedModelsPath() => Path.Combine(DataRoot(), ".gpu-rent-wanted-models.yaml"); - - string WantedCardsDir() => Path.Combine(DataRoot(), ".gpu-rent-wanted-cards"); - - public async Task 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()); - lock (WantedFileLock) - { - Dictionary> sections = LoadWantedYaml(File.Exists(path) ? File.ReadAllText(path, Encoding.UTF8) : ""); - - if (version_id > 0) - { - foreach (List 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 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 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> LoadWantedYaml(string raw) - { - Dictionary> 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 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> 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 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 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(); - } -} diff --git a/Config/_base/assistant.json b/Config/_base/assistant.json index 21f9aa9..99938ee 100644 --- a/Config/_base/assistant.json +++ b/Config/_base/assistant.json @@ -1,7 +1,6 @@ { "num_ctx": 16384, "num_predict": 3072, - "max_civitai_hops": 2, "max_loras_inventory": 150, "max_checkpoints_inventory": 60, "max_wildcards_inventory": 80, @@ -19,7 +18,7 @@ "memory_min_score": 0.32, "user_prefs_weight": 1.0, "user_prefs_max": 16, - "max_tool_hops": 4, + "max_tool_hops": 2, "tag_lookup_limit": 20, "identity_always_shelves": ["persona", "voice", "rules", "likes", "dislikes"], "memory_quotas": { @@ -36,5 +35,8 @@ "keywords": ["krea"] }, "context_prompt_max": 2000, - "history_keep_turns": 4 + "history_keep_turns": 4, + "compress_at": 0.70, + "chars_per_token": 3.2, + "compress_auto": true } diff --git a/Config/_base/core/core.md b/Config/_base/core/core.md index 93d3ce3..5a3aeb1 100644 --- a/Config/_base/core/core.md +++ b/Config/_base/core/core.md @@ -10,84 +10,63 @@ 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). 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. -4. **Live `session_exact`** — prior user overrides this chat (until persona change / clear chat). +4. **Chat session** (`## Live SwarmUI context` / session JSON) — current generation settings for **this chat** (prompt, params, LoRAs, board). Source of truth for Generate. 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. -7. **`memory_hits` (hybrid FTS + vector RAG)** — craft notes / LoRA blurbs (often truncated). Prefer over guesses; never override Exact, About the user, or the user’s 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. +6. 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. Session = what Generate will run. Soft craft notes may appear in context from the server — never override Exact, About the user, or the user’s param request. -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 2–4 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. +**Sparse deltas only.** Do **not** re-emit `steps` / `cfg` / `sigma_shift` / `aspect` / full `prompt` when they already match the session and the user did not ask to change them. -## Live context +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. Chat prose may stay RU; **`prompt` must be English** (Krea 2). Keep prose short. **After the closing ``` of the JSON fence, STOP.** **One turn = one patch.** Several options → `variants` (2–4). Prompt structure lives in skill `prompting`. -"Live SwarmUI context" JSON is ground truth for this turn: +## Live context (chat session) -- 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. -- `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. **Do not** `look_at` just because a frame exists. Emit `look_at` only when you cannot continue without pixels (user asked to look/critique/describe/compare, or a defect you cannot infer from the prompt). Never invent what the image looks like. A new Generate / «ещё» / prompt edit does **not** need vision. -- Prefer `krea_likely` / Krea architecture; ignore FLUX/SDXL. Respect current params unless asked or pack is `fix_params`. -- Init/inpaint flags and `image_slots` are in the JSON. Extra pack fields are documented in the active pack. +Compact session JSON is ground truth for this turn: -## Memory (short) - -- 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`. -- Fat memory skill text: `skill_load` + `skills: ["memory"]` when you need the full write/read playbook. +- `selected_loras` / checkpoint — use only names present there (or after `ask:["inventory"]`). +- Board: `board.has_generate`, ref ids — for `look_at` only when you need pixels. +- For full numeric/Exact dump: `"ask": ["settings"]`. For LoRA/ckpt list: `"ask": ["inventory"]`. ## Output contract (mandatory) 1. Short helpful reply in the user's language (RU or EN). -2. **Frame turns only:** one fenced JSON patch with **only fields you want to change**. Chat / Q&A / opinion / remember: **prose only — omit the JSON patch.** +2. **Only when changing settings or commanding generate/look/ask:** one fenced JSON with **only fields you want to change** (+ optional commands). Pure chat / Q&A / opinion: **prose only — omit the JSON.** ```json { "prompt": "A fluffy red fox in fresh powder snow, soft morning light, 85mm f/2.8…", - "negative": "bad quality, worst quality", - "loras": [{"name": "exact_name_from_list", "weight": 0.8, "triggers": ["listed_trigger"]}], "aspect": "16:9", - "actions": ["generate"], - "notes": "one-line why" + "generate": true } ``` -Several options in one ask (still one fence): +Several options (still one fence): ```json { "prompt": "same subject base…", - "aspect": "16:9", - "actions": ["generate"], + "generate": true, "variants": [ { "label": "warm light", "prompt": "… warm window light …" }, - { "label": "cool light", "prompt": "… cool moonlight …" }, - { "label": "portrait 9:16", "aspect": "9:16" } + { "label": "cool light", "prompt": "… cool moonlight …" } ] } ``` ### Patch rules -- Omit unchanged **params** (`steps`/`cfg`/`sigma_shift`/`aspect`). For Generate, still include **`negative`**: create if live is empty, lightly supplement if the scene needs a specific omit, or echo the live/Exact box unchanged — never drop it. -- 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. -- 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. -- **`controls`** — only keys declared in this persona's `controls.json` (Exact). Clamp to min/max. Example: `"controls": { "horny": 55 }`. Do not invent control ids. +- Omit unchanged params. Include `negative` when starting Generate if live/session negative is empty or the scene needs a specific omit. +- `loras` replaces the full intended set for this chat when you change LoRAs. +- Prefer `aspect` over raw width/height. +- Optional: seed, vary, init/mask, controls, pack, `variants`. - Do not invent model or LoRA filenames. -### Actions / hops +### Commands -- `"generate"` — Apply + start generation when this turn is a **new/updated frame** (they described a shot, asked to draw/edit/«ещё», or clearly want to see a result). They do **not** have to type «генерируй». **Do not** emit `generate` for chat, opinions («нравится»), trivia, look/critique without a redraw, remember/save, describe_ref, Cards/authoring. Chat-only turns: prose, no JSON patch (or prompt-only without `actions`). -- 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). Opt-in: user asked, or you truly need pixels. Do not pair `look_at` with `actions:["generate"]` on a normal write turn (that stares at the *old* frame and delays the new one). +- `"generate": true` — merge this delta into the chat session and run Generate (new/updated frame). **Omit** for chat, opinions, remember/save, look-only. Legacy `actions:["generate"]` is accepted as the same. +- If the user says not to generate / only remember / only answer — **omit** `generate` and do not look. +- `"look_at": ["generate"|"ref1"|…]` — vision hop (JPEG on follow-up). Use when you need pixels; do not pair with `generate` on the same normal write turn. +- `"ask": ["settings"]` — request full settings dump (Exact + all fields). +- `"ask": ["inventory"]` — request LoRA/checkpoint list. - Pure Q&A: omit the JSON patch. diff --git a/Config/_base/packs/catalog_card.json b/Config/_base/packs/catalog_card.json deleted file mode 100644 index f5b1097..0000000 --- a/Config/_base/packs/catalog_card.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "id": "catalog_card", - "title": "Карточка модели", - "order": 70, - "aliases": ["card", "catalog"], - "prompt_file": "catalog_card.md" -} diff --git a/Config/_base/packs/catalog_card.md b/Config/_base/packs/catalog_card.md deleted file mode 100644 index 973c0d4..0000000 --- a/Config/_base/packs/catalog_card.md +++ /dev/null @@ -1,34 +0,0 @@ -# Mode: catalog_card - -Goal: write a **recommendation card** for one checkpoint or LoRA so future Assistent turns know how to use it. - -## Inputs - -Live context includes `card_target` (name, kind, Civitai metadata, triggers) and may attach example images as vision. - -## Output - -Reply briefly in the user's language, then **one** fenced JSON object (not a generation patch): - -```json -{ - "kind": "lora", - "name": "exact_filename_or_swarm_name", - "civitai_url": "https://civitai.red/models/…?modelVersionId=…", - "version_id": 123, - "triggers": ["exact", "from", "metadata"], - "weight": 0.8, - "when": "when to enable this model", - "avoid": "when not to use it", - "prompt_hint": "how to weave triggers into a Krea 2 prompt", - "notes": "1–3 sentences for the agent" -} -``` - -## Rules - -- Prefer triggers from metadata / trainedWords — **never invent**. -- `weight` typical 0.6–1.0 for LoRA; omit or 1.0 for checkpoints. -- Do **not** emit `actions: ["generate"]`. This mode does not start Generate. -- Do not invent other LoRAs. Stay on the single `card_target`. -- Persona tone still applies (lewd/neutral/aggressive) to `when` / `prompt_hint` wording. diff --git a/Config/_base/packs/compose_scene.md b/Config/_base/packs/compose_scene.md index 490f9ea..10c2bcf 100644 --- a/Config/_base/packs/compose_scene.md +++ b/Config/_base/packs/compose_scene.md @@ -9,7 +9,7 @@ Goal: co-create a scene / moodboard direction for **Krea 2** (local Swarm). - **Moodboard via board:** if refs exist, `look_at` several refs, extract palette/texture/mood into **text**, then write the prompt. Prefer text distillation over dumping refs as Prompt Images. - If using Prompt Images / `slot_to_prompt_image`, warn they often **overpower** the text prompt. - Suggest available LoRAs only from the live list, with triggers. -- Missing style LoRA → `search_civitai` + `search_query` (Krea-compatible). +- Missing style LoRA → ask the user or use `"ask": ["inventory"]` for names already on disk (Krea-compatible). - Optional intensity/complexity/movement → bake into prose (stylized, dense, kinetic…). ## Deliverable diff --git a/Config/_base/packs/compress_history.json b/Config/_base/packs/compress_history.json new file mode 100644 index 0000000..b3f4e06 --- /dev/null +++ b/Config/_base/packs/compress_history.json @@ -0,0 +1,9 @@ +{ + "id": "compress_history", + "title": "Compress history", + "order": 998, + "hidden": true, + "enabled": true, + "aliases": ["compress_history", "compress"], + "prompt_file": "compress_history.md" +} diff --git a/Config/_base/packs/compress_history.md b/Config/_base/packs/compress_history.md new file mode 100644 index 0000000..27b2381 --- /dev/null +++ b/Config/_base/packs/compress_history.md @@ -0,0 +1,32 @@ +# Mode: compress_history (hidden) + +You compress older chat turns into a rolling memory for the next Assistent turns. This is not a generation turn and not a Q&A with the user. + +## Hard rules + +- Write in the **user's language** (match the dialogue). +- **No** fenced JSON. **No** `### JSON Patch`. **No** `actions`. **No** `generate`. **No** `look_at`. **No** tool hops. +- Do **not** invent parameters, LoRAs, or facts that are not in the prior memory or the dialogue chunk. +- Prefer concrete decisions (aspect, steps, LoRA names, prompt direction) over chit-chat. +- Keep the whole reply under ~600 words. + +## Input + +You receive: +1. Optional **previous conversation memory** (already compressed). +2. A chunk of **older user/assistant turns** that must be folded into memory. +3. Recent turns may be omitted — they stay as raw history. + +## Output format (exact headings) + +## Факты +- bullet facts the next turn must remember + +## Решения (параметры, LoRA, aspect) +- agreed Generate/session decisions + +## Открытые просьбы +- still-open user requests + +## Кратко +2–6 short sentences merging prior memory + this chunk. diff --git a/Config/_base/packs/ordinary.md b/Config/_base/packs/ordinary.md index f325556..6e64c28 100644 --- a/Config/_base/packs/ordinary.md +++ b/Config/_base/packs/ordinary.md @@ -1,31 +1,30 @@ # Mode: ordinary (комбайн) -Default all-rounder. Handle this turn from the user message + live context — do **not** wait for a specialized pack. +Default all-rounder. Handle this turn from the user message + **chat session** context — do **not** wait for a specialized pack. ## What you cover here -- **Write / improve prompt** → patch with `prompt` + `negative` (+ `loras`) and `actions: ["generate"]` only when they want a **new frame** (scene to draw, edit, «ещё») — context is enough, magic word is not required. Chat / «нравится» / Q&A → prose only, **no** generate. Do **not** `look_at` the last frame first. `negative` on Generate: create / supplement / echo live. -- **Light critique / improve last frame** → only when the user asks to look / critique / describe the picture. Then `look_at: ["generate"]` if `images_in_request` is false. Otherwise edit the prompt from text; `has_vision_image` alone is not a reason to look. -- **Scene / mood** → compose direction into the prompt (same patch rules). -- **Params** → only when they ask (steps/CFG/aspect/seed); omit Exact-matching numbers otherwise. -- **Inpaint / img2img** → set init/mask fields when they ask and flags allow; else say what is missing. -- **Describe a ref** → only with a real attached / look_at frame. +- **Write / improve prompt** → sparse JSON with only changed fields (`prompt`, optional `negative`/`loras`) and `"generate": true` when they want a **new frame**. Chat / «нравится» / Q&A → prose only, **no** JSON. Do **not** `look_at` the last frame first. +- **Light critique** → only when they ask to look / critique. Then `look_at: ["generate"]` if pixels are not already in the request. +- **Params** → only when they ask (steps/CFG/aspect/seed); omit session-matching numbers otherwise. +- **Inpaint / img2img** → set init/mask fields when they ask. +- Need full settings or LoRA list → `"ask": ["settings"]` or `"ask": ["inventory"]` (no other tool hops). Prompt prose recipe = skill `prompting`. Creativity sliders = skill `creativity_sliders`. ## When to leave this mode -Emit `"pack": ""` in the JSON patch only if the user clearly needs a dedicated workflow: +Emit `"pack": ""` only if they clearly need a dedicated workflow: - `critique_image` — deep frame critique loop - `inpaint_edit` — regional edit / mask workflow -- `catalog_card` / `author_persona` — Cards or persona authoring +- `author_persona` — persona authoring - `describe_ref` — reverse-prompt a reference at length -Otherwise **stay in ordinary** and just do the work. +Otherwise **stay in ordinary**. ## Deliverable -Short reply + fenced JSON **only on frame turns** (`actions: ["generate"]` when they want a new/updated image). Chat/Q&A/opinion/remember: prose only — no fence. -«давай дальше» / next frame = new English `prompt` + `negative` (echo live if unchanged) + `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` (2–4 partial patches with `label`); still one fence, still STOP after it. +Short reply + fenced JSON **only when changing session fields or commanding generate/look/ask**. Chat/Q&A: prose only. +«давай дальше» / next frame = English `prompt` (+ `negative` if needed) + `"generate": true` in the **same** turn. +Several options → `variants` (2–4); still one fence, still STOP after it. diff --git a/Config/_base/patch-keys.json b/Config/_base/patch-keys.json index b1ed1b2..5f4a647 100644 --- a/Config/_base/patch-keys.json +++ b/Config/_base/patch-keys.json @@ -1,15 +1,13 @@ { "keys": [ "prompt", "negative", "loras", "width", "height", "steps", "cfg", "seed", "sigma_shift", "sampler", "scheduler", - "actions", "search_query", "civitai_query", + "actions", "generate", "ask", "use_init_image", "clear_init_image", "init_creativity", "denoise", "use_mask_image", "clear_mask_image", "mask_blur", "mask_grow", "look_at", "vision_from", "vision_slots", "slot_to_init", "slot_to_mask", "snapshot_generate", "select_slot", "aspect", "images", "batch", "vary", "lock_seed", "creativity", "intensity", "complexity", "movement", - "clear_prompt_images", "slot_to_prompt_image", "pack", "memories", "memory", - "memory_query", "memory_kind", "tag_query", "user_prefs", - "inventory_query", "skills", "persona_shelves", "persona_clone", "persona", "controls", - "variants" + "clear_prompt_images", "slot_to_prompt_image", "pack", "persona", "controls", + "inventory_query", "variants" ] } diff --git a/Config/_base/skills/prompting.md b/Config/_base/skills/prompting.md index 5973247..ec7e06e 100644 --- a/Config/_base/skills/prompting.md +++ b/Config/_base/skills/prompting.md @@ -12,15 +12,16 @@ The **chat model** prepares the Generate-box text for **Krea 2** (Qwen3-VL). Do 5. **`negative` on every Generate** — create if live is empty (Exact `generation.negative`), supplement if the scene needs a specific omit, or echo live unchanged. Put “no blur / no people” ideas as positives in `prompt` instead of stuffing the negative box. Never clear `negative`. 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"]`) +## Prep checklist (before `"generate": true`) - English only in `prompt` -- `negative` present (new / supplemented / echoed — not omitted) +- `negative` present when needed (new / supplemented / echoed — not omitted if live empty) - 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 +- Only changed fields in the JSON — session already holds the rest ## Deliverable diff --git a/Config/_base/ui.json b/Config/_base/ui.json index b01b531..ad854fd 100644 --- a/Config/_base/ui.json +++ b/Config/_base/ui.json @@ -1,6 +1,6 @@ { - "welcome_html": "
Assistent · Krea 2
  • Generate слева — живой просмотр. Несколько вариантов → сетка + клик для просмотра.
  • Refs — референсы на отдельной вкладке: drop / paste / Снимок gen.
  • Галочка vision на окне — отправить кадр модели.
  • Чипсы aspect / seed / Vary / Turbo·RAW. В чате: /help.
  • Кнопки патча только у последнего предложения.
Напиши, что сгенерировать — или кинь референс и попроси правку.", - "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 — клон с указанной\n/persona save — записать согласованный черновик\n/civitai — поиск LoRA (Confirm в чате)\n/inventory — rescan моделей + обновить список LoRA\n\nНесколько вариантов в одном запросе («оба», разный свет) → патч с variants[] → сетка на Generate, клик / Открыть / Enter — просмотр.\nЧипсы над полем ввода делают то же для aspect / seed / vary / Turbo·RAW.\nПри старте всегда новый чат; смена чата восстанавливает параметры.\nOverlay-личности удаляет только кнопка ✕ рядом с селектом (не модель).", + "welcome_html": "
Assistent · Krea 2
  • Generate слева — живой просмотр. Несколько вариантов → сетка + клик для просмотра.
  • Refs — референсы на отдельной вкладке: drop / paste / Снимок gen.
  • У каждого чата свои параметры, LoRA, последний кадр и refs.
  • Чипсы aspect / seed / Vary / Turbo·RAW. В чате: /help.
  • Модель шлёт только дельту настроек + generate.
Напиши, что сгенерировать — или кинь референс и попроси правку.", + "help_text": "Slash-команды (без LLM):\n/help — этот список\n/new — новый чат\n/history — список чатов\n/compress — сжать старые ходы в саммари (та же модель)\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|persona\n/persona new — интервью: клон текущей личности (overlay)\n/persona clone — клон с указанной\n/persona save — записать согласованный черновик\n/inventory — rescan моделей + обновить список LoRA\n\nНесколько вариантов в одном запросе («оба», разный свет) → патч с variants[] → сетка на Generate.\nЧипсы над полем ввода делают то же для aspect / seed / vary / Turbo·RAW.\nУ каждого чата свои параметры Generate; смена чата восстанавливает кадр и refs.\nOverlay-личности удаляет только кнопка ✕ рядом с селектом (не модель).\nЧип контекста в шапке чата показывает бюджет окна; клик — панель слоёв и ручное сжатие.", "chips": [ { "label": "1:1", "action": "aspect", "value": "1:1", "title": "1024×1024" }, { "label": "4:5", "action": "aspect", "value": "4:5", "title": "928×1152" }, @@ -19,9 +19,10 @@ { "cmd": "/help", "hint": "список команд", "action": "help" }, { "cmd": "/new", "hint": "новый чат", "action": "new" }, { "cmd": "/history", "hint": "история чатов", "action": "history" }, + { "cmd": "/compress", "hint": "сжать старые ходы", "action": "compress" }, { "cmd": "/debug", "hint": "сводка · ask = с LLM", "action": "debug" }, { "cmd": "/why", "hint": "debug + пояснение LLM", "action": "why" }, - { "cmd": "/gen", "hint": "Generate сейчас", "action": "gen" }, + { "cmd": "/gen", "hint": "Generate из сессии", "action": "gen" }, { "cmd": "/look ", "hint": "generate|refN", "action": "look" }, { "cmd": "/init", "hint": "как Init", "action": "init" }, { "cmd": "/mask", "hint": "как Mask", "action": "mask" }, @@ -31,7 +32,6 @@ { "cmd": "/seed ", "hint": "lock|random", "action": "seed" }, { "cmd": "/vary", "hint": "новый seed", "action": "vary" }, { "cmd": "/pack ", "hint": "write|critique|…", "action": "pack" }, - { "cmd": "/civitai ", "hint": "запрос LoRA", "action": "civitai" }, { "cmd": "/inventory", "hint": "rescan моделей", "action": "inventory" }, { "cmd": "/persona new", "hint": "клон / новая личность", "action": "persona_new" }, { "cmd": "/persona clone ", "hint": "клон с id", "action": "persona_clone" }, @@ -55,9 +55,6 @@ "inpaint_edit": "inpaint_edit", "describe": "describe_ref", "describe_ref": "describe_ref", - "card": "catalog_card", - "catalog": "catalog_card", - "catalog_card": "catalog_card", "persona": "author_persona", "author": "author_persona", "author_persona": "author_persona", diff --git a/README.md b/README.md index 7421ade..ab35700 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ SwarmUI extension for **collaborative Krea 2** prompting via **Ollama**: chat + **Turn model:** one user message is one *turn*. A turn may fan out into nested LLM *hops* — Krea prompt prep, empty-patch retry, vision, auto-critique. Hops share one `HOP_BUDGET`, never re-read the user's text (their prompt is client-authored), and pass the busy gate that blocks new user sends. What a reply does to generation state is decided once, in `resolveTurnIntent`: the model's `actions:["generate"]` / `look_at` win, RU intent heuristics only back it up when the model forgets, and an explicit «запомни, не генерируй» vetoes both. +**Version 0.14.0** — **Чат = сессия генерации**: у каждого чата свои params/LoRA/checkpoint/кадр/refs; модель шлёт sparse-дельту + `generate`/`look_at`/`ask`; без вкладки Карточки и Civitai/wanted hops. **Сжатие контекста**: rolling-саммари той же Ollama-моделью, чип бюджета `N / num_ctx`, авто перед отправкой, `/compress`. + **Version 0.13.1** — Сборка 0.13.0: `using` для `WebSocket`/`HttpClient`, instance-методы с `Config`/`FilePath`, Sqlite dll рядом с extension (иначе вкладка не грузится / API пустые). **Version 0.13.0** — **Реальный QLoRA-пайплайн**: `train_qlora.py` (TRL SFTTrainer + PEFT), HF-датасеты с маппингом (preset fiction title/tags→text), `max_samples`, полный post-train: safetensors → GGUF (`convert_lora_to_gguf.py`) → `ollama create` с `FROM ollama_base` + `ADAPTER`. Раннер: `builtin` + `custom`. Зависимости: `scripts/requirements-train.txt`. @@ -150,10 +152,9 @@ Two layers in `memory/assistent.sqlite` (`persona` column; empty = shared): | `/aspect 16:9` | Set size from the official 1K table | | `/seed lock\|random` | Lock or randomize seed | | `/vary` | New seed, same prompt (+ generate if auto) | -| `/pack write\|ordinary\|critique\|compose\|params\|inpaint\|describe\|card\|persona` | Switch pack | +| `/pack write\|ordinary\|critique\|compose\|params\|inpaint\|describe\|persona` | Switch pack | | `/persona new\|clone\|save` | Overlay persona authoring | -| `/civitai ` | Ask LLM to search Civitai | -| `/inventory` | Rescan models + refresh LoRA list | +| `/inventory` | Rescan моделей + обновить список LoRA | ## Requirements @@ -176,7 +177,7 @@ Restart / rebuild SwarmUI after clone. gpu-rent: `seed-extensions` + restart. ## Packs & skills -**Packs** (one active): `ordinary` (default комбайн; covers write/critique/params flows), `write_prompt` (alias → same as ordinary), `critique_image`, `compose_scene`, `fix_params`, `inpaint_edit`, `describe_ref`, `catalog_card`, `author_persona`. +**Packs** (one active): `ordinary` (default комбайн; covers write/critique/params flows), `write_prompt` (alias → same as ordinary), `critique_image`, `compose_scene`, `fix_params`, `inpaint_edit`, `describe_ref`, `author_persona`. Patch fence keys: single source `Config/_base/patch-keys.json` → C# + client via `AssistentGetConfig.patch_keys`. @@ -200,12 +201,8 @@ Patch fence keys: single source `Config/_base/patch-keys.json` → C# + client v | `AssistentSaveKnobs` | Overlay `_base/assistant.json` + Exact turbo/raw profiles | | `AssistentListInventory` | LoRA / checkpoint / wildcard inventory | | `AssistentListPersonas` | Persona catalog | -| `AssistentGetCard` / `AssistentSaveCard` | `.assistent.json` cards (+ memory ingest) | -| `AssistentGetCardMeta` | Local sidecar + optional Civitai by-hash | -| `AssistentEnqueueWanted` / `AssistentListWanted` | Wanted YAML queue (write / read + count) | | `AssistentListUserPrefs` / `AssistentUpsertUserPref` / `AssistentForgetUserPref` / `AssistentClearUserPrefs` | About the user | -| `AssistentSearchCivitai` | Civitai LoRA search | -| `AssistentChat` / `AssistentChatWS` | Chat (+ user prefs + hybrid craft memory + hops) | +| `AssistentChat` / `AssistentChatWS` | Chat (+ user prefs + hybrid craft memory + ask hops) | | `AssistentListMemory` / `AssistentUpsertMemory` / `AssistentForgetMemory` / `AssistentClearMemory` | Craft vector store | | `AssistentSearchMemory` / `AssistentGetMemory` | Hybrid search / exact kind+key | | `AssistentLookupTags` | Danbooru csv FTS (no embeddings) | diff --git a/SwarmAssistentExtension.cs b/SwarmAssistentExtension.cs index 254c51e..2a1ab4d 100644 --- a/SwarmAssistentExtension.cs +++ b/SwarmAssistentExtension.cs @@ -33,7 +33,7 @@ public partial class SwarmAssistentExtension : Extension ExtensionAuthor = "mrleo1nid"; Description = "Collaborative Krea 2 assistant: Ollama chat, persona presets, vector memory, model cards, Generate loop."; License = "MIT"; - Version = "0.13.1"; + Version = "0.14.0"; Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory", "training", "heard", "qlora"]; } @@ -47,11 +47,6 @@ public partial class SwarmAssistentExtension : Extension API.RegisterAPICall(AssistentGetConfig, false, PermUse); API.RegisterAPICall(AssistentSaveSettings, true, 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); API.RegisterAPICall(AssistentListChats, false, PermUse); @@ -68,7 +63,6 @@ public partial class SwarmAssistentExtension : Extension API.RegisterAPICall(AssistentSearchMemory, false, PermUse); API.RegisterAPICall(AssistentGetMemory, false, PermUse); API.RegisterAPICall(AssistentLookupTags, false, PermUse); - API.RegisterAPICall(AssistentListWanted, false, PermUse); API.RegisterAPICall(AssistentSaveControls, true, PermUse); API.RegisterAPICall(AssistentGetPersonaShelves, false, PermUse); API.RegisterAPICall(AssistentClonePersona, true, PermUse); @@ -104,7 +98,7 @@ public partial class SwarmAssistentExtension : Extension API.RegisterAPICall(AssistentLinkTrainSampleToAgent, true, PermUse); API.RegisterAPICall(AssistentUnlinkTrainSampleFromAgent, true, PermUse); API.RegisterAPICall(AssistentSyncDatasetToAgent, true, PermUse); - Logs.Init("Swarm Assistent extension loaded (0.13.1 QLoRA pipeline)"); + Logs.Init("Swarm Assistent extension loaded (0.14.0 chat session)"); } int CfgInt(string key, int fallback) diff --git a/Tabs/Text2Image/Assistent.html b/Tabs/Text2Image/Assistent.html index 72d48da..852b1a4 100644 --- a/Tabs/Text2Image/Assistent.html +++ b/Tabs/Text2Image/Assistent.html @@ -10,7 +10,6 @@
-
@@ -61,6 +60,25 @@
+
+ + +
-
-
Клик по чату восстанавливает сообщения и параметры Generate.
+
+ + +
+
- -