diff --git a/Assets/assistent.bundle.js b/Assets/assistent.bundle.js index ccab9cd..66a9c95 100644 --- a/Assets/assistent.bundle.js +++ b/Assets/assistent.bundle.js @@ -1792,7 +1792,7 @@ } if (window.SA && typeof SA.createActivityController === "function") { state.activity = SA.createActivityController({ - getMessagesEl: () => $2("sa_messages"), + getMessagesEl: () => $("sa_messages"), scrollToBottom: () => scrollMessagesToBottom(), hideEmpty: () => hideChatEmpty() }); @@ -1863,7 +1863,7 @@ function diskPersist() { return window.SA && window.SA.persist || null; } - function $2(id) { + function $(id) { return document.getElementById(id); } function modelShort(name) { @@ -1879,21 +1879,21 @@ return `${Math.floor(s / 60)}m ${String(s % 60).padStart(2, "0")}s`; } function hideChatEmpty() { - const empty = $2("sa_chat_empty"); + const empty = $("sa_chat_empty"); if (empty) { empty.hidden = true; } } let scrollMessagesRaf = 0; function messagesNearBottom(thresholdPx = 96) { - const box = $2("sa_messages"); + const box = $("sa_messages"); if (!box) { return true; } return box.scrollHeight - box.scrollTop - box.clientHeight <= thresholdPx; } function scrollMessagesToBottom({ force = false } = {}) { - const box = $2("sa_messages"); + const box = $("sa_messages"); if (!box) { return; } @@ -1905,15 +1905,15 @@ } scrollMessagesRaf = requestAnimationFrame(() => { scrollMessagesRaf = 0; - const el = $2("sa_messages"); + const el = $("sa_messages"); if (el && (force || messagesNearBottom(120))) { el.scrollTop = el.scrollHeight; } }); } function showChatEmptyIfIdle() { - const box = $2("sa_messages"); - const empty = $2("sa_chat_empty"); + const box = $("sa_messages"); + const empty = $("sa_chat_empty"); if (!box || !empty) { return; } @@ -1934,7 +1934,7 @@ if (!state.gotDelta && (state.busyPhase === "thinking" || state.busyPhase === "waiting") && elapsed > 1600) { state.busyPhase = state.llmParked || state.expectColdLoad ? "loading" : "waiting"; } - const model = modelShort($2("sa_model")?.value); + const model = modelShort($("sa_model")?.value); const labels = { encoding: "Encoding image\u2026", waiting: "\u0416\u0434\u0443 Ollama / \u043F\u0435\u0440\u0432\u044B\u0439 \u0442\u043E\u043A\u0435\u043D\u2026", @@ -1983,15 +1983,15 @@ } } } - const barText = $2("sa_livebar_text"); + const barText = $("sa_livebar_text"); if (barText) { barText.textContent = text; } - const elapsedEl = $2("sa_elapsed"); + const elapsedEl = $("sa_elapsed"); if (elapsedEl) { elapsedEl.textContent = fmtElapsed(elapsed); } - const status = $2("sa_status"); + const status = $("sa_status"); if (status) { status.textContent = text; status.classList.add("sa-status-busy"); @@ -2001,24 +2001,24 @@ state.busyStarted = Date.now(); state.gotDelta = false; state.busyPhase = phase || "thinking"; - $2("swarm_assistent_root")?.classList.add("sa-is-busy"); - $2("sa_composer")?.classList.add("sa-composer-busy"); + $("swarm_assistent_root")?.classList.add("sa-is-busy"); + $("sa_composer")?.classList.add("sa-composer-busy"); state.lastBusyPhaseShown = ""; - const send = $2("sa_btn_send"); + const send = $("sa_btn_send"); if (send) { send.disabled = false; } - const input = $2("sa_input"); + const input = $("sa_input"); if (input) { input.readOnly = false; input.disabled = false; input.classList.add("sa-input-busy"); } - const bar = $2("sa_livebar"); + const bar = $("sa_livebar"); if (bar) { bar.hidden = false; } - const dot = $2("sa_live_dot"); + const dot = $("sa_live_dot"); if (dot) { dot.hidden = false; } @@ -2040,25 +2040,25 @@ state.busyPhase = "idle"; state.lastBusyPhaseShown = ""; 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"); + $("swarm_assistent_root")?.classList.remove("sa-is-busy"); + $("sa_composer")?.classList.remove("sa-composer-busy"); + const send = $("sa_btn_send"); if (send) { send.disabled = false; } - const input = $2("sa_input"); + const input = $("sa_input"); if (input) { input.classList.remove("sa-input-busy"); } - const bar = $2("sa_livebar"); + const bar = $("sa_livebar"); if (bar) { bar.hidden = true; } - const dot = $2("sa_live_dot"); + const dot = $("sa_live_dot"); if (dot) { dot.hidden = true; } - const status = $2("sa_status"); + const status = $("sa_status"); if (status) { status.classList.remove("sa-status-busy"); } @@ -2070,13 +2070,13 @@ syncGenerateBusy(); } function setStatus(text) { - const el = $2("sa_status"); + const el = $("sa_status"); if (el) { el.textContent = text || ""; } } function setInterruptVisible(on) { - const btn = $2("sa_btn_interrupt"); + const btn = $("sa_btn_interrupt"); if (btn) { btn.hidden = !on; btn.classList.toggle("sa-interrupt-active", !!on); @@ -2157,8 +2157,8 @@ let lastExactForceCkpt = null; function updateGate() { const ok = isKreaSelected(); - const gate = $2("sa_gate"); - const layout = $2("sa_layout"); + const gate = $("sa_gate"); + const layout = $("sa_layout"); if (gate) { gate.hidden = ok; if (!ok) { @@ -2166,7 +2166,7 @@ const seen = [m.architecture, m.compat_class, m.name].filter(Boolean).join(" \xB7 "); const p = gate.querySelector("p"); if (p) { - p.innerHTML = seen ? `Swarm Assistent is for Krea 2 models only. Current: ${escapeHtml2( + p.innerHTML = seen ? `Swarm Assistent is for Krea 2 models only. Current: ${escapeHtml( seen )} \u2014 pick a checkpoint with architecture krea-2.` : "Swarm Assistent is for Krea 2 models only. Select a Krea 2 checkpoint on Generate to enable the chat."; } @@ -2190,7 +2190,7 @@ } return ok; } - function escapeHtml2(s) { + function escapeHtml(s) { return String(s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } const PROSE_SECTION_TITLES = { @@ -2271,7 +2271,7 @@ return; } parts.push( - `` + `` ); listItems = []; }; @@ -2285,7 +2285,7 @@ } const level = Math.min((line.match(/^#+/) || ["###"])[0].length, 3); parts.push( - `
${escapeHtml2(title)}
` + `
${escapeHtml(title)}
` ); continue; } @@ -2299,7 +2299,7 @@ parts.push(''); continue; } - parts.push(`

${formatProseInline(escapeHtml2(line))}

`); + parts.push(`

${formatProseInline(escapeHtml(line))}

`); } flushList(); return parts.join(""); @@ -2759,7 +2759,7 @@ ${patch.prompt}`; } } function syncBuildGenButton() { - const btn = $2("sa_btn_build_gen"); + const btn = $("sa_btn_build_gen"); if (!btn) { return; } @@ -2773,11 +2773,11 @@ ${patch.prompt}`; } } function defaultPackId() { - return state.config?.assistant?.default_pack || $2("sa_pack")?.querySelector("option")?.value || "ordinary"; + return state.config?.assistant?.default_pack || $("sa_pack")?.querySelector("option")?.value || "ordinary"; } function syncModeBadge() { - const badge = $2("sa_mode_badge"); - const pack = $2("sa_pack")?.value || defaultPackId(); + const badge = $("sa_mode_badge"); + const pack = $("sa_pack")?.value || defaultPackId(); if (!badge) { return; } @@ -2867,7 +2867,7 @@ ${patch.prompt}`; function paramDisplayTag(label, displayValue, isCustom, defaultHint) { const cls = isCustom ? "sa-param-tag sa-param-custom" : "sa-param-tag"; const hint = isCustom && defaultHint != null ? ` title="Exact: ${defaultHint}"` : ""; - return `${escapeHtml2(label)} ${escapeHtml2(String(displayValue))}`; + return `${escapeHtml(label)} ${escapeHtml(String(displayValue))}`; } function buildLiveParamsHtml() { const ckptProfile = detectKreaProfileName2(); @@ -2978,8 +2978,8 @@ ${patch.prompt}`; } function syncLiveParamsBar() { const html = buildLiveParamsHtml(); - const boardEl = $2("sa_live_params"); - const composerEl = $2("sa_composer_params"); + const boardEl = $("sa_live_params"); + const composerEl = $("sa_composer_params"); if (boardEl) { boardEl.innerHTML = html; } @@ -3272,7 +3272,7 @@ ${patch.prompt}`; const tab = document.getElementById(TAB_BUTTON_ID); if (tab) { tab.click(); - setTimeout(() => $2("sa_input")?.focus(), 50); + setTimeout(() => $("sa_input")?.focus(), 50); return true; } const pane = document.getElementById("assistent"); @@ -3282,7 +3282,7 @@ ${patch.prompt}`; } catch (e) { } } - setTimeout(() => $2("sa_input")?.focus(), 50); + setTimeout(() => $("sa_input")?.focus(), 50); return !!tab; } function historyMessageLimit() { @@ -3331,7 +3331,7 @@ ${patch.prompt}`; 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 numCtx = Number($("sa_num_ctx")?.value) || state.config?.assistant?.num_ctx || 16384; const numPredict = Number(state.config?.assistant?.num_predict) || 3072; if (!C?.estimateBudget) { return { @@ -3477,7 +3477,7 @@ ${patch.prompt}`; if (!fold.length) { return false; } - const model = $2("sa_model")?.value; + const model = $("sa_model")?.value; if (!model) { setStatus("\u0412\u044B\u0431\u0435\u0440\u0438 \u043C\u043E\u0434\u0435\u043B\u044C Ollama \u0432 \u2699"); return false; @@ -3490,8 +3490,8 @@ ${patch.prompt}`; 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 persona = $("sa_persona")?.value || "neutral"; + const baseUrl = $("sa_base_url")?.value || "http://127.0.0.1:11434"; const payload = { baseUrl, model, @@ -3505,7 +3505,7 @@ ${patch.prompt}`; fold_count: foldCount }), skills: [], - embed_model: $2("sa_embed_model")?.value || state.preferredEmbed || "" + embed_model: $("sa_embed_model")?.value || state.preferredEmbed || "" }; try { const data = await callOllamaOnce(payload); @@ -3560,7 +3560,7 @@ ${patch.prompt}`; return `${used} / ${cap}`; } function updateCtxChip() { - const chip = $2("sa_ctx_chip"); + const chip = $("sa_ctx_chip"); if (!chip) { return; } @@ -3590,8 +3590,8 @@ ${patch.prompt}`; } } function toggleCtxPanel(force) { - const panel = $2("sa_ctx_panel"); - const chip = $2("sa_ctx_chip"); + const panel = $("sa_ctx_panel"); + const chip = $("sa_ctx_chip"); if (!panel || !chip) { return; } @@ -3604,16 +3604,16 @@ ${patch.prompt}`; } } function renderCtxPanel() { - const body = $2("sa_ctx_panel_body"); - const bar = $2("sa_ctx_bar_fill"); - const auto = $2("sa_ctx_auto"); + const body = $("sa_ctx_panel_body"); + const bar = $("sa_ctx_bar_fill"); + const auto = $("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 layerRows = Object.entries(layers).filter(([k]) => k !== "total").map(([k, v]) => `
${escapeHtml(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 = ` @@ -3623,7 +3623,7 @@ ${patch.prompt}`;
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.
'} + ${mem.summary ? `
${escapeHtml(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)); @@ -3640,7 +3640,7 @@ ${patch.prompt}`; 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; + const model = $("sa_model")?.value; if (!model) { setStatus("\u0412\u044B\u0431\u0435\u0440\u0438 \u043C\u043E\u0434\u0435\u043B\u044C Ollama \u0432 \u2699"); return; @@ -4118,7 +4118,7 @@ ${patch.prompt}`; await runGenerateFromPatch({ actions: ["generate"] }, { force: true, fromSession: true }); } function renderBoard() { - const board = $2("sa_board"); + const board = $("sa_board"); if (!board) { return; } @@ -4242,11 +4242,11 @@ ${patch.prompt}`; return []; } function ensureGenLightbox() { - let root = $2("sa_gen_lightbox"); + let root = $("sa_gen_lightbox"); if (root) { return root; } - const host = $2("swarm_assistent_root") || document.body; + const host = $("swarm_assistent_root") || document.body; root = document.createElement("div"); root.id = "sa_gen_lightbox"; root.className = "sa-lightbox"; @@ -4320,9 +4320,9 @@ ${patch.prompt}`; return; } root.hidden = false; - const img = $2("sa_lb_img"); - const title = $2("sa_lb_title"); - const idx = $2("sa_lb_idx"); + const img = $("sa_lb_img"); + const title = $("sa_lb_title"); + const idx = $("sa_lb_idx"); if (img) { img.src = row.src; img.alt = row.label || row.id; @@ -4350,7 +4350,7 @@ ${patch.prompt}`; } function closeGenLightbox() { state.lightboxIndex = -1; - const root = $2("sa_gen_lightbox"); + const root = $("sa_gen_lightbox"); if (root) { root.hidden = true; } @@ -4369,23 +4369,23 @@ ${patch.prompt}`; } function syncBoardChrome() { const tab = state.boardTab === "refs" ? "refs" : "generate"; - $2("sa_board_tab_gen")?.classList.toggle("sa-board-tab-active", tab === "generate"); - $2("sa_board_tab_refs")?.classList.toggle("sa-board-tab-active", tab === "refs"); - $2("sa_board_tab_gen")?.setAttribute("aria-selected", tab === "generate" ? "true" : "false"); - $2("sa_board_tab_refs")?.setAttribute("aria-selected", tab === "refs" ? "true" : "false"); - const addBtn = $2("sa_btn_add_ref"); + $("sa_board_tab_gen")?.classList.toggle("sa-board-tab-active", tab === "generate"); + $("sa_board_tab_refs")?.classList.toggle("sa-board-tab-active", tab === "refs"); + $("sa_board_tab_gen")?.setAttribute("aria-selected", tab === "generate" ? "true" : "false"); + $("sa_board_tab_refs")?.setAttribute("aria-selected", tab === "refs" ? "true" : "false"); + const addBtn = $("sa_btn_add_ref"); if (addBtn) { addBtn.hidden = tab !== "refs"; } - const maskBtn = $2("sa_btn_as_mask"); - const clearSlotBtn = $2("sa_btn_clear_image"); + const maskBtn = $("sa_btn_as_mask"); + const clearSlotBtn = $("sa_btn_clear_image"); if (maskBtn) { maskBtn.hidden = tab !== "refs"; } if (clearSlotBtn) { clearSlotBtn.hidden = tab !== "refs"; } - const badge = $2("sa_refs_badge"); + const badge = $("sa_refs_badge"); if (badge) { const refs = refSlots(); const withImg = refs.filter((s) => s.src).length; @@ -4397,9 +4397,9 @@ ${patch.prompt}`; badge.hidden = true; } } - let genBadge = $2("sa_gen_badge"); + let genBadge = $("sa_gen_badge"); if (!genBadge) { - const genTab = $2("sa_board_tab_gen"); + const genTab = $("sa_board_tab_gen"); if (genTab) { genBadge = document.createElement("span"); genBadge.id = "sa_gen_badge"; @@ -4567,11 +4567,11 @@ ${patch.prompt}`; if (localStorage.getItem(LS_WELCOMED) === "1") { return; } - if (!$2("sa_messages")) { + if (!$("sa_messages")) { return; } localStorage.setItem(LS_WELCOMED, "1"); - const box = $2("sa_messages"); + const box = $("sa_messages"); hideChatEmpty(); const div = document.createElement("div"); div.className = "sa-msg assistant sa-welcome"; @@ -4674,8 +4674,8 @@ ${patch.prompt}`; state.chatSession = S.snapshotFromLive({ genFields: readLiveGenFields(), board: boardSnapshotForSession(), - persona: $2("sa_persona")?.value || "neutral", - pack: $2("sa_pack")?.value || "ordinary", + persona: $("sa_persona")?.value || "neutral", + pack: $("sa_pack")?.value || "ordinary", context_memory: getContextMemory() }); return state.chatSession; @@ -4752,7 +4752,7 @@ ${patch.prompt}`; function applyPersonaForChat(personaId, { quiet = false } = {}) { const id = String(personaId || "neutral").trim() || "neutral"; return new Promise((resolve) => { - const sel = $2("sa_persona"); + const sel = $("sa_persona"); if (sel && [...sel.options].some((o) => o.value === id)) { sel.value = id; } @@ -4766,7 +4766,7 @@ ${patch.prompt}`; resolve(); return; } - const packKeep = $2("sa_pack")?.value; + const packKeep = $("sa_pack")?.value; genericRequest( "AssistentGetConfig", { persona: id }, @@ -4901,7 +4901,7 @@ ${patch.prompt}`; persistChatsStore(); } function resetMessagesUi(emptyHint) { - const box = $2("sa_messages"); + const box = $("sa_messages"); if (!box) { return; } @@ -4913,7 +4913,7 @@ ${patch.prompt}`; box.appendChild(empty); } function renderHistoryIntoUi(messages) { - const box = $2("sa_messages"); + const box = $("sa_messages"); if (!box) { return; } @@ -4965,7 +4965,7 @@ ${patch.prompt}`; updateCtxChip(); } function updateSessionLabel() { - const el = $2("sa_session_label"); + const el = $("sa_session_label"); if (!el) { return; } @@ -4987,8 +4987,8 @@ ${patch.prompt}`; return !chat || !chatHasTranscript(chat); } function syncHistoryBadge() { - const btn = $2("sa_btn_chats"); - const countEl = $2("sa_chats_count"); + const btn = $("sa_btn_chats"); + const countEl = $("sa_chats_count"); const n = (state.chats || []).filter(chatHasTranscript).length; const open = !!state.chatsPanelOpen; if (btn) { @@ -5061,7 +5061,7 @@ ${patch.prompt}`; return bits.join(" \xB7 "); } function renderChatsList() { - const root = $2("sa_chats_list"); + const root = $("sa_chats_list"); if (!root) { return; } @@ -5086,19 +5086,19 @@ ${patch.prompt}`; row.dataset.id = c.id; row.setAttribute("role", "listitem"); const when = formatChatWhen(c.updatedAt); - const title = escapeHtml2(c.title || "\u041D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442"); + const title = escapeHtml(c.title || "\u041D\u043E\u0432\u044B\u0439 \u0447\u0430\u0442"); const tipBits = [chatListParamsBits(c.params)].filter(Boolean); - const tip = tipBits.length ? ` title="${escapeHtml2(tipBits.join(" \xB7 "))}"` : ""; - row.innerHTML = ``; + const tip = tipBits.length ? ` title="${escapeHtml(tipBits.join(" \xB7 "))}"` : ""; + row.innerHTML = ``; root.appendChild(row); } } function setChatsPanelOpen(open) { state.chatsPanelOpen = !!open; state.chatsDrawerOpen = state.chatsPanelOpen; - const panel = $2("sa_chats_panel"); - const btn = $2("sa_btn_chats"); - const root = $2("swarm_assistent_root"); + const panel = $("sa_chats_panel"); + const btn = $("sa_btn_chats"); + const root = $("swarm_assistent_root"); if (panel) { panel.hidden = !state.chatsPanelOpen; } @@ -5109,7 +5109,7 @@ ${patch.prompt}`; syncHistoryBadge(); if (state.chatsPanelOpen) { saveActiveChatToStore(); - const search = $2("sa_chats_search"); + const search = $("sa_chats_search"); if (search) { search.value = state.chatsQuery || ""; search.focus(); @@ -5124,7 +5124,7 @@ ${patch.prompt}`; if (openDrawer) { setChatsPanelOpen(true); } - $2("sa_input")?.focus(); + $("sa_input")?.focus(); return; } if (state.busy || state.generating) { @@ -5174,7 +5174,7 @@ ${patch.prompt}`; setChatsPanelOpen(true); } if (openDrawer || !force) { - $2("sa_input")?.focus(); + $("sa_input")?.focus(); } } async function switchToChat(id) { @@ -5323,7 +5323,7 @@ ${patch.prompt}`; updateCtxChip(); } function hideSlashMenu() { - const menu = $2("sa_slash_menu"); + const menu = $("sa_slash_menu"); if (menu) { menu.hidden = true; menu.innerHTML = ""; @@ -5339,7 +5339,7 @@ ${patch.prompt}`; return SLASH_COMMANDS.filter((c) => c.cmd.toLowerCase().startsWith(q) || q === "/" || c.cmd.toLowerCase().includes(q.slice(1))); } function renderSlashMenu(items) { - const menu = $2("sa_slash_menu"); + const menu = $("sa_slash_menu"); if (!menu) { return; } @@ -5355,7 +5355,7 @@ ${patch.prompt}`; btn.type = "button"; btn.className = "sa-slash-item" + (i === state.slashIndex ? " sa-slash-active" : ""); btn.setAttribute("role", "option"); - btn.innerHTML = `${escapeHtml2(item.cmd.trim())} \u2014 ${escapeHtml2(item.hint)}`; + btn.innerHTML = `${escapeHtml(item.cmd.trim())} \u2014 ${escapeHtml(item.hint)}`; btn.addEventListener("mousedown", (e) => { e.preventDefault(); applySlashPick(item); @@ -5364,7 +5364,7 @@ ${patch.prompt}`; }); } function applySlashPick(item) { - const input = $2("sa_input"); + const input = $("sa_input"); if (!input || !item) { return; } @@ -5375,7 +5375,7 @@ ${patch.prompt}`; input.setSelectionRange(pos, pos); } function updateSlashMenuFromInput() { - const text = $2("sa_input")?.value || ""; + const text = $("sa_input")?.value || ""; if (!text.startsWith("/") || text.includes("\n") || /\s/.test(text.trim().slice(1)) && !text.endsWith(" ")) { const token2 = text.split(/\s/)[0] || ""; if (!token2.startsWith("/") || text.includes(" ") && !SLASH_COMMANDS.some((c) => c.cmd.startsWith(token2))) { @@ -5393,7 +5393,7 @@ ${patch.prompt}`; renderSlashMenu(slashMatches(token)); } function onPersonaChanged() { - const id = $2("sa_persona")?.value || "neutral"; + const id = $("sa_persona")?.value || "neutral"; state.sessionExact = {}; state.lastUserParamIntent = false; saveSettings(); @@ -5404,10 +5404,10 @@ ${patch.prompt}`; } appendSystemNote(`\u0422\u043E\u043D \u2192 ${title}`); state.pendingPersonaNote = `Persona is now ${id} (${title}). Adopt this voice from now on.`; - if (data?.assistant?.default_pack && $2("sa_pack") && !state.packUserTouched) { + if (data?.assistant?.default_pack && $("sa_pack") && !state.packUserTouched) { const packId = data.assistant.default_pack; - if ([...$2("sa_pack").options || []].some((o) => o.value === packId)) { - $2("sa_pack").value = packId; + if ([...$("sa_pack").options || []].some((o) => o.value === packId)) { + $("sa_pack").value = packId; } } fillEmptyParamsFromExact(); @@ -5616,7 +5616,7 @@ ${patch.prompt}`; 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_apply: !!$("sa_auto_apply")?.checked, auto_generate: true, krea_profile: kreaProfile, recommended_params: { @@ -5833,8 +5833,8 @@ ${patch.prompt}`; setStatus("\u041F\u0430\u0442\u0447\u0438 \u0443\u0431\u0440\u0430\u043D\u044B \u0438\u0437 \u0447\u0430\u0442\u0430"); } function toggleMoreMenu(menuId, btnId) { - const menu = $2(menuId); - const btn = $2(btnId); + const menu = $(menuId); + const btn = $(btnId); if (!menu) { return; } @@ -5855,7 +5855,7 @@ ${patch.prompt}`; document.querySelectorAll("#sa_btn_board_more, #sa_btn_clear_more").forEach((b) => b.setAttribute("aria-expanded", "false")); } function setPackValue(packName, { flash, user } = {}) { - const pack = $2("sa_pack"); + const pack = $("sa_pack"); if (!pack || !packName) { return false; } @@ -5881,7 +5881,7 @@ ${patch.prompt}`; if (state.packUserTouched) { return null; } - const cur = $2("sa_pack")?.value || defaultPackId(); + const cur = $("sa_pack")?.value || defaultPackId(); if (cur === "ordinary") { return null; } @@ -5916,7 +5916,7 @@ ${patch.prompt}`; if (state.packUserTouched) { return; } - const cur = $2("sa_pack")?.value || ""; + const cur = $("sa_pack")?.value || ""; if (cur === "critique_image" || cur === "describe_ref") { setPackValue(defaultPackId(), { flash: true }); } @@ -6244,15 +6244,15 @@ ${patch.prompt}`; accent: p.accent, source: p.source })); - renderPersonaOptions(state.personas, preferId || $2("sa_persona")?.value); + renderPersonaOptions(state.personas, preferId || $("sa_persona")?.value); } - if (preferId && $2("sa_persona")) { - if ([...$2("sa_persona").options].some((o) => o.value === preferId)) { - $2("sa_persona").value = preferId; + if (preferId && $("sa_persona")) { + if ([...$("sa_persona").options].some((o) => o.value === preferId)) { + $("sa_persona").value = preferId; await applyPersonaForChat(preferId, { quiet: true }); } } else { - loadConfig($2("sa_persona")?.value, () => resolve()); + loadConfig($("sa_persona")?.value, () => resolve()); return; } resolve(); @@ -6279,16 +6279,16 @@ ${patch.prompt}`; return false; } function shouldParkLlmBeforeGen() { - return !!$2("sa_park_llm")?.checked; + return !!$("sa_park_llm")?.checked; } function parkLlm() { return new Promise((resolve) => { - const model = $2("sa_model")?.value; + const model = $("sa_model")?.value; if (!shouldParkLlmBeforeGen() || !model || state.llmParked || typeof genericRequest !== "function") { resolve(false); return; } - const baseUrl = $2("sa_base_url")?.value || "http://127.0.0.1:11434"; + const baseUrl = $("sa_base_url")?.value || "http://127.0.0.1:11434"; let settled = false; const finish = (ok) => { if (settled) { @@ -6307,7 +6307,7 @@ ${patch.prompt}`; } function warmLlm({ force = false } = {}) { return new Promise((resolve) => { - const model = $2("sa_model")?.value; + const model = $("sa_model")?.value; if (!model || typeof genericRequest !== "function") { resolve({ ok: false, alreadyResident: false }); return; @@ -6316,7 +6316,7 @@ ${patch.prompt}`; resolve({ ok: false, alreadyResident: false }); return; } - const baseUrl = $2("sa_base_url")?.value || "http://127.0.0.1:11434"; + const baseUrl = $("sa_base_url")?.value || "http://127.0.0.1:11434"; let settled = false; const finish = (ok, alreadyResident = false) => { if (settled) { @@ -6387,8 +6387,8 @@ ${patch.prompt}`; if (!text) { return; } - const persona = $2("sa_persona")?.value || localStorage.getItem(LS_PERSONA) || "neutral"; - const pack = $2("sa_pack")?.value || defaultPackId(); + const persona = $("sa_persona")?.value || localStorage.getItem(LS_PERSONA) || "neutral"; + const pack = $("sa_pack")?.value || defaultPackId(); const prose = visibleAssistantProse(text); if (state.streamEl) { finalizeStreamMessage(text, []); @@ -6751,7 +6751,7 @@ ${patch.prompt}`; } const paneVisible = !!document.getElementById("swarm_assistent_root")?.offsetParent; const multiDone = !!(jobs && finishedGenResultCount() > 1); - const willAutoCritique = !multiDone && !!$2("sa_auto_critique")?.checked; + const willAutoCritique = !multiDone && !!$("sa_auto_critique")?.checked; if (state.view === "chat" && paneVisible && !willAutoCritique && epoch === state.chatEpoch) { if (parkedBeforeWarm || state.expectColdLoad) { startBusyUi("warming"); @@ -6808,7 +6808,7 @@ ${patch.prompt}`; return src && !looksLikeModelPreview(src) ? src : null; } async function maybeAutoCritique(imageSrc) { - if (!$2("sa_auto_critique")?.checked || turnHopUsed("critique") || isMultiGenResults()) { + if (!$("sa_auto_critique")?.checked || turnHopUsed("critique") || isMultiGenResults()) { return; } const src = await resolveFinishedGenerateSrc(imageSrc); @@ -6820,8 +6820,8 @@ ${patch.prompt}`; return; } setPackValue("critique_image", { flash: true }); - if ($2("sa_input")) { - $2("sa_input").value = "Strict QC of this Generate frame vs the live prompt: list concrete defects and artifacts first, then one fenced JSON patch with an improved prompt and any param/LoRA fixes. No empty praise."; + if ($("sa_input")) { + $("sa_input").value = "Strict QC of this Generate frame vs the live prompt: list concrete defects and artifacts first, then one fenced JSON patch with an improved prompt and any param/LoRA fixes. No empty praise."; } const gen = generateSlot(); if (gen) { @@ -6834,7 +6834,7 @@ ${patch.prompt}`; restoreDefaultPackAfterHop(); } async function maybeAutoVisionLook(imageSrc) { - if (!wantsAutoVision() || $2("sa_auto_critique")?.checked || turnHopUsed("vision") || state.busy || isMultiGenResults()) { + if (!wantsAutoVision() || $("sa_auto_critique")?.checked || turnHopUsed("vision") || state.busy || isMultiGenResults()) { return; } const src = await resolveFinishedGenerateSrc(imageSrc); @@ -6851,8 +6851,8 @@ ${patch.prompt}`; return; } setPackValue("critique_image", { flash: true }); - if ($2("sa_input")) { - $2("sa_input").value = "Strict review of the Generate frame vs the prompt: what failed, what artifacts you see, what to change in prompt and params next. Skip hollow compliments."; + if ($("sa_input")) { + $("sa_input").value = "Strict review of the Generate frame vs the prompt: what failed, what artifacts you see, what to change in prompt and params next. Skip hollow compliments."; } setStatus("Auto look_at\u2026"); await sendChat({ fromVisionHop: true, forceSlotIds: [GEN_ID], skipAutoPack: true }); @@ -6883,13 +6883,13 @@ ${patch.prompt}`; setView("chat"); const label = (state.genResults || []).find((r) => r.id === state.selectedGenResultId)?.label; setPackValue("critique_image", { flash: true }); - if ($2("sa_input")) { - $2("sa_input").value = label ? `\u041A\u0440\u0438\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u0440\u0430\u0437\u0431\u0435\u0440\u0438 \xAB${label}\xBB: \u0447\u0442\u043E \u043D\u0435 \u0441\u0445\u043E\u0434\u0438\u0442\u0441\u044F \u0441 \u043F\u0440\u043E\u043C\u043F\u0442\u043E\u043C, \u0430\u0440\u0442\u0435\u0444\u0430\u043A\u0442\u044B, \u043A\u043E\u043C\u043F\u043E\u0437\u0438\u0446\u0438\u044F/\u0441\u0432\u0435\u0442 \u2014 \u0438 \u043A\u0430\u043A \u043F\u043E\u043F\u0440\u0430\u0432\u0438\u0442\u044C prompt \u0438 params \u0434\u043B\u044F \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0433\u043E \u043A\u0430\u0434\u0440\u0430. \u0411\u0435\u0437 \u043E\u0431\u0449\u0438\u0445 \u043F\u043E\u0445\u0432\u0430\u043B.` : "\u041A\u0440\u0438\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u0440\u0430\u0437\u0431\u0435\u0440\u0438 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442: \u0447\u0442\u043E \u043D\u0435 \u0441\u0445\u043E\u0434\u0438\u0442\u0441\u044F \u0441 \u043F\u0440\u043E\u043C\u043F\u0442\u043E\u043C, \u0430\u0440\u0442\u0435\u0444\u0430\u043A\u0442\u044B, \u043A\u043E\u043C\u043F\u043E\u0437\u0438\u0446\u0438\u044F/\u0441\u0432\u0435\u0442 \u2014 \u0438 \u043A\u0430\u043A \u043F\u043E\u043F\u0440\u0430\u0432\u0438\u0442\u044C prompt \u0438 params \u0434\u043B\u044F \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0433\u043E \u043A\u0430\u0434\u0440\u0430. \u0411\u0435\u0437 \u043E\u0431\u0449\u0438\u0445 \u043F\u043E\u0445\u0432\u0430\u043B."; + if ($("sa_input")) { + $("sa_input").value = label ? `\u041A\u0440\u0438\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u0440\u0430\u0437\u0431\u0435\u0440\u0438 \xAB${label}\xBB: \u0447\u0442\u043E \u043D\u0435 \u0441\u0445\u043E\u0434\u0438\u0442\u0441\u044F \u0441 \u043F\u0440\u043E\u043C\u043F\u0442\u043E\u043C, \u0430\u0440\u0442\u0435\u0444\u0430\u043A\u0442\u044B, \u043A\u043E\u043C\u043F\u043E\u0437\u0438\u0446\u0438\u044F/\u0441\u0432\u0435\u0442 \u2014 \u0438 \u043A\u0430\u043A \u043F\u043E\u043F\u0440\u0430\u0432\u0438\u0442\u044C prompt \u0438 params \u0434\u043B\u044F \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0433\u043E \u043A\u0430\u0434\u0440\u0430. \u0411\u0435\u0437 \u043E\u0431\u0449\u0438\u0445 \u043F\u043E\u0445\u0432\u0430\u043B.` : "\u041A\u0440\u0438\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u0440\u0430\u0437\u0431\u0435\u0440\u0438 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442: \u0447\u0442\u043E \u043D\u0435 \u0441\u0445\u043E\u0434\u0438\u0442\u0441\u044F \u0441 \u043F\u0440\u043E\u043C\u043F\u0442\u043E\u043C, \u0430\u0440\u0442\u0435\u0444\u0430\u043A\u0442\u044B, \u043A\u043E\u043C\u043F\u043E\u0437\u0438\u0446\u0438\u044F/\u0441\u0432\u0435\u0442 \u2014 \u0438 \u043A\u0430\u043A \u043F\u043E\u043F\u0440\u0430\u0432\u0438\u0442\u044C prompt \u0438 params \u0434\u043B\u044F \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0433\u043E \u043A\u0430\u0434\u0440\u0430. \u0411\u0435\u0437 \u043E\u0431\u0449\u0438\u0445 \u043F\u043E\u0445\u0432\u0430\u043B."; } await sendChat({ forceSlotIds: [GEN_ID], skipAutoPack: true }); } function currentPersonaInfo() { - const id = ($2("sa_persona")?.value || localStorage.getItem(LS_PERSONA) || "neutral").trim() || "neutral"; + const id = ($("sa_persona")?.value || localStorage.getItem(LS_PERSONA) || "neutral").trim() || "neutral"; const known = (state.personas || []).find((p) => p && p.id === id); return { id, @@ -6905,7 +6905,7 @@ ${patch.prompt}`; return; } const persona = meta.persona || currentPersonaInfo(); - const pack = meta.pack || $2("sa_pack")?.value || ""; + const pack = meta.pack || $("sa_pack")?.value || ""; div.dataset.persona = persona.id || "neutral"; if (pack) { div.dataset.pack = pack; @@ -6927,7 +6927,7 @@ ${patch.prompt}`; div.insertBefore(row, div.firstChild); } function appendMessage(role, text, patch, civitaiResults, meta) { - const box = $2("sa_messages"); + const box = $("sa_messages"); if (!box) { return null; } @@ -6959,7 +6959,7 @@ ${patch.prompt}`; return div; } function beginStreamMessage(meta) { - const box = $2("sa_messages"); + const box = $("sa_messages"); if (!box) { return null; } @@ -7091,71 +7091,16 @@ ${patch.prompt}`; } scrollMessagesToBottom(); } - function mountCurateButtons(msgEl, meta) { - if (!msgEl || msgEl.querySelector(".sa-msg-curate")) { - return; - } - const wrap = document.createElement("div"); - wrap.className = "sa-msg-curate"; - const ok = document.createElement("button"); - ok.type = "button"; - ok.className = "basic-button"; - ok.title = "\u0412 \u0434\u0430\u0442\u0430\u0441\u0435\u0442 (\u043E\u0434\u043E\u0431\u0440\u0438\u0442\u044C)"; - ok.textContent = "+ \u0434\u0430\u0442\u0430\u0441\u0435\u0442"; - ok.addEventListener("click", () => curateAssistantMessage(msgEl, "approved")); - const bad = document.createElement("button"); - bad.type = "button"; - bad.className = "basic-button"; - bad.title = "\u041E\u0442\u043A\u043B\u043E\u043D\u0438\u0442\u044C \u0434\u043B\u044F \u0434\u0430\u0442\u0430\u0441\u0435\u0442\u0430"; - bad.textContent = "\u0431\u0440\u0430\u043A"; - bad.addEventListener("click", () => curateAssistantMessage(msgEl, "rejected")); - wrap.appendChild(ok); - wrap.appendChild(bad); - msgEl.appendChild(wrap); + function mountCurateButtons() { } - function curateAssistantMessage(msgEl, status) { - const hist = state.history || []; - let asstText = msgEl.querySelector(".sa-msg-body")?.textContent?.trim() || msgEl.textContent?.trim() || ""; - let userText = ""; - for (let i = hist.length - 1; i >= 0; i--) { - if (hist[i]?.role === "assistant" && (hist[i].content || "").trim() === asstText.trim()) { - for (let j = i - 1; j >= 0; j--) { - if (hist[j]?.role === "user") { - userText = hist[j].content || ""; - break; - } - } - break; - } - } - if (!userText) { - for (let i = hist.length - 1; i >= 0; i--) { - if (hist[i]?.role === "user") { - userText = hist[i].content || ""; - break; - } - } - } - const messages = [ - { role: "user", content: userText }, - { role: "assistant", content: asstText } - ]; - window.SA?.training?.curateFromChat?.(messages, { - chatId: state.activeChatId, - persona: $2("sa_persona")?.value || "neutral", - pack: $2("sa_pack")?.value || defaultPackId(), - status - })?.then?.((ok) => { - if (ok) { - setStatus(status === "approved" ? "\u041F\u0440\u0438\u043C\u0435\u0440 \u0434\u043E\u0431\u0430\u0432\u043B\u0435\u043D \u0432 \u0434\u0430\u0442\u0430\u0441\u0435\u0442" : "\u041F\u0440\u0438\u043C\u0435\u0440 \u043E\u0442\u043C\u0435\u0447\u0435\u043D \u043A\u0430\u043A \u0431\u0440\u0430\u043A"); - } - }); + function curateAssistantMessage() { + setStatus("\u041E\u0431\u0443\u0447\u0435\u043D\u0438\u0435 \u0441\u043D\u044F\u0442\u043E \u2014 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439 books/knowledge"); } function isTrainingLocked() { - return !!state.trainingLock || document.getElementById("swarm_assistent_root")?.classList.contains("sa-root-training-lock"); + return false; } function wantsAutoVision() { - return !!$2("sa_auto_vision")?.checked; + return !!$("sa_auto_vision")?.checked; } function looksLikeModelPreview(src) { const s = String(src || "").toLowerCase(); @@ -7342,32 +7287,32 @@ ${patch.prompt}`; const autoDl = localStorage.getItem(LS_AUTO_DOWNLOAD); const parkLlm2 = localStorage.getItem(LS_PARK_LLM); const paneW = localStorage.getItem(LS_PANE_WIDTH); - if (base && $2("sa_base_url")) { - $2("sa_base_url").value = base; + if (base && $("sa_base_url")) { + $("sa_base_url").value = base; } - if (pack && $2("sa_pack")) { - $2("sa_pack").value = pack; + if (pack && $("sa_pack")) { + $("sa_pack").value = pack; } - if (persona && $2("sa_persona")) { - $2("sa_persona").value = persona; + if (persona && $("sa_persona")) { + $("sa_persona").value = persona; } - if (auto != null && $2("sa_auto_vision")) { - $2("sa_auto_vision").checked = auto === "1"; + if (auto != null && $("sa_auto_vision")) { + $("sa_auto_vision").checked = auto === "1"; } - if ($2("sa_auto_apply")) { - $2("sa_auto_apply").checked = autoApply == null ? true : autoApply === "1"; + if ($("sa_auto_apply")) { + $("sa_auto_apply").checked = autoApply == null ? true : autoApply === "1"; } - if ($2("sa_auto_generate")) { - $2("sa_auto_generate").checked = autoGen == null ? true : autoGen === "1"; + if ($("sa_auto_generate")) { + $("sa_auto_generate").checked = autoGen == null ? true : autoGen === "1"; } - if ($2("sa_auto_critique") && autoCrit != null) { - $2("sa_auto_critique").checked = autoCrit === "1"; + if ($("sa_auto_critique") && autoCrit != null) { + $("sa_auto_critique").checked = autoCrit === "1"; } - if ($2("sa_auto_download") && autoDl != null) { - $2("sa_auto_download").checked = autoDl === "1"; + if ($("sa_auto_download") && autoDl != null) { + $("sa_auto_download").checked = autoDl === "1"; } - if ($2("sa_park_llm")) { - $2("sa_park_llm").checked = parkLlm2 === "1"; + if ($("sa_park_llm")) { + $("sa_park_llm").checked = parkLlm2 === "1"; } if (model) { state.preferredModel = model; @@ -7382,7 +7327,7 @@ ${patch.prompt}`; if (view === "cards") { view = "chat"; } - if (view === "chat" || view === "settings" || view === "train") { + if (view === "chat" || view === "settings") { state.view = view; } const drawer = localStorage.getItem(LS_CHATS_DRAWER); @@ -7397,18 +7342,18 @@ ${patch.prompt}`; } function collectUiState() { return { - pack: $2("sa_pack")?.value || defaultPackId(), - persona: $2("sa_persona")?.value || "neutral", - auto_vision: !!$2("sa_auto_vision")?.checked, - auto_apply: !!$2("sa_auto_apply")?.checked, + pack: $("sa_pack")?.value || defaultPackId(), + persona: $("sa_persona")?.value || "neutral", + auto_vision: !!$("sa_auto_vision")?.checked, + auto_apply: !!$("sa_auto_apply")?.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, + auto_critique: !!$("sa_auto_critique")?.checked, + auto_download: !!$("sa_auto_download")?.checked, + park_llm: !!$("sa_park_llm")?.checked, pane_width: localStorage.getItem(LS_PANE_WIDTH) || "", - embed_model: $2("sa_embed_model")?.value || state.preferredEmbed || "", - base_url: $2("sa_base_url")?.value || "", - model: $2("sa_model")?.value || "", + embed_model: $("sa_embed_model")?.value || state.preferredEmbed || "", + base_url: $("sa_base_url")?.value || "", + model: $("sa_model")?.value || "", view: state.view || "chat", board_tab: state.boardTab || "generate", chats_drawer: state.chatsDrawerOpen ? "1" : "0" @@ -7436,8 +7381,8 @@ ${patch.prompt}`; apply?.(String(value)); }; fill(LS_BASE, ui.base_url, (v) => { - if ($2("sa_base_url")) { - $2("sa_base_url").value = v; + if ($("sa_base_url")) { + $("sa_base_url").value = v; } }); fill(LS_MODEL, ui.model, (v) => { @@ -7447,20 +7392,20 @@ ${patch.prompt}`; state.preferredEmbed = v; }); fill(LS_PACK, ui.pack, (v) => { - if ($2("sa_pack")) { - $2("sa_pack").value = v; + if ($("sa_pack")) { + $("sa_pack").value = v; } }); fill(LS_PERSONA, ui.persona, (v) => { - if ($2("sa_persona")) { - $2("sa_persona").value = v; + if ($("sa_persona")) { + $("sa_persona").value = v; } }); fill(LS_PANE_WIDTH, ui.pane_width, (v) => document.documentElement.style.setProperty("--sa-image-width", v)); if (ui.view === "cards") { ui.view = "chat"; } - if (ui.view === "chat" || ui.view === "settings" || ui.view === "train") { + if (ui.view === "chat" || ui.view === "settings") { fill(LS_VIEW, ui.view, (v) => { state.view = v; }); @@ -7492,7 +7437,7 @@ ${patch.prompt}`; continue; } localStorage.setItem(lsKey, on ? "1" : "0"); - const el = $2(id); + const el = $(id); if (el) { el.checked = on; } @@ -7502,18 +7447,18 @@ ${patch.prompt}`; diskPersist()?.saveUiState(collectUiState()); } function saveSettings() { - localStorage.setItem(LS_BASE, $2("sa_base_url")?.value || ""); - localStorage.setItem(LS_MODEL, $2("sa_model")?.value || ""); - localStorage.setItem(LS_EMBED, $2("sa_embed_model")?.value || state.preferredEmbed || ""); - localStorage.setItem(LS_PACK, $2("sa_pack")?.value || defaultPackId()); - localStorage.setItem(LS_PERSONA, $2("sa_persona")?.value || "neutral"); + localStorage.setItem(LS_BASE, $("sa_base_url")?.value || ""); + localStorage.setItem(LS_MODEL, $("sa_model")?.value || ""); + localStorage.setItem(LS_EMBED, $("sa_embed_model")?.value || state.preferredEmbed || ""); + localStorage.setItem(LS_PACK, $("sa_pack")?.value || defaultPackId()); + localStorage.setItem(LS_PERSONA, $("sa_persona")?.value || "neutral"); localStorage.setItem(LS_VIEW, state.view || "chat"); - localStorage.setItem(LS_AUTO_VISION, $2("sa_auto_vision")?.checked ? "1" : "0"); - localStorage.setItem(LS_AUTO_APPLY, $2("sa_auto_apply")?.checked ? "1" : "0"); - localStorage.setItem(LS_AUTO_GENERATE, $2("sa_auto_generate")?.checked ? "1" : "0"); - localStorage.setItem(LS_AUTO_CRITIQUE, $2("sa_auto_critique")?.checked ? "1" : "0"); - localStorage.setItem(LS_AUTO_DOWNLOAD, $2("sa_auto_download")?.checked ? "1" : "0"); - localStorage.setItem(LS_PARK_LLM, $2("sa_park_llm")?.checked ? "1" : "0"); + localStorage.setItem(LS_AUTO_VISION, $("sa_auto_vision")?.checked ? "1" : "0"); + localStorage.setItem(LS_AUTO_APPLY, $("sa_auto_apply")?.checked ? "1" : "0"); + localStorage.setItem(LS_AUTO_GENERATE, $("sa_auto_generate")?.checked ? "1" : "0"); + localStorage.setItem(LS_AUTO_CRITIQUE, $("sa_auto_critique")?.checked ? "1" : "0"); + localStorage.setItem(LS_AUTO_DOWNLOAD, $("sa_auto_download")?.checked ? "1" : "0"); + localStorage.setItem(LS_PARK_LLM, $("sa_park_llm")?.checked ? "1" : "0"); persistServerSettings(); saveUiStateToDisk(); } @@ -7525,10 +7470,10 @@ ${patch.prompt}`; document.querySelectorAll("#sa_skills_box input[data-skill]")?.forEach((el) => { skills[el.getAttribute("data-skill")] = !!el.checked; }); - const persona = $2("sa_persona")?.value || "neutral"; + const persona = $("sa_persona")?.value || "neutral"; const settings = { - embed_model: $2("sa_embed_model")?.value || state.preferredEmbed || "", - base_url: $2("sa_base_url")?.value || "", + embed_model: $("sa_embed_model")?.value || state.preferredEmbed || "", + base_url: $("sa_base_url")?.value || "", [persona]: { skills } }; genericRequest("AssistentSaveSettings", { settings }, () => { @@ -7539,12 +7484,9 @@ ${patch.prompt}`; if (!data || data.error) { return; } - const prevPersona = state.config?.persona || $2("sa_persona")?.value || ""; + const prevPersona = state.config?.persona || $("sa_persona")?.value || ""; const prevControls = state.config?.control_values && typeof state.config.control_values === "object" ? { ...state.config.control_values } : null; state.config = data; - if (window.SA?.training?.onConfig) { - window.SA.training.onConfig(data); - } if (window.SA?.applyConfigPatchKeys) { window.SA.applyConfigPatchKeys(data); } @@ -7601,8 +7543,8 @@ ${data.ui.help_extra}`.trim(); renderPackOptions(data.packs || [], applyDefaults ? data.assistant?.default_pack : null); renderChips(data.ui?.chips || []); renderSkillChecks(data.skills || [], state.enabledSkills); - if (applyDefaults && data.assistant?.default_pack && $2("sa_pack") && !localStorage.getItem(LS_PACK)) { - $2("sa_pack").value = data.assistant.default_pack; + if (applyDefaults && data.assistant?.default_pack && $("sa_pack") && !localStorage.getItem(LS_PACK)) { + $("sa_pack").value = data.assistant.default_pack; } if (data.assistant?.embed_model && !state.preferredEmbed) { state.preferredEmbed = data.assistant.embed_model; @@ -7645,7 +7587,7 @@ ${data.ui.help_extra}`.trim(); lastExactForceCkpt = resolveCurrentCheckpoint()?.name || null; } } - const nextPersona = data.persona || $2("sa_persona")?.value || ""; + const nextPersona = data.persona || $("sa_persona")?.value || ""; let controlValues = data.control_values || data.exact?.controls || {}; if (!applyDefaults && prevControls && nextPersona === prevPersona) { controlValues = { ...controlValues, ...prevControls }; @@ -7655,14 +7597,14 @@ ${data.ui.help_extra}`.trim(); } } renderPersonaControls(data.controls || {}, controlValues); - syncPersonaDeleteButton(data.persona_source || data.personas?.find((p) => p.id === (data.persona || $2("sa_persona")?.value))?.source); + syncPersonaDeleteButton(data.persona_source || data.personas?.find((p) => p.id === (data.persona || $("sa_persona")?.value))?.source); } function isDeletablePersonaSource(source) { const src = String(source || ""); return src === "overlay" || src === "overlay+bundled" || src === "overlay+pack"; } function syncPersonaDeleteButton(source) { - const btn = $2("sa_persona_delete"); + const btn = $("sa_persona_delete"); if (!btn) { return; } @@ -7674,7 +7616,7 @@ ${data.ui.help_extra}`.trim(); let controlsPointerDown = false; let pendingControlsRender = null; function renderPersonaControls(schema, values) { - const box = $2("sa_persona_controls"); + const box = $("sa_persona_controls"); if (!box) { return; } @@ -7829,7 +7771,7 @@ ${data.ui.help_extra}`.trim(); setStatus("/horny-game\u2026"); } function savePersonaControls(partial) { - const persona = $2("sa_persona")?.value || "neutral"; + const persona = $("sa_persona")?.value || "neutral"; if (typeof genericRequest !== "function") { return; } @@ -7864,7 +7806,7 @@ ${data.ui.help_extra}`.trim(); ); } function syncPersonaControlInputs(values) { - const box = $2("sa_persona_controls"); + const box = $("sa_persona_controls"); if (!box || !values || typeof values !== "object") { return; } @@ -7887,7 +7829,7 @@ ${data.ui.help_extra}`.trim(); }); } async function deleteCurrentOverlayPersona() { - const id = $2("sa_persona")?.value; + const id = $("sa_persona")?.value; if (!id) { return; } @@ -7917,8 +7859,8 @@ ${data.ui.help_extra}`.trim(); state.personas = data.personas; } renderPersonaOptions(state.personas || [], next); - if ($2("sa_persona")) { - $2("sa_persona").value = next; + if ($("sa_persona")) { + $("sa_persona").value = next; } await applyPersonaForChat(next, { quiet: false }); setStatus(`\u0423\u0434\u0430\u043B\u0435\u043D\u043E: ${id}`); @@ -7933,7 +7875,7 @@ ${data.ui.help_extra}`.trim(); }); } function renderPersonaOptions(personas, selected) { - const sel = $2("sa_persona"); + const sel = $("sa_persona"); if (!sel) { return; } @@ -7955,7 +7897,7 @@ ${data.ui.help_extra}`.trim(); syncPersonaDeleteButton(meta?.source || state.config?.persona_source); } function renderPackOptions(packs, preferred) { - const sel = $2("sa_pack"); + const sel = $("sa_pack"); if (!sel) { return; } @@ -7973,7 +7915,7 @@ ${data.ui.help_extra}`.trim(); } } function renderChips(chips) { - const box = $2("sa_chips"); + const box = $("sa_chips"); if (!box || !Array.isArray(chips) || !chips.length) { return; } @@ -8012,7 +7954,7 @@ ${data.ui.help_extra}`.trim(); } } function renderSkillChecks(skills, enabled) { - const box = $2("sa_skills_box"); + const box = $("sa_skills_box"); if (!box) { return; } @@ -8042,7 +7984,7 @@ ${data.ui.help_extra}`.trim(); } genericRequest( "AssistentGetConfig", - { persona: persona || $2("sa_persona")?.value || "neutral" }, + { persona: persona || $("sa_persona")?.value || "neutral" }, (data) => { applyConfigPayload(data, { applyDefaults: true }); done?.(data); @@ -8089,8 +8031,8 @@ ${data.ui.help_extra}`.trim(); return preferred || list[0]; } function setModelOptions(models, { error, preferred } = {}) { - const sel = $2("sa_model"); - const sel2 = $2("sa_settings_chat_model"); + const sel = $("sa_model"); + const sel2 = $("sa_settings_chat_model"); const apply = (target) => { if (!target) { return; @@ -8129,7 +8071,7 @@ ${data.ui.help_extra}`.trim(); apply(sel2); } function setEmbedModelOptions(models) { - const sel = $2("sa_embed_model"); + const sel = $("sa_embed_model"); if (!sel) { return; } @@ -8160,7 +8102,7 @@ ${data.ui.help_extra}`.trim(); } } function refreshModels() { - const baseUrl = $2("sa_base_url")?.value || "http://127.0.0.1:11434"; + const baseUrl = $("sa_base_url")?.value || "http://127.0.0.1:11434"; setStatus("Loading models\u2026"); if (typeof genericRequest !== "function") { setStatus("SwarmUI API not ready"); @@ -8177,10 +8119,10 @@ ${data.ui.help_extra}`.trim(); setModelOptions(models, { preferred }); setEmbedModelOptions(memoryModels); const pick = resolveChatModel(models, preferred); - if (pick && $2("sa_model")) { - $2("sa_model").value = pick; - if ($2("sa_settings_chat_model")) { - $2("sa_settings_chat_model").value = pick; + if (pick && $("sa_model")) { + $("sa_model").value = pick; + if ($("sa_settings_chat_model")) { + $("sa_settings_chat_model").value = pick; } state.preferredModel = pick; localStorage.setItem(LS_MODEL, pick); @@ -8244,16 +8186,16 @@ ${data.ui.help_extra}`.trim(); return new Promise((resolve) => refreshInventory(resolve, opts)); } function memoryKindFilter() { - return $2("sa_mem_kind")?.value || "all"; + return $("sa_mem_kind")?.value || "all"; } function memoryScopeFilter() { - return $2("sa_mem_scope")?.value || "all"; + return $("sa_mem_scope")?.value || "all"; } function memorySearchFilter() { - return ($2("sa_mem_search")?.value || "").trim().toLowerCase(); + return ($("sa_mem_search")?.value || "").trim().toLowerCase(); } function renderMemoryKinds(kinds) { - const sel = $2("sa_mem_kind"); + const sel = $("sa_mem_kind"); if (!sel) { return; } @@ -8277,7 +8219,7 @@ ${data.ui.help_extra}`.trim(); const filter = memoryKindFilter(); const scope = memoryScopeFilter(); const q = memorySearchFilter(); - const persona = $2("sa_persona")?.value || "neutral"; + const persona = $("sa_persona")?.value || "neutral"; return (state.memoryRows || []).filter((m) => { if (filter !== "all" && m.kind !== filter) { return false; @@ -8298,7 +8240,7 @@ ${data.ui.help_extra}`.trim(); }); } function renderMemoryList() { - const root = $2("sa_mem_list"); + const root = $("sa_mem_list"); if (!root) { return; } @@ -8314,7 +8256,7 @@ ${data.ui.help_extra}`.trim(); const bundled = row.source === "bundled"; const when = row.updated ? formatChatWhen(row.updated * 1e3) : ""; const scope = row.scope === "personal" ? `\u043F\u0435\u0440\u0441\u043E\u043D\u0430 ${row.persona || "\u2014"}` : "\u043E\u0431\u0449\u0430\u044F"; - el.innerHTML = `
${escapeHtml2(row.kind || "note")}${escapeHtml2(row.key || "")}
${escapeHtml2(clipDebug(row.text, 220))}
${escapeHtml2([scope, row.source || "user", when].filter(Boolean).join(" \xB7 "))}
`; + el.innerHTML = `
${escapeHtml(row.kind || "note")}${escapeHtml(row.key || "")}
${escapeHtml(clipDebug(row.text, 220))}
${escapeHtml([scope, row.source || "user", when].filter(Boolean).join(" \xB7 "))}
`; const forget = document.createElement("button"); forget.type = "button"; forget.className = "basic-button sa-mem-forget"; @@ -8334,7 +8276,7 @@ ${data.ui.help_extra}`.trim(); if (typeof genericRequest !== "function") { return; } - const list = $2("sa_mem_list"); + const list = $("sa_mem_list"); if (list && !state.memoryRows.length) { list.innerHTML = '
\u0427\u0438\u0442\u0430\u044E \u043F\u0430\u043C\u044F\u0442\u044C\u2026
'; } @@ -8345,7 +8287,7 @@ ${data.ui.help_extra}`.trim(); state.memoryRows = Array.isArray(data?.memories) ? data.memories : []; renderMemoryKinds(data?.kinds || []); renderMemoryList(); - const foot = $2("sa_mem_total"); + const foot = $("sa_mem_total"); if (foot) { foot.textContent = `\u0412\u0441\u0435\u0433\u043E: ${data?.total ?? state.memoryRows.length} \xB7 ${data?.embed_model || "\u2014"}`; } @@ -8353,7 +8295,7 @@ ${data.ui.help_extra}`.trim(); 0, (err) => { if (list) { - list.innerHTML = `
\u041F\u0430\u043C\u044F\u0442\u044C \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430: ${escapeHtml2(String(err || "\u043E\u0448\u0438\u0431\u043A\u0430"))}
`; + list.innerHTML = `
\u041F\u0430\u043C\u044F\u0442\u044C \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430: ${escapeHtml(String(err || "\u043E\u0448\u0438\u0431\u043A\u0430"))}
`; } } ); @@ -8425,9 +8367,9 @@ ${data.ui.help_extra}`.trim(); } if (state.settingsTab === "models") { syncSettingsHealthLine(); - const m = $2("sa_model")?.value; - if (m && $2("sa_settings_chat_model")) { - $2("sa_settings_chat_model").value = m; + const m = $("sa_model")?.value; + if (m && $("sa_settings_chat_model")) { + $("sa_settings_chat_model").value = m; } } if (state.settingsTab === "more") { @@ -8438,7 +8380,7 @@ ${data.ui.help_extra}`.trim(); const asst = data?.assistant || state.config?.assistant || {}; const exact = data?.exact || state.config?.exact || state.exact || {}; const setNum = (id, v) => { - const el = $2(id); + const el = $(id); if (el && v != null && Number.isFinite(Number(v))) { el.value = String(v); } @@ -8447,16 +8389,16 @@ ${data.ui.help_extra}`.trim(); 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"); + const autoEl = $("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"); + const weightEl = $("sa_user_prefs_weight"); if (weightEl) { weightEl.value = String(Math.max(0, Math.min(1.5, w))); - const lab = $2("sa_user_prefs_weight_val"); + const lab = $("sa_user_prefs_weight_val"); if (lab) { lab.textContent = Number(weightEl.value).toFixed(1); } @@ -8475,7 +8417,7 @@ ${data.ui.help_extra}`.trim(); return; } const num = (id) => { - const v = parseFloat($2(id)?.value); + const v = parseFloat($(id)?.value); return Number.isFinite(v) ? v : null; }; const assistant = { @@ -8483,7 +8425,7 @@ ${data.ui.help_extra}`.trim(); 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, + compress_auto: $("sa_compress_auto") ? !!$("sa_compress_auto").checked : null, memory_top_k: num("sa_memory_top_k"), user_prefs_weight: num("sa_user_prefs_weight") }; @@ -8534,8 +8476,8 @@ ${data.ui.help_extra}`.trim(); ); } function syncSettingsHealthLine() { - const line = $2("sa_settings_health_line"); - const badge = $2("sa_ollama_health"); + const line = $("sa_settings_health_line"); + const badge = $("sa_ollama_health"); if (line && badge) { line.textContent = badge.textContent || "Ollama \xB7 \u2026"; line.className = "sa-settings-health " + (badge.className || "").replace("sa-health", "").trim(); @@ -8557,12 +8499,12 @@ ${data.ui.help_extra}`.trim(); return "\u0432\u0441\u0442\u0440\u043E\u0435\u043D\u043E"; } function renderPersonaSettingsList() { - const root = $2("sa_persona_list"); + const root = $("sa_persona_list"); if (!root) { return; } const list = state.personas || state.config?.personas || []; - const cur = state.settingsPersonaId || $2("sa_persona")?.value || list[0]?.id; + const cur = state.settingsPersonaId || $("sa_persona")?.value || list[0]?.id; state.settingsPersonaId = cur; root.innerHTML = ""; for (const p of list) { @@ -8570,7 +8512,7 @@ ${data.ui.help_extra}`.trim(); btn.type = "button"; btn.className = "sa-persona-item" + (p.id === cur ? " sa-persona-item-active" : ""); const accent = p.accent || "currentColor"; - btn.innerHTML = `
${escapeHtml2(p.title || p.id)}
${escapeHtml2(personaSourceLabel(p.source))}
`; + btn.innerHTML = `
${escapeHtml(p.title || p.id)}
${escapeHtml(personaSourceLabel(p.source))}
`; btn.addEventListener("click", () => { state.settingsPersonaId = p.id; renderPersonaSettingsList(); @@ -8587,17 +8529,21 @@ ${data.ui.help_extra}`.trim(); const id = state.settingsPersonaId; const p = (state.personas || []).find((x) => x.id === id); const canDelete = p && isDeletablePersonaSource(p.source); - const del = $2("sa_btn_persona_delete_panel"); + const del = $("sa_btn_persona_delete_panel"); if (del) { del.disabled = !canDelete; } } function loadPersonaPreview(id) { - const box = $2("sa_persona_preview"); + const box = $("sa_persona_preview"); + const knowPanel = $("sa_knowledge_panel"); if (!box || typeof genericRequest !== "function") { return; } box.innerHTML = '
\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430\u2026
'; + if (knowPanel) { + knowPanel.hidden = true; + } genericRequest( "AssistentGetPersonaShelves", { persona: id }, @@ -8608,15 +8554,82 @@ ${data.ui.help_extra}`.trim(); ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; syncPersonaPanelActions(); + loadPersonaKnowledge(id); }, 0, (err) => { - box.innerHTML = `
${escapeHtml2(String(err || "\u043E\u0448\u0438\u0431\u043A\u0430"))}
`; + box.innerHTML = `
${escapeHtml(String(err || "\u043E\u0448\u0438\u0431\u043A\u0430"))}
`; + } + ); + } + function loadPersonaKnowledge(personaId) { + const panel = $("sa_knowledge_panel"); + const list = $("sa_knowledge_list"); + const saveBtn = $("sa_btn_knowledge_save"); + const hint = $("sa_knowledge_hint"); + if (!panel || !list || typeof genericRequest !== "function") { + return; + } + list.innerHTML = '
\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u043A\u043D\u0438\u0433\u2026
'; + panel.hidden = false; + genericRequest( + "AssistentListKnowledgeCatalog", + { persona: personaId }, + (data) => { + const cat = data?.knowledge || {}; + const attach = new Set((cat.attach || []).map(String)); + const books = cat.books || []; + const editable = !!data?.editable; + if (hint) { + hint.textContent = editable ? "\u041E\u0442\u043C\u0435\u0442\u044C \u043A\u043D\u0438\u0433\u0438 \u0434\u043B\u044F FTS-\u043F\u043E\u0438\u0441\u043A\u0430 (ask:knowledge). \u0421\u043E\u0445\u0440\u0430\u043D\u044F\u0435\u0442\u0441\u044F \u0432 overlay knowledge.json." : "Bundled/pack \u2014 attach \u0437\u0430\u0434\u0430\u043D \u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E \u0438\u043B\u0438 assistent-pack.yaml. \u041A\u043B\u043E\u043D\u0438\u0440\u0443\u0439 \u0432 overlay \u0434\u043B\u044F \u043F\u0440\u0430\u0432\u043E\u043A."; + } + if (saveBtn) { + saveBtn.hidden = !editable; + } + list.innerHTML = ""; + if (!books.length) { + list.innerHTML = '
\u041A\u043D\u0438\u0433\u0438 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u044B \u043D\u0430 \u0434\u0438\u0441\u043A\u0435. gpu-rent seed-books + up.
'; + return; + } + for (const b of books) { + const row = document.createElement("label"); + row.className = "sa-check sa-knowledge-row"; + const cb = document.createElement("input"); + cb.type = "checkbox"; + cb.value = b.id; + cb.checked = attach.has(String(b.id)); + cb.disabled = !editable; + row.appendChild(cb); + const title = b.title || b.id; + const kind = b.content_kind ? ` \xB7 ${b.content_kind}` : ""; + const idx = b.indexed ? "" : " \xB7 \u043D\u0435\u0442 search.jsonl"; + row.appendChild(document.createTextNode(` ${title} (${b.id})${kind}${idx}`)); + list.appendChild(row); + } + if (saveBtn && editable) { + saveBtn.onclick = () => { + const picked = [...list.querySelectorAll("input[type=checkbox]:checked")].map((el) => el.value); + genericRequest( + "AssistentSaveKnowledgeAttach", + { persona: personaId, attach: picked }, + () => { + setStatus("Knowledge attach \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D"); + loadPersonaKnowledge(personaId); + }, + 0, + (err) => setStatus(String(err || "save failed")) + ); + }; + } + }, + 0, + (err) => { + list.innerHTML = `
${escapeHtml(String(err || "\u043E\u0448\u0438\u0431\u043A\u0430"))}
`; } ); } function exportSelectedPersona() { - const id = state.settingsPersonaId || $2("sa_persona")?.value; + const id = state.settingsPersonaId || $("sa_persona")?.value; if (!id || typeof genericRequest !== "function") { return; } @@ -8677,7 +8690,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; reader.readAsText(file); } function cloneSelectedPersona() { - const from = state.settingsPersonaId || $2("sa_persona")?.value; + const from = state.settingsPersonaId || $("sa_persona")?.value; if (!from) { return; } @@ -8731,7 +8744,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; if (typeof genericRequest !== "function") { return; } - const persona = $2("sa_persona")?.value || "neutral"; + const persona = $("sa_persona")?.value || "neutral"; genericRequest( "AssistentListUserPrefs", { persona, limit: 200 }, @@ -8744,11 +8757,11 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; ); } function renderUserPrefsLists() { - const persona = $2("sa_persona")?.value || "neutral"; + const persona = $("sa_persona")?.value || "neutral"; const global = (state.userPrefs || []).filter((p) => p.scope === "global"); const personal = (state.userPrefs || []).filter((p) => p.scope === "persona" && (p.persona_id === persona || p.persona === persona)); const fill = (rootId, rows) => { - const root = $2(rootId); + const root = $(rootId); if (!root) { return; } @@ -8761,7 +8774,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; const el = document.createElement("div"); el.className = "sa-mem-row"; const pin = row.pinned ? " \u2605" : ""; - el.innerHTML = `
${escapeHtml2(row.key || "")}${pin}
${escapeHtml2(clipDebug(row.text, 200))}
`; + el.innerHTML = `
${escapeHtml(row.key || "")}${pin}
${escapeHtml(clipDebug(row.text, 200))}
`; el.querySelector(".sa-mem-row-body")?.addEventListener("click", () => editUserPref(row)); el.querySelector(".sa-mem-row-body")?.setAttribute("title", "\u041A\u043B\u0438\u043A \u2014 \u0440\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C"); const pinBtn = document.createElement("button"); @@ -8801,7 +8814,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; key: row.key, text: String(text).trim(), scope: row.scope || "global", - persona: row.persona_id || row.persona || $2("sa_persona")?.value || "neutral", + persona: row.persona_id || row.persona || $("sa_persona")?.value || "neutral", source: "user", pinned: !!row.pinned }, @@ -8817,7 +8830,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; key: row.key, text: row.text, scope: row.scope || "global", - persona: row.persona_id || row.persona || $2("sa_persona")?.value || "neutral", + persona: row.persona_id || row.persona || $("sa_persona")?.value || "neutral", source: row.source || "user", pinned: !row.pinned }, @@ -8841,7 +8854,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; key: key.trim(), text: text.trim(), scope, - persona: $2("sa_persona")?.value || "neutral", + persona: $("sa_persona")?.value || "neutral", source: "user", pinned: false }, @@ -8859,7 +8872,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; { key: row.key, scope: row.scope || "global", - persona: row.persona_id || row.persona || $2("sa_persona")?.value + persona: row.persona_id || row.persona || $("sa_persona")?.value }, () => refreshUserPrefs(), 0, @@ -8873,7 +8886,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; } genericRequest( "AssistentClearUserPrefs", - { scope, persona: $2("sa_persona")?.value || "neutral" }, + { scope, persona: $("sa_persona")?.value || "neutral" }, (data) => { setStatus(`\u0423\u0434\u0430\u043B\u0435\u043D\u043E: ${data?.deleted ?? 0}`); refreshUserPrefs(); @@ -8898,7 +8911,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; } function setOllamaHealth(level, text, title) { state.ollamaHealth = level; - const el = $2("sa_ollama_health"); + const el = $("sa_ollama_health"); if (!el) { return; } @@ -8913,7 +8926,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; if (typeof genericRequest !== "function") { return; } - const baseUrl = $2("sa_base_url")?.value || "http://127.0.0.1:11434"; + const baseUrl = $("sa_base_url")?.value || "http://127.0.0.1:11434"; genericRequest( "AssistentListModels", { baseUrl }, @@ -8940,36 +8953,27 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; } if (view === "settings") { state.view = "settings"; - } else if (view === "train") { - state.view = "train"; } else { state.view = "chat"; } - const chat = $2("sa_view_chat"); - const settings = $2("sa_view_settings"); - const train = $2("sa_view_train"); + const chat = $("sa_view_chat"); + const settings = $("sa_view_settings"); if (chat) { chat.hidden = state.view !== "chat"; } if (settings) { settings.hidden = state.view !== "settings"; } - if (train) { - train.hidden = state.view !== "train"; - } const tabActive = (id, on) => { - $2(id)?.classList.toggle("sa-subtab-active", on); - $2(id)?.classList.toggle("sa-app-tab-active", on); - $2(id)?.setAttribute("aria-selected", on ? "true" : "false"); + $(id)?.classList.toggle("sa-subtab-active", on); + $(id)?.classList.toggle("sa-app-tab-active", on); + $(id)?.setAttribute("aria-selected", on ? "true" : "false"); }; tabActive("sa_tab_chat", state.view === "chat"); tabActive("sa_tab_settings", state.view === "settings"); - tabActive("sa_tab_train", state.view === "train"); saveSettings(); if (state.view === "settings") { setSettingsTab(state.settingsTab || "behavior"); - } else if (state.view === "train") { - window.SA?.training?.render?.(); } else if ((state.llmParked || state.expectColdLoad) && !state.generating) { warmLlm({ force: true }); } @@ -9020,7 +9024,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; renderLoraChips(); } function renderLoraChips() { - const root = $2("sa_lora_chips"); + const root = $("sa_lora_chips"); if (!root) { return; } @@ -9113,7 +9117,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; }; filter.addEventListener("input", draw); draw(); - const composer = $2("sa_composer") || document.body; + const composer = $("sa_composer") || document.body; composer.style.position = composer.style.position || "relative"; composer.appendChild(picker); const onDoc = (ev) => { @@ -9170,7 +9174,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; pop.remove(); } }); - const composer = $2("sa_composer") || document.body; + const composer = $("sa_composer") || document.body; composer.style.position = composer.style.position || "relative"; composer.appendChild(pop); const rect = anchor.getBoundingClientRect(); @@ -9400,8 +9404,8 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; intent, generating: !!intent.generate, swarm_prompt: val("alt_prompt_textbox") || val("input_prompt") || "", - persona: $2("sa_persona")?.value || "", - pack: $2("sa_pack")?.value || "" + persona: $("sa_persona")?.value || "", + pack: $("sa_pack")?.value || "" }); } function reportDebugClientTurn(payload) { @@ -9471,7 +9475,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; syncLiveParamsBar(); } function syncChipHighlight() { - const bar = $2("sa_chips"); + const bar = $("sa_chips"); if (!bar) { return; } @@ -9516,7 +9520,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; }); } function appendSystemNote(text) { - const box = $2("sa_messages"); + const box = $("sa_messages"); if (!box) { return; } @@ -9545,10 +9549,10 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; }).join(", "); } function buildDebugSummary() { - const persona = $2("sa_persona")?.value || "neutral"; - const pack = $2("sa_pack")?.value || defaultPackId(); - const chatModel = $2("sa_model")?.value || "\u2014"; - const embed = $2("sa_embed_model")?.value || state.preferredEmbed || "\u2014"; + const persona = $("sa_persona")?.value || "neutral"; + const pack = $("sa_pack")?.value || defaultPackId(); + const chatModel = $("sa_model")?.value || "\u2014"; + const embed = $("sa_embed_model")?.value || state.preferredEmbed || "\u2014"; const profile = detectKreaProfileName2(); const defaults = mergedGenerationDefaults(profile); const session = state.sessionExact || {}; @@ -9583,7 +9587,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=${true} vision=${!!$2("sa_auto_vision")?.checked} critique=${!!$2("sa_auto_critique")?.checked}`, + `auto: apply=${!!$("sa_auto_apply")?.checked} gen=${true} vision=${!!$("sa_auto_vision")?.checked} critique=${!!$("sa_auto_critique")?.checked}`, "", "Live SwarmUI:", ` ckpt=${ctx.checkpoint?.name || "\u2014"} \xB7 krea_profile=${ctx.krea_profile || profile}`, @@ -9716,9 +9720,9 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; } slot.attach = true; renderBoard(); - if ($2("sa_input")) { + if ($("sa_input")) { const critical = id === GEN_ID || slot.type === "generate"; - $2("sa_input").value = critical ? "Strict QC of this Generate frame vs the live prompt: concrete defects first, then JSON patch with prompt/param fixes. No empty praise." : `Critically review ${id} vs the intended scene: defects and how to fix prompt/params.`; + $("sa_input").value = critical ? "Strict QC of this Generate frame vs the live prompt: concrete defects first, then JSON patch with prompt/param fixes. No empty praise." : `Critically review ${id} vs the intended scene: defects and how to fix prompt/params.`; } setPackValue("critique_image", { flash: true }); await sendChat({ forceSlotIds: [id], skipAutoPack: true }); @@ -9810,7 +9814,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; if (!setPackValue(arg, { flash: true, user: true })) { setStatus("Pack: write|ordinary|critique|compose|params|inpaint|describe|persona"); } else { - setStatus(`Pack \u2192 ${$2("sa_pack")?.value}`); + setStatus(`Pack \u2192 ${$("sa_pack")?.value}`); } return true; } @@ -9833,7 +9837,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; }); return true; } - const fromId = sub === "clone" && rest ? rest.split(/\s+/)[0] : $2("sa_persona")?.value || "neutral"; + const fromId = sub === "clone" && rest ? rest.split(/\s+/)[0] : $("sa_persona")?.value || "neutral"; await sendChat({ skipAutoPack: true, forcedUserText: `\u041D\u0430\u0447\u043D\u0438 \u0438\u043D\u0442\u0435\u0440\u0432\u044C\u044E author_persona: \u043A\u043B\u043E\u043D \u0441 \u0438\u0441\u0442\u043E\u0447\u043D\u0438\u043A\u0430 \xAB${fromId}\xBB. \u0421\u043F\u0440\u0430\u0448\u0438\u0432\u0430\u0439 \u043F\u043E \u043F\u043E\u043B\u043A\u0430\u043C \u0433\u0440\u0443\u043F\u043F\u0430\u043C\u0438. \u041D\u0435 \u043F\u0438\u0448\u0438 \u043D\u0430 \u0434\u0438\u0441\u043A, \u043F\u043E\u043A\u0430 \u043C\u0430\u043B\u043E \u043E\u0442\u0432\u0435\u0442\u043E\u0432. \u041D\u0435 \u0443\u0434\u0430\u043B\u044F\u0439 \u043B\u0438\u0447\u043D\u043E\u0441\u0442\u0438.` @@ -9879,8 +9883,8 @@ ${HELP_TEXT}`); s.attach = true; } renderBoard(); - if ($2("sa_input")) { - $2("sa_input").value = `Look at board slots: ${need.map((s) => s.id).join(", ")}. Continue using these images.`; + if ($("sa_input")) { + $("sa_input").value = `Look at board slots: ${need.map((s) => s.id).join(", ")}. Continue using these images.`; } setStatus(`Vision hop \u2190 ${need.map((s) => s.label).join(", ")}`); await sendChat({ fromVisionHop: true, forceSlotIds: need.map((s) => s.id) }); @@ -9891,7 +9895,7 @@ ${HELP_TEXT}`); setStatus("\u0418\u0434\u0451\u0442 \u0442\u0440\u0435\u043D\u0438\u0440\u043E\u0432\u043A\u0430 \u2014 \u0447\u0430\u0442 \u0437\u0430\u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u0430\u043D"); return; } - const rawInput = ($2("sa_input")?.value || "").trim(); + const rawInput = ($("sa_input")?.value || "").trim(); const text = (opts.forcedUserText || rawInput).trim(); if (!text) { return; @@ -9911,8 +9915,8 @@ ${HELP_TEXT}`); } if (!isMachineTurn(opts) && !opts.skipSlash) { if (rawInput.startsWith("/")) { - if ($2("sa_input")) { - $2("sa_input").value = ""; + if ($("sa_input")) { + $("sa_input").value = ""; } const handled = await handleSlashCommand(rawInput); if (handled) { @@ -9923,8 +9927,8 @@ ${HELP_TEXT}`); if (!isMachineTurn(opts) && isSameButAspectRequest(text)) { const aspect = parseAspectFromUserText(text); if (aspect) { - if ($2("sa_input")) { - $2("sa_input").value = ""; + if ($("sa_input")) { + $("sa_input").value = ""; } appendMessage("user", text); state.history.push({ role: "user", content: text }); @@ -9955,9 +9959,9 @@ ${HELP_TEXT}`); 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"; - const model = $2("sa_model")?.value; + const pack = opts.fromDebug ? "debug_explain" : $("sa_pack")?.value || defaultPackId(); + const persona = $("sa_persona")?.value || localStorage.getItem(LS_PERSONA) || "neutral"; + const model = $("sa_model")?.value; if (!model) { setStatus("\u0412\u044B\u0431\u0435\u0440\u0438 \u043C\u043E\u0434\u0435\u043B\u044C Ollama \u0432 \u2699"); refreshModels(); @@ -10033,8 +10037,8 @@ ${HELP_TEXT}`); state.pendingPersonaNote = null; } appendMessage("user", opts.historyUserText || text); - if ($2("sa_input")) { - $2("sa_input").value = ""; + if ($("sa_input")) { + $("sa_input").value = ""; } persistHistory(); } else { @@ -10097,7 +10101,7 @@ ${HELP_TEXT}`); messages[messages.length - 1].images = images; } startBusyUi("thinking"); - const baseUrl = $2("sa_base_url")?.value || "http://127.0.0.1:11434"; + const baseUrl = $("sa_base_url")?.value || "http://127.0.0.1:11434"; const payload = { baseUrl, model, @@ -10107,7 +10111,7 @@ ${HELP_TEXT}`); messages, context_json: JSON.stringify(context), skills: opts.fromDebug ? [] : state.enabledSkills || [], - embed_model: $2("sa_embed_model")?.value || state.preferredEmbed || "" + embed_model: $("sa_embed_model")?.value || state.preferredEmbed || "" }; const finishOk = async (reply, civitaiResults, meta = {}) => { if (chatEpoch !== state.chatEpoch) { @@ -10157,6 +10161,14 @@ ${HELP_TEXT}`); }); } } + if (meta.knowledge?.hops?.length) { + const hopLabels = meta.knowledge.hops.map((h) => `${h.tool || "hop"}:${h.count ?? "?"}`).slice(0, 6); + activityDone("knowledge", { + kind: "ask", + label: "knowledge hops", + detail: hopLabels.join(" \xB7 ") + }); + } captureActivityTrace(); state.history.push({ role: "assistant", @@ -10265,7 +10277,8 @@ ${HELP_TEXT}`); 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 + prompt_eval_count: data.prompt_eval_count ?? data.raw?.prompt_eval_count, + knowledge: data.knowledge }); } }, @@ -10296,7 +10309,8 @@ ${HELP_TEXT}`); 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 + prompt_eval_count: data.prompt_eval_count ?? data.raw?.prompt_eval_count, + knowledge: data.knowledge }); }, 0, @@ -10322,7 +10336,8 @@ ${HELP_TEXT}`); 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 + prompt_eval_count: data.prompt_eval_count ?? data.raw?.prompt_eval_count, + knowledge: data.knowledge }); }, 0, @@ -10330,8 +10345,8 @@ ${HELP_TEXT}`); ); } function wireDropZone() { - const board = $2("sa_board"); - const layout = $2("sa_layout"); + const board = $("sa_board"); + const layout = $("sa_layout"); layout?.addEventListener("dragover", (e) => { if (e.dataTransfer?.types?.includes("Files") || e.dataTransfer?.types?.includes("text/uri-list")) { e.preventDefault(); @@ -10418,9 +10433,9 @@ ${HELP_TEXT}`); }); } function wireSplitter() { - const splitter = $2("sa_splitter"); - const layout = $2("sa_layout"); - const pane = $2("sa_image_pane"); + const splitter = $("sa_splitter"); + const layout = $("sa_layout"); + const pane = $("sa_image_pane"); if (!splitter || !layout || !pane) { return; } @@ -10471,7 +10486,7 @@ ${HELP_TEXT}`); note: "Image sent to Assistent", preferSelected: false }); - const pack = $2("sa_pack"); + const pack = $("sa_pack"); if (pack && (pack.value === "ordinary" || pack.value === "write_prompt")) { pack.value = "critique_image"; saveSettings(); @@ -10503,7 +10518,7 @@ ${HELP_TEXT}`); probeOllamaHealth(); } function wire() { - if (!$2("swarm_assistent_root")) { + if (!$("swarm_assistent_root")) { return; } if (typeof genericRequest !== "function") { @@ -10516,7 +10531,6 @@ ${HELP_TEXT}`); window.__swarmAssistentWired = true; loadSettings(); setView(state.view || "chat"); - void window.SA?.training?.resumePolling?.(); updateGate(); ensureBoard(); setBoardTab(state.boardTab || "generate", { persist: false }); @@ -10530,28 +10544,28 @@ ${HELP_TEXT}`); wireSplitter(); registerSendButton(); wireSlashInput(); - $2("sa_btn_new_chat")?.addEventListener("click", (e) => { + $("sa_btn_new_chat")?.addEventListener("click", (e) => { e.stopPropagation(); startNewChat({ saveCurrent: true, openDrawer: true }); }); - $2("sa_btn_new_chat_bar")?.addEventListener("click", (e) => { + $("sa_btn_new_chat_bar")?.addEventListener("click", (e) => { e.stopPropagation(); startNewChat({ saveCurrent: true, openDrawer: true }); }); - $2("sa_ctx_chip")?.addEventListener("click", (e) => { + $("sa_ctx_chip")?.addEventListener("click", (e) => { e.stopPropagation(); toggleCtxPanel(); }); - $2("sa_ctx_close")?.addEventListener("click", (e) => { + $("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"); + $("sa_ctx_panel")?.addEventListener("click", (e) => e.stopPropagation()); + $("sa_ctx_compress")?.addEventListener("click", () => compressNowFromUi()); + $("sa_ctx_reset")?.addEventListener("click", () => resetCompressionFromUi()); + $("sa_ctx_auto")?.addEventListener("change", () => { + COMPRESS_AUTO = !!$("sa_ctx_auto")?.checked; + const settingsAuto = $("sa_compress_auto"); if (settingsAuto) { settingsAuto.checked = COMPRESS_AUTO; } @@ -10560,20 +10574,20 @@ ${HELP_TEXT}`); } 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"); + $("sa_compress_auto")?.addEventListener("change", () => { + COMPRESS_AUTO = !!$("sa_compress_auto")?.checked; + const panelAuto = $("sa_ctx_auto"); if (panelAuto) { panelAuto.checked = COMPRESS_AUTO; } }); - $2("sa_btn_chats")?.addEventListener("click", (e) => { + $("sa_btn_chats")?.addEventListener("click", (e) => { e.stopPropagation(); setChatsPanelOpen(!state.chatsPanelOpen); }); - $2("sa_btn_chats_close")?.addEventListener("click", () => setChatsPanelOpen(false)); - $2("sa_chats_panel")?.addEventListener("click", (e) => e.stopPropagation()); - $2("sa_chats_list")?.addEventListener("click", (e) => { + $("sa_btn_chats_close")?.addEventListener("click", () => setChatsPanelOpen(false)); + $("sa_chats_panel")?.addEventListener("click", (e) => e.stopPropagation()); + $("sa_chats_list")?.addEventListener("click", (e) => { const row = e.target.closest(".sa-chat-row"); if (!row) { return; @@ -10589,8 +10603,8 @@ ${HELP_TEXT}`); switchToChat(id); }); let chatsSearchTimer = null; - $2("sa_chats_search")?.addEventListener("input", () => { - const q = ($2("sa_chats_search")?.value || "").trim(); + $("sa_chats_search")?.addEventListener("input", () => { + const q = ($("sa_chats_search")?.value || "").trim(); state.chatsQuery = q; if (!q) { state.chatsSearchHits = null; @@ -10611,75 +10625,74 @@ ${HELP_TEXT}`); } }, 220); }); - $2("sa_tab_chat")?.addEventListener("click", () => setView("chat")); - $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()); + $("sa_tab_chat")?.addEventListener("click", () => setView("chat")); + $("sa_tab_settings")?.addEventListener("click", () => openSettings(state.settingsTab || "behavior")); + $("sa_board_tab_gen")?.addEventListener("click", () => setBoardTab("generate")); + $("sa_board_tab_refs")?.addEventListener("click", () => setBoardTab("refs")); + $("sa_persona")?.addEventListener("change", onPersonaChanged); + $("sa_persona_delete")?.addEventListener("click", () => deleteCurrentOverlayPersona()); document.querySelectorAll("#sa_settings .sa-stab").forEach((btn) => { btn.addEventListener("click", () => setSettingsTab(btn.getAttribute("data-stab"))); }); - $2("sa_btn_mem_refresh")?.addEventListener("click", () => { + $("sa_btn_mem_refresh")?.addEventListener("click", () => { refreshMemoryList(); }); - $2("sa_mem_kind")?.addEventListener("change", renderMemoryList); - $2("sa_mem_scope")?.addEventListener("change", renderMemoryList); - $2("sa_mem_search")?.addEventListener("input", () => renderMemoryList()); - $2("sa_btn_mem_clear_kind")?.addEventListener("click", () => { + $("sa_mem_kind")?.addEventListener("change", renderMemoryList); + $("sa_mem_scope")?.addEventListener("change", renderMemoryList); + $("sa_mem_search")?.addEventListener("input", () => renderMemoryList()); + $("sa_btn_mem_clear_kind")?.addEventListener("click", () => { const kind = memoryKindFilter(); clearCraftMemory({ kind: kind === "all" ? "" : kind, label: kind === "all" ? "\u0432\u0435\u0441\u044C \u043A\u0440\u0430\u0444\u0442 (\u0444\u0438\u043B\u044C\u0442\u0440 \u0442\u0438\u043F\u0430)" : `\u0442\u0438\u043F ${kind}` }); }); - $2("sa_btn_mem_clear_persona")?.addEventListener("click", () => { - clearCraftMemory({ scope: "personal", persona: $2("sa_persona")?.value || "neutral", label: "\u043A\u0440\u0430\u0444\u0442 \u044D\u0442\u043E\u0439 \u043B\u0438\u0447\u043D\u043E\u0441\u0442\u0438" }); + $("sa_btn_mem_clear_persona")?.addEventListener("click", () => { + clearCraftMemory({ scope: "personal", persona: $("sa_persona")?.value || "neutral", label: "\u043A\u0440\u0430\u0444\u0442 \u044D\u0442\u043E\u0439 \u043B\u0438\u0447\u043D\u043E\u0441\u0442\u0438" }); }); - $2("sa_btn_mem_clear_shared")?.addEventListener("click", () => { + $("sa_btn_mem_clear_shared")?.addEventListener("click", () => { clearCraftMemory({ scope: "shared", label: "\u043E\u0431\u0449\u0443\u044E \u043A\u0440\u0430\u0444\u0442-\u043F\u0430\u043C\u044F\u0442\u044C" }); }); - $2("sa_btn_mem_clear_all")?.addEventListener("click", () => { + $("sa_btn_mem_clear_all")?.addEventListener("click", () => { clearCraftMemory({ label: "\u0432\u0435\u0441\u044C \u043A\u0440\u0430\u0444\u0442 (non-bundled)" }); }); - $2("sa_btn_prefs_refresh")?.addEventListener("click", () => refreshUserPrefs()); - $2("sa_btn_pref_add_global")?.addEventListener("click", () => addUserPref("global")); - $2("sa_btn_pref_add_persona")?.addEventListener("click", () => addUserPref("persona")); - $2("sa_btn_prefs_clear_global")?.addEventListener("click", () => clearUserPrefs("global")); - $2("sa_btn_prefs_clear_persona")?.addEventListener("click", () => clearUserPrefs("persona")); - $2("sa_btn_prefs_clear_all")?.addEventListener("click", () => clearUserPrefs("all")); - $2("sa_user_prefs_weight")?.addEventListener("input", () => { - const lab = $2("sa_user_prefs_weight_val"); + $("sa_btn_prefs_refresh")?.addEventListener("click", () => refreshUserPrefs()); + $("sa_btn_pref_add_global")?.addEventListener("click", () => addUserPref("global")); + $("sa_btn_pref_add_persona")?.addEventListener("click", () => addUserPref("persona")); + $("sa_btn_prefs_clear_global")?.addEventListener("click", () => clearUserPrefs("global")); + $("sa_btn_prefs_clear_persona")?.addEventListener("click", () => clearUserPrefs("persona")); + $("sa_btn_prefs_clear_all")?.addEventListener("click", () => clearUserPrefs("all")); + $("sa_user_prefs_weight")?.addEventListener("input", () => { + const lab = $("sa_user_prefs_weight_val"); if (lab) { - lab.textContent = Number($2("sa_user_prefs_weight").value).toFixed(1); + lab.textContent = Number($("sa_user_prefs_weight").value).toFixed(1); } }); - $2("sa_user_prefs_weight")?.addEventListener("change", () => saveKnobs()); - $2("sa_memory_top_k")?.addEventListener("change", () => saveKnobs()); - $2("sa_btn_knobs_save")?.addEventListener("click", () => saveKnobs()); - $2("sa_btn_reset_ui")?.addEventListener("click", () => resetUiState()); - $2("sa_btn_persona_export")?.addEventListener("click", () => exportSelectedPersona()); - $2("sa_btn_persona_import")?.addEventListener("click", () => $2("sa_persona_import_file")?.click()); - $2("sa_persona_import_file")?.addEventListener("change", (e) => { + $("sa_user_prefs_weight")?.addEventListener("change", () => saveKnobs()); + $("sa_memory_top_k")?.addEventListener("change", () => saveKnobs()); + $("sa_btn_knobs_save")?.addEventListener("click", () => saveKnobs()); + $("sa_btn_reset_ui")?.addEventListener("click", () => resetUiState()); + $("sa_btn_persona_export")?.addEventListener("click", () => exportSelectedPersona()); + $("sa_btn_persona_import")?.addEventListener("click", () => $("sa_persona_import_file")?.click()); + $("sa_persona_import_file")?.addEventListener("change", (e) => { const file = e.target?.files?.[0]; if (file) { importPersonaFile(file); } e.target.value = ""; }); - $2("sa_btn_persona_clone")?.addEventListener("click", () => cloneSelectedPersona()); - $2("sa_btn_persona_delete_panel")?.addEventListener("click", () => deleteSelectedOverlayPersona()); - $2("sa_btn_settings_health")?.addEventListener("click", () => { + $("sa_btn_persona_clone")?.addEventListener("click", () => cloneSelectedPersona()); + $("sa_btn_persona_delete_panel")?.addEventListener("click", () => deleteSelectedOverlayPersona()); + $("sa_btn_settings_health")?.addEventListener("click", () => { probeOllamaHealth(); setTimeout(syncSettingsHealthLine, 400); }); - $2("sa_settings_chat_model")?.addEventListener("change", () => { - const v = $2("sa_settings_chat_model")?.value; - if (v && $2("sa_model")) { - $2("sa_model").value = v; + $("sa_settings_chat_model")?.addEventListener("change", () => { + const v = $("sa_settings_chat_model")?.value; + if (v && $("sa_model")) { + $("sa_model").value = v; saveSettings(); } }); - $2("sa_btn_look_result")?.addEventListener("click", () => askLookAtResult()); - $2("sa_ollama_health")?.addEventListener("click", () => probeOllamaHealth()); + $("sa_btn_look_result")?.addEventListener("click", () => askLookAtResult()); + $("sa_ollama_health")?.addEventListener("click", () => probeOllamaHealth()); document.addEventListener("keydown", (e) => { if (state.lightboxIndex >= 0) { if (e.key === "Escape") { @@ -10710,7 +10723,7 @@ ${HELP_TEXT}`); setChatsPanelOpen(false); closed = true; } - const slash = $2("sa_slash_menu"); + const slash = $("sa_slash_menu"); if (slash && !slash.hidden) { slash.hidden = true; closed = true; @@ -10721,22 +10734,22 @@ ${HELP_TEXT}`); } }); document.getElementById(TAB_BUTTON_ID)?.addEventListener("click", () => { - setTimeout(() => $2("sa_input")?.focus(), 80); + setTimeout(() => $("sa_input")?.focus(), 80); }); - $2("sa_btn_refresh_models")?.addEventListener("click", () => { + $("sa_btn_refresh_models")?.addEventListener("click", () => { saveSettings(); refreshModels(); probeOllamaHealth(); }); - $2("sa_btn_refresh_inventory")?.addEventListener("click", () => refreshInventory(() => { + $("sa_btn_refresh_inventory")?.addEventListener("click", () => refreshInventory(() => { renderLoraChips(); }, { rescan: true })); - $2("sa_btn_add_ref")?.addEventListener("click", () => { + $("sa_btn_add_ref")?.addEventListener("click", () => { setBoardTab("refs"); addRefSlot({ select: true }); }); - $2("sa_btn_use_current")?.addEventListener("click", () => snapshotGenerateToRef()); - $2("sa_btn_as_init")?.addEventListener("click", async () => { + $("sa_btn_use_current")?.addEventListener("click", () => snapshotGenerateToRef()); + $("sa_btn_as_init")?.addEventListener("click", async () => { closeAllMoreMenus(); const src = selectedSrc() || findCurrentGenerateSrc(); if (!src) { @@ -10744,12 +10757,12 @@ ${HELP_TEXT}`); return; } await setInitFromSrc(src); - const pack = $2("sa_pack"); + const pack = $("sa_pack"); if (pack && (pack.value === "ordinary" || pack.value === "write_prompt")) { setPackValue("inpaint_edit", { flash: true }); } }); - $2("sa_btn_as_mask")?.addEventListener("click", async () => { + $("sa_btn_as_mask")?.addEventListener("click", async () => { closeAllMoreMenus(); const src = selectedSrc(); if (!src) { @@ -10759,38 +10772,38 @@ ${HELP_TEXT}`); await setMaskFromSrc(src); setPackValue("inpaint_edit", { flash: true }); }); - $2("sa_btn_clear_init")?.addEventListener("click", () => { + $("sa_btn_clear_init")?.addEventListener("click", () => { if (window.confirm("\u0421\u0431\u0440\u043E\u0441\u0438\u0442\u044C Init \u0438 Mask?")) { clearInitAndMask(); } closeAllMoreMenus(); }); - $2("sa_btn_clear_image")?.addEventListener("click", () => clearSlot(state.selectedSlotId)); - $2("sa_btn_board_more")?.addEventListener("click", (e) => { + $("sa_btn_clear_image")?.addEventListener("click", () => clearSlot(state.selectedSlotId)); + $("sa_btn_board_more")?.addEventListener("click", (e) => { e.stopPropagation(); toggleMoreMenu("sa_board_more_menu", "sa_btn_board_more"); }); - $2("sa_btn_send")?.addEventListener("click", () => sendChat()); - $2("sa_btn_build_gen")?.addEventListener("click", () => buildCurrentAndGenerate()); - $2("sa_btn_interrupt")?.addEventListener("click", () => { + $("sa_btn_send")?.addEventListener("click", () => sendChat()); + $("sa_btn_build_gen")?.addEventListener("click", () => buildCurrentAndGenerate()); + $("sa_btn_interrupt")?.addEventListener("click", () => { doInterruptNow(); }); - $2("sa_btn_clear")?.addEventListener("click", () => { + $("sa_btn_clear")?.addEventListener("click", () => { if (window.confirm("\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C \u0432\u0435\u0441\u044C \u0447\u0430\u0442 Assistent?")) { clearChatHistory(); } }); - $2("sa_btn_clear_more")?.addEventListener("click", (e) => { + $("sa_btn_clear_more")?.addEventListener("click", (e) => { e.stopPropagation(); toggleMoreMenu("sa_clear_more_menu", "sa_btn_clear_more"); }); - $2("sa_btn_clear_confirm")?.addEventListener("click", () => { + $("sa_btn_clear_confirm")?.addEventListener("click", () => { closeAllMoreMenus(); if (window.confirm("\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C \u0432\u0435\u0441\u044C \u0447\u0430\u0442 Assistent?")) { clearChatHistory(); } }); - $2("sa_btn_clear_patches")?.addEventListener("click", () => { + $("sa_btn_clear_patches")?.addEventListener("click", () => { closeAllMoreMenus(); clearPatchBlocksOnly(); }); @@ -10803,26 +10816,26 @@ ${HELP_TEXT}`); } closeAllMoreMenus(); }); - $2("sa_board_more_menu")?.addEventListener("click", (e) => e.stopPropagation()); - $2("sa_clear_more_menu")?.addEventListener("click", (e) => e.stopPropagation()); - $2("sa_base_url")?.addEventListener("change", saveSettings); - $2("sa_model")?.addEventListener("change", () => { - const v = $2("sa_model")?.value; - if (v && $2("sa_settings_chat_model")) { - $2("sa_settings_chat_model").value = v; + $("sa_board_more_menu")?.addEventListener("click", (e) => e.stopPropagation()); + $("sa_clear_more_menu")?.addEventListener("click", (e) => e.stopPropagation()); + $("sa_base_url")?.addEventListener("change", saveSettings); + $("sa_model")?.addEventListener("change", () => { + const v = $("sa_model")?.value; + if (v && $("sa_settings_chat_model")) { + $("sa_settings_chat_model").value = v; } saveSettings(); }); - $2("sa_embed_model")?.addEventListener("change", () => { - state.preferredEmbed = $2("sa_embed_model")?.value || ""; + $("sa_embed_model")?.addEventListener("change", () => { + state.preferredEmbed = $("sa_embed_model")?.value || ""; saveSettings(); }); - $2("sa_pack")?.addEventListener("change", () => { + $("sa_pack")?.addEventListener("change", () => { state.packUserTouched = true; saveSettings(); syncModeBadge(); }); - $2("sa_chips")?.addEventListener("click", async (e) => { + $("sa_chips")?.addEventListener("click", async (e) => { const btn = e.target.closest(".sa-chip"); if (!btn) { return; @@ -10866,7 +10879,7 @@ ${HELP_TEXT}`); } renderLoraChips(); }); - $2("sa_auto_vision")?.addEventListener("change", () => { + $("sa_auto_vision")?.addEventListener("change", () => { saveSettings(); const gen = generateSlot(); if (gen) { @@ -10874,11 +10887,11 @@ ${HELP_TEXT}`); renderBoard(); } }); - $2("sa_auto_apply")?.addEventListener("change", saveSettings); - $2("sa_auto_generate")?.addEventListener("change", saveSettings); - $2("sa_auto_critique")?.addEventListener("change", saveSettings); - $2("sa_auto_download")?.addEventListener("change", saveSettings); - $2("sa_park_llm")?.addEventListener("change", saveSettings); + $("sa_auto_apply")?.addEventListener("change", saveSettings); + $("sa_auto_generate")?.addEventListener("change", saveSettings); + $("sa_auto_critique")?.addEventListener("change", saveSettings); + $("sa_auto_download")?.addEventListener("change", saveSettings); + $("sa_park_llm")?.addEventListener("change", saveSettings); syncChipHighlight(); setInterval(syncChipHighlight, 2500); setInterval(renderLoraChips, 4e3); @@ -10947,14 +10960,14 @@ ${HELP_TEXT}`); }; } function wireSlashInput() { - const input = $2("sa_input"); + const input = $("sa_input"); if (!input || input.dataset.saSlashWired) { return; } input.dataset.saSlashWired = "1"; input.addEventListener("input", () => updateSlashMenuFromInput()); input.addEventListener("keydown", (e) => { - const menu = $2("sa_slash_menu"); + const menu = $("sa_slash_menu"); const open = menu && !menu.hidden; if (open) { const items = slashMatches(input.value.split(/\s/)[0] || ""); @@ -10998,897 +11011,6 @@ ${HELP_TEXT}`); } })(); - // src/training.js - var $ = (id) => document.getElementById(id); - function escapeHtml(s) { - return String(s ?? "").replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); - } - var QLORA_HF_CUSTOM = "__custom__"; - function attachTraining(SA2) { - const state = { - ttab: "dataset", - samples: [], - hfResults: [], - hfSelected: null, - hfCheck: null, - hfMapping: null, - trainWs: null, - polling: null, - qloraTraining: null, - agentSettings: { enabled: true, auto_link_on_approve: true, heard_quota: 3 }, - agentLinked: 0 - }; - function setAgentHeardStats(linked) { - const el = $("sa_agent_heard_stats"); - if (el) { - el.textContent = `\u041F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u043E: ${linked ?? state.agentLinked ?? "\u2014"}`; - } - } - async function loadAgentHeardSettings() { - try { - const data = await SA2.request("AssistentGetDatasetAgentSettings", {}); - const s = data?.settings || {}; - state.agentSettings = { - enabled: s.enabled !== false, - auto_link_on_approve: s.auto_link_on_approve !== false, - heard_quota: s.heard_quota ?? 3 - }; - state.agentLinked = data?.linked ?? 0; - if ($("sa_agent_heard_enabled")) $("sa_agent_heard_enabled").checked = state.agentSettings.enabled; - if ($("sa_agent_auto_link")) $("sa_agent_auto_link").checked = state.agentSettings.auto_link_on_approve; - if ($("sa_agent_heard_quota")) $("sa_agent_heard_quota").value = String(state.agentSettings.heard_quota); - setAgentHeardStats(state.agentLinked); - } catch (e) { - const msg = String(e.message || e); - setTrainStatus(formatTrainError(msg)); - console.warn("loadAgentHeardSettings", e); - } - } - async function saveAgentHeardSettings() { - const settings = { - enabled: !!$("sa_agent_heard_enabled")?.checked, - auto_link_on_approve: !!$("sa_agent_auto_link")?.checked, - heard_quota: Math.max(0, Math.min(8, parseInt($("sa_agent_heard_quota")?.value, 10) || 3)) - }; - try { - const data = await SA2.request("AssistentSaveDatasetAgentSettings", { settings }); - state.agentSettings = data?.settings || settings; - setTrainStatus("\u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 \xAB\u0443\u0441\u043B\u044B\u0448\u0430\u043D\u043D\u043E\u0433\u043E\xBB \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u044B"); - } catch (e) { - setTrainStatus(formatTrainError(e.message || e)); - } - } - async function syncAllToAgent() { - setTrainStatus("\u041F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u0435 \u043A \u0430\u0433\u0435\u043D\u0442\u0443\u2026"); - try { - await saveAgentHeardSettings(); - const data = await SA2.request("AssistentSyncDatasetToAgent", { approved_only: true, relink: false }); - state.agentLinked = data?.total_linked ?? state.agentLinked; - setAgentHeardStats(state.agentLinked); - setTrainStatus(`\u041F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u043E: +${data?.linked_now ?? 0}, \u0432\u0441\u0435\u0433\u043E ${data?.total_linked ?? "\u2014"}`); - await refreshSamples(); - } catch (e) { - setTrainStatus(formatTrainError(e.message || e)); - } - } - function setTrainStatus(msg) { - const el = $("sa_train_status"); - if (el) el.textContent = msg || ""; - } - function isSqliteError(msg) { - const s = String(msg || "").toLowerCase(); - return s.includes("sqlite") || s.includes("sqlconnection") || s.includes("train_samples") || s.includes("training database unavailable"); - } - function isGenericServerError(msg) { - return /internal error occurred/i.test(String(msg || "")); - } - function formatTrainError(msg) { - const s = String(msg || ""); - if (isSqliteError(s)) { - return `${s} \u2014 ${sqliteHint()}`; - } - if (isGenericServerError(s)) { - return `${s} \u2014 \u0447\u0430\u0441\u0442\u043E SQLite/\u0438\u043C\u043F\u043E\u0440\u0442 HF: gpu-rent seed-extensions, restart SwarmUI, HF token \u0434\u043B\u044F gated.`; - } - return s; - } - function sqliteHint() { - return "\u0411\u0430\u0437\u0430 Assistent (SQLite) \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430 \u2014 gpu-rent seed-extensions + restart SwarmUI (\u22650.15.6)."; - } - function parseJobProgress(job) { - if (!job) return null; - const raw = job.progress_json; - if (!raw) return null; - try { - return typeof raw === "string" ? JSON.parse(raw) : raw; - } catch { - return null; - } - } - function renderLastTrainJob(job) { - const box = $("sa_train_last_job"); - if (!box) return; - if (!job) { - box.hidden = true; - box.innerHTML = ""; - return; - } - const prog = parseJobProgress(job); - const status = job.status || prog?.status || "\u2014"; - const out = job.output_name || prog?.ollama?.name || "\u2014"; - const base = job.base_model || "\u2014"; - const ollama = prog?.ollama; - let ollamaLine = ""; - if (ollama?.success) { - ollamaLine = `
Ollama: ${escapeHtml(ollama.name || out)} \u2014 \u0432\u044B\u0431\u0435\u0440\u0438 \u0432 \u0448\u0430\u043F\u043A\u0435 \u0447\u0430\u0442\u0430
`; - } else if (ollama?.error) { - ollamaLine = `
Ollama: ${escapeHtml(ollama.error)}
`; - } else if (ollama?.skipped) { - ollamaLine = `
${escapeHtml(ollama.note || "\u0410\u0434\u0430\u043F\u0442\u0435\u0440 \u043D\u0430 \u0434\u0438\u0441\u043A\u0435, ollama create \u0432\u0440\u0443\u0447\u043D\u0443\u044E")}
`; - } - box.hidden = false; - box.innerHTML = ` -
\u041F\u043E\u0441\u043B\u0435\u0434\u043D\u044F\u044F \u0442\u0440\u0435\u043D\u0438\u0440\u043E\u0432\u043A\u0430 \xB7 ${escapeHtml(job.kind || "qlora")} \xB7 ${escapeHtml(status)}
-
HF base: ${escapeHtml(base)} \u2192 \u0438\u043C\u044F: ${escapeHtml(out)}
- ${ollamaLine} - ${prog?.log ? `
${escapeHtml(String(prog.log).slice(-4e3))}
` : ""}`; - } - function setTrainingTab(id) { - state.ttab = id || "dataset"; - document.querySelectorAll("#sa_training .sa-ttab").forEach((btn) => { - const on = btn.getAttribute("data-ttab") === state.ttab; - btn.classList.toggle("sa-ttab-active", on); - btn.setAttribute("aria-selected", on ? "true" : "false"); - }); - document.querySelectorAll("#sa_training .sa-tpane").forEach((pane) => { - pane.hidden = pane.getAttribute("data-tpane") !== state.ttab; - }); - if (state.ttab === "dataset") { - refreshSamples(); - loadAgentHeardSettings(); - } - if (state.ttab === "train") { - syncModelfileModels(); - syncQloraModels(); - void resumeTrainJobPolling(); - } - if (state.ttab === "models") refreshTrainModels(); - } - async function refreshSamples() { - try { - const status = $("sa_train_filter_status")?.value || "all"; - const persona = $("sa_train_filter_persona")?.value || "all"; - const data = await SA2.request("AssistentListTrainSamples", { status, persona, limit: 300 }); - if (data?.error) { - const msg = data.error; - setTrainStatus(formatTrainError(msg)); - const stats2 = $("sa_train_stats"); - if (stats2) stats2.textContent = "\u0414\u0430\u0442\u0430\u0441\u0435\u0442 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D (SQLite)"; - state.samples = []; - renderSamples(); - return; - } - state.samples = data?.samples || []; - const stats = $("sa_train_stats"); - if (stats) { - const appr = data?.approved ?? "\u2014"; - const draft = data?.draft ?? "\u2014"; - const total = data?.total ?? "\u2014"; - stats.textContent = `\u041E\u0434\u043E\u0431\u0440\u0435\u043D\u043E: ${appr} \xB7 \u0447\u0435\u0440\u043D\u043E\u0432\u0438\u043A\u0438: ${draft} \xB7 \u0432\u0441\u0435\u0433\u043E: ${total}`; - } - const personaSel = $("sa_train_filter_persona"); - if (personaSel && $("sa_persona")) { - const cur = personaSel.value || "all"; - personaSel.innerHTML = ''; - for (const opt of $("sa_persona").options) { - const o = document.createElement("option"); - o.value = opt.value; - o.textContent = opt.textContent; - personaSel.appendChild(o); - } - personaSel.value = cur; - } - renderSamples(); - } catch (e) { - setTrainStatus(formatTrainError(e.message || e)); - } - } - function renderSamples() { - const root = $("sa_train_samples"); - if (!root) return; - const filter = $("sa_train_filter_status")?.value || "all"; - if (!state.samples.length) { - const hint = filter === "approved" ? "\u041F\u043E\u0434 \u0444\u0438\u043B\u044C\u0442\u0440\u043E\u043C \xAB\u041E\u0434\u043E\u0431\u0440\u0435\u043D\u043D\u044B\u0435\xBB \u043F\u0443\u0441\u0442\u043E. HF-\u0438\u043C\u043F\u043E\u0440\u0442 \u0441\u043E\u0437\u0434\u0430\u0451\u0442 \u0447\u0435\u0440\u043D\u043E\u0432\u0438\u043A\u0438 \u2014 \u043F\u0435\u0440\u0435\u043A\u043B\u044E\u0447\u0438 \u043D\u0430 \xAB\u0427\u0435\u0440\u043D\u043E\u0432\u0438\u043A\u0438\xBB \u0438\u043B\u0438 \xAB\u0412\u0441\u0435 \u0441\u0442\u0430\u0442\u0443\u0441\u044B\xBB." : filter === "draft" ? "\u041D\u0435\u0442 \u0447\u0435\u0440\u043D\u043E\u0432\u0438\u043A\u043E\u0432. \u0418\u043C\u043F\u043E\u0440\u0442\u0438\u0440\u0443\u0439 HF \u0438\u043B\u0438 \u043E\u0442\u043C\u0435\u0442\u044C \u043F\u0440\u0438\u043C\u0435\u0440\u044B \u0432 \u0447\u0430\u0442\u0435." : "\u041D\u0435\u0442 \u043F\u0440\u0438\u043C\u0435\u0440\u043E\u0432. \u041E\u0442\u043C\u0435\u0442\u044C \u043E\u0442\u0432\u0435\u0442\u044B \u0432 \u0447\u0430\u0442\u0435 \u0438\u043B\u0438 \u0438\u043C\u043F\u043E\u0440\u0442\u0438\u0440\u0443\u0439 \u0434\u0430\u0442\u0430\u0441\u0435\u0442 (HF / \u0444\u0430\u0439\u043B)."; - root.innerHTML = `
${hint}
`; - return; - } - root.innerHTML = ""; - for (const s of state.samples) { - const div = document.createElement("div"); - div.className = "sa-train-sample"; - div.dataset.id = s.id; - const msgs = s.messages || []; - const preview = msgs.map((m) => `${m.role}: ${(m.content || "").slice(0, 120)}`).join("\n"); - const linked = s.agent_linked ? " \xB7 \u{1F517} \u0430\u0433\u0435\u043D\u0442" : ""; - div.innerHTML = ` -
- ${escapeHtml(s.status)} - ${escapeHtml(s.source)} \xB7 ${escapeHtml(s.persona || "\u2014")} \xB7 ${escapeHtml(s.pack || "\u2014")}${linked} - - - - - -
- `; - root.appendChild(div); - } - } - async function upsertSample(patch) { - await SA2.request("AssistentUpsertTrainSample", patch); - await refreshSamples(); - } - function hfStringColumns(check) { - const cols = check?.schema?.columns; - if (Array.isArray(cols) && cols.length) return cols; - const feats = check?.features; - if (Array.isArray(feats)) { - return feats.map((f) => f?.name).filter(Boolean); - } - if (feats && typeof feats === "object") return Object.keys(feats); - return []; - } - function renderHfMappingUI(check) { - const row = $("sa_hf_mapping_row"); - if (!row) return; - const gate = check?.gate; - const schemaKind = check?.schema?.kind; - const needsMapping = gate === "mapping" || schemaKind === "fiction_tags_text"; - row.hidden = !needsMapping; - if (!needsMapping) { - state.hfMapping = null; - return; - } - const cols = hfStringColumns(check); - const userSel = $("sa_hf_user_col"); - const asstSel = $("sa_hf_asst_col"); - const presetSel = $("sa_hf_mapping_preset"); - if (userSel) { - userSel.innerHTML = cols.map((c) => ``).join(""); - if (cols.includes("tags")) userSel.value = "tags"; - else if (cols.includes("title")) userSel.value = "title"; - } - if (asstSel) { - asstSel.innerHTML = cols.map((c) => ``).join(""); - if (cols.includes("text")) asstSel.value = "text"; - else if (cols.includes("output")) asstSel.value = "output"; - } - if (schemaKind === "fiction_tags_text" && presetSel) { - presetSel.value = "fiction_tags_text"; - state.hfMapping = { kind: "fiction_tags_text", preset: "fiction_tags_text" }; - } - } - function buildHfMappingPayload() { - const preset = $("sa_hf_mapping_preset")?.value; - if (preset === "fiction_tags_text") { - return { kind: "fiction_tags_text", preset: "fiction_tags_text" }; - } - const userCol = $("sa_hf_user_col")?.value; - const asstCol = $("sa_hf_asst_col")?.value; - if (userCol && asstCol) { - return { kind: "custom", user_col: userCol, assistant_col: asstCol }; - } - return state.hfMapping; - } - function getQloraHfBase() { - const sel = $("sa_qlora_base"); - if (!sel) return ""; - if (sel.value === QLORA_HF_CUSTOM) { - return ($("sa_qlora_base_custom")?.value || "").trim(); - } - return (sel.value || "").trim(); - } - function applyQloraPresetFromSelect({ fillName = true } = {}) { - const sel = $("sa_qlora_base"); - const customRow = $("sa_qlora_base_custom_row"); - if (!sel) return; - if (sel.value === QLORA_HF_CUSTOM) { - if (customRow) customRow.hidden = false; - return; - } - if (customRow) customRow.hidden = true; - const presetJson = sel.selectedOptions[0]?.dataset?.preset; - if (!presetJson) return; - let preset; - try { - preset = JSON.parse(presetJson); - } catch { - return; - } - const nameEl = $("sa_qlora_name"); - if (fillName && nameEl && !nameEl.value.trim() && preset.default_output) { - nameEl.value = preset.default_output; - } - const ollamaSel = $("sa_qlora_ollama_base"); - if (ollamaSel && preset.ollama_hint) { - const hint = preset.ollama_hint; - if ([...ollamaSel.options].some((o) => o.value === hint)) { - ollamaSel.value = hint; - } - } - if (preset.rank != null && $("sa_qlora_rank")) { - $("sa_qlora_rank").value = preset.rank; - } - if (preset.seq_len != null && $("sa_qlora_seq")) { - $("sa_qlora_seq").value = preset.seq_len; - } - } - function populateQloraHfPresets(training) { - const sel = $("sa_qlora_base"); - if (!sel) return; - const prevBase = getQloraHfBase(); - const models = Array.isArray(training?.hf_models) ? training.hf_models : []; - sel.innerHTML = ''; - for (const m of models) { - const hfId = (m.hf_id || m.id || "").trim(); - if (!hfId) continue; - const opt = document.createElement("option"); - opt.value = hfId; - opt.textContent = m.title ? `${m.title} (${hfId})` : hfId; - opt.dataset.preset = JSON.stringify(m); - sel.appendChild(opt); - } - const customOpt = document.createElement("option"); - customOpt.value = QLORA_HF_CUSTOM; - customOpt.textContent = "\u0414\u0440\u0443\u0433\u0430\u044F (\u0432\u0432\u0435\u0441\u0442\u0438 HF id\u2026)"; - sel.appendChild(customOpt); - if (prevBase && [...sel.options].some((o) => o.value === prevBase)) { - sel.value = prevBase; - } else if (prevBase) { - sel.value = QLORA_HF_CUSTOM; - const custom = $("sa_qlora_base_custom"); - if (custom) custom.value = prevBase; - } else if (models.length) { - sel.value = (models[0].hf_id || models[0].id || "").trim(); - } - applyQloraPresetFromSelect({ fillName: !prevBase }); - } - async function ensureTrainingQloraConfig(force = false) { - if (!force && state.qloraTraining) { - return state.qloraTraining; - } - try { - const persona = $("sa_persona")?.value || ""; - const data = await SA2.request("AssistentGetConfig", { persona }); - state.qloraTraining = data?.training && typeof data.training === "object" ? data.training : { hf_models: [] }; - return state.qloraTraining; - } catch { - state.qloraTraining = state.qloraTraining || { hf_models: [] }; - return state.qloraTraining; - } - } - async function syncQloraModels() { - try { - const training = await ensureTrainingQloraConfig(); - populateQloraHfPresets(training); - const baseUrl = $("sa_base_url")?.value || localStorage.getItem("swarm_assistent_base_url") || ""; - const data = await SA2.request("AssistentListModels", { baseUrl }); - const models = data?.models || []; - const sel = $("sa_qlora_ollama_base"); - if (!sel) return; - const cur = sel.value; - sel.innerHTML = ''; - for (const m of models) { - const opt = document.createElement("option"); - opt.value = m; - opt.textContent = m; - sel.appendChild(opt); - } - if (cur) sel.value = cur; - else if ($("sa_model")?.value) sel.value = $("sa_model").value; - applyQloraPresetFromSelect({ fillName: false }); - } catch (e) { - } - } - function renderHfList() { - const root = $("sa_hf_list"); - if (!root) return; - root.innerHTML = ""; - const showAll = !!$("sa_hf_show_all")?.checked; - for (const r of state.hfResults) { - if (!showAll && r.gate === "rejected") continue; - const row = document.createElement("div"); - row.className = "sa-hf-row" + (state.hfSelected === r.id ? " sa-hf-row-active" : "") + (r.gate === "rejected" ? " sa-hf-rejected" : ""); - row.dataset.id = r.id; - const badge = r.gate === "ok" ? "ok" : r.gate === "mapping" ? "map" : "no"; - row.innerHTML = `${escapeHtml(r.gate)}${escapeHtml(r.id)}${escapeHtml(r.reason || "")}`; - root.appendChild(row); - } - } - async function searchHf() { - const q = ($("sa_hf_search")?.value || "").trim(); - setTrainStatus("\u041F\u043E\u0438\u0441\u043A\u2026"); - try { - const data = await SA2.request("AssistentSearchHfDatasets", { - q, - limit: 24, - show_all: !!$("sa_hf_show_all")?.checked - }); - state.hfResults = data?.results || []; - renderHfList(); - setTrainStatus(`\u041D\u0430\u0439\u0434\u0435\u043D\u043E: ${state.hfResults.length}`); - } catch (e) { - setTrainStatus(formatTrainError(e.message || e)); - } - } - async function checkHfLink() { - const link = ($("sa_hf_link")?.value || "").trim(); - const status = $("sa_hf_status"); - if (!link) return; - if (status) status.textContent = "\u041F\u0440\u043E\u0432\u0435\u0440\u044F\u044E\u2026"; - try { - const data = await SA2.request("AssistentCheckHfDataset", { dataset: link }); - state.hfCheck = data; - state.hfSelected = data.id; - if (status) { - status.textContent = data.gate === "rejected" ? `\u041E\u0442\u043A\u043B\u043E\u043D\u0435\u043D\u043E: ${data.reason}` : `${data.gate}: ${data.reason || "OK"}`; - } - const preview = $("sa_hf_preview"); - if (preview) { - preview.hidden = false; - preview.textContent = JSON.stringify(data.sample_rows || data.features || data, null, 2).slice(0, 8e3); - } - const importRow = $("sa_hf_import_row"); - if (importRow) importRow.hidden = data.gate === "rejected"; - renderHfMappingUI(data); - } catch (e) { - if (status) status.textContent = String(e.message || e); - } - } - async function importHf() { - const link = ($("sa_hf_link")?.value || "").trim(); - if (!state.hfCheck?.id && link) { - await checkHfLink(); - } - if (!state.hfSelected && !state.hfCheck?.id) { - setTrainStatus("\u0421\u043D\u0430\u0447\u0430\u043B\u0430 \u043F\u0440\u043E\u0432\u0435\u0440\u044C \u043D\u0430\u0431\u043E\u0440"); - const hfSt2 = $("sa_hf_status"); - if (hfSt2) hfSt2.textContent = "\u0412\u0441\u0442\u0430\u0432\u044C \u0441\u0441\u044B\u043B\u043A\u0443 \u0438 \u043D\u0430\u0436\u043C\u0438 \xAB\u041F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C\xBB"; - return; - } - const id = state.hfSelected || state.hfCheck.id; - const limit = Number($("sa_hf_import_limit")?.value) || 200; - const mapping = buildHfMappingPayload(); - const btn = $("sa_btn_hf_import"); - const hfSt = $("sa_hf_status"); - const busy = limit > 400 ? `\u0418\u043C\u043F\u043E\u0440\u0442\u0438\u0440\u0443\u044E \u0434\u043E ${limit} \u0441\u0442\u0440\u043E\u043A\u2026 (1\u20132 \u043C\u0438\u043D)` : `\u0418\u043C\u043F\u043E\u0440\u0442\u0438\u0440\u0443\u044E \u0434\u043E ${limit}\u2026`; - setTrainStatus(busy); - if (hfSt) { - hfSt.textContent = busy; - hfSt.classList.add("sa-hf-busy"); - } - if (btn) { - btn.disabled = true; - btn.dataset.label = btn.textContent; - btn.textContent = "\u2026"; - } - try { - const data = await SA2.request("AssistentImportHfDataset", { dataset: id, limit, mapping }); - const n = Number(data?.imported) || 0; - let msg; - if (data?.runner_only) { - msg = `Runner-only: ${data.note || id} (\u0432 sqlite \u043D\u0435 \u0438\u043C\u043F\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u043B\u0438)`; - } else if (n > 0) { - const filt = $("sa_train_filter_status"); - if (filt && filt.value === "approved") { - filt.value = "draft"; - } - msg = `\u0418\u043C\u043F\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u043E: ${n} \u0447\u0435\u0440\u043D\u043E\u0432\u0438\u043A(\u043E\u0432) \u2014 \u0441\u043F\u0438\u0441\u043E\u043A \u043D\u0438\u0436\u0435 (\u0444\u0438\u043B\u044C\u0442\u0440 \u2192 \u0447\u0435\u0440\u043D\u043E\u0432\u0438\u043A\u0438)`; - } else { - msg = "\u0418\u043C\u043F\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u043E: 0 \u2014 HF token \u0432 User Settings, \u043C\u0430\u043F\u043F\u0438\u043D\u0433 \u0438\u043B\u0438 gated-\u043D\u0430\u0431\u043E\u0440"; - } - setTrainStatus(msg); - if (hfSt) hfSt.textContent = msg; - await refreshSamples(); - $("sa_train_samples")?.scrollIntoView({ behavior: "smooth", block: "nearest" }); - } catch (e) { - const err = String(e.message || e); - const show = formatTrainError(err); - setTrainStatus(show); - if (hfSt) hfSt.textContent = show; - } finally { - if (hfSt) hfSt.classList.remove("sa-hf-busy"); - if (btn) { - btn.disabled = false; - btn.textContent = btn.dataset.label || "\u0418\u043C\u043F\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C"; - } - } - } - async function syncModelfileModels() { - try { - const baseUrl = $("sa_base_url")?.value || localStorage.getItem("swarm_assistent_base_url") || ""; - const data = await SA2.request("AssistentListModels", { baseUrl }); - const models = data?.models || []; - for (const selId of ["sa_modelfile_base"]) { - const sel = $(selId); - if (!sel) continue; - const cur = sel.value; - sel.innerHTML = ''; - for (const m of models) { - const opt = document.createElement("option"); - opt.value = m; - opt.textContent = m; - sel.appendChild(opt); - } - if (cur) sel.value = cur; - } - const personaSel = $("sa_modelfile_persona"); - if (personaSel && $("sa_persona")) { - personaSel.innerHTML = $("sa_persona").innerHTML; - personaSel.value = $("sa_persona").value || "neutral"; - } - } catch (e) { - } - } - async function createModelfile() { - const btn = $("sa_btn_modelfile_create"); - btn?.setAttribute("disabled", "disabled"); - setTrainStatus("\u0421\u043E\u0437\u0434\u0430\u044E Modelfile \u0432 Ollama\u2026"); - try { - const data = await SA2.request("AssistentCreateOllamaModel", { - base_url: $("sa_base_url")?.value, - base_model: $("sa_modelfile_base")?.value, - name: $("sa_modelfile_name")?.value, - persona: $("sa_modelfile_persona")?.value, - system: $("sa_modelfile_system")?.value, - shots: Number($("sa_modelfile_shots")?.value) || 8, - num_ctx: Number($("sa_modelfile_num_ctx")?.value) || 16384, - temperature: Number($("sa_modelfile_temp")?.value) || 0.7 - }); - setTrainStatus(`\u0413\u043E\u0442\u043E\u0432\u043E: ${data.name}`); - SA2.app?.refreshModels?.(); - } catch (e) { - setTrainStatus(formatTrainError(e.message || e)); - } finally { - btn?.removeAttribute("disabled"); - } - } - function setTrainMode(mode) { - $("sa_train_form_modelfile").hidden = mode !== "modelfile"; - $("sa_train_form_qlora").hidden = mode !== "qlora"; - const radio = document.querySelector(`input[name="sa_train_mode"][value="${mode}"]`); - if (radio) radio.checked = true; - } - function setTrainingLock(on, text) { - const root = $("swarm_assistent_root"); - const banner = $("sa_train_banner"); - if (root) root.classList.toggle("sa-root-training-lock", !!on); - if (banner) { - banner.hidden = !on; - const t = $("sa_train_banner_text"); - if (t && text) t.textContent = text; - } - SA2.app?.setTrainingLock?.(!!on); - } - async function pollTrainJob() { - try { - const data = await SA2.request("AssistentGetTrainJob", {}); - const prog = data?.progress || (data?.job?.progress_json ? JSON.parse(data.job.progress_json) : null); - const active = data?.training_active || data?.job?.status === "running"; - const status = data?.job?.status || prog?.status; - const pct = prog?.percent; - const bannerText = active && pct != null ? `QLoRA \xB7 ${pct}%` : active ? "\u0418\u0434\u0451\u0442 QLoRA\u2026" : "\u0418\u0434\u0451\u0442 \u0442\u0440\u0435\u043D\u0438\u0440\u043E\u0432\u043A\u0430\u2026"; - setTrainingLock(active, bannerText); - const logEl = $("sa_train_log"); - const bar = $("sa_train_progress_fill"); - const box = $("sa_train_progress"); - if (prog) { - if (box) box.hidden = false; - if (bar && pct != null) bar.style.width = `${pct}%`; - if (logEl && prog.log) logEl.textContent = prog.log; - } - if (active) { - setTrainStatus(pct != null ? `QLoRA \xB7 ${pct}% \u2014 \u043F\u043E\u043B\u043D\u044B\u0439 \u043B\u043E\u0433 \u043D\u0430 \u0432\u043A\u043B\u0430\u0434\u043A\u0435 \xAB\u0422\u0440\u0435\u043D\u0438\u0440\u043E\u0432\u043A\u0430\xBB` : "QLoRA \u0437\u0430\u043F\u0443\u0449\u0435\u043D\u0430 \u2014 \u043F\u043E\u043B\u043D\u044B\u0439 \u043B\u043E\u0433 \u043D\u0430 \u0432\u043A\u043B\u0430\u0434\u043A\u0435 \xAB\u0422\u0440\u0435\u043D\u0438\u0440\u043E\u0432\u043A\u0430\xBB"); - } - if (!active) { - clearInterval(state.polling); - state.polling = null; - $("sa_btn_qlora_cancel").hidden = true; - setTrainingLock(false); - if (status === "completed" || status === "completed_with_warnings") { - const ollama = prog?.ollama; - if (ollama?.success) { - setTrainStatus(`\u0413\u043E\u0442\u043E\u0432\u043E: \u043C\u043E\u0434\u0435\u043B\u044C ${ollama.name} \u0432 Ollama \u2014 \u0432\u043A\u043B\u0430\u0434\u043A\u0430 \xAB\u041C\u043E\u0434\u0435\u043B\u0438\xBB`); - SA2.app?.refreshModels?.(); - } else if (ollama?.skipped) { - setTrainStatus(ollama.note || ollama.error || "\u0410\u0434\u0430\u043F\u0442\u0435\u0440 \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D \u2014 \u0441\u043C. \u0432\u043A\u043B\u0430\u0434\u043A\u0443 \xAB\u041C\u043E\u0434\u0435\u043B\u0438\xBB"); - } else if (ollama?.error) { - setTrainStatus(`\u041E\u0431\u0443\u0447\u0435\u043D\u0438\u0435 OK, Ollama: ${ollama.error}`); - } else if (status === "completed_with_warnings") { - setTrainStatus("\u0417\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u043E \u0441 \u043F\u0440\u0435\u0434\u0443\u043F\u0440\u0435\u0436\u0434\u0435\u043D\u0438\u044F\u043C\u0438 \u2014 \u043B\u043E\u0433 \u043D\u0430 \xAB\u041C\u043E\u0434\u0435\u043B\u0438\xBB"); - } else { - setTrainStatus("QLoRA \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u043E \u2014 \u0432\u043A\u043B\u0430\u0434\u043A\u0430 \xAB\u041C\u043E\u0434\u0435\u043B\u0438\xBB"); - } - setTrainingTab("models"); - await refreshTrainModels(); - } else if (status === "failed") { - setTrainStatus(`\u041E\u0448\u0438\u0431\u043A\u0430 \u0442\u0440\u0435\u043D\u0438\u0440\u043E\u0432\u043A\u0438 (exit ${prog?.exit_code ?? "?"})`); - } - } - } catch (e) { - } - } - async function resumeTrainJobPolling() { - try { - const data = await SA2.request("AssistentGetTrainJob", {}); - const active = data?.training_active || data?.job?.status === "running"; - await pollTrainJob(); - if (!active) return; - setTrainMode("qlora"); - $("sa_btn_qlora_cancel").hidden = false; - if (state.polling) clearInterval(state.polling); - state.polling = setInterval(pollTrainJob, 1500); - } catch (e) { - } - } - async function startQlora() { - const baseModel = getQloraHfBase(); - const outputName = ($("sa_qlora_name")?.value || "").trim(); - if (!baseModel) { - setTrainStatus("\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 HF base model \u0438\u0437 \u0441\u043F\u0438\u0441\u043A\u0430 \u0438\u043B\u0438 \u0443\u043A\u0430\u0436\u0438\u0442\u0435 \u0441\u0432\u043E\u0439 HF id"); - $("sa_qlora_base")?.focus(); - return; - } - if (!outputName) { - setTrainStatus("\u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u0438\u043C\u044F \u043C\u043E\u0434\u0435\u043B\u0438 \u0432 Ollama (\u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 my-lora:v1)"); - $("sa_qlora_name")?.focus(); - return; - } - setTrainStatus("\u0417\u0430\u043F\u0443\u0441\u043A\u2026"); - try { - const hfDs = ($("sa_qlora_hf_dataset")?.value || "").trim(); - const mapping = hfDs ? buildHfMappingPayload() : void 0; - await SA2.request("AssistentStartTrainJob", { - base_url: $("sa_base_url")?.value, - chat_model: $("sa_model")?.value, - base_model: baseModel, - ollama_base: $("sa_qlora_ollama_base")?.value, - output_name: outputName, - rank: Number($("sa_qlora_rank")?.value) || 16, - alpha: Number($("sa_qlora_alpha")?.value) || 32, - lr: Number($("sa_qlora_lr")?.value) || 2e-4, - epochs: Number($("sa_qlora_epochs")?.value) || 3, - seq_len: Number($("sa_qlora_seq")?.value) || 2048, - max_samples: Number($("sa_qlora_max_samples")?.value) || 0, - four_bit: !!$("sa_qlora_4bit")?.checked, - hf_dataset: hfDs || void 0, - hf_mapping: mapping - }); - $("sa_btn_qlora_cancel").hidden = false; - setTrainingLock(true, "\u0418\u0434\u0451\u0442 \u0442\u0440\u0435\u043D\u0438\u0440\u043E\u0432\u043A\u0430\u2026"); - if (state.polling) clearInterval(state.polling); - state.polling = setInterval(pollTrainJob, 1500); - pollTrainJob(); - setTrainMode("qlora"); - setTrainingTab("train"); - setTrainStatus("QLoRA \u0437\u0430\u043F\u0443\u0449\u0435\u043D\u0430 \u2014 \u043F\u0440\u043E\u0433\u0440\u0435\u0441\u0441 \u043D\u0438\u0436\u0435"); - } catch (e) { - setTrainStatus(formatTrainError(e.message || e)); - } - } - async function cancelQlora() { - try { - await SA2.request("AssistentCancelTrainJob", {}); - setTrainingLock(false); - setTrainStatus("\u041E\u0442\u043C\u0435\u043D\u0435\u043D\u043E"); - } catch (e) { - setTrainStatus(formatTrainError(e.message || e)); - } - } - async function refreshTrainModels() { - const root = $("sa_train_models_list"); - if (!root) return; - try { - const jobData = await SA2.request("AssistentGetTrainJob", {}); - renderLastTrainJob(jobData?.last_job || jobData?.job); - const data = await SA2.request("AssistentListModels", { baseUrl: $("sa_base_url")?.value }); - const models = data?.models || []; - const lastOut = jobData?.last_job?.output_name; - root.innerHTML = models.length ? models.map((m) => { - const hit = lastOut && String(m).includes(String(lastOut).split(":")[0]); - return `
${escapeHtml(m)}${hit ? " \xB7 \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u044F\u044F \u0442\u0440\u0435\u043D\u0438\u0440\u043E\u0432\u043A\u0430" : ""}
`; - }).join("") : '
\u041D\u0435\u0442 \u043C\u043E\u0434\u0435\u043B\u0435\u0439 \u0432 Ollama \u2014 \u043F\u043E\u0441\u043B\u0435 QLoRA \u043D\u0430\u0436\u043C\u0438 \xAB\u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C\xBB \u0438\u043B\u0438 \u043F\u0440\u043E\u0432\u0435\u0440\u044C \u043B\u043E\u0433 \u0442\u0440\u0435\u043D\u0438\u0440\u043E\u0432\u043A\u0438
'; - } catch (e) { - const msg = String(e.message || e); - root.innerHTML = `
${escapeHtml(isSqliteError(msg) ? sqliteHint() : msg)}
`; - } - } - async function saveRunner() { - try { - await SA2.request("AssistentSaveRunnerSettings", { - python: $("sa_runner_python")?.value, - kind: $("sa_runner_kind")?.value || "builtin", - workdir: $("sa_runner_workdir")?.value, - cmd: $("sa_runner_cmd")?.value, - gguf_script: $("sa_runner_gguf_script")?.value, - gguf_base_path: $("sa_runner_gguf_base")?.value, - gguf_cmd: $("sa_runner_gguf_cmd")?.value - }); - setTrainStatus("\u0420\u0430\u043D\u043D\u0435\u0440 \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D"); - } catch (e) { - setTrainStatus(formatTrainError(e.message || e)); - } - } - async function loadRunner() { - try { - const data = await SA2.request("AssistentGetRunnerSettings", {}); - const s = data?.settings || {}; - if ($("sa_runner_python") && s.python) $("sa_runner_python").value = s.python; - if ($("sa_runner_kind")) $("sa_runner_kind").value = s.kind || "builtin"; - if ($("sa_runner_workdir") && s.workdir) $("sa_runner_workdir").value = s.workdir; - if ($("sa_runner_cmd") && s.cmd) $("sa_runner_cmd").value = s.cmd; - if ($("sa_runner_gguf_script") && s.gguf_script) $("sa_runner_gguf_script").value = s.gguf_script; - if ($("sa_runner_gguf_base") && s.gguf_base_path) $("sa_runner_gguf_base").value = s.gguf_base_path; - if ($("sa_runner_gguf_cmd") && s.gguf_cmd) $("sa_runner_gguf_cmd").value = s.gguf_cmd; - } catch (e) { - } - } - function wireTraining() { - if (window.__saTrainingWired) return; - window.__saTrainingWired = true; - document.querySelectorAll("#sa_training .sa-ttab").forEach((btn) => { - btn.addEventListener("click", () => setTrainingTab(btn.getAttribute("data-ttab"))); - }); - $("sa_btn_agent_sync")?.addEventListener("click", syncAllToAgent); - $("sa_agent_heard_enabled")?.addEventListener("change", saveAgentHeardSettings); - $("sa_agent_auto_link")?.addEventListener("change", saveAgentHeardSettings); - $("sa_agent_heard_quota")?.addEventListener("change", saveAgentHeardSettings); - loadAgentHeardSettings(); - $("sa_btn_train_from_chats")?.addEventListener("click", async () => { - try { - const data = await SA2.request("AssistentBuildDatasetFromChats", {}); - setTrainStatus(`\u0418\u0437 \u0447\u0430\u0442\u043E\u0432: +${data.added}`); - await refreshSamples(); - } catch (e) { - setTrainStatus(String(e.message || e)); - } - }); - $("sa_btn_train_import_file")?.addEventListener("click", () => $("sa_train_import_file")?.click()); - $("sa_train_import_file")?.addEventListener("change", async (e) => { - const file = e.target?.files?.[0]; - if (!file) return; - const text = await file.text(); - try { - const data = await SA2.request("AssistentImportDataset", { format: "auto", content: text }); - setTrainStatus(`\u0418\u043C\u043F\u043E\u0440\u0442: ${data.imported}`); - await refreshSamples(); - } catch (err) { - setTrainStatus(formatTrainError(err.message || err)); - } - e.target.value = ""; - }); - $("sa_btn_train_export")?.addEventListener("click", async () => { - try { - const data = await SA2.request("AssistentExportDataset", { status: "approved" }); - if (data.content) { - const blob = new Blob([data.content], { type: "application/jsonl" }); - const a = document.createElement("a"); - a.href = URL.createObjectURL(blob); - a.download = "assistent-dataset.jsonl"; - a.click(); - } - setTrainStatus(`\u042D\u043A\u0441\u043F\u043E\u0440\u0442: ${data.count} \u043F\u0440\u0438\u043C\u0435\u0440\u043E\u0432`); - } catch (e) { - setTrainStatus(String(e.message || e)); - } - }); - $("sa_train_filter_status")?.addEventListener("change", refreshSamples); - $("sa_train_filter_persona")?.addEventListener("change", refreshSamples); - $("sa_train_samples")?.addEventListener("click", async (e) => { - const row = e.target.closest(".sa-train-sample"); - if (!row) return; - const id = row.dataset.id; - const sample = state.samples.find((s) => s.id === id); - if (!sample) return; - if (e.target.closest("[data-approve]")) { - await upsertSample({ ...sample, status: "approved" }); - await loadAgentHeardSettings(); - } else if (e.target.closest("[data-reject]")) { - await upsertSample({ ...sample, status: "rejected" }); - await loadAgentHeardSettings(); - } else if (e.target.closest("[data-link]")) { - try { - const data = await SA2.request("AssistentLinkTrainSampleToAgent", { id }); - state.agentLinked = data?.linked ?? state.agentLinked; - setAgentHeardStats(state.agentLinked); - setTrainStatus("\u041F\u0440\u0438\u043C\u0435\u0440 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0451\u043D \u043A \u0430\u0433\u0435\u043D\u0442\u0443"); - await refreshSamples(); - } catch (err) { - setTrainStatus(formatTrainError(err.message || err)); - } - } else if (e.target.closest("[data-unlink]")) { - try { - const data = await SA2.request("AssistentUnlinkTrainSampleFromAgent", { id }); - state.agentLinked = data?.linked ?? state.agentLinked; - setAgentHeardStats(state.agentLinked); - setTrainStatus("\u041F\u0440\u0438\u043C\u0435\u0440 \u043E\u0442\u043A\u043B\u044E\u0447\u0451\u043D \u043E\u0442 \u0430\u0433\u0435\u043D\u0442\u0430"); - await refreshSamples(); - } catch (err) { - setTrainStatus(formatTrainError(err.message || err)); - } - } else if (e.target.closest("[data-del]")) { - if (window.confirm("\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u043F\u0440\u0438\u043C\u0435\u0440?")) { - await SA2.request("AssistentDeleteTrainSample", { id }); - await refreshSamples(); - } - } - }); - $("sa_btn_hf_search")?.addEventListener("click", searchHf); - $("sa_hf_show_all")?.addEventListener("change", () => { - renderHfList(); - }); - $("sa_hf_list")?.addEventListener("click", async (e) => { - const row = e.target.closest(".sa-hf-row"); - if (!row || row.classList.contains("sa-hf-rejected")) return; - state.hfSelected = row.dataset.id; - $("sa_hf_link").value = row.dataset.id; - renderHfList(); - await checkHfLink(); - }); - $("sa_btn_hf_check")?.addEventListener("click", checkHfLink); - $("sa_hf_mapping_preset")?.addEventListener("change", () => { - state.hfMapping = buildHfMappingPayload(); - }); - $("sa_btn_hf_import")?.addEventListener("click", importHf); - document.querySelectorAll('input[name="sa_train_mode"]').forEach((r) => { - r.addEventListener("change", () => setTrainMode(r.value)); - }); - $("sa_btn_modelfile_create")?.addEventListener("click", createModelfile); - $("sa_qlora_base")?.addEventListener("change", () => applyQloraPresetFromSelect()); - $("sa_btn_qlora_start")?.addEventListener("click", startQlora); - $("sa_btn_qlora_cancel")?.addEventListener("click", cancelQlora); - $("sa_btn_train_models_refresh")?.addEventListener("click", refreshTrainModels); - $("sa_btn_save_runner")?.addEventListener("click", saveRunner); - loadRunner(); - setTrainMode("modelfile"); - void resumeTrainJobPolling(); - } - SA2.training = { - render() { - wireTraining(); - setTrainingTab(state.ttab); - }, - onConfig(data) { - if (data?.training && typeof data.training === "object") { - state.qloraTraining = data.training; - if (state.ttab === "train") { - populateQloraHfPresets(state.qloraTraining); - applyQloraPresetFromSelect({ fillName: false }); - } - } - }, - resumePolling: resumeTrainJobPolling, - async curateFromChat(messages, meta) { - try { - await SA2.request("AssistentUpsertTrainSample", { - source: "chat", - chat_id: meta?.chatId, - persona: meta?.persona, - pack: meta?.pack, - status: meta?.status || "approved", - messages - }); - return true; - } catch (e) { - console.warn("curateFromChat", e); - return false; - } - }, - setTrainingLock, - pollTrainJob - }; - } - // src/main.js window.SA = window.SA || {}; window.SA.userAsksGenerate = userAsksGenerate; @@ -11908,5 +11030,4 @@ ${HELP_TEXT}`); window.SA.PATCH_KEYS = keys; } }; - attachTraining(window.SA); })(); diff --git a/AssistentChatPipeline.cs b/AssistentChatPipeline.cs index 084752e..f5d1625 100644 --- a/AssistentChatPipeline.cs +++ b/AssistentChatPipeline.cs @@ -98,6 +98,11 @@ public partial class SwarmAssistentExtension } AddLayer("skills", skillsBlock.ToString()); AddLayer("identity", Config.RenderIdentityBlock(pid)); + string knowledgeLayer = BuildKnowledgeSystemLayer(pid); + if (!string.IsNullOrWhiteSpace(knowledgeLayer)) + { + AddLayer("knowledge", knowledgeLayer); + } } if (!string.IsNullOrWhiteSpace(packName)) @@ -179,7 +184,7 @@ public partial class SwarmAssistentExtension return (ollamaMessages, layers); } - async Task<(string reply, JObject raw, JArray civitaiResults, int systemChars, JObject systemLayers)> RunChatWithHops( + async Task<(string reply, JObject raw, JArray civitaiResults, JObject knowledge, int systemChars, JObject systemLayers)> RunChatWithHops( Session session, string root, string modelName, @@ -238,6 +243,8 @@ public partial class SwarmAssistentExtension ?? 0; string reply = ""; JObject lastRaw = null; + JArray knowledgeHops = []; + JArray knowledgeResults = []; int maxHops = slimUtility ? 1 : CfgInt("max_tool_hops", MaxToolHopsFallback); HashSet hopDone = new(StringComparer.OrdinalIgnoreCase); for (int hop = 0; hop < maxHops; hop++) @@ -267,7 +274,7 @@ public partial class SwarmAssistentExtension follow = null; break; } - follow = await RunToolHop(session, pid, patch, tool, hopDone); + follow = await RunToolHop(session, pid, patch, tool, hopDone, knowledgeHops, knowledgeResults); if (follow is not null) { break; @@ -285,7 +292,9 @@ public partial class SwarmAssistentExtension messages.Add(new JObject { ["role"] = "assistant", ["content"] = assistantContent }); messages.Add(new JObject { ["role"] = "user", ["content"] = follow }); } - return (reply, lastRaw, [], systemChars, systemLayers); + JObject knowledge = BuildKnowledgeResponse(pid, knowledgeHops, knowledgeResults); + JArray civitai = CivitaiResultsShim(knowledgeResults); + return (reply, lastRaw, civitai, knowledge, systemChars, systemLayers); } static string BuildRetrieveQuery(JArray userMessages, string contextJson, string packName = null) @@ -413,7 +422,9 @@ public partial class SwarmAssistentExtension string pid, JObject patch, string tool, - HashSet hopDone) + HashSet hopDone, + JArray knowledgeHops = null, + JArray knowledgeResults = null) { if (tool == "ask_settings") { @@ -452,6 +463,35 @@ public partial class SwarmAssistentExtension + "omit ask:inventory unless you need a different query.\n```json\n" + rows.ToString(Newtonsoft.Json.Formatting.None) + "\n```"; } + if (tool == "ask_knowledge") + { + string q = patch?["knowledge_query"]?.ToString()?.Trim() + ?? patch?["example_query"]?.ToString()?.Trim() + ?? patch?["memory_query"]?.ToString()?.Trim() + ?? ""; + string rating = patch?["knowledge_rating"]?.ToString()?.Trim() + ?? patch?["example_rating"]?.ToString()?.Trim(); + string sig = "ask_knowledge:" + q.ToLowerInvariant() + ":" + (rating ?? ""); + if (!hopDone.Add(sig)) + { + return null; + } + if (string.IsNullOrWhiteSpace(q)) + { + return + "ask:knowledge needs knowledge_query (tags / scene / style). " + + "Retry with \"ask\":[\"knowledge\"], \"knowledge_query\":\"redhead stockings\"."; + } + int lim = Config.LoadAssistant(pid)["knowledge_hop_limit"]?.Value() + ?? Config.LoadAssistant(pid)["examples_hop_limit"]?.Value() ?? 6; + JArray rows = SearchKnowledge(pid, q, lim, rating); + knowledgeHops?.Add(new JObject { ["tool"] = "ask_knowledge", ["query"] = q, ["count"] = rows.Count }); + MergeKnowledgeResults(knowledgeResults, rows); + return + "ask:knowledge — FTS over attached books (read-only references). " + + "Remix ideas; do not paste long verbatim.\n```json\n" + + rows.ToString(Newtonsoft.Json.Formatting.None) + "\n```"; + } if (tool == "ask_examples") { string q = patch?["example_query"]?.ToString()?.Trim() @@ -471,6 +511,8 @@ public partial class SwarmAssistentExtension } int lim = Config.LoadAssistant(pid)["examples_hop_limit"]?.Value() ?? 5; JArray examples = Memory?.LookupExamples(q, lim, rating) ?? []; + knowledgeHops?.Add(new JObject { ["tool"] = "ask_examples", ["query"] = q, ["count"] = examples.Count }); + MergeKnowledgeResults(knowledgeResults, examples, legacyExamples: true); return "ask:examples — Civitai Krea2 prompt references (FTS, no embeddings). " + "These are EXAMPLES to remix, not copy 1:1. Prefer craft over pasting.\n```json\n" diff --git a/AssistentConfig.cs b/AssistentConfig.cs index 705067b..43a0171 100644 --- a/AssistentConfig.cs +++ b/AssistentConfig.cs @@ -239,6 +239,134 @@ public sealed class AssistentConfig return map; } + static List ParseYamlBracketList(string value) + { + value = (value ?? "").Trim(); + if (value.StartsWith('[') && value.EndsWith(']')) + { + value = value[1..^1]; + } + return value.Split(',') + .Select(s => s.Trim().Trim('"', '\'')) + .Where(s => !string.IsNullOrWhiteSpace(s)) + .ToList(); + } + + public string TryFindPackManifestPath(string personaId) + { + string shelf = PackShelfRoot(personaId); + if (string.IsNullOrWhiteSpace(shelf)) + { + return null; + } + string dir = shelf; + for (int i = 0; i < 5 && !string.IsNullOrWhiteSpace(dir); i++) + { + string yaml = Path.Combine(dir, "assistent-pack.yaml"); + if (File.Exists(yaml)) + { + return yaml; + } + dir = Directory.GetParent(dir)?.FullName; + } + return null; + } + + /// Pack manifest knowledge.attach list for a pack persona. + public List TryParsePackKnowledgeAttach(string personaId) + { + string path = TryFindPackManifestPath(personaId); + if (string.IsNullOrWhiteSpace(path)) + { + return []; + } + List items = []; + try + { + foreach (string rawLine in File.ReadAllLines(path, Encoding.UTF8)) + { + string line = rawLine.Trim(); + if (string.IsNullOrWhiteSpace(line) || line.StartsWith('#')) + { + continue; + } + if (line.StartsWith("knowledge.attach:", StringComparison.OrdinalIgnoreCase)) + { + string val = line["knowledge.attach:".Length..].Trim(); + items.AddRange(ParseYamlBracketList(val)); + break; + } + } + } + catch (Exception ex) + { + Logs.Debug($"AssistentConfig pack knowledge.attach {path}: {ex.Message}"); + } + return items + .Select(SafeId) + .Where(id => id is not null) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + } + + public JObject LoadKnowledgeAttachOverlay(string personaId) + { + string id = SafeId(personaId); + if (id is null) + { + return null; + } + string path = Path.Combine(_overlayRoot, "personas", id, "knowledge.json"); + return File.Exists(path) ? TryReadJson(path) : null; + } + + public void SaveKnowledgeAttachOverlay(string personaId, JArray attach) + { + lock (_lock) + { + string id = SafeId(personaId) ?? throw new InvalidOperationException("invalid persona"); + string dir = Path.Combine(_overlayRoot, "personas", id); + Directory.CreateDirectory(dir); + string path = Path.Combine(dir, "knowledge.json"); + JObject doc = new() + { + ["attach"] = attach ?? new JArray(), + }; + File.WriteAllText(path, doc.ToString(Newtonsoft.Json.Formatting.Indented) + "\n", Encoding.UTF8); + } + } + + static readonly string[] DefaultBundledKnowledgeAttach = ["civitai-krea2", "ru-fictext-rplus"]; + + static readonly HashSet BundledKnowledgePersonas = new(StringComparer.OrdinalIgnoreCase) + { + "neutral", "aggressive", "dreamer", + }; + + /// Effective book attach list for a persona (overlay → pack → bundled defaults). + public List ResolveKnowledgeAttach(string personaId) + { + string pid = SafeId(personaId) ?? DefaultPersonaId(); + JObject overlay = LoadKnowledgeAttachOverlay(pid); + if (overlay?["attach"] is JArray custom && custom.Count > 0) + { + return custom.Select(t => SafeId(t?.ToString())) + .Where(id => id is not null) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + } + List fromPack = TryParsePackKnowledgeAttach(pid); + if (fromPack.Count > 0) + { + return fromPack; + } + if (IsBundledPersona(pid) && BundledKnowledgePersonas.Contains(pid)) + { + return DefaultBundledKnowledgeAttach.ToList(); + } + return []; + } + /// Discover persona packs under Assistent/extensions/*/. public List DiscoverPackPersonas() { @@ -477,7 +605,7 @@ public sealed class AssistentConfig static readonly HashSet ReservedConfigFiles = new(StringComparer.OrdinalIgnoreCase) { - "exact.json", "controls.json", "skills.json", "ui.json", "assistant.json", "training-qlora.json", + "exact.json", "controls.json", "skills.json", "ui.json", "assistant.json", "knowledge.json", }; static readonly HashSet ReservedConfigDirs = new(StringComparer.OrdinalIgnoreCase) @@ -780,7 +908,7 @@ public sealed class AssistentConfig { "persona.json", "bio.json", "voice.json", "humor.json", "craft.json", "appearance.json", "outfits.json", "roleplay.json", "likes.json", "dislikes.json", - "rules.json", "controls.json", "exact.json", "extra.md", + "rules.json", "controls.json", "exact.json", "knowledge.json", "extra.md", }; public static bool IsWritableShelfName(string fileName) @@ -934,8 +1062,6 @@ public sealed class AssistentConfig public JObject LoadUi(string personaId) => MergeJsonLayers("ui.json", LayerRoots(personaId)); - public JObject LoadTrainingQlora(string personaId) => MergeJsonLayers("training-qlora.json", LayerRoots(personaId)); - /// Exact (KV) memory: bundled base → persona overlays → disk overlays. Persona keys overwrite base. public JObject LoadExact(string personaId) => MergeJsonLayers("exact.json", LayerRoots(personaId)); @@ -1549,40 +1675,6 @@ public sealed class AssistentConfig } } - public JObject LoadTrainingRunner() - => TryReadJson(Path.Combine(_overlayRoot, "training-runner.json")) ?? new JObject(); - - public void SaveTrainingRunner(JObject settings) - { - lock (_lock) - { - Directory.CreateDirectory(_overlayRoot); - string path = Path.Combine(_overlayRoot, "training-runner.json"); - JObject merged = DeepMerge(LoadTrainingRunner(), settings ?? new JObject()); - File.WriteAllText(path, merged.ToString(Newtonsoft.Json.Formatting.Indented), Encoding.UTF8); - } - } - - public JObject LoadTrainingAgent() - => TryReadJson(Path.Combine(_overlayRoot, "training-agent.json")) - ?? new JObject - { - ["enabled"] = true, - ["auto_link_on_approve"] = true, - ["heard_quota"] = 3, - }; - - public void SaveTrainingAgent(JObject settings) - { - lock (_lock) - { - Directory.CreateDirectory(_overlayRoot); - string path = Path.Combine(_overlayRoot, "training-agent.json"); - JObject merged = DeepMerge(LoadTrainingAgent(), settings ?? new JObject()); - File.WriteAllText(path, merged.ToString(Newtonsoft.Json.Formatting.Indented), Encoding.UTF8); - } - } - public JObject LoadOllamaRoles() { return TryReadJson(Path.Combine(_overlayRoot, "ollama-roles.json")) @@ -1643,7 +1735,6 @@ public sealed class AssistentConfig JObject ui = LoadUi(id); JObject model = LoadModelProfile(id); JObject exact = LoadExact(id); - JObject training = LoadTrainingQlora(id); var packs = ListPacks(id); var skills = ListSkills(id); var personas = ListPersonaCatalog(); @@ -1659,7 +1750,6 @@ public sealed class AssistentConfig ["ui"] = ui, ["model"] = model, ["exact"] = exact, - ["training"] = training, ["controls"] = controlsSchema, ["control_values"] = controlValues, ["persona_source"] = PersonaSource(id), @@ -1687,6 +1777,10 @@ public sealed class AssistentConfig ["identity_summary"] = RenderIdentityBlock(id, includeAllShelves: true), ["enabled_skills"] = new JArray(ResolveEnabledSkills(id, null)), ["patch_keys"] = new JArray(LoadPatchKeys()), + ["knowledge"] = new JObject + { + ["attach"] = new JArray(ResolveKnowledgeAttach(id)), + }, }; } } diff --git a/AssistentHuggingFace.cs b/AssistentHuggingFace.cs deleted file mode 100644 index 24aff97..0000000 --- a/AssistentHuggingFace.cs +++ /dev/null @@ -1,684 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Newtonsoft.Json.Linq; -using SwarmUI.Accounts; -using SwarmUI.Utils; - -namespace Mrleo1nid.SwarmAssistent; - -/// Hugging Face datasets: search, compatibility gate, preview, import. -public partial class SwarmAssistentExtension -{ - static readonly Regex HfRepoIdRe = new(@"^[A-Za-z0-9][A-Za-z0-9._-]{0,95}(/[A-Za-z0-9][A-Za-z0-9._-]{0,95})?$", RegexOptions.Compiled); - - const string HfDatasetsServer = "https://datasets-server.huggingface.co"; - const string HfHubApi = "https://huggingface.co/api/datasets"; - - static string GetHfToken(Session session) - => session?.User?.GetGenericData("huggingface_api", "key")?.Trim(); - - static HttpRequestMessage HfRequest(string url, Session session) - { - HttpRequestMessage req = new(HttpMethod.Get, url); - string token = GetHfToken(session); - if (!string.IsNullOrWhiteSpace(token)) - { - req.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); - } - return req; - } - - /// Normalize owner/name or HF datasets URL. Returns null when invalid. - public static string NormalizeHfDatasetId(string raw) - { - string s = (raw ?? "").Trim(); - if (string.IsNullOrWhiteSpace(s)) - { - return null; - } - if (s.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || s.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) - { - if (!Uri.TryCreate(s, UriKind.Absolute, out Uri uri)) - { - return null; - } - if (!string.Equals(uri.Host, "huggingface.co", StringComparison.OrdinalIgnoreCase)) - { - return null; - } - string[] parts = uri.AbsolutePath.Trim('/').Split('/'); - if (parts.Length < 2 || !string.Equals(parts[0], "datasets", StringComparison.OrdinalIgnoreCase)) - { - return null; - } - s = $"{parts[1]}/{parts[2]}"; - } - s = s.Trim().TrimEnd('/'); - return HfRepoIdRe.IsMatch(s) ? s : null; - } - - public async Task AssistentSearchHfDatasets(Session session, string q = null, int limit = 20, bool show_all = false) - { - int take = Math.Clamp(limit, 1, 50); - string search = (q ?? "").Trim(); - StringBuilder url = new($"{HfHubApi}?limit={take}&full=true"); - url.Append("&filter=task_categories:text-generation"); - url.Append("&filter=modality:text"); - if (!string.IsNullOrWhiteSpace(search)) - { - url.Append("&search=").Append(Uri.EscapeDataString(search)); - } - try - { - using HttpRequestMessage req = HfRequest(url.ToString(), session); - using HttpResponseMessage resp = await HttpClient.SendAsync(req); - string body = await resp.Content.ReadAsStringAsync(); - if (!resp.IsSuccessStatusCode) - { - return new JObject { ["error"] = $"HF search HTTP {(int)resp.StatusCode}: {Clip(body, 300)}" }; - } - JArray rawList = JArray.Parse(body); - JArray results = []; - foreach (JToken item in rawList) - { - if (item is not JObject o) - { - continue; - } - string id = o["id"]?.ToString(); - if (string.IsNullOrWhiteSpace(id)) - { - continue; - } - JObject check = await CheckHfDatasetInternal(session, id, useCache: true); - string gate = check["gate"]?.ToString() ?? "rejected"; - if (!show_all && gate == "rejected") - { - continue; - } - results.Add(new JObject - { - ["id"] = id, - ["title"] = o["id"], - ["downloads"] = o["downloads"], - ["gate"] = gate, - ["reason"] = check["reason"], - ["schema"] = check["schema"], - }); - } - return new JObject { ["success"] = true, ["results"] = results, ["has_hf_token"] = !string.IsNullOrWhiteSpace(GetHfToken(session)) }; - } - catch (Exception ex) - { - return new JObject { ["error"] = $"HF search: {ex.Message}" }; - } - } - - public async Task AssistentCheckHfDataset(Session session, string dataset) - { - string id = NormalizeHfDatasetId(dataset); - if (id is null) - { - return new JObject { ["success"] = false, ["error"] = "Нужен owner/name или ссылка huggingface.co/datasets/…" }; - } - JObject check = await CheckHfDatasetInternal(session, id, useCache: false); - check["success"] = check["gate"]?.ToString() != "rejected"; - check["id"] = id; - return check; - } - - async Task CheckHfDatasetInternal(Session session, string datasetId, bool useCache) - { - string cacheKey = $"hf:{datasetId}"; - if (useCache) - { - JObject cached = Memory.GetKvObject(AssistentMemory.KvHfDatasetCache)?[cacheKey] as JObject; - if (cached is not null && cached["checked_at"]?.Value() > DateTimeOffset.UtcNow.AddHours(-6).ToUnixTimeMilliseconds()) - { - return cached; - } - } - JObject result = new() { ["id"] = datasetId, ["gate"] = "rejected", ["reason"] = "unknown" }; - try - { - using HttpRequestMessage validReq = HfRequest($"{HfDatasetsServer}/is-valid?dataset={Uri.EscapeDataString(datasetId)}", session); - using HttpResponseMessage validResp = await HttpClient.SendAsync(validReq); - string validBody = await validResp.Content.ReadAsStringAsync(); - if (!validResp.IsSuccessStatusCode) - { - result["reason"] = $"is-valid HTTP {(int)validResp.StatusCode}"; - return CacheHfCheck(cacheKey, result); - } - JObject valid = JObject.Parse(validBody); - bool viewer = valid["viewer"]?.Value() == true; - bool preview = valid["preview"]?.Value() == true; - if (!viewer && !preview) - { - result["reason"] = "Набор не читается через datasets (viewer/preview = false)"; - return CacheHfCheck(cacheKey, result); - } - using HttpRequestMessage splitReq = HfRequest($"{HfDatasetsServer}/splits?dataset={Uri.EscapeDataString(datasetId)}", session); - using HttpResponseMessage splitResp = await HttpClient.SendAsync(splitReq); - string splitBody = await splitResp.Content.ReadAsStringAsync(); - if (!splitResp.IsSuccessStatusCode) - { - result["reason"] = $"splits HTTP {(int)splitResp.StatusCode}"; - return CacheHfCheck(cacheKey, result); - } - JArray splits = JObject.Parse(splitBody)["splits"] as JArray ?? []; - if (splits.Count == 0) - { - result["reason"] = "Нет splits"; - return CacheHfCheck(cacheKey, result); - } - JObject first = splits[0] as JObject; - string config = first?["config"]?.ToString() ?? "default"; - string split = first?["split"]?.ToString() ?? "train"; - using HttpRequestMessage rowsReq = HfRequest($"{HfDatasetsServer}/first-rows?dataset={Uri.EscapeDataString(datasetId)}&config={Uri.EscapeDataString(config)}&split={Uri.EscapeDataString(split)}", session); - using HttpResponseMessage rowsResp = await HttpClient.SendAsync(rowsReq); - string rowsBody = await rowsResp.Content.ReadAsStringAsync(); - if (!rowsResp.IsSuccessStatusCode) - { - result["reason"] = $"first-rows HTTP {(int)rowsResp.StatusCode}: {Clip(rowsBody, 200)}"; - return CacheHfCheck(cacheKey, result); - } - JObject rowsData = JObject.Parse(rowsBody); - JObject features = FeaturesToObject(rowsData["features"]); - (string gate, string reason, JObject schema) = ClassifyHfFeatures(features); - if (gate == "mapping" && TryFictionTagsTextPreset(features, out JObject presetSchema)) - { - gate = "ok"; - reason = "Fiction preset: title/tags → user, text → assistant"; - schema = presetSchema; - } - result["gate"] = gate; - result["reason"] = reason; - result["schema"] = schema; - result["config"] = config; - result["split"] = split; - result["features"] = features; - result["sample_rows"] = rowsData["rows"]; - result["runner_only"] = await HasHugeSizeTagAsync(session, datasetId); - return CacheHfCheck(cacheKey, result); - } - catch (Exception ex) - { - result["reason"] = ex.Message; - return CacheHfCheck(cacheKey, result); - } - } - - static bool TryFictionTagsTextPreset(JObject features, out JObject schema) - { - schema = null; - if (features is null) - { - return false; - } - HashSet names = new(StringComparer.OrdinalIgnoreCase); - foreach (JProperty p in features.Properties()) - { - names.Add(p.Name); - } - if (!names.Contains("text") || (!names.Contains("tags") && !names.Contains("title"))) - { - return false; - } - schema = new JObject - { - ["kind"] = "fiction_tags_text", - ["assistant_col"] = "text", - }; - return true; - } - - async Task HasHugeSizeTagAsync(Session session, string datasetId) - { - try - { - using HttpRequestMessage req = HfRequest($"{HfHubApi}/{Uri.EscapeDataString(datasetId)}", session); - using HttpResponseMessage resp = await HttpClient.SendAsync(req); - if (!resp.IsSuccessStatusCode) - { - return false; - } - JObject meta = JObject.Parse(await resp.Content.ReadAsStringAsync()); - foreach (JToken t in meta["tags"] as JArray ?? []) - { - string tag = t?.ToString() ?? ""; - if (tag.Contains("100K<", StringComparison.OrdinalIgnoreCase) - || tag.Contains("1M<", StringComparison.OrdinalIgnoreCase) - || tag.Contains("10M<", StringComparison.OrdinalIgnoreCase) - || tag.Contains("100M<", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - } - catch (Exception ex) - { - Logs.Debug($"HasHugeSizeTag {datasetId}: {ex.Message}"); - } - return false; - } - - static JObject ResolveHfMapping(JObject check, JObject mapping) - { - JObject schema = check?["schema"] as JObject; - string kind = schema?["kind"]?.ToString(); - if (string.Equals(kind, "fiction_tags_text", StringComparison.OrdinalIgnoreCase)) - { - return new JObject { ["kind"] = "fiction_tags_text", ["preset"] = "fiction_tags_text" }; - } - if (mapping is not null && mapping.Count > 0) - { - return mapping; - } - if (string.Equals(mapping?["preset"]?.ToString(), "fiction_tags_text", StringComparison.OrdinalIgnoreCase)) - { - return new JObject { ["kind"] = "fiction_tags_text" }; - } - return mapping; - } - - static bool MappingRequired(JObject check, JObject mapping) - { - string gate = check?["gate"]?.ToString(); - if (gate != "mapping") - { - return false; - } - JObject resolved = ResolveHfMapping(check, mapping); - return resolved is null || !resolved.Properties().Any(); - } - - JObject CacheHfCheck(string cacheKey, JObject result) - { - result["checked_at"] = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - try - { - JObject bag = Memory.GetKvObject(AssistentMemory.KvHfDatasetCache) ?? new JObject(); - bag[cacheKey] = result; - Memory.SetKvObject(AssistentMemory.KvHfDatasetCache, bag); - } - catch (Exception ex) - { - Logs.Debug($"CacheHfCheck: {ex.Message}"); - } - return result; - } - - static JObject FeaturesToObject(JToken tok) - { - if (tok is JObject obj) - { - return obj; - } - if (tok is JArray arr) - { - JObject map = new(); - foreach (JToken t in arr) - { - if (t is not JObject row) - { - continue; - } - string name = row["name"]?.ToString(); - if (string.IsNullOrWhiteSpace(name)) - { - continue; - } - map[name] = row["type"] ?? row; - } - return map.Count > 0 ? map : null; - } - return null; - } - - static (string gate, string reason, JObject schema) ClassifyHfFeatures(JObject features) - { - if (features is null || !features.Properties().Any()) - { - return ("rejected", "Нет колонок (features пуст)", null); - } - HashSet names = new(StringComparer.OrdinalIgnoreCase); - foreach (JProperty p in features.Properties()) - { - names.Add(p.Name); - JToken dtype = p.Value?["dtype"] ?? p.Value?["type"]; - string dt = dtype?.ToString() ?? ""; - if (dt.Contains("image", StringComparison.OrdinalIgnoreCase) - || dt.Contains("audio", StringComparison.OrdinalIgnoreCase) - || dt.Contains("video", StringComparison.OrdinalIgnoreCase)) - { - return ("rejected", $"Мультимодальная колонка {p.Name} ({dt})", null); - } - } - if (names.Contains("chosen") && names.Contains("rejected")) - { - return ("rejected", "DPO-набор (chosen/rejected) — не для SFT", null); - } - if (names.Count == 1 && names.Contains("text")) - { - return ("rejected", "Предобучение (одна колонка text), не диалоги", null); - } - if (names.Contains("messages")) - { - return ("ok", "OpenAI messages", new JObject { ["kind"] = "messages" }); - } - if (names.Contains("conversations")) - { - return ("ok", "ShareGPT conversations", new JObject { ["kind"] = "conversations" }); - } - if (names.Contains("instruction") && names.Contains("output")) - { - return ("ok", "Alpaca instruction/output", new JObject { ["kind"] = "alpaca" }); - } - if (names.Contains("prompt") && (names.Contains("response") || names.Contains("completion") || names.Contains("answer"))) - { - string respCol = names.Contains("response") ? "response" : names.Contains("completion") ? "completion" : "answer"; - return ("ok", "prompt/response", new JObject { ["kind"] = "prompt_response", ["response_col"] = respCol }); - } - if (names.Contains("question") && names.Contains("answer")) - { - return ("ok", "question/answer", new JObject { ["kind"] = "qa" }); - } - List stringCols = []; - foreach (JProperty p in features.Properties()) - { - JToken dtype = p.Value?["dtype"] ?? p.Value?["type"]; - string dt = dtype?.ToString() ?? ""; - if (dt.Contains("string", StringComparison.OrdinalIgnoreCase) || dt == "value") - { - stringCols.Add(p.Name); - } - } - if (stringCols.Count >= 2) - { - return ("mapping", "Нужен ручной маппинг колонок", new JObject - { - ["kind"] = "custom", - ["columns"] = new JArray(stringCols), - }); - } - return ("rejected", "Схема не подходит для SFT", null); - } - - public async Task AssistentPreviewHfDataset(Session session, string dataset, string config = null, string split = null) - { - string id = NormalizeHfDatasetId(dataset); - if (id is null) - { - return new JObject { ["error"] = "invalid dataset id" }; - } - JObject check = await CheckHfDatasetInternal(session, id, useCache: true); - if (check["gate"]?.ToString() == "rejected") - { - return new JObject { ["error"] = check["reason"]?.ToString() ?? "rejected", ["check"] = check }; - } - return new JObject { ["success"] = true, ["check"] = check }; - } - - public async Task AssistentImportHfDataset(Session session, JObject raw) - { - try - { - string dataset = raw?["dataset"]?.ToString(); - int limit = raw?["limit"]?.Value() ?? 200; - JObject mapping = raw?["mapping"] as JObject; - string id = NormalizeHfDatasetId(dataset); - if (id is null) - { - return new JObject { ["error"] = "invalid dataset id" }; - } - JObject check = await CheckHfDatasetInternal(session, id, useCache: true); - string gate = check["gate"]?.ToString(); - if (gate == "rejected") - { - return new JObject { ["error"] = check["reason"]?.ToString() ?? "rejected" }; - } - if (MappingRequired(check, mapping)) - { - return new JObject { ["error"] = "Нужен маппинг колонок", ["check"] = check }; - } - mapping = ResolveHfMapping(check, mapping); - int take = Math.Clamp(limit, 1, 5000); - JArray rows = []; - string config = check["config"]?.ToString() ?? "default"; - string split = check["split"]?.ToString() ?? "train"; - int offset = 0; - while (rows.Count < take) - { - int chunk = Math.Min(100, take - rows.Count); - using HttpRequestMessage rowsReq = HfRequest($"{HfDatasetsServer}/rows?dataset={Uri.EscapeDataString(id)}&config={Uri.EscapeDataString(config)}&split={Uri.EscapeDataString(split)}&offset={offset}&length={chunk}", session); - using HttpResponseMessage rowsResp = await HttpClient.SendAsync(rowsReq); - string rowsBody = await rowsResp.Content.ReadAsStringAsync(); - if (!rowsResp.IsSuccessStatusCode) - { - if (rows.Count == 0) - { - return new JObject - { - ["error"] = $"HF rows HTTP {(int)rowsResp.StatusCode}: {Clip(rowsBody, 240)}. " - + "Для gated/NSFW добавь huggingface_api в User Settings.", - }; - } - break; - } - JObject parsed = JObject.Parse(rowsBody); - JArray batch = parsed["rows"] as JArray ?? []; - if (batch.Count == 0) - { - break; - } - foreach (JToken t in batch) - { - rows.Add(t); - } - offset += batch.Count; - if (batch.Count < chunk) - { - break; - } - } - if (rows.Count == 0) - { - rows = check["sample_rows"] as JArray ?? []; - } - List toSave = []; - foreach (JToken rowTok in rows.Take(take)) - { - if (rowTok is not JObject row) - { - continue; - } - JObject rowData = row["row"] as JObject ?? row; - JArray messages = ConvertHfRowToMessages(rowData, check["schema"] as JObject, mapping); - if (messages is null || messages.Count == 0) - { - continue; - } - toSave.Add(new JObject - { - ["source"] = "hf", - ["hf_repo"] = id, - ["messages"] = messages, - ["status"] = "draft", - }); - } - int imported = toSave.Count > 0 ? Memory.ImportTrainSamplesBatch(toSave) : 0; - if (imported == 0 && check["runner_only"]?.Value() == true) - { - return new JObject { ["success"] = true, ["imported"] = 0, ["runner_only"] = true, ["id"] = id, ["note"] = "Большой набор — используй HF id в QLoRA-раннере" }; - } - if (imported == 0) - { - return new JObject - { - ["error"] = rows.Count == 0 - ? "HF не отдал строки — проверь token (gated/NSFW) и маппинг колонок" - : "0 строк после маппинга — проверь колонки user/assistant", - ["rows_fetched"] = rows.Count, - ["id"] = id, - }; - } - return new JObject - { - ["success"] = true, - ["imported"] = imported, - ["id"] = id, - ["rows_fetched"] = rows.Count, - ["status"] = "draft", - }; - } - catch (Exception ex) - { - Logs.Error($"AssistentImportHfDataset: {ex}"); - return new JObject { ["error"] = ex.Message }; - } - } - - static string HfCellString(JToken tok) - { - if (tok is null || tok.Type == JTokenType.Null) - { - return ""; - } - if (tok is JArray arr) - { - List parts = []; - foreach (JToken t in arr) - { - string s = t?.Type == JTokenType.String ? t.ToString() : t?.ToString(Newtonsoft.Json.Formatting.None); - if (!string.IsNullOrWhiteSpace(s)) - { - parts.Add(s.Trim()); - } - } - return string.Join(", ", parts); - } - return tok.ToString().Trim(); - } - - static JArray ConvertHfRowToMessages(JObject row, JObject schema, JObject mapping) - { - string kind = schema?["kind"]?.ToString() ?? mapping?["kind"]?.ToString(); - if (kind == "messages" && row["messages"] is JArray msgs) - { - return NormalizeMessagesArray(msgs); - } - if (kind == "conversations" && row["conversations"] is JArray conv) - { - JArray outArr = []; - foreach (JToken c in conv) - { - if (c is not JObject co) - { - continue; - } - string from = co["from"]?.ToString() ?? ""; - string val = co["value"]?.ToString() ?? ""; - string role = from is "human" or "user" ? "user" : from is "gpt" or "assistant" or "chatgpt" ? "assistant" : "user"; - outArr.Add(new JObject { ["role"] = role, ["content"] = val }); - } - return outArr.Count > 0 ? outArr : null; - } - if (kind == "alpaca") - { - string instr = row["instruction"]?.ToString() ?? ""; - string inp = row["input"]?.ToString() ?? ""; - string output = row["output"]?.ToString() ?? ""; - string user = string.IsNullOrWhiteSpace(inp) ? instr : $"{instr}\n{inp}"; - return new JArray - { - new JObject { ["role"] = "user", ["content"] = user }, - new JObject { ["role"] = "assistant", ["content"] = output }, - }; - } - if (kind == "prompt_response") - { - string respCol = schema?["response_col"]?.ToString() ?? "response"; - return new JArray - { - new JObject { ["role"] = "user", ["content"] = row["prompt"]?.ToString() ?? "" }, - new JObject { ["role"] = "assistant", ["content"] = row[respCol]?.ToString() ?? "" }, - }; - } - if (kind == "qa") - { - return new JArray - { - new JObject { ["role"] = "user", ["content"] = row["question"]?.ToString() ?? "" }, - new JObject { ["role"] = "assistant", ["content"] = row["answer"]?.ToString() ?? "" }, - }; - } - if (kind == "fiction_tags_text") - { - string text = HfCellString(row["text"]); - if (string.IsNullOrWhiteSpace(text)) - { - return null; - } - List userParts = []; - string title = HfCellString(row["title"]); - string tags = HfCellString(row["tags"]); - if (!string.IsNullOrWhiteSpace(title)) - { - userParts.Add($"Title: {title}"); - } - if (!string.IsNullOrWhiteSpace(tags)) - { - userParts.Add($"Tags: {tags}"); - } - string user = userParts.Count > 0 ? string.Join("\n", userParts) : tags ?? title ?? ""; - if (string.IsNullOrWhiteSpace(user)) - { - return null; - } - return new JArray - { - new JObject { ["role"] = "user", ["content"] = user }, - new JObject { ["role"] = "assistant", ["content"] = text }, - }; - } - if (kind == "custom" && mapping is not null) - { - string userCol = mapping["user_col"]?.ToString(); - string asstCol = mapping["assistant_col"]?.ToString(); - if (!string.IsNullOrWhiteSpace(userCol) && !string.IsNullOrWhiteSpace(asstCol)) - { - return new JArray - { - new JObject { ["role"] = "user", ["content"] = row[userCol]?.ToString() ?? "" }, - new JObject { ["role"] = "assistant", ["content"] = row[asstCol]?.ToString() ?? "" }, - }; - } - } - return null; - } - - static JArray NormalizeMessagesArray(JArray msgs) - { - JArray outArr = []; - foreach (JToken m in msgs) - { - if (m is not JObject mo) - { - continue; - } - string role = mo["role"]?.ToString() ?? "user"; - string content = mo["content"]?.ToString() ?? mo["text"]?.ToString() ?? ""; - if (string.IsNullOrWhiteSpace(content)) - { - continue; - } - outArr.Add(new JObject { ["role"] = role, ["content"] = content }); - } - return outArr.Count > 0 ? outArr : null; - } -} diff --git a/AssistentKnowledge.cs b/AssistentKnowledge.cs new file mode 100644 index 0000000..ed43664 --- /dev/null +++ b/AssistentKnowledge.cs @@ -0,0 +1,214 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Newtonsoft.Json.Linq; +using SwarmUI.Utils; + +namespace Mrleo1nid.SwarmAssistent; + +/// Unified knowledge hub: books FTS + legacy examples shim + catalog for personas. +public partial class SwarmAssistentExtension +{ + public List ResolveAttachedBooks(string personaId) + => Config.ResolveKnowledgeAttach(personaId); + + public JObject BuildKnowledgeCatalog(string personaId) + { + string pid = AssistentConfig.SafeId(personaId) ?? Config.DefaultPersonaId(); + List attached = ResolveAttachedBooks(pid); + JArray books = Memory?.ListBooksOnDisk() ?? []; + HashSet attachSet = attached.ToHashSet(StringComparer.OrdinalIgnoreCase); + JArray catalog = []; + foreach (JToken t in books) + { + if (t is not JObject b) + { + continue; + } + string id = b["id"]?.ToString() ?? ""; + catalog.Add(new JObject + { + ["id"] = id, + ["title"] = b["title"] ?? id, + ["description"] = b["description"] ?? "", + ["content_kind"] = b["content_kind"] ?? "", + ["language"] = b["language"] ?? "", + ["attached"] = attachSet.Contains(id), + ["indexed"] = b["indexed"]?.Value() ?? false, + }); + } + return new JObject + { + ["persona"] = pid, + ["attach"] = new JArray(attached), + ["books"] = catalog, + ["books_indexed"] = Memory?.BookRowCount() ?? 0, + ["examples_indexed"] = Memory?.ExampleCount() ?? 0, + ["tags_indexed"] = Memory?.TagCount() ?? 0, + }; + } + + public string BuildKnowledgeSystemLayer(string personaId) + { + JObject cat = BuildKnowledgeCatalog(personaId); + JArray attach = cat["attach"] as JArray ?? []; + if (attach.Count == 0) + { + return ""; + } + StringBuilder sb = new(); + sb.AppendLine("## Knowledge books (FTS reference — read-only)"); + sb.AppendLine("Attached corpora for this persona. Search via ask:[\"knowledge\"] + knowledge_query (or legacy ask:[\"examples\"] + example_query)."); + foreach (JToken t in attach) + { + string id = t?.ToString()?.Trim(); + if (string.IsNullOrWhiteSpace(id)) + { + continue; + } + JObject meta = (cat["books"] as JArray)?.OfType() + .FirstOrDefault(b => string.Equals(b["id"]?.ToString(), id, StringComparison.OrdinalIgnoreCase)); + string title = meta?["title"]?.ToString() ?? id; + string kind = meta?["content_kind"]?.ToString() ?? ""; + string desc = meta?["description"]?.ToString() ?? ""; + sb.AppendLine($"- **{id}** ({title}){ (string.IsNullOrWhiteSpace(kind) ? "" : $" · {kind}")}"); + if (!string.IsNullOrWhiteSpace(desc)) + { + sb.AppendLine($" {desc}"); + } + } + return sb.ToString().TrimEnd(); + } + + public JArray SearchKnowledge(string personaId, string query, int limit = 8, string rating = null) + { + query = (query ?? "").Trim(); + if (query.Length < 1 || Memory is null) + { + return []; + } + List attached = ResolveAttachedBooks(personaId); + JArray hits = []; + HashSet seen = []; + if (attached.Count > 0) + { + foreach (JToken t in Memory.LookupBooks(query, limit, attached, rating)) + { + string key = t?["id"]?.ToString() ?? t?.ToString(); + if (!string.IsNullOrWhiteSpace(key) && seen.Add(key)) + { + hits.Add(t); + } + } + } + if (hits.Count < limit && attached.Any(b => string.Equals(b, "civitai-krea2", StringComparison.OrdinalIgnoreCase))) + { + int remain = limit - hits.Count; + foreach (JToken t in Memory.LookupExamples(query, remain, rating)) + { + string key = "legacy:" + (t?["id"]?.ToString() ?? ""); + if (!seen.Add(key)) + { + continue; + } + JObject row = t as JObject ?? new JObject(); + hits.Add(new JObject + { + ["id"] = row["id"], + ["book"] = "civitai-krea2", + ["source"] = "examples-legacy", + ["title"] = "", + ["tags"] = row["tags"], + ["text"] = row["prompt"], + ["body"] = row["prompt"], + ["rating"] = row["rating"], + ["params"] = row["params"], + ["loras"] = row["loras"], + ["note"] = row["note"], + }); + } + } + return hits; + } + + public static JArray CivitaiResultsShim(JArray knowledgeResults) + { + JArray outRows = []; + foreach (JToken t in knowledgeResults ?? []) + { + if (t is not JObject row) + { + continue; + } + string book = row["book"]?.ToString() ?? ""; + string source = row["source"]?.ToString() ?? ""; + if (!string.Equals(book, "civitai-krea2", StringComparison.OrdinalIgnoreCase) + && source is not "examples-legacy") + { + continue; + } + if (row["prompt"] is not null) + { + outRows.Add(row); + continue; + } + outRows.Add(new JObject + { + ["id"] = row["id"], + ["rating"] = row["rating"], + ["tags"] = row["tags"], + ["prompt"] = row["text"] ?? row["body"], + ["negative"] = row["meta"]?["negative"] ?? "", + ["params"] = row["params"] ?? row["meta"]?["params"], + ["loras"] = row["loras"] ?? row["meta"]?["loras"], + ["note"] = row["note"] ?? "EXAMPLE from Civitai — remix, do not copy 1:1", + }); + } + return outRows; + } + + JObject BuildKnowledgeResponse(string personaId, JArray hops, JArray results) + => new() + { + ["catalog"] = BuildKnowledgeCatalog(personaId), + ["hops"] = hops ?? new JArray(), + ["results"] = results ?? new JArray(), + }; + + static void MergeKnowledgeResults(JArray target, JArray rows, bool legacyExamples = false) + { + if (target is null || rows is null) + { + return; + } + HashSet seen = target + .Select(t => t?["id"]?.ToString() ?? t?.ToString()) + .Where(s => !string.IsNullOrWhiteSpace(s)) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + foreach (JToken t in rows) + { + JObject row = t as JObject ?? new JObject { ["text"] = t?.ToString() }; + if (legacyExamples) + { + row = new JObject + { + ["id"] = row["id"], + ["book"] = "civitai-krea2", + ["source"] = "examples-legacy", + ["tags"] = row["tags"], + ["text"] = row["prompt"], + ["body"] = row["prompt"], + ["rating"] = row["rating"], + ["note"] = row["note"], + }; + } + string key = row["id"]?.ToString() ?? row["text"]?.ToString(); + if (string.IsNullOrWhiteSpace(key) || !seen.Add(key)) + { + continue; + } + target.Add(row); + } + } +} diff --git a/AssistentKnowledgeApi.cs b/AssistentKnowledgeApi.cs new file mode 100644 index 0000000..b49dda5 --- /dev/null +++ b/AssistentKnowledgeApi.cs @@ -0,0 +1,129 @@ +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using SwarmUI.Accounts; + +namespace Mrleo1nid.SwarmAssistent; + +public partial class SwarmAssistentExtension +{ + public async Task AssistentListKnowledgeCatalog(Session session, string persona = null) + { + await Task.CompletedTask; + if (Memory is null) + { + return new JObject { ["error"] = "memory not ready" }; + } + try + { + Memory.EnsureBooksIndex(); + string pid = AssistentConfig.SafeId(persona) ?? Config.DefaultPersonaId(); + return new JObject + { + ["success"] = true, + ["knowledge"] = BuildKnowledgeCatalog(pid), + ["editable"] = Config.IsOverlayPersona(pid) && !Config.IsProtectedPersona(pid), + }; + } + catch (System.Exception ex) + { + return new JObject { ["error"] = $"knowledge catalog: {ex.Message}" }; + } + } + + public async Task AssistentSearchKnowledge(Session session, string query, string persona = null, int limit = 8, string rating = null, string book = null) + { + await Task.CompletedTask; + if (Memory is null) + { + return new JObject { ["error"] = "memory not ready" }; + } + query = (query ?? "").Trim(); + if (query.Length < 1) + { + return new JObject { ["error"] = "query required" }; + } + try + { + string pid = AssistentConfig.SafeId(persona) ?? Config.DefaultPersonaId(); + JArray results; + if (!string.IsNullOrWhiteSpace(book)) + { + results = Memory.LookupBooks(query, limit, [book.Trim()], rating); + } + else + { + results = SearchKnowledge(pid, query, limit, rating); + } + return new JObject + { + ["success"] = true, + ["query"] = query, + ["persona"] = pid, + ["results"] = results, + ["civitai_results"] = CivitaiResultsShim(results), + }; + } + catch (System.Exception ex) + { + return new JObject { ["error"] = $"knowledge search: {ex.Message}" }; + } + } + + public async Task AssistentGetKnowledgeAttach(Session session, string persona = null) + { + await Task.CompletedTask; + string pid = AssistentConfig.SafeId(persona) ?? Config.DefaultPersonaId(); + return new JObject + { + ["success"] = true, + ["persona"] = pid, + ["attach"] = new JArray(ResolveAttachedBooks(pid)), + ["editable"] = Config.IsOverlayPersona(pid) && !Config.IsProtectedPersona(pid), + ["source"] = Config.PersonaSource(pid), + }; + } + + public async Task AssistentSaveKnowledgeAttach(Session session, string persona, JArray attach) + { + await Task.CompletedTask; + string pid = AssistentConfig.SafeId(persona); + if (pid is null) + { + return new JObject { ["error"] = "persona required" }; + } + if (Config.IsProtectedPersona(pid)) + { + return new JObject { ["error"] = "bundled/pack personas cannot edit attach here — clone to overlay first" }; + } + if (!Config.IsOverlayPersona(pid)) + { + return new JObject { ["error"] = "overlay persona required" }; + } + try + { + JArray cleaned = []; + if (attach is not null) + { + foreach (JToken t in attach) + { + string id = AssistentConfig.SafeId(t?.ToString()); + if (id is not null) + { + cleaned.Add(id); + } + } + } + Config.SaveKnowledgeAttachOverlay(pid, cleaned); + return new JObject + { + ["success"] = true, + ["persona"] = pid, + ["attach"] = new JArray(ResolveAttachedBooks(pid)), + }; + } + catch (System.Exception ex) + { + return new JObject { ["error"] = $"save knowledge attach: {ex.Message}" }; + } + } +} diff --git a/AssistentMemory.Books.cs b/AssistentMemory.Books.cs new file mode 100644 index 0000000..6e40a9a --- /dev/null +++ b/AssistentMemory.Books.cs @@ -0,0 +1,458 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using Microsoft.Data.Sqlite; +using Newtonsoft.Json.Linq; +using SwarmUI.Utils; + +namespace Mrleo1nid.SwarmAssistent; + +/// Reference books (search.jsonl) — FTS only, seeded by gpu-rent to Assistent/books/. +public sealed partial class AssistentMemory +{ + const string BooksMetaPrefix = "books_fp:"; + + void TryIndexBooks() + { + try + { + EnsureBooksIndex(); + } + catch (Exception ex) + { + Logs.Debug($"AssistentMemory books index: {ex.Message}"); + } + } + + public string BooksRoot() + => Path.Combine(_dataRoot, "Assistent", "books"); + + void EnsureBooksSchema() + { + Exec( + """ + CREATE TABLE IF NOT EXISTS books_rows ( + id TEXT PRIMARY KEY, + book_id TEXT NOT NULL, + title TEXT NOT NULL DEFAULT '', + tags TEXT NOT NULL DEFAULT '', + text TEXT NOT NULL DEFAULT '', + body TEXT NOT NULL DEFAULT '', + rating TEXT NOT NULL DEFAULT '', + meta_json TEXT NOT NULL DEFAULT '{}', + search_blob TEXT NOT NULL DEFAULT '' + ); + """); + Exec("CREATE INDEX IF NOT EXISTS idx_books_rows_book ON books_rows(book_id);"); + Exec( + """ + CREATE VIRTUAL TABLE IF NOT EXISTS books_fts USING fts5( + book_id, + title, + tags, + text, + body, + search_blob, + tokenize = 'unicode61 remove_diacritics 2' + ); + """); + Exec( + """ + CREATE TRIGGER IF NOT EXISTS books_fts_ai AFTER INSERT ON books_rows BEGIN + INSERT INTO books_fts(rowid, book_id, title, tags, text, body, search_blob) + VALUES (new.rowid, new.book_id, new.title, new.tags, new.text, new.body, new.search_blob); + END; + """); + Exec( + """ + CREATE TRIGGER IF NOT EXISTS books_fts_ad AFTER DELETE ON books_rows BEGIN + INSERT INTO books_fts(books_fts, rowid) VALUES('delete', old.rowid); + END; + """); + Exec( + """ + CREATE TRIGGER IF NOT EXISTS books_fts_au AFTER UPDATE ON books_rows BEGIN + INSERT INTO books_fts(books_fts, rowid) VALUES('delete', old.rowid); + INSERT INTO books_fts(rowid, book_id, title, tags, text, body, search_blob) + VALUES (new.rowid, new.book_id, new.title, new.tags, new.text, new.body, new.search_blob); + END; + """); + } + + static string ReadBookContentSha(string bookDir) + { + foreach (string name in new[] { ".gpu-rent-meta.json", "meta.json" }) + { + string path = Path.Combine(bookDir, name); + if (!File.Exists(path)) + { + continue; + } + try + { + JObject meta = JObject.Parse(File.ReadAllText(path, Encoding.UTF8)); + string sha = meta["content_sha"]?.ToString()?.Trim(); + if (!string.IsNullOrWhiteSpace(sha)) + { + return sha; + } + } + catch + { + // ignore + } + } + string jsonl = Path.Combine(bookDir, "search.jsonl"); + if (File.Exists(jsonl)) + { + return FileFingerprint(jsonl); + } + return null; + } + + /// Reindex changed books from disk. Returns total row count. + public int EnsureBooksIndex() + { + string root = BooksRoot(); + if (!Directory.Exists(root)) + { + return BookRowCount(); + } + int total = 0; + foreach (string bookDir in Directory.GetDirectories(root).OrderBy(d => d, StringComparer.OrdinalIgnoreCase)) + { + string bookId = Path.GetFileName(bookDir); + if (string.IsNullOrWhiteSpace(bookId)) + { + continue; + } + string jsonl = Path.Combine(bookDir, "search.jsonl"); + if (!File.Exists(jsonl)) + { + continue; + } + string fp = ReadBookContentSha(bookDir) ?? FileFingerprint(jsonl); + lock (_lock) + { + EnsureOpen(); + EnsureBooksSchema(); + string metaKey = BooksMetaPrefix + bookId; + if (string.Equals(GetMeta(metaKey), fp, StringComparison.Ordinal)) + { + continue; + } + } + int n = IndexBookJsonl(bookId, jsonl, fp); + total += n; + } + if (total > 0) + { + Logs.Info($"AssistentMemory: indexed {total} book rows under {root}"); + } + return BookRowCount(); + } + + int IndexBookJsonl(string bookId, string jsonlPath, string fingerprint) + { + List<(string id, string title, string tags, string text, string body, string rating, string metaJson, string blob)> rows = []; + foreach (string line in File.ReadLines(jsonlPath, Encoding.UTF8)) + { + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + JObject o; + try + { + o = JObject.Parse(line); + } + catch + { + continue; + } + string id = o["id"]?.ToString()?.Trim(); + if (string.IsNullOrWhiteSpace(id)) + { + id = $"{bookId}:{rows.Count + 1}"; + } + string title = o["title"]?.ToString() ?? ""; + string tagsJoined = ""; + if (o["tags"] is JArray tagArr) + { + tagsJoined = string.Join(", ", tagArr.Select(t => t?.ToString()?.Trim()).Where(t => !string.IsNullOrWhiteSpace(t))); + } + string text = o["text"]?.ToString() ?? ""; + string body = o["body"]?.ToString() ?? text; + if (string.IsNullOrWhiteSpace(text) && string.IsNullOrWhiteSpace(body)) + { + continue; + } + string rating = o["rating"]?.ToString() ?? ""; + string metaJson = (o["meta"] as JObject)?.ToString(Newtonsoft.Json.Formatting.None) ?? "{}"; + string blob = $"{title}\n{tagsJoined}\n{text}\n{body}\n{rating}"; + rows.Add((id, title, tagsJoined, text, body, rating, metaJson, blob)); + } + + lock (_lock) + { + EnsureOpen(); + EnsureBooksSchema(); + using SqliteTransaction tx = _conn.BeginTransaction(); + using (SqliteCommand del = _conn.CreateCommand()) + { + del.Transaction = tx; + del.CommandText = "DELETE FROM books_rows WHERE book_id = $b"; + del.Parameters.AddWithValue("$b", bookId); + del.ExecuteNonQuery(); + } + using (SqliteCommand ins = _conn.CreateCommand()) + { + ins.Transaction = tx; + ins.CommandText = + """ + INSERT OR REPLACE INTO books_rows( + id, book_id, title, tags, text, body, rating, meta_json, search_blob) + VALUES($id,$b,$t,$tg,$tx,$bd,$r,$mj,$bl) + """; + var pid = ins.Parameters.Add("$id", SqliteType.Text); + var pb = ins.Parameters.Add("$b", SqliteType.Text); + var pt = ins.Parameters.Add("$t", SqliteType.Text); + var ptg = ins.Parameters.Add("$tg", SqliteType.Text); + var ptx = ins.Parameters.Add("$tx", SqliteType.Text); + var pbd = ins.Parameters.Add("$bd", SqliteType.Text); + var pr = ins.Parameters.Add("$r", SqliteType.Text); + var pmj = ins.Parameters.Add("$mj", SqliteType.Text); + var pbl = ins.Parameters.Add("$bl", SqliteType.Text); + foreach (var row in rows) + { + pid.Value = row.id; + pb.Value = bookId; + pt.Value = row.title; + ptg.Value = row.tags; + ptx.Value = row.text; + pbd.Value = row.body; + pr.Value = row.rating; + pmj.Value = row.metaJson; + pbl.Value = row.blob; + ins.ExecuteNonQuery(); + } + } + tx.Commit(); + SetMeta(BooksMetaPrefix + bookId, fingerprint); + Logs.Info($"AssistentMemory: indexed book {bookId} ({rows.Count} rows)"); + return rows.Count; + } + } + + public int BookRowCount() + { + lock (_lock) + { + EnsureOpen(); + if (!TableExists("books_rows")) + { + return 0; + } + using SqliteCommand c = _conn.CreateCommand(); + c.CommandText = "SELECT COUNT(*) FROM books_rows"; + return Convert.ToInt32(c.ExecuteScalar()); + } + } + + public JArray ListBooksOnDisk() + { + JArray list = []; + string root = BooksRoot(); + if (!Directory.Exists(root)) + { + return list; + } + foreach (string bookDir in Directory.GetDirectories(root).OrderBy(d => d, StringComparer.OrdinalIgnoreCase)) + { + string id = Path.GetFileName(bookDir); + if (string.IsNullOrWhiteSpace(id)) + { + continue; + } + JObject spec = new() { ["id"] = id }; + string yaml = Path.Combine(bookDir, "book.yaml"); + if (File.Exists(yaml)) + { + foreach (string rawLine in File.ReadAllLines(yaml, Encoding.UTF8)) + { + string line = rawLine.Trim(); + if (string.IsNullOrWhiteSpace(line) || line.StartsWith('#')) + { + continue; + } + int colon = line.IndexOf(':'); + if (colon <= 0) + { + continue; + } + string key = line[..colon].Trim(); + string val = line[(colon + 1)..].Trim().Trim('"', '\''); + if (key is "title" or "description" or "content_kind" or "language") + { + spec[key] = val; + } + } + } + string jsonl = Path.Combine(bookDir, "search.jsonl"); + spec["indexed"] = File.Exists(jsonl); + spec["content_sha"] = ReadBookContentSha(bookDir) ?? ""; + list.Add(spec); + } + return list; + } + + /// FTS lookup over attached books (filter by book_id list when provided). + public JArray LookupBooks(string query, int limit = 8, IEnumerable bookIds = null, string rating = null) + { + query = (query ?? "").Trim(); + if (query.Length < 1) + { + return []; + } + TryIndexBooks(); + int cap = Math.Clamp(limit, 1, 30); + HashSet bookFilter = null; + if (bookIds is not null) + { + bookFilter = bookIds + .Select(b => (b ?? "").Trim()) + .Where(b => !string.IsNullOrWhiteSpace(b)) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + if (bookFilter.Count == 0) + { + return []; + } + } + string ratingFilter = string.IsNullOrWhiteSpace(rating) ? null : rating.Trim().ToLowerInvariant(); + List hits = []; + HashSet seen = []; + + void Add(SqliteDataReader reader) + { + string id = reader.GetString(0); + if (!seen.Add(id)) + { + return; + } + string bookId = reader.IsDBNull(1) ? "" : reader.GetString(1); + if (bookFilter is not null && !bookFilter.Contains(bookId)) + { + seen.Remove(id); + return; + } + string title = reader.IsDBNull(2) ? "" : reader.GetString(2); + string tags = reader.IsDBNull(3) ? "" : reader.GetString(3); + string text = reader.IsDBNull(4) ? "" : reader.GetString(4); + string body = reader.IsDBNull(5) ? "" : reader.GetString(5); + string rowRating = reader.IsDBNull(6) ? "" : reader.GetString(6); + JToken metaTok = new JObject(); + if (!reader.IsDBNull(7)) + { + try { metaTok = JToken.Parse(reader.GetString(7)); } catch { metaTok = new JObject(); } + } + hits.Add(new JObject + { + ["id"] = id, + ["book"] = bookId, + ["source"] = "book", + ["title"] = title, + ["tags"] = tags, + ["text"] = text.Length > 600 ? text[..600] + "…" : text, + ["body"] = body.Length > 1200 ? body[..1200] + "…" : body, + ["rating"] = rowRating, + ["meta"] = metaTok, + ["note"] = "BOOK reference — remix style/ideas, do not paste long verbatim", + }); + } + + lock (_lock) + { + EnsureOpen(); + if (!TableExists("books_rows")) + { + return []; + } + + string match = BuildFtsMatch(query); + if (!string.IsNullOrWhiteSpace(match) && TableExists("books_fts")) + { + try + { + using SqliteCommand cmd = _conn.CreateCommand(); + string sql = + """ + SELECT b.id, b.book_id, b.title, b.tags, b.text, b.body, b.rating, b.meta_json + FROM books_rows b + WHERE b.rowid IN (SELECT rowid FROM books_fts WHERE books_fts MATCH $q) + """; + if (ratingFilter is not null) + { + sql += " AND lower(b.rating) = $r"; + } + sql += " LIMIT $lim"; + cmd.CommandText = sql; + cmd.Parameters.AddWithValue("$q", match); + if (ratingFilter is not null) + { + cmd.Parameters.AddWithValue("$r", ratingFilter); + } + cmd.Parameters.AddWithValue("$lim", cap); + using SqliteDataReader reader = cmd.ExecuteReader(); + while (reader.Read() && hits.Count < cap) + { + Add(reader); + } + } + catch (Exception ex) + { + Logs.Debug($"AssistentMemory books FTS: {ex.Message}"); + } + } + + if (hits.Count < cap) + { + try + { + using SqliteCommand cmd = _conn.CreateCommand(); + string sql = + """ + SELECT id, book_id, title, tags, text, body, rating, meta_json + FROM books_rows + WHERE (title LIKE $p ESCAPE '\' OR tags LIKE $p ESCAPE '\' OR text LIKE $p ESCAPE '\' OR body LIKE $p ESCAPE '\' OR search_blob LIKE $p ESCAPE '\') + """; + if (ratingFilter is not null) + { + sql += " AND lower(rating) = $r"; + } + sql += " LIMIT $lim"; + cmd.CommandText = sql; + cmd.Parameters.AddWithValue("$p", "%" + EscapeLike(query) + "%"); + if (ratingFilter is not null) + { + cmd.Parameters.AddWithValue("$r", ratingFilter); + } + cmd.Parameters.AddWithValue("$lim", cap); + using SqliteDataReader reader = cmd.ExecuteReader(); + while (reader.Read() && hits.Count < cap) + { + Add(reader); + } + } + catch (Exception ex) + { + Logs.Debug($"AssistentMemory books LIKE: {ex.Message}"); + } + } + } + + return new JArray(hits.Take(cap)); + } +} diff --git a/AssistentMemory.Heard.cs b/AssistentMemory.Heard.cs index ffcdd03..2d4075a 100644 --- a/AssistentMemory.Heard.cs +++ b/AssistentMemory.Heard.cs @@ -44,47 +44,10 @@ public sealed partial class AssistentMemory public void SetTrainSampleAgentLinked(string id, bool linked) { - if (string.IsNullOrWhiteSpace(id)) - { - return; - } - lock (_lock) - { - EnsureTrainingReady(); - if (!HasColumn("train_samples", "agent_linked")) - { - return; - } - using SqliteCommand cmd = _conn.CreateCommand(); - cmd.CommandText = "UPDATE train_samples SET agent_linked = $v, updated_at = $u WHERE id = $id"; - cmd.Parameters.AddWithValue("$v", linked ? 1 : 0); - cmd.Parameters.AddWithValue("$u", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); - cmd.Parameters.AddWithValue("$id", id.Trim()); - cmd.ExecuteNonQuery(); - } + // Training UI removed — no-op. } - public int CountAgentLinkedTrainSamples() - { - lock (_lock) - { - try - { - EnsureTrainingReady(); - } - catch - { - return 0; - } - if (!HasColumn("train_samples", "agent_linked")) - { - return 0; - } - using SqliteCommand cmd = _conn.CreateCommand(); - cmd.CommandText = "SELECT COUNT(*) FROM train_samples WHERE agent_linked = 1"; - return Convert.ToInt32(cmd.ExecuteScalar()); - } - } + public int CountAgentLinkedTrainSamples() => 0; public async Task LinkTrainSampleToAgentAsync(string baseUrl, JObject sample, string embedModel) { diff --git a/AssistentMemory.Training.cs b/AssistentMemory.Training.cs deleted file mode 100644 index 868413f..0000000 --- a/AssistentMemory.Training.cs +++ /dev/null @@ -1,385 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Data.Sqlite; -using Newtonsoft.Json.Linq; -using SwarmUI.Utils; - -namespace Mrleo1nid.SwarmAssistent; - -/// Training datasets, samples, and job metadata in assistent.sqlite. -public sealed partial class AssistentMemory -{ - public const string KvHfDatasetCache = "hf_dataset_cache"; - - void EnsureTrainingSchema() - { - Exec( - """ - CREATE TABLE IF NOT EXISTS train_datasets ( - id TEXT PRIMARY KEY, - title TEXT NOT NULL DEFAULT '', - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL, - meta_json TEXT - ); - CREATE TABLE IF NOT EXISTS train_samples ( - id TEXT PRIMARY KEY, - dataset_id TEXT NOT NULL DEFAULT 'default', - source TEXT NOT NULL DEFAULT 'manual', - chat_id TEXT, - persona TEXT, - pack TEXT, - hf_repo TEXT, - messages_json TEXT NOT NULL DEFAULT '[]', - status TEXT NOT NULL DEFAULT 'draft', - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL, - FOREIGN KEY(dataset_id) REFERENCES train_datasets(id) ON DELETE CASCADE - ); - CREATE INDEX IF NOT EXISTS idx_train_samples_status ON train_samples(status); - CREATE INDEX IF NOT EXISTS idx_train_samples_dataset ON train_samples(dataset_id); - CREATE TABLE IF NOT EXISTS train_jobs ( - id TEXT PRIMARY KEY, - kind TEXT NOT NULL, - status TEXT NOT NULL DEFAULT 'pending', - config_json TEXT, - base_model TEXT, - output_name TEXT, - log_path TEXT, - progress_json TEXT, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL, - finished_at INTEGER - ); - CREATE INDEX IF NOT EXISTS idx_train_jobs_status ON train_jobs(status); - """); - if (!HasColumn("train_samples", "agent_linked")) - { - Exec("ALTER TABLE train_samples ADD COLUMN agent_linked INTEGER NOT NULL DEFAULT 0"); - } - using SqliteCommand cmd = _conn.CreateCommand(); - cmd.CommandText = "INSERT OR IGNORE INTO train_datasets(id, title, created_at, updated_at) VALUES('default', 'Default', $u, $u)"; - cmd.Parameters.AddWithValue("$u", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); - cmd.ExecuteNonQuery(); - } - - public List ListTrainSamples(string status = null, string persona = null, string datasetId = null, int limit = 200) - { - lock (_lock) - { - EnsureTrainingReady(); - int take = Math.Clamp(limit, 1, 2000); - List where = []; - if (!string.IsNullOrWhiteSpace(status) && !string.Equals(status, "all", StringComparison.OrdinalIgnoreCase)) - { - where.Add("status = $status"); - } - if (!string.IsNullOrWhiteSpace(persona) && !string.Equals(persona, "all", StringComparison.OrdinalIgnoreCase)) - { - where.Add("persona = $persona"); - } - if (!string.IsNullOrWhiteSpace(datasetId)) - { - where.Add("dataset_id = $ds"); - } - bool hasAgentLinked = HasColumn("train_samples", "agent_linked"); - string sql = hasAgentLinked - ? "SELECT id, dataset_id, source, chat_id, persona, pack, hf_repo, messages_json, status, created_at, updated_at, agent_linked FROM train_samples" - : "SELECT id, dataset_id, source, chat_id, persona, pack, hf_repo, messages_json, status, created_at, updated_at FROM train_samples"; - if (where.Count > 0) - { - sql += " WHERE " + string.Join(" AND ", where); - } - sql += " ORDER BY updated_at DESC LIMIT $lim"; - using SqliteCommand cmd = _conn.CreateCommand(); - cmd.CommandText = sql; - if (where.Any(w => w.Contains("$status"))) - { - cmd.Parameters.AddWithValue("$status", status.Trim()); - } - if (where.Any(w => w.Contains("$persona"))) - { - cmd.Parameters.AddWithValue("$persona", persona.Trim()); - } - if (where.Any(w => w.Contains("$ds"))) - { - cmd.Parameters.AddWithValue("$ds", datasetId.Trim()); - } - cmd.Parameters.AddWithValue("$lim", take); - List list = []; - using SqliteDataReader r = cmd.ExecuteReader(); - while (r.Read()) - { - list.Add(ReadTrainSampleRow(r)); - } - return list; - } - } - - static JObject ReadTrainSampleRow(SqliteDataReader r) - { - JArray messages = []; - try - { - messages = JArray.Parse(r.GetString(7)); - } - catch - { - // ignore - } - bool hasAgentLinked = r.FieldCount > 11; - return new JObject - { - ["id"] = r.GetString(0), - ["dataset_id"] = r.GetString(1), - ["source"] = r.GetString(2), - ["chat_id"] = r.IsDBNull(3) ? null : r.GetString(3), - ["persona"] = r.IsDBNull(4) ? null : r.GetString(4), - ["pack"] = r.IsDBNull(5) ? null : r.GetString(5), - ["hf_repo"] = r.IsDBNull(6) ? null : r.GetString(6), - ["messages"] = messages, - ["status"] = r.GetString(8), - ["createdAt"] = r.GetInt64(9), - ["updatedAt"] = r.GetInt64(10), - ["agent_linked"] = hasAgentLinked && !r.IsDBNull(11) && r.GetInt64(11) != 0, - }; - } - - public JObject GetTrainSample(string id) - { - if (string.IsNullOrWhiteSpace(id)) - { - return null; - } - lock (_lock) - { - EnsureOpen(); - bool hasAgentLinked = HasColumn("train_samples", "agent_linked"); - string sql = hasAgentLinked - ? "SELECT id, dataset_id, source, chat_id, persona, pack, hf_repo, messages_json, status, created_at, updated_at, agent_linked FROM train_samples WHERE id = $id LIMIT 1" - : "SELECT id, dataset_id, source, chat_id, persona, pack, hf_repo, messages_json, status, created_at, updated_at FROM train_samples WHERE id = $id LIMIT 1"; - using SqliteCommand cmd = _conn.CreateCommand(); - cmd.CommandText = sql; - cmd.Parameters.AddWithValue("$id", id.Trim()); - using SqliteDataReader r = cmd.ExecuteReader(); - return r.Read() ? ReadTrainSampleRow(r) : null; - } - } - - public int ImportTrainSamplesBatch(IEnumerable samples) - { - lock (_lock) - { - EnsureTrainingReady(); - int imported = 0; - using SqliteTransaction tx = _conn.BeginTransaction(); - foreach (JObject sample in samples) - { - if (sample is null) - { - continue; - } - UpsertTrainSampleCore(sample, tx); - imported++; - } - tx.Commit(); - return imported; - } - } - - public JObject UpsertTrainSample(JObject sample) - { - lock (_lock) - { - EnsureTrainingReady(); - return UpsertTrainSampleCore(sample, null); - } - } - - JObject UpsertTrainSampleCore(JObject sample, SqliteTransaction tx) - { - long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - string id = sample["id"]?.ToString()?.Trim(); - if (string.IsNullOrWhiteSpace(id)) - { - id = $"ts_{now}_{Guid.NewGuid():N}"[..24]; - } - string datasetId = sample["dataset_id"]?.ToString()?.Trim() ?? "default"; - JArray messages = sample["messages"] as JArray ?? []; - using SqliteCommand cmd = _conn.CreateCommand(); - if (tx is not null) - { - cmd.Transaction = tx; - } - cmd.CommandText = - """ - INSERT INTO train_samples(id, dataset_id, source, chat_id, persona, pack, hf_repo, messages_json, status, created_at, updated_at) - VALUES($id, $ds, $src, $chat, $persona, $pack, $hf, $msg, $status, $c, $u) - ON CONFLICT(id) DO UPDATE SET - dataset_id = excluded.dataset_id, - source = excluded.source, - chat_id = excluded.chat_id, - persona = excluded.persona, - pack = excluded.pack, - hf_repo = excluded.hf_repo, - messages_json = excluded.messages_json, - status = excluded.status, - updated_at = excluded.updated_at - """; - cmd.Parameters.AddWithValue("$id", id); - cmd.Parameters.AddWithValue("$ds", datasetId); - cmd.Parameters.AddWithValue("$src", sample["source"]?.ToString() ?? "manual"); - cmd.Parameters.AddWithValue("$chat", (object)sample["chat_id"]?.ToString() ?? DBNull.Value); - cmd.Parameters.AddWithValue("$persona", (object)sample["persona"]?.ToString() ?? DBNull.Value); - cmd.Parameters.AddWithValue("$pack", (object)sample["pack"]?.ToString() ?? DBNull.Value); - cmd.Parameters.AddWithValue("$hf", (object)sample["hf_repo"]?.ToString() ?? DBNull.Value); - cmd.Parameters.AddWithValue("$msg", messages.ToString(Newtonsoft.Json.Formatting.None)); - cmd.Parameters.AddWithValue("$status", sample["status"]?.ToString() ?? "draft"); - long created = sample["createdAt"]?.Value() ?? now; - cmd.Parameters.AddWithValue("$c", created); - cmd.Parameters.AddWithValue("$u", now); - cmd.ExecuteNonQuery(); - return new JObject { ["id"] = id, ["updatedAt"] = now }; - } - - public bool DeleteTrainSample(string id) - { - lock (_lock) - { - EnsureTrainingReady(); - using SqliteCommand cmd = _conn.CreateCommand(); - cmd.CommandText = "DELETE FROM train_samples WHERE id = $id"; - cmd.Parameters.AddWithValue("$id", id ?? ""); - return cmd.ExecuteNonQuery() > 0; - } - } - - public int CountTrainSamples(string status = null) - { - lock (_lock) - { - EnsureTrainingReady(); - using SqliteCommand cmd = _conn.CreateCommand(); - if (string.IsNullOrWhiteSpace(status) || string.Equals(status, "all", StringComparison.OrdinalIgnoreCase)) - { - cmd.CommandText = "SELECT COUNT(*) FROM train_samples"; - } - else - { - cmd.CommandText = "SELECT COUNT(*) FROM train_samples WHERE status = $s"; - cmd.Parameters.AddWithValue("$s", status.Trim()); - } - return Convert.ToInt32(cmd.ExecuteScalar()); - } - } - - public JObject SaveTrainJob(JObject job) - { - lock (_lock) - { - EnsureOpen(); - long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - string id = job["id"]?.ToString()?.Trim(); - if (string.IsNullOrWhiteSpace(id)) - { - id = $"tj_{now}_{Guid.NewGuid():N}"[..24]; - } - using SqliteCommand cmd = _conn.CreateCommand(); - cmd.CommandText = - """ - INSERT INTO train_jobs(id, kind, status, config_json, base_model, output_name, log_path, progress_json, created_at, updated_at, finished_at) - VALUES($id, $kind, $status, $cfg, $base, $out, $log, $prog, $c, $u, $f) - ON CONFLICT(id) DO UPDATE SET - status = excluded.status, - config_json = excluded.config_json, - log_path = excluded.log_path, - progress_json = excluded.progress_json, - updated_at = excluded.updated_at, - finished_at = excluded.finished_at - """; - cmd.Parameters.AddWithValue("$id", id); - cmd.Parameters.AddWithValue("$kind", job["kind"]?.ToString() ?? "qlora"); - cmd.Parameters.AddWithValue("$status", job["status"]?.ToString() ?? "pending"); - cmd.Parameters.AddWithValue("$cfg", job["config"]?.ToString(Newtonsoft.Json.Formatting.None) ?? job["config_json"]?.ToString() ?? "{}"); - cmd.Parameters.AddWithValue("$base", (object)job["base_model"]?.ToString() ?? DBNull.Value); - cmd.Parameters.AddWithValue("$out", (object)job["output_name"]?.ToString() ?? DBNull.Value); - cmd.Parameters.AddWithValue("$log", (object)job["log_path"]?.ToString() ?? DBNull.Value); - cmd.Parameters.AddWithValue("$prog", (object)(job["progress"]?.ToString(Newtonsoft.Json.Formatting.None) ?? job["progress_json"]?.ToString()) ?? DBNull.Value); - cmd.Parameters.AddWithValue("$c", job["created_at"]?.Value() ?? job["createdAt"]?.Value() ?? now); - cmd.Parameters.AddWithValue("$u", now); - cmd.Parameters.AddWithValue("$f", (object)(job["finished_at"]?.Value() ?? job["finishedAt"]?.Value()) ?? DBNull.Value); - cmd.ExecuteNonQuery(); - return new JObject { ["id"] = id }; - } - } - - public JObject GetTrainJob(string id) - { - lock (_lock) - { - EnsureTrainingReady(); - using SqliteCommand cmd = _conn.CreateCommand(); - cmd.CommandText = "SELECT id, kind, status, config_json, base_model, output_name, log_path, progress_json, created_at, updated_at, finished_at FROM train_jobs WHERE id = $id"; - cmd.Parameters.AddWithValue("$id", id ?? ""); - using SqliteDataReader r = cmd.ExecuteReader(); - if (!r.Read()) - { - return null; - } - return ReadTrainJobRow(r); - } - } - - public JObject GetLastTrainJob() - { - lock (_lock) - { - EnsureTrainingReady(); - using SqliteCommand cmd = _conn.CreateCommand(); - cmd.CommandText = - "SELECT id, kind, status, config_json, base_model, output_name, log_path, progress_json, created_at, updated_at, finished_at " - + "FROM train_jobs ORDER BY updated_at DESC LIMIT 1"; - using SqliteDataReader r = cmd.ExecuteReader(); - if (!r.Read()) - { - return null; - } - return ReadTrainJobRow(r); - } - } - - static JObject ReadTrainJobRow(SqliteDataReader r) - { - return new JObject - { - ["id"] = r.GetString(0), - ["kind"] = r.GetString(1), - ["status"] = r.GetString(2), - ["config_json"] = r.IsDBNull(3) ? null : r.GetString(3), - ["base_model"] = r.IsDBNull(4) ? null : r.GetString(4), - ["output_name"] = r.IsDBNull(5) ? null : r.GetString(5), - ["log_path"] = r.IsDBNull(6) ? null : r.GetString(6), - ["progress_json"] = r.IsDBNull(7) ? null : r.GetString(7), - ["created_at"] = r.GetInt64(8), - ["updated_at"] = r.GetInt64(9), - ["finished_at"] = r.IsDBNull(10) ? null : r.GetInt64(10), - }; - } - - public JObject GetActiveTrainJob() - { - lock (_lock) - { - EnsureTrainingReady(); - using SqliteCommand cmd = _conn.CreateCommand(); - cmd.CommandText = "SELECT id, kind, status, config_json, base_model, output_name, log_path, progress_json, created_at, updated_at, finished_at FROM train_jobs WHERE status IN ('pending','running') ORDER BY updated_at DESC LIMIT 1"; - using SqliteDataReader r = cmd.ExecuteReader(); - if (!r.Read()) - { - return null; - } - return ReadTrainJobRow(r); - } - } -} diff --git a/AssistentMemory.cs b/AssistentMemory.cs index 0142e7f..5d7c282 100644 --- a/AssistentMemory.cs +++ b/AssistentMemory.cs @@ -144,14 +144,6 @@ public sealed partial class AssistentMemory : IDisposable Logs.Debug($"AssistentMemory store schema: {ex.Message}"); } try - { - EnsureTrainingSchema(); - } - catch (Exception ex) - { - Logs.Error($"AssistentMemory training schema failed: {ex.Message}"); - } - try { EnsureUserPrefsSchema(); } @@ -193,17 +185,8 @@ public sealed partial class AssistentMemory : IDisposable internal void EnsureTrainingReady() { + // Training UI removed in 0.16 — heard examples live in memories only. EnsureOpen(); - if (!HasTable("train_samples")) - { - EnsureTrainingSchema(); - } - if (!HasTable("train_samples")) - { - throw new InvalidOperationException( - "Assistent training database unavailable (train_samples). " - + "Run gpu-rent seed-extensions and restart SwarmUI (≥0.15.6)."); - } } void MigratePersonaColumn() @@ -443,6 +426,7 @@ public sealed partial class AssistentMemory : IDisposable { TryIndexTags(); TryIndexExamples(); + TryIndexBooks(); return; } @@ -478,6 +462,7 @@ public sealed partial class AssistentMemory : IDisposable Logs.Debug($"AssistentMemory seed defer (embed unavailable): {ex.Message}"); TryIndexTags(); TryIndexExamples(); + TryIndexBooks(); return; } diff --git a/AssistentOllama.cs b/AssistentOllama.cs index 93e4975..8278157 100644 --- a/AssistentOllama.cs +++ b/AssistentOllama.cs @@ -339,7 +339,7 @@ public partial class SwarmAssistentExtension string embedModel = raw?["embed_model"]?.ToString() ?? Config.LoadSettings()["embed_model"]?.ToString(); try { - (string reply, JObject parsed, JArray civitai, int systemChars, JObject systemLayers) = await RunChatWithHops( + (string reply, JObject parsed, JArray civitai, JObject knowledge, int systemChars, JObject systemLayers) = await RunChatWithHops( session, root, modelName, packName, includeBase, contextJson, userMessages, personaId: persona, skillIds: skills, embedModel: embedModel); JObject result = new() { @@ -350,6 +350,7 @@ public partial class SwarmAssistentExtension ["persona"] = persona, ["raw"] = parsed, ["civitai_results"] = civitai, + ["knowledge"] = knowledge, ["system_chars"] = systemChars, ["system_layers"] = systemLayers, }; @@ -409,7 +410,7 @@ public partial class SwarmAssistentExtension }, API.WebsocketTimeout); } } - (string reply, JObject parsed, JArray civitai, int systemChars, JObject systemLayers) = await RunChatWithHops( + (string reply, JObject parsed, JArray civitai, JObject knowledge, int systemChars, JObject systemLayers) = await RunChatWithHops( session, root, modelName, packName, includeBase, contextJson, userMessages, OnDelta, OnHopStart, persona, skills, embedModel); JObject done = new() { @@ -421,6 +422,7 @@ public partial class SwarmAssistentExtension ["persona"] = persona, ["raw"] = parsed, ["civitai_results"] = civitai, + ["knowledge"] = knowledge, ["system_chars"] = systemChars, ["system_layers"] = systemLayers, }; diff --git a/AssistentPatch.cs b/AssistentPatch.cs index 667f198..f70fe01 100644 --- a/AssistentPatch.cs +++ b/AssistentPatch.cs @@ -313,6 +313,10 @@ public partial class SwarmAssistentExtension { return "ask_inventory"; } + if ((AskContains(patch, "knowledge") || ActionsContain(patch, "lookup_knowledge")) && !Skip("ask_knowledge")) + { + return "ask_knowledge"; + } if ((AskContains(patch, "examples") || ActionsContain(patch, "lookup_examples")) && !Skip("ask_examples")) { return "ask_examples"; diff --git a/AssistentTraining.Agent.cs b/AssistentTraining.Agent.cs deleted file mode 100644 index 3506a03..0000000 --- a/AssistentTraining.Agent.cs +++ /dev/null @@ -1,181 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Newtonsoft.Json.Linq; -using SwarmUI.Accounts; -using SwarmUI.Utils; - -namespace Mrleo1nid.SwarmAssistent; - -/// Link training dataset samples to the live agent as retrievable "heard" examples. -public partial class SwarmAssistentExtension -{ - string MemoryEmbedForTraining(string personaId = null) - { - string pid = AssistentConfig.SafeId(personaId) ?? Config.DefaultPersonaId(); - return Config.LoadSettings()["embed_model"]?.ToString() - ?? Config.LoadAssistant(pid)["embed_model"]?.ToString() - ?? "nomic-embed-text"; - } - - string MemoryBaseForTraining(JObject raw = null) - => MemoryBaseUrl(raw?["base_url"]?.ToString()); - - public async Task AssistentGetDatasetAgentSettings(Session session) - { - await Task.CompletedTask; - try - { - JObject settings = Config.LoadTrainingAgent(); - return new JObject - { - ["success"] = true, - ["settings"] = settings, - ["linked"] = Memory.CountAgentLinkedTrainSamples(), - ["approved"] = Memory.CountTrainSamples("approved"), - }; - } - catch (Exception ex) - { - return new JObject { ["error"] = ex.Message }; - } - } - - public async Task AssistentSaveDatasetAgentSettings(Session session, JObject settings) - { - await Task.CompletedTask; - if (settings is null) - { - return new JObject { ["error"] = "settings required" }; - } - Config.SaveTrainingAgent(settings); - return new JObject { ["success"] = true, ["settings"] = Config.LoadTrainingAgent() }; - } - - public async Task AssistentLinkTrainSampleToAgent(Session session, string id, JObject raw = null) - { - if (string.IsNullOrWhiteSpace(id)) - { - return new JObject { ["error"] = "id required" }; - } - JObject sample = Memory.GetTrainSample(id.Trim()); - if (sample is null) - { - return new JObject { ["error"] = "sample not found" }; - } - if (!string.Equals(sample["status"]?.ToString(), "approved", StringComparison.OrdinalIgnoreCase)) - { - return new JObject { ["error"] = "only approved samples can be linked to the agent" }; - } - try - { - string embed = MemoryEmbedForTraining(sample["persona"]?.ToString()); - bool ok = await Memory.LinkTrainSampleToAgentAsync(MemoryBaseForTraining(raw), sample, embed); - return new JObject { ["success"] = ok, ["linked"] = Memory.CountAgentLinkedTrainSamples() }; - } - catch (Exception ex) - { - return new JObject { ["error"] = ex.Message }; - } - } - - public async Task AssistentUnlinkTrainSampleFromAgent(Session session, string id) - { - await Task.CompletedTask; - if (string.IsNullOrWhiteSpace(id)) - { - return new JObject { ["error"] = "id required" }; - } - JObject sample = Memory.GetTrainSample(id.Trim()); - if (sample is null) - { - return new JObject { ["error"] = "sample not found" }; - } - Memory.UnlinkTrainSampleFromAgent(sample); - return new JObject { ["success"] = true, ["linked"] = Memory.CountAgentLinkedTrainSamples() }; - } - - public async Task AssistentSyncDatasetToAgent(Session session, JObject raw = null) - { - bool approvedOnly = raw?["approved_only"]?.Value() ?? true; - bool relink = raw?["relink"]?.Value() ?? false; - string persona = raw?["persona"]?.ToString(); - string status = approvedOnly ? "approved" : "all"; - List samples = Memory.ListTrainSamples(status, persona, null, 2000); - string baseUrl = MemoryBaseForTraining(raw); - int linked = 0; - int skipped = 0; - List errors = []; - foreach (JObject sample in samples) - { - bool already = sample["agent_linked"]?.Value() ?? false; - if (already && !relink) - { - skipped++; - continue; - } - if (!string.Equals(sample["status"]?.ToString(), "approved", StringComparison.OrdinalIgnoreCase)) - { - skipped++; - continue; - } - try - { - string embed = MemoryEmbedForTraining(sample["persona"]?.ToString()); - if (await Memory.LinkTrainSampleToAgentAsync(baseUrl, sample, embed)) - { - linked++; - } - else - { - skipped++; - } - } - catch (Exception ex) - { - errors.Add($"{sample["id"]}: {ex.Message}"); - } - } - return new JObject - { - ["success"] = true, - ["linked_now"] = linked, - ["skipped"] = skipped, - ["total_linked"] = Memory.CountAgentLinkedTrainSamples(), - ["errors"] = new JArray(errors.Take(8)), - }; - } - - async Task TryAutoLinkTrainSample(Session session, JObject sample, JObject raw = null) - { - if (sample is null || Memory is null) - { - return; - } - JObject agent = Config.LoadTrainingAgent(); - if (agent["enabled"]?.Value() == false) - { - return; - } - if (agent["auto_link_on_approve"]?.Value() == false) - { - return; - } - string status = sample["status"]?.ToString() ?? ""; - if (!string.Equals(status, "approved", StringComparison.OrdinalIgnoreCase)) - { - Memory.UnlinkTrainSampleFromAgent(sample); - return; - } - try - { - string embed = MemoryEmbedForTraining(sample["persona"]?.ToString()); - await Memory.LinkTrainSampleToAgentAsync(MemoryBaseForTraining(raw), sample, embed); - } - catch (Exception ex) - { - Logs.Debug($"TryAutoLinkTrainSample: {ex.Message}"); - } - } -} diff --git a/AssistentTraining.cs b/AssistentTraining.cs deleted file mode 100644 index fc04e3c..0000000 --- a/AssistentTraining.cs +++ /dev/null @@ -1,456 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; -using Newtonsoft.Json.Linq; -using SwarmUI.Accounts; -using SwarmUI.Utils; - -namespace Mrleo1nid.SwarmAssistent; - -/// Training samples, dataset import/export, Ollama Modelfile builder. -public partial class SwarmAssistentExtension -{ - static string TrainingRoot() - { - string root = Path.Combine(DataRoot(), "Assistent", "training"); - Directory.CreateDirectory(root); - Directory.CreateDirectory(Path.Combine(root, "datasets")); - Directory.CreateDirectory(Path.Combine(root, "jobs")); - Directory.CreateDirectory(Path.Combine(root, "adapters")); - return root; - } - - public async Task AssistentListTrainSamples(Session session, string status = null, string persona = null, int limit = 200) - { - await Task.CompletedTask; - try - { - List list = Memory.ListTrainSamples(status, persona, null, limit); - return new JObject - { - ["success"] = true, - ["samples"] = new JArray(list), - ["approved"] = Memory.CountTrainSamples("approved"), - ["draft"] = Memory.CountTrainSamples("draft"), - ["total"] = Memory.CountTrainSamples(null), - }; - } - catch (Exception ex) - { - return new JObject { ["error"] = ex.Message }; - } - } - - public async Task AssistentUpsertTrainSample(Session session, JObject raw) - { - await Task.CompletedTask; - if (raw is null) - { - return new JObject { ["error"] = "body required" }; - } - try - { - JObject saved = Memory.UpsertTrainSample(raw); - JObject full = Memory.GetTrainSample(saved["id"]?.ToString()) ?? raw; - await TryAutoLinkTrainSample(session, full, raw); - return new JObject { ["success"] = true, ["sample"] = full }; - } - catch (Exception ex) - { - return new JObject { ["error"] = ex.Message }; - } - } - - public async Task AssistentDeleteTrainSample(Session session, string id) - { - await Task.CompletedTask; - if (string.IsNullOrWhiteSpace(id)) - { - return new JObject { ["error"] = "id required" }; - } - JObject sample = Memory.GetTrainSample(id.Trim()); - bool ok = Memory.DeleteTrainSample(id.Trim()); - if (ok && sample is not null) - { - Memory.UnlinkTrainSampleFromAgent(sample); - } - return new JObject { ["success"] = true, ["deleted"] = ok }; - } - - public async Task AssistentBuildDatasetFromChats(Session session, bool approved_only = false) - { - await Task.CompletedTask; - try - { - List chats = Memory.ListChats(withMessages: true, limit: AssistentMemory.MaxChatsStored); - int added = 0; - foreach (JObject chat in chats) - { - JArray messages = chat["messages"] as JArray ?? []; - for (int i = 0; i < messages.Count - 1; i++) - { - if (messages[i] is not JObject u || messages[i + 1] is not JObject a) - { - continue; - } - if (!string.Equals(u["role"]?.ToString(), "user", StringComparison.OrdinalIgnoreCase)) - { - continue; - } - if (!string.Equals(a["role"]?.ToString(), "assistant", StringComparison.OrdinalIgnoreCase)) - { - continue; - } - Memory.UpsertTrainSample(new JObject - { - ["source"] = "chat", - ["chat_id"] = chat["id"], - ["persona"] = a["persona"] ?? u["persona"], - ["pack"] = a["pack"] ?? u["pack"], - ["status"] = approved_only ? "approved" : "draft", - ["messages"] = new JArray { u.DeepClone(), a.DeepClone() }, - }); - added++; - } - } - return new JObject { ["success"] = true, ["added"] = added }; - } - catch (Exception ex) - { - return new JObject { ["error"] = ex.Message }; - } - } - - public async Task AssistentImportDataset(Session session, JObject raw) - { - await Task.CompletedTask; - string content = raw?["content"]?.ToString(); - string format = raw?["format"]?.ToString() ?? "auto"; - if (string.IsNullOrWhiteSpace(content)) - { - return new JObject { ["error"] = "content required" }; - } - try - { - int imported = 0; - string fmt = (format ?? "auto").Trim().ToLowerInvariant(); - List records = ParseDatasetContent(content, fmt); - foreach (JObject rec in records) - { - JArray messages = rec["messages"] as JArray; - if (messages is null || messages.Count == 0) - { - continue; - } - Memory.UpsertTrainSample(new JObject - { - ["source"] = "import", - ["messages"] = messages, - ["status"] = "draft", - }); - imported++; - } - return new JObject { ["success"] = true, ["imported"] = imported }; - } - catch (Exception ex) - { - return new JObject { ["error"] = ex.Message }; - } - } - - static List ParseDatasetContent(string content, string format) - { - List list = []; - string trimmed = content.Trim(); - if (trimmed.StartsWith('[')) - { - JArray arr = JArray.Parse(trimmed); - foreach (JToken t in arr) - { - if (t is JObject o) - { - JArray msgs = ExtractMessagesFromRecord(o); - if (msgs != null) - { - list.Add(new JObject { ["messages"] = msgs }); - } - } - } - return list; - } - if (format == "csv" || LooksLikeCsv(trimmed)) - { - return ParseCsvDataset(trimmed); - } - foreach (string line in trimmed.Split('\n')) - { - string ln = line.Trim(); - if (string.IsNullOrWhiteSpace(ln)) - { - continue; - } - try - { - JObject o = JObject.Parse(ln); - JArray msgs = ExtractMessagesFromRecord(o); - if (msgs != null) - { - list.Add(new JObject { ["messages"] = msgs }); - } - } - catch - { - // skip bad line - } - } - return list; - } - - static bool LooksLikeCsv(string s) => s.Contains(',') && s.Contains('\n') && !s.TrimStart().StartsWith('{'); - - static List ParseCsvDataset(string csv) - { - List list = []; - string[] lines = csv.Split('\n').Select(l => l.Trim()).Where(l => l.Length > 0).ToArray(); - if (lines.Length < 2) - { - return list; - } - string[] headers = lines[0].Split(',').Select(h => h.Trim().Trim('"')).ToArray(); - int promptIdx = Array.FindIndex(headers, h => h.Equals("prompt", StringComparison.OrdinalIgnoreCase) || h.Equals("question", StringComparison.OrdinalIgnoreCase) || h.Equals("instruction", StringComparison.OrdinalIgnoreCase)); - int respIdx = Array.FindIndex(headers, h => h.Equals("response", StringComparison.OrdinalIgnoreCase) || h.Equals("answer", StringComparison.OrdinalIgnoreCase) || h.Equals("output", StringComparison.OrdinalIgnoreCase) || h.Equals("completion", StringComparison.OrdinalIgnoreCase)); - if (promptIdx < 0 || respIdx < 0) - { - return list; - } - for (int i = 1; i < lines.Length; i++) - { - string[] cols = SplitCsvLine(lines[i]); - if (cols.Length <= Math.Max(promptIdx, respIdx)) - { - continue; - } - list.Add(new JObject - { - ["messages"] = new JArray - { - new JObject { ["role"] = "user", ["content"] = cols[promptIdx] }, - new JObject { ["role"] = "assistant", ["content"] = cols[respIdx] }, - }, - }); - } - return list; - } - - static string[] SplitCsvLine(string line) - { - List parts = []; - StringBuilder cur = new(); - bool inQ = false; - foreach (char c in line) - { - if (c == '"') - { - inQ = !inQ; - continue; - } - if (c == ',' && !inQ) - { - parts.Add(cur.ToString().Trim()); - cur.Clear(); - continue; - } - cur.Append(c); - } - parts.Add(cur.ToString().Trim()); - return parts.ToArray(); - } - - static JArray ExtractMessagesFromRecord(JObject o) - { - if (o["messages"] is JArray msgs) - { - return NormalizeMessagesArray(msgs); - } - if (o["conversations"] is JArray conv) - { - return ConvertHfRowToMessages(new JObject { ["conversations"] = conv }, new JObject { ["kind"] = "conversations" }, null); - } - if (o["instruction"] != null && o["output"] != null) - { - return ConvertHfRowToMessages(o, new JObject { ["kind"] = "alpaca" }, null); - } - if (o["prompt"] != null && (o["response"] != null || o["completion"] != null)) - { - return ConvertHfRowToMessages(o, new JObject { ["kind"] = "prompt_response", ["response_col"] = o["response"] != null ? "response" : "completion" }, null); - } - return null; - } - - public async Task AssistentExportDataset(Session session, string status = "approved", string format = "jsonl") - { - await Task.CompletedTask; - try - { - List samples = Memory.ListTrainSamples(status, null, null, 5000); - StringBuilder sb = new(); - int exported = 0; - foreach (JObject s in samples) - { - JArray messages = s["messages"] as JArray ?? []; - if (messages.Count == 0) - { - continue; - } - exported++; - if (string.Equals(format, "sharegpt", StringComparison.OrdinalIgnoreCase)) - { - JArray conv = []; - foreach (JToken m in messages) - { - if (m is not JObject mo) - { - continue; - } - string role = mo["role"]?.ToString() ?? "user"; - conv.Add(new JObject - { - ["from"] = role == "assistant" ? "gpt" : "human", - ["value"] = mo["content"]?.ToString() ?? "", - }); - } - sb.AppendLine(new JObject { ["conversations"] = conv }.ToString(Newtonsoft.Json.Formatting.None)); - } - else - { - sb.AppendLine(new JObject { ["messages"] = messages }.ToString(Newtonsoft.Json.Formatting.None)); - } - } - string path = Path.Combine(TrainingRoot(), "datasets", $"export_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}.jsonl"); - await File.WriteAllTextAsync(path, sb.ToString(), Encoding.UTF8); - return new JObject { ["success"] = true, ["path"] = path, ["count"] = exported, ["content"] = sb.ToString() }; - } - catch (Exception ex) - { - return new JObject { ["error"] = ex.Message }; - } - } - - public async Task AssistentCreateOllamaModel(Session session, JObject raw) - { - if (raw is null) - { - return new JObject { ["error"] = "body required" }; - } - string baseUrl = NormalizeBaseUrl(raw["base_url"]?.ToString()); - string baseModel = raw["base_model"]?.ToString()?.Trim(); - string name = raw["name"]?.ToString()?.Trim(); - string system = raw["system"]?.ToString() ?? ""; - int shots = raw["shots"]?.Value() ?? 8; - if (string.IsNullOrWhiteSpace(baseModel) || string.IsNullOrWhiteSpace(name)) - { - return new JObject { ["error"] = "base_model and name required" }; - } - if (string.IsNullOrWhiteSpace(system)) - { - string persona = AssistentConfig.SafeId(raw["persona"]?.ToString()) ?? Config.DefaultPersonaId(); - system = Config.LoadCorePrompt(persona) + "\n\n" + Config.RenderIdentityBlock(persona, includeAllShelves: true); - } - StringBuilder mf = new(); - mf.AppendLine($"FROM {baseModel}"); - mf.AppendLine($"SYSTEM \"\"\"{system}\"\"\""); - List samples = Memory.ListTrainSamples("approved", null, null, Math.Clamp(shots, 0, 32)); - foreach (JObject s in samples.Take(shots)) - { - JArray messages = s["messages"] as JArray ?? []; - foreach (JToken m in messages) - { - if (m is not JObject mo) - { - continue; - } - string role = mo["role"]?.ToString() ?? "user"; - string content = mo["content"]?.ToString() ?? ""; - if (string.IsNullOrWhiteSpace(content)) - { - continue; - } - mf.AppendLine($"MESSAGE {role} \"\"\"{content.Replace("\"\"\"", "\"\"\"\"\"\"\"")}\"\"\""); - } - } - if (raw["num_ctx"] != null) - { - mf.AppendLine($"PARAMETER num_ctx {raw["num_ctx"]}"); - } - if (raw["temperature"] != null) - { - mf.AppendLine($"PARAMETER temperature {raw["temperature"]}"); - } - string modelfilePath = Path.Combine(TrainingRoot(), "jobs", $"modelfile_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}.Modelfile"); - Directory.CreateDirectory(Path.GetDirectoryName(modelfilePath)!); - await File.WriteAllTextAsync(modelfilePath, mf.ToString(), Encoding.UTF8); - try - { - JObject payload = new() - { - ["name"] = name, - ["modelfile"] = mf.ToString(), - ["stream"] = false, - }; - using StringContent content = new(payload.ToString(Newtonsoft.Json.Formatting.None), Encoding.UTF8, "application/json"); - using HttpResponseMessage resp = await HttpClient.PostAsync($"{baseUrl}/api/create", content); - string body = await resp.Content.ReadAsStringAsync(); - if (!resp.IsSuccessStatusCode) - { - return new JObject { ["error"] = $"Ollama create HTTP {(int)resp.StatusCode}: {Clip(body, 400)}", ["modelfile_path"] = modelfilePath }; - } - return new JObject { ["success"] = true, ["name"] = name, ["modelfile_path"] = modelfilePath, ["ollama"] = body }; - } - catch (Exception ex) - { - return new JObject { ["error"] = ex.Message, ["modelfile_path"] = modelfilePath }; - } - } - - public async Task AssistentGetTrainJob(Session session, string id = null) - { - await Task.CompletedTask; - JObject job = string.IsNullOrWhiteSpace(id) ? Memory.GetActiveTrainJob() : Memory.GetTrainJob(id); - JObject lastJob = Memory.GetLastTrainJob(); - if (job is not null && TrainingJobManager.IsRunning && string.Equals(job["id"]?.ToString(), TrainingJobManager.CurrentJobId, StringComparison.OrdinalIgnoreCase)) - { - JObject live = TrainingJobManager.GetProgress(); - job["progress_json"] = live.ToString(Newtonsoft.Json.Formatting.None); - job["status"] = live["status"]?.ToString() ?? job["status"]; - } - return new JObject - { - ["success"] = true, - ["job"] = job, - ["last_job"] = lastJob, - ["training_active"] = TrainingJobManager.IsRunning, - ["progress"] = TrainingJobManager.IsRunning ? TrainingJobManager.GetProgress() : null, - }; - } - - public async Task AssistentSaveRunnerSettings(Session session, JObject settings) - { - await Task.CompletedTask; - if (settings is null) - { - return new JObject { ["error"] = "settings required" }; - } - Config.SaveTrainingRunner(settings); - return new JObject { ["success"] = true }; - } - - public async Task AssistentGetRunnerSettings(Session session) - { - await Task.CompletedTask; - return new JObject { ["success"] = true, ["settings"] = Config.LoadTrainingRunner() }; - } -} diff --git a/AssistentTrainingJobs.cs b/AssistentTrainingJobs.cs deleted file mode 100644 index c9bee12..0000000 --- a/AssistentTrainingJobs.cs +++ /dev/null @@ -1,607 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Net.WebSockets; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using Newtonsoft.Json.Linq; -using SwarmUI.Accounts; -using SwarmUI.Utils; -using SwarmUI.WebAPI; - -namespace Mrleo1nid.SwarmAssistent; - -/// QLoRA training job runner with VRAM lock and progress streaming. -public partial class SwarmAssistentExtension -{ - static readonly TrainingJobManager TrainingJobManager = new(); - - public async Task AssistentStartTrainJob(Session session, JObject raw) - { - if (raw is null) - { - return new JObject { ["error"] = "body required" }; - } - if (TrainingJobManager.IsRunning) - { - return new JObject { ["error"] = "Тренировка уже идёт" }; - } - string hfBase = raw["base_model"]?.ToString()?.Trim(); - string outputName = raw["output_name"]?.ToString()?.Trim(); - if (string.IsNullOrWhiteSpace(hfBase) || string.IsNullOrWhiteSpace(outputName)) - { - return new JObject { ["error"] = "base_model and output_name required" }; - } - string hfDataset = null; - JObject hfCheck = null; - JObject hfMapping = raw["hf_mapping"] as JObject; - if (!string.IsNullOrWhiteSpace(raw["hf_dataset"]?.ToString())) - { - hfDataset = NormalizeHfDatasetId(raw["hf_dataset"]?.ToString()); - if (hfDataset is null) - { - return new JObject { ["error"] = "invalid hf_dataset id" }; - } - hfCheck = await CheckHfDatasetInternal(session, hfDataset, useCache: true); - if (hfCheck["gate"]?.ToString() == "rejected") - { - return new JObject { ["error"] = hfCheck["reason"]?.ToString() ?? "hf dataset rejected" }; - } - hfMapping = ResolveHfMapping(hfCheck, hfMapping); - if (MappingRequired(hfCheck, hfMapping)) - { - return new JObject { ["error"] = "Нужен маппинг колонок для HF набора", ["check"] = hfCheck }; - } - } - JObject runner = Config.LoadTrainingRunner(); - string kind = runner["kind"]?.ToString()?.Trim(); - if (string.IsNullOrWhiteSpace(kind)) - { - kind = "builtin"; - } - string baseUrl = NormalizeBaseUrl(raw["base_url"]?.ToString()); - string chatModel = raw["chat_model"]?.ToString()?.Trim(); - if (!string.IsNullOrWhiteSpace(chatModel)) - { - await AssistentParkLlm(session, baseUrl, chatModel); - } - string datasetPath = null; - int exportCount = 0; - if (string.IsNullOrWhiteSpace(hfDataset)) - { - JObject export = await AssistentExportDataset(session, "approved", "jsonl"); - datasetPath = export["path"]?.ToString(); - exportCount = export["count"]?.Value() ?? 0; - if (string.IsNullOrWhiteSpace(datasetPath) || !File.Exists(datasetPath) || exportCount <= 0) - { - return new JObject { ["error"] = "Нет одобренных примеров для тренировки (или укажи hf_dataset)" }; - } - } - else - { - JObject export = await AssistentExportDataset(session, "approved", "jsonl"); - exportCount = export["count"]?.Value() ?? 0; - if (exportCount > 0 && File.Exists(export["path"]?.ToString() ?? "")) - { - datasetPath = export["path"]?.ToString(); - } - } - long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - string jobId = $"tj_{now}"; - string jobDir = Path.Combine(TrainingRoot(), "jobs", jobId); - Directory.CreateDirectory(jobDir); - string configPath = Path.Combine(jobDir, "config.json"); - string logPath = Path.Combine(jobDir, "log.txt"); - string adapterDir = Path.Combine(TrainingRoot(), "adapters", SanitizeAdapterName(outputName)); - Directory.CreateDirectory(adapterDir); - JObject jobConfig = new() - { - ["base_model"] = hfBase, - ["output_name"] = outputName, - ["ollama_base"] = raw["ollama_base"]?.ToString()?.Trim(), - ["gguf_base_path"] = raw["gguf_base_path"]?.ToString()?.Trim() ?? runner["gguf_base_path"]?.ToString()?.Trim(), - ["dataset_path"] = datasetPath, - ["hf_dataset"] = hfDataset, - ["hf_mapping"] = hfMapping, - ["hf_schema"] = hfCheck?["schema"], - ["max_samples"] = raw["max_samples"] ?? 0, - ["rank"] = raw["rank"] ?? 16, - ["alpha"] = raw["alpha"] ?? 32, - ["lr"] = raw["lr"] ?? 0.0002, - ["epochs"] = raw["epochs"] ?? 3, - ["seq_len"] = raw["seq_len"] ?? 2048, - ["four_bit"] = raw["four_bit"] ?? true, - ["batch_size"] = raw["batch_size"] ?? 1, - ["gradient_accumulation_steps"] = raw["gradient_accumulation_steps"] ?? 4, - ["adapter_dir"] = adapterDir, - ["local_export_count"] = exportCount, - }; - await File.WriteAllTextAsync(configPath, jobConfig.ToString(Newtonsoft.Json.Formatting.Indented), Encoding.UTF8); - Memory.SaveTrainJob(new JObject - { - ["id"] = jobId, - ["kind"] = "qlora", - ["status"] = "running", - ["config"] = jobConfig, - ["base_model"] = hfBase, - ["output_name"] = outputName, - ["log_path"] = logPath, - ["created_at"] = now, - }); - string cmdLine = BuildRunnerCommand(runner, configPath, logPath, jobDir); - bool started = TrainingJobManager.Start(this, session, jobId, cmdLine, logPath, baseUrl, chatModel, GetHfToken(session)); - if (!started) - { - Memory.SaveTrainJob(new JObject { ["id"] = jobId, ["status"] = "failed", ["progress"] = new JObject { ["error"] = "process start failed" } }); - return new JObject { ["error"] = "Не удалось запустить процесс тренировки" }; - } - return new JObject { ["success"] = true, ["job_id"] = jobId, ["log_path"] = logPath }; - } - - static string SanitizeAdapterName(string name) - { - if (string.IsNullOrWhiteSpace(name)) - { - return "adapter"; - } - char[] bad = Path.GetInvalidFileNameChars(); - StringBuilder sb = new(); - foreach (char c in name) - { - sb.Append(Array.IndexOf(bad, c) >= 0 ? '_' : c); - } - return sb.ToString().Trim(); - } - - string BuildRunnerCommand(JObject runner, string configPath, string logPath, string workDir) - { - string python = runner["python"]?.ToString()?.Trim(); - if (string.IsNullOrWhiteSpace(python)) - { - python = "python"; - } - string kind = runner["kind"]?.ToString()?.Trim() ?? "builtin"; - string custom = runner["cmd"]?.ToString()?.Trim(); - string scriptPath = Path.Combine(FilePath, "scripts", "train_qlora.py"); - if (string.Equals(kind, "custom", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(custom)) - { - return custom - .Replace("{python}", python, StringComparison.OrdinalIgnoreCase) - .Replace("{config}", configPath, StringComparison.OrdinalIgnoreCase) - .Replace("{log}", logPath, StringComparison.OrdinalIgnoreCase) - .Replace("{workdir}", workDir, StringComparison.OrdinalIgnoreCase); - } - return $"\"{python}\" \"{scriptPath}\" --config \"{configPath}\" --log \"{logPath}\""; - } - - public async Task AssistentCancelTrainJob(Session session, string id = null) - { - await Task.CompletedTask; - TrainingJobManager.Cancel(); - string jobId = id ?? TrainingJobManager.CurrentJobId; - if (!string.IsNullOrWhiteSpace(jobId)) - { - Memory.SaveTrainJob(new JObject - { - ["id"] = jobId, - ["status"] = "cancelled", - ["finished_at"] = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), - }); - } - return new JObject { ["success"] = true, ["cancelled"] = true }; - } - - public async Task AssistentTrainWS(Session session, WebSocket ws, JObject raw) - { - await Task.CompletedTask; - try - { - while (TrainingJobManager.IsRunning && ws.State == System.Net.WebSockets.WebSocketState.Open) - { - JObject progress = TrainingJobManager.GetProgress(); - string msg = progress.ToString(Newtonsoft.Json.Formatting.None); - await ws.SendAsync(Encoding.UTF8.GetBytes(msg), System.Net.WebSockets.WebSocketMessageType.Text, true, CancellationToken.None); - await Task.Delay(800); - } - JObject final = TrainingJobManager.GetProgress(); - final["done"] = true; - await ws.SendAsync(Encoding.UTF8.GetBytes(final.ToString(Newtonsoft.Json.Formatting.None)), System.Net.WebSockets.WebSocketMessageType.Text, true, CancellationToken.None); - } - catch (Exception ex) - { - Logs.Debug($"AssistentTrainWS: {ex.Message}"); - } - return new JObject { ["success"] = true }; - } - - internal async Task FinishTrainJobAsync( - string jobId, - bool success, - string logPath, - Session session, - string baseUrl, - string chatModel, - JObject jobConfig, - JObject runner) - { - long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - string adapterDir = jobConfig?["adapter_dir"]?.ToString() ?? ""; - string outputName = jobConfig?["output_name"]?.ToString() ?? ""; - JObject progress = TrainingJobManager.GetProgress(); - string finalStatus = success ? "completed" : "failed"; - if (success && Directory.Exists(adapterDir)) - { - JObject reg = await RegisterAdapterPipeline(session, baseUrl, outputName, adapterDir, jobConfig, runner); - progress["ollama"] = reg; - if (reg["success"]?.Value() != true && reg["skipped"]?.Value() != true) - { - finalStatus = "completed_with_warnings"; - } - } - progress["status"] = finalStatus; - Memory.SaveTrainJob(new JObject - { - ["id"] = jobId, - ["status"] = finalStatus, - ["finished_at"] = now, - ["progress"] = progress, - }); - if (!string.IsNullOrWhiteSpace(chatModel)) - { - await AssistentWarmLlm(session, baseUrl, chatModel); - } - TrainingJobManager.ClearRunning(); - } - - async Task RegisterAdapterPipeline(Session session, string baseUrl, string outputName, string adapterDir, JObject jobConfig, JObject runner) - { - string safetensors = Directory.GetFiles(adapterDir, "adapter_model.safetensors").FirstOrDefault(); - if (string.IsNullOrWhiteSpace(safetensors)) - { - return new JObject { ["success"] = false, ["error"] = "adapter_model.safetensors not found" }; - } - string ggufPath = Directory.GetFiles(adapterDir, "*.gguf").FirstOrDefault(); - string ggufScript = runner?["gguf_script"]?.ToString()?.Trim(); - string ggufBase = jobConfig?["gguf_base_path"]?.ToString()?.Trim() ?? runner?["gguf_base_path"]?.ToString()?.Trim(); - string python = runner?["python"]?.ToString()?.Trim(); - if (string.IsNullOrWhiteSpace(python)) - { - python = "python"; - } - if (string.IsNullOrWhiteSpace(ggufPath) && !string.IsNullOrWhiteSpace(ggufScript) && File.Exists(ggufScript)) - { - if (string.IsNullOrWhiteSpace(ggufBase) || !File.Exists(ggufBase)) - { - return new JObject - { - ["success"] = false, - ["skipped"] = true, - ["error"] = "gguf_base_path не задан или файл не найден — адаптер сохранён как safetensors", - ["adapter_dir"] = adapterDir, - }; - } - ggufPath = Path.Combine(adapterDir, "adapter.gguf"); - string ggufCmd = runner?["gguf_cmd"]?.ToString()?.Trim(); - if (string.IsNullOrWhiteSpace(ggufCmd)) - { - ggufCmd = "\"{python}\" \"{script}\" \"{base}\" \"{lora}\" \"{out}\""; - } - string cmd = ggufCmd - .Replace("{python}", python, StringComparison.OrdinalIgnoreCase) - .Replace("{script}", ggufScript, StringComparison.OrdinalIgnoreCase) - .Replace("{base}", ggufBase, StringComparison.OrdinalIgnoreCase) - .Replace("{lora}", adapterDir, StringComparison.OrdinalIgnoreCase) - .Replace("{out}", ggufPath, StringComparison.OrdinalIgnoreCase); - int code = await RunShellCommandAsync(cmd, adapterDir); - if (code != 0 || !File.Exists(ggufPath)) - { - return new JObject - { - ["success"] = false, - ["error"] = $"GGUF convert failed exit={code}", - ["adapter_dir"] = adapterDir, - }; - } - } - if (string.IsNullOrWhiteSpace(ggufPath) || !File.Exists(ggufPath)) - { - return new JObject - { - ["success"] = false, - ["skipped"] = true, - ["note"] = "Настрой convert_lora_to_gguf.py и gguf_base_path для регистрации в Ollama", - ["adapter_dir"] = adapterDir, - }; - } - string ollamaBase = jobConfig?["ollama_base"]?.ToString()?.Trim(); - if (string.IsNullOrWhiteSpace(ollamaBase)) - { - return new JObject - { - ["success"] = false, - ["skipped"] = true, - ["error"] = "ollama_base не задан — укажи базовую Ollama-модель на форме QLoRA", - ["adapter_dir"] = adapterDir, - ["gguf"] = ggufPath, - }; - } - return await RegisterAdapterInOllama(baseUrl, outputName, ollamaBase, ggufPath); - } - - static async Task RunShellCommandAsync(string commandLine, string workDir) - { - try - { - ProcessStartInfo psi = new() - { - FileName = "cmd.exe", - Arguments = $"/c {commandLine}", - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true, - WorkingDirectory = workDir ?? Environment.CurrentDirectory, - }; - using Process proc = Process.Start(psi); - if (proc is null) - { - return -1; - } - await proc.WaitForExitAsync(); - return proc.ExitCode; - } - catch (Exception ex) - { - Logs.Debug($"RunShellCommand: {ex.Message}"); - return -1; - } - } - - async Task RegisterAdapterInOllama(string baseUrl, string outputName, string ollamaBase, string adapterGguf) - { - StringBuilder mf = new(); - mf.AppendLine($"FROM {ollamaBase}"); - mf.AppendLine($"ADAPTER {adapterGguf.Replace("\\", "/")}"); - JObject payload = new() - { - ["name"] = outputName, - ["modelfile"] = mf.ToString(), - ["stream"] = false, - }; - using StringContent content = new(payload.ToString(Newtonsoft.Json.Formatting.None), Encoding.UTF8, "application/json"); - using HttpResponseMessage resp = await HttpClient.PostAsync($"{NormalizeBaseUrl(baseUrl)}/api/create", content); - string body = await resp.Content.ReadAsStringAsync(); - if (!resp.IsSuccessStatusCode) - { - return new JObject - { - ["success"] = false, - ["error"] = $"ollama create HTTP {(int)resp.StatusCode}: {Clip(body, 400)}", - ["modelfile"] = mf.ToString(), - }; - } - return new JObject - { - ["success"] = true, - ["name"] = outputName, - ["ollama_base"] = ollamaBase, - ["adapter"] = adapterGguf, - ["response"] = body, - }; - } -} - -sealed class TrainingJobManager -{ - static readonly Regex LossRe = new(@"loss[:\s]+([0-9.]+)", RegexOptions.IgnoreCase | RegexOptions.Compiled); - static readonly Regex StepRe = new(@"step\s+(\d+)\s*/\s*(\d+)", RegexOptions.IgnoreCase | RegexOptions.Compiled); - - Process _process; - readonly object _lock = new(); - JObject _progress = new() { ["status"] = "idle" }; - string _logPath; - SwarmAssistentExtension _ext; - Session _session; - string _jobId; - string _baseUrl; - string _chatModel; - long _lastProgressSaveMs; - int _lastSavedStep = -1; - - public bool IsRunning { get; private set; } - public string CurrentJobId => _jobId; - - public bool Start(SwarmAssistentExtension ext, Session session, string jobId, string commandLine, string logPath, string baseUrl, string chatModel, string hfToken) - { - lock (_lock) - { - if (IsRunning) - { - return false; - } - _ext = ext; - _session = session; - _jobId = jobId; - _logPath = logPath; - _baseUrl = baseUrl; - _chatModel = chatModel; - _lastProgressSaveMs = 0; - _lastSavedStep = -1; - _progress = new JObject { ["status"] = "running", ["step"] = 0, ["loss"] = null, ["log"] = "" }; - try - { - ProcessStartInfo psi = new() - { - FileName = "cmd.exe", - Arguments = $"/c {commandLine}", - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true, - WorkingDirectory = Path.GetDirectoryName(logPath) ?? Environment.CurrentDirectory, - }; - if (!string.IsNullOrWhiteSpace(hfToken)) - { - psi.Environment["HF_TOKEN"] = hfToken; - psi.Environment["HUGGING_FACE_HUB_TOKEN"] = hfToken; - } - _process = new Process { StartInfo = psi, EnableRaisingEvents = true }; - _process.OutputDataReceived += (_, e) => AppendLog(e.Data); - _process.ErrorDataReceived += (_, e) => AppendLog(e.Data); - _process.Exited += async (_, _) => await OnExited(); - _process.Start(); - _process.BeginOutputReadLine(); - _process.BeginErrorReadLine(); - IsRunning = true; - return true; - } - catch (Exception ex) - { - _progress["error"] = ex.Message; - IsRunning = false; - return false; - } - } - } - - void AppendLog(string line) - { - if (string.IsNullOrWhiteSpace(line)) - { - return; - } - lock (_lock) - { - try - { - File.AppendAllText(_logPath, line + Environment.NewLine); - } - catch - { - // ignore - } - string prev = _progress["log"]?.ToString() ?? ""; - string combined = prev + line + "\n"; - if (combined.Length > 12000) - { - combined = combined[^12000..]; - } - _progress["log"] = combined; - Match lossM = LossRe.Match(line); - if (lossM.Success) - { - _progress["loss"] = lossM.Groups[1].Value; - } - Match stepM = StepRe.Match(line); - if (stepM.Success) - { - int step = int.Parse(stepM.Groups[1].Value); - int total = int.Parse(stepM.Groups[2].Value); - _progress["step"] = step; - _progress["total_steps"] = total; - _progress["percent"] = total > 0 ? (int)(100.0 * step / total) : 0; - } - MaybeSaveProgress(stepM.Success ? int.Parse(stepM.Groups[1].Value) : -1); - } - } - - void MaybeSaveProgress(int step) - { - long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - bool stepChanged = step >= 0 && step != _lastSavedStep; - if (!stepChanged && now - _lastProgressSaveMs < 2500) - { - return; - } - _lastProgressSaveMs = now; - if (step >= 0) - { - _lastSavedStep = step; - } - try - { - _ext?.Memory?.SaveTrainJob(new JObject - { - ["id"] = _jobId, - ["status"] = "running", - ["progress"] = _progress, - }); - } - catch - { - // ignore - } - } - - async Task OnExited() - { - bool ok = false; - JObject jobConfig = new(); - lock (_lock) - { - ok = _process?.ExitCode == 0; - IsRunning = false; - _progress["status"] = ok ? "completed" : "failed"; - _progress["exit_code"] = _process?.ExitCode; - } - if (_ext != null) - { - JObject job = _ext.Memory.GetTrainJob(_jobId); - try - { - string cfgRaw = job?["config_json"]?.ToString(); - if (!string.IsNullOrWhiteSpace(cfgRaw)) - { - jobConfig = JObject.Parse(cfgRaw); - } - } - catch - { - // ignore - } - JObject runner = _ext.Config.LoadTrainingRunner(); - await _ext.FinishTrainJobAsync(_jobId, ok, _logPath, _session, _baseUrl, _chatModel, jobConfig, runner); - } - } - - public void Cancel() - { - lock (_lock) - { - try - { - if (_process != null && !_process.HasExited) - { - _process.Kill(entireProcessTree: true); - } - } - catch - { - // ignore - } - IsRunning = false; - _progress["status"] = "cancelled"; - } - } - - public JObject GetProgress() - { - lock (_lock) - { - return (JObject)_progress.DeepClone(); - } - } - - public void ClearRunning() - { - lock (_lock) - { - IsRunning = false; - } - } -} diff --git a/Config/_base/patch-keys.json b/Config/_base/patch-keys.json index 10d2861..cd1b18b 100644 --- a/Config/_base/patch-keys.json +++ b/Config/_base/patch-keys.json @@ -8,6 +8,6 @@ "snapshot_generate", "select_slot", "aspect", "images", "batch", "vary", "lock_seed", "creativity", "intensity", "complexity", "movement", "clear_prompt_images", "slot_to_prompt_image", "pack", "persona", "controls", - "inventory_query", "variants" + "inventory_query", "knowledge_query", "example_query", "knowledge_rating", "example_rating", "variants" ] } diff --git a/Config/_base/skills/knowledge.json b/Config/_base/skills/knowledge.json new file mode 100644 index 0000000..638add5 --- /dev/null +++ b/Config/_base/skills/knowledge.json @@ -0,0 +1,6 @@ +{ + "id": "knowledge", + "title": "Books + knowledge search", + "default": false, + "prompt_file": "knowledge.md" +} diff --git a/Config/_base/skills/knowledge.md b/Config/_base/skills/knowledge.md new file mode 100644 index 0000000..2263283 --- /dev/null +++ b/Config/_base/skills/knowledge.md @@ -0,0 +1,22 @@ +# Knowledge skill + +Attached **books** are read-only FTS corpora (Civitai prompts, RU fic style, …). They are **not** mutable memory. + +## When to search + +- User asks for reference prompts, scene wording, style/tone, or «как на Civitai». +- You need concrete tag/prompt patterns before generating. + +## How to search (server hop) + +```json +{ "ask": ["knowledge"], "knowledge_query": "redhead stockings cinematic window light" } +``` + +Optional: `knowledge_rating` (`pg`, `pg13`, …). Legacy `ask:["examples"]` + `example_query` still works for Civitai-only. + +## Rules + +- **Remix** — never paste long verbatim from books. +- Pure chat / opinions: **prose only**, no JSON. +- Do **not** emit `generate:true` on knowledge Q&A turns. diff --git a/Config/_base/skills/memory.md b/Config/_base/skills/memory.md index 7d944b8..c8865cc 100644 --- a/Config/_base/skills/memory.md +++ b/Config/_base/skills/memory.md @@ -6,7 +6,7 @@ You have five memory tools: 2. **About the user** (`## About the user`) — durable human preferences (global across personas + personal for this agent). Tunable weight in settings. Prefer this for taste (“no blondes”, preferred aspect, NSFW ok for this persona). 3. **Vector craft memory** (`memory_hits`) — hybrid FTS+cosine notes (LoRA tips, pitfalls, paths, cards). Shared + this persona; personal overwrites shared on the same `kind`+`key`. 4. **Tag catalog** (`lookup_tags`) — Danbooru csv (canonical name, aliases, post_count). **Not** RAG. Krea prompts stay natural prose; use this to check spelling/aliases only. -5. **Civitai examples** (`ask: examples` / `lookup_examples`) — popular Krea 2 prompts indexed with FTS (no embeddings). Use when you want a **reference** for how others phrased a scene — remix, do not copy 1:1. +5. **Civitai / knowledge books** (`ask: knowledge` / `ask: examples`) — FTS over attached books (gpu-rent seeded) and legacy Civitai examples. Remix, do not copy 1:1. ## Priority diff --git a/Config/_base/training-qlora.json b/Config/_base/training-qlora.json deleted file mode 100644 index bd8f4e8..0000000 --- a/Config/_base/training-qlora.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "hf_models": [ - { - "id": "qwen2.5-7b-instruct", - "title": "Qwen2.5 7B Instruct", - "hf_id": "Qwen/Qwen2.5-7B-Instruct", - "ollama_hint": "qwen2.5:7b-instruct", - "default_output": "assistent-qwen25-7b:v1", - "rank": 16, - "seq_len": 2048 - }, - { - "id": "qwen2.5-3b-instruct", - "title": "Qwen2.5 3B Instruct", - "hf_id": "Qwen/Qwen2.5-3B-Instruct", - "ollama_hint": "qwen2.5:3b-instruct", - "default_output": "assistent-qwen25-3b:v1", - "rank": 16, - "seq_len": 2048 - }, - { - "id": "llama-3.2-3b-instruct", - "title": "Llama 3.2 3B Instruct", - "hf_id": "meta-llama/Llama-3.2-3B-Instruct", - "ollama_hint": "llama3.2:3b-instruct", - "default_output": "assistent-llama32-3b:v1", - "rank": 16, - "seq_len": 2048 - }, - { - "id": "llama-3.1-8b-instruct", - "title": "Llama 3.1 8B Instruct", - "hf_id": "meta-llama/Llama-3.1-8B-Instruct", - "ollama_hint": "llama3.1:8b-instruct", - "default_output": "assistent-llama31-8b:v1", - "rank": 16, - "seq_len": 2048 - }, - { - "id": "phi-3-mini-instruct", - "title": "Phi-3 Mini 4K Instruct", - "hf_id": "microsoft/Phi-3-mini-4k-instruct", - "ollama_hint": "phi3:mini", - "default_output": "assistent-phi3-mini:v1", - "rank": 16, - "seq_len": 2048 - } - ] -} diff --git a/SwarmAssistentExtension.cs b/SwarmAssistentExtension.cs index a837959..3be7d58 100644 --- a/SwarmAssistentExtension.cs +++ b/SwarmAssistentExtension.cs @@ -33,8 +33,8 @@ 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.15.17"; - Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory", "training", "heard", "qlora"]; + Version = "0.16.0"; + Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory", "knowledge", "heard", "books"]; } public override void OnInit() @@ -64,6 +64,10 @@ public partial class SwarmAssistentExtension : Extension API.RegisterAPICall(AssistentGetMemory, false, PermUse); API.RegisterAPICall(AssistentLookupTags, false, PermUse); API.RegisterAPICall(AssistentLookupExamples, false, PermUse); + API.RegisterAPICall(AssistentListKnowledgeCatalog, false, PermUse); + API.RegisterAPICall(AssistentSearchKnowledge, false, PermUse); + API.RegisterAPICall(AssistentGetKnowledgeAttach, false, PermUse); + API.RegisterAPICall(AssistentSaveKnowledgeAttach, true, PermUse); API.RegisterAPICall(AssistentSaveControls, true, PermUse); API.RegisterAPICall(AssistentGetPersonaShelves, false, PermUse); API.RegisterAPICall(AssistentClonePersona, true, PermUse); @@ -77,29 +81,7 @@ public partial class SwarmAssistentExtension : Extension API.RegisterAPICall(AssistentForgetUserPref, true, PermUse); API.RegisterAPICall(AssistentClearUserPrefs, true, PermUse); API.RegisterAPICall(AssistentClearMemory, true, PermUse); - API.RegisterAPICall(AssistentListTrainSamples, false, PermUse); - API.RegisterAPICall(AssistentUpsertTrainSample, true, PermUse); - API.RegisterAPICall(AssistentDeleteTrainSample, true, PermUse); - API.RegisterAPICall(AssistentBuildDatasetFromChats, false, PermUse); - API.RegisterAPICall(AssistentImportDataset, true, PermUse); - API.RegisterAPICall(AssistentExportDataset, false, PermUse); - API.RegisterAPICall(AssistentCreateOllamaModel, true, PermUse); - API.RegisterAPICall(AssistentSearchHfDatasets, false, PermUse); - API.RegisterAPICall(AssistentCheckHfDataset, false, PermUse); - API.RegisterAPICall(AssistentPreviewHfDataset, false, PermUse); - API.RegisterAPICall(AssistentImportHfDataset, true, PermUse); - API.RegisterAPICall(AssistentStartTrainJob, true, PermUse); - API.RegisterAPICall(AssistentCancelTrainJob, true, PermUse); - API.RegisterAPICall(AssistentGetTrainJob, false, PermUse); - API.RegisterAPICall(AssistentTrainWS, true, PermUse); - API.RegisterAPICall(AssistentSaveRunnerSettings, true, PermUse); - API.RegisterAPICall(AssistentGetRunnerSettings, false, PermUse); - API.RegisterAPICall(AssistentGetDatasetAgentSettings, false, PermUse); - API.RegisterAPICall(AssistentSaveDatasetAgentSettings, true, PermUse); - API.RegisterAPICall(AssistentLinkTrainSampleToAgent, true, PermUse); - API.RegisterAPICall(AssistentUnlinkTrainSampleFromAgent, true, PermUse); - API.RegisterAPICall(AssistentSyncDatasetToAgent, true, PermUse); - Logs.Init("Swarm Assistent extension loaded (0.15.0 persona packs / dreamer)"); + Logs.Init("Swarm Assistent extension loaded (0.16.0 knowledge books hub)"); } int CfgInt(string key, int fallback) diff --git a/Tabs/Text2Image/Assistent.html b/Tabs/Text2Image/Assistent.html index 1abd53f..4e8c27d 100644 --- a/Tabs/Text2Image/Assistent.html +++ b/Tabs/Text2Image/Assistent.html @@ -10,13 +10,8 @@
-
-
@@ -161,140 +156,6 @@
- -