Ship Assistent 0.13.0: real QLoRA pipeline and GGUF Ollama register.

Replace fake train loop with TRL SFTTrainer, HF column mapping with fiction preset,
safetensors to GGUF conversion, and ollama create using ollama_base plus ADAPTER.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-22 14:45:07 +03:00
co-authored by Cursor
parent 1a03c3178f
commit 474a674a35
12 changed files with 1002 additions and 133 deletions
+116 -7
View File
@@ -9168,6 +9168,7 @@ ${HELP_TEXT}`);
hfResults: [],
hfSelected: null,
hfCheck: null,
hfMapping: null,
trainWs: null,
polling: null,
agentSettings: { enabled: true, auto_link_on_approve: true, heard_quota: 3 },
@@ -9242,7 +9243,10 @@ ${HELP_TEXT}`);
refreshSamples();
loadAgentHeardSettings();
}
if (state.ttab === "train") syncModelfileModels();
if (state.ttab === "train") {
syncModelfileModels();
syncQloraModels();
}
if (state.ttab === "models") refreshTrainModels();
}
async function refreshSamples() {
@@ -9303,6 +9307,78 @@ ${HELP_TEXT}`);
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) => `<option value="${escapeHtml(c)}">${escapeHtml(c)}</option>`).join("");
if (cols.includes("tags")) userSel.value = "tags";
else if (cols.includes("title")) userSel.value = "title";
}
if (asstSel) {
asstSel.innerHTML = cols.map((c) => `<option value="${escapeHtml(c)}">${escapeHtml(c)}</option>`).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;
}
async function syncQloraModels() {
try {
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 = '<option value="">\u2014</option>';
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;
} catch (e) {
}
}
function renderHfList() {
const root = $("sa_hf_list");
if (!root) return;
@@ -9353,6 +9429,7 @@ ${HELP_TEXT}`);
}
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);
}
@@ -9364,8 +9441,9 @@ ${HELP_TEXT}`);
}
const id = state.hfSelected || state.hfCheck.id;
const limit = Number($("sa_hf_import_limit")?.value) || 200;
const mapping = buildHfMappingPayload();
try {
const data = await SA2.request("AssistentImportHfDataset", { dataset: id, limit });
const data = await SA2.request("AssistentImportHfDataset", { dataset: id, limit, mapping });
setTrainStatus(`\u0418\u043C\u043F\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u043E: ${data.imported}${data.runner_only ? " (runner-only)" : ""}`);
await refreshSamples();
} catch (e) {
@@ -9435,8 +9513,9 @@ ${HELP_TEXT}`);
async function pollTrainJob() {
try {
const data = await SA2.request("AssistentGetTrainJob", {});
const prog = data?.job?.progress_json ? JSON.parse(data.job.progress_json) : null;
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;
setTrainingLock(active, prog?.status === "running" ? `\u0422\u0440\u0435\u043D\u0438\u0440\u043E\u0432\u043A\u0430 \xB7 ${prog?.percent ?? 0}%` : "\u0418\u0434\u0451\u0442 \u0442\u0440\u0435\u043D\u0438\u0440\u043E\u0432\u043A\u0430\u2026");
const logEl = $("sa_train_log");
const bar = $("sa_train_progress_fill");
@@ -9450,6 +9529,24 @@ ${HELP_TEXT}`);
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`);
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, Ollama \u2014 \u0432\u0440\u0443\u0447\u043D\u0443\u044E");
} else if (ollama?.error) {
setTrainStatus(`\u041E\u0431\u0443\u0447\u0435\u043D\u0438\u0435 OK, Ollama: ${ollama.error}`);
} else if (status === "completed_with_warnings") {
setTrainStatus("\u041E\u0431\u0443\u0447\u0435\u043D\u0438\u0435 \u0437\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 \u0441\u043C. \u043B\u043E\u0433");
} else {
setTrainStatus("QLoRA \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u043E");
}
} 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) {
}
@@ -9457,18 +9554,23 @@ ${HELP_TEXT}`);
async function startQlora() {
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: $("sa_qlora_base")?.value,
ollama_base: $("sa_qlora_ollama_base")?.value,
output_name: $("sa_qlora_name")?.value,
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: ($("sa_qlora_hf_dataset")?.value || "").trim() || void 0
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");
@@ -9504,10 +9606,12 @@ ${HELP_TEXT}`);
try {
await SA2.request("AssistentSaveRunnerSettings", {
python: $("sa_runner_python")?.value,
kind: $("sa_runner_kind")?.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_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) {
@@ -9519,10 +9623,12 @@ ${HELP_TEXT}`);
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") && s.kind) $("sa_runner_kind").value = s.kind;
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) {
}
}
@@ -9629,6 +9735,9 @@ ${HELP_TEXT}`);
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));
+15
View File
@@ -2281,6 +2281,21 @@
width: 2.75rem;
}
.sa-hf-mapping-row {
display: flex;
flex-wrap: wrap;
gap: 0.45rem 0.75rem;
align-items: center;
padding: 0.35rem 0;
}
.sa-hf-mapping-row label {
display: inline-flex;
align-items: center;
gap: 0.3rem;
font-size: 0.82rem;
}
.sa-hf-panel {
border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
border-radius: 0.45rem;