From 63538fe738d897502f9edfd8c07987b4f01821dc Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Sun, 23 Aug 2026 20:11:18 +0300 Subject: [PATCH] Fix HF dataset import internal error and training SQLite readiness. Wrap AssistentImportHfDataset in try/catch, batch inserts in a transaction, clearer HF rows errors for gated datasets, EnsureTrainingReady with actionable message, and friendlier training UI errors instead of generic internal error. Co-authored-by: Cursor --- Assets/assistent.bundle.js | 37 ++++--- AssistentHuggingFace.cs | 193 ++++++++++++++++++++++-------------- AssistentMemory.Heard.cs | 11 +- AssistentMemory.Training.cs | 120 +++++++++++++--------- AssistentMemory.cs | 29 +++++- AssistentTraining.Agent.cs | 21 ++-- SwarmAssistentExtension.cs | 2 +- scripts/_forbid014.json | 17 ++++ src/training.js | 48 ++++++--- 9 files changed, 323 insertions(+), 155 deletions(-) create mode 100644 scripts/_forbid014.json diff --git a/Assets/assistent.bundle.js b/Assets/assistent.bundle.js index 0690e2f..f05dde8 100644 --- a/Assets/assistent.bundle.js +++ b/Assets/assistent.bundle.js @@ -10708,7 +10708,7 @@ ${HELP_TEXT}`); setAgentHeardStats(state.agentLinked); } catch (e) { const msg = String(e.message || e); - setTrainStatus(isSqliteError(msg) ? `${msg} \u2014 ${sqliteHint()}` : msg); + setTrainStatus(formatTrainError(msg)); console.warn("loadAgentHeardSettings", e); } } @@ -10723,7 +10723,7 @@ ${HELP_TEXT}`); 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(String(e.message || e)); + setTrainStatus(formatTrainError(e.message || e)); } } async function syncAllToAgent() { @@ -10736,7 +10736,7 @@ ${HELP_TEXT}`); 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(String(e.message || e)); + setTrainStatus(formatTrainError(e.message || e)); } } function setTrainStatus(msg) { @@ -10745,7 +10745,20 @@ ${HELP_TEXT}`); } function isSqliteError(msg) { const s = String(msg || "").toLowerCase(); - return s.includes("sqlite") || s.includes("sqlconnection"); + 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)."; @@ -10816,7 +10829,7 @@ ${HELP_TEXT}`); const data = await SA2.request("AssistentListTrainSamples", { status, persona, limit: 300 }); if (data?.error) { const msg = data.error; - setTrainStatus(isSqliteError(msg) ? `${msg} \u2014 ${sqliteHint()}` : msg); + 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 = []; @@ -10845,7 +10858,7 @@ ${HELP_TEXT}`); } renderSamples(); } catch (e) { - setTrainStatus(String(e.message || e)); + setTrainStatus(formatTrainError(e.message || e)); } } function renderSamples() { @@ -11073,7 +11086,7 @@ ${HELP_TEXT}`); renderHfList(); setTrainStatus(`\u041D\u0430\u0439\u0434\u0435\u043D\u043E: ${state.hfResults.length}`); } catch (e) { - setTrainStatus(String(e.message || e)); + setTrainStatus(formatTrainError(e.message || e)); } } async function checkHfLink() { @@ -11148,7 +11161,7 @@ ${HELP_TEXT}`); $("sa_train_samples")?.scrollIntoView({ behavior: "smooth", block: "nearest" }); } catch (e) { const err = String(e.message || e); - const show = isSqliteError(err) ? `${err} \u2014 ${sqliteHint()}` : err; + const show = formatTrainError(err); setTrainStatus(show); if (hfSt) hfSt.textContent = show; } finally { @@ -11203,7 +11216,7 @@ ${HELP_TEXT}`); setTrainStatus(`\u0413\u043E\u0442\u043E\u0432\u043E: ${data.name}`); SA2.app?.refreshModels?.(); } catch (e) { - setTrainStatus(String(e.message || e)); + setTrainStatus(formatTrainError(e.message || e)); } finally { btn?.removeAttribute("disabled"); } @@ -11328,7 +11341,7 @@ ${HELP_TEXT}`); 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(String(e.message || e)); + setTrainStatus(formatTrainError(e.message || e)); } } async function cancelQlora() { @@ -11337,7 +11350,7 @@ ${HELP_TEXT}`); setTrainingLock(false); setTrainStatus("\u041E\u0442\u043C\u0435\u043D\u0435\u043D\u043E"); } catch (e) { - setTrainStatus(String(e.message || e)); + setTrainStatus(formatTrainError(e.message || e)); } } async function refreshTrainModels() { @@ -11371,7 +11384,7 @@ ${HELP_TEXT}`); }); setTrainStatus("\u0420\u0430\u043D\u043D\u0435\u0440 \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D"); } catch (e) { - setTrainStatus(String(e.message || e)); + setTrainStatus(formatTrainError(e.message || e)); } } async function loadRunner() { diff --git a/AssistentHuggingFace.cs b/AssistentHuggingFace.cs index ce98be9..24aff97 100644 --- a/AssistentHuggingFace.cs +++ b/AssistentHuggingFace.cs @@ -426,94 +426,143 @@ public partial class SwarmAssistentExtension public async Task AssistentImportHfDataset(Session session, JObject raw) { - 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) + try { - 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) + 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) { - break; + return new JObject { ["error"] = "invalid dataset id" }; } - JObject parsed = JObject.Parse(rowsBody); - JArray batch = parsed["rows"] as JArray ?? []; - if (batch.Count == 0) + JObject check = await CheckHfDatasetInternal(session, id, useCache: true); + string gate = check["gate"]?.ToString(); + if (gate == "rejected") { - break; + return new JObject { ["error"] = check["reason"]?.ToString() ?? "rejected" }; } - foreach (JToken t in batch) + if (MappingRequired(check, mapping)) { - rows.Add(t); + return new JObject { ["error"] = "Нужен маппинг колонок", ["check"] = check }; } - offset += batch.Count; - if (batch.Count < chunk) + 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) { - break; + 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 ?? []; - } - int imported = 0; - foreach (JToken rowTok in rows.Take(take)) - { - if (rowTok is not JObject row) + if (rows.Count == 0) { - continue; + rows = check["sample_rows"] as JArray ?? []; } - JObject rowData = row["row"] as JObject ?? row; - JArray messages = ConvertHfRowToMessages(rowData, check["schema"] as JObject, mapping); - if (messages is null || messages.Count == 0) + List toSave = []; + foreach (JToken rowTok in rows.Take(take)) { - continue; + 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", + }); } - Memory.UpsertTrainSample(new JObject + int imported = toSave.Count > 0 ? Memory.ImportTrainSamplesBatch(toSave) : 0; + if (imported == 0 && check["runner_only"]?.Value() == true) { - ["source"] = "hf", - ["hf_repo"] = id, - ["messages"] = messages, + 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", - }); - imported++; + }; } - if (imported == 0 && check["runner_only"]?.Value() == true) + catch (Exception ex) { - return new JObject { ["success"] = true, ["imported"] = 0, ["runner_only"] = true, ["id"] = id, ["note"] = "Большой набор — используй HF id в QLoRA-раннере" }; + Logs.Error($"AssistentImportHfDataset: {ex}"); + return new JObject { ["error"] = ex.Message }; } - return new JObject + } + + static string HfCellString(JToken tok) + { + if (tok is null || tok.Type == JTokenType.Null) { - ["success"] = true, - ["imported"] = imported, - ["id"] = id, - ["rows_fetched"] = rows.Count, - ["status"] = "draft", - }; + 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) @@ -570,14 +619,14 @@ public partial class SwarmAssistentExtension } if (kind == "fiction_tags_text") { - string text = row["text"]?.ToString() ?? ""; + string text = HfCellString(row["text"]); if (string.IsNullOrWhiteSpace(text)) { return null; } List userParts = []; - string title = row["title"]?.ToString()?.Trim(); - string tags = row["tags"]?.ToString()?.Trim(); + string title = HfCellString(row["title"]); + string tags = HfCellString(row["tags"]); if (!string.IsNullOrWhiteSpace(title)) { userParts.Add($"Title: {title}"); diff --git a/AssistentMemory.Heard.cs b/AssistentMemory.Heard.cs index bd462cf..ffcdd03 100644 --- a/AssistentMemory.Heard.cs +++ b/AssistentMemory.Heard.cs @@ -50,7 +50,7 @@ public sealed partial class AssistentMemory } lock (_lock) { - EnsureOpen(); + EnsureTrainingReady(); if (!HasColumn("train_samples", "agent_linked")) { return; @@ -68,7 +68,14 @@ public sealed partial class AssistentMemory { lock (_lock) { - EnsureOpen(); + try + { + EnsureTrainingReady(); + } + catch + { + return 0; + } if (!HasColumn("train_samples", "agent_linked")) { return 0; diff --git a/AssistentMemory.Training.cs b/AssistentMemory.Training.cs index c34f7fb..868413f 100644 --- a/AssistentMemory.Training.cs +++ b/AssistentMemory.Training.cs @@ -68,7 +68,7 @@ public sealed partial class AssistentMemory { lock (_lock) { - EnsureOpen(); + EnsureTrainingReady(); int take = Math.Clamp(limit, 1, 2000); List where = []; if (!string.IsNullOrWhiteSpace(status) && !string.Equals(status, "all", StringComparison.OrdinalIgnoreCase)) @@ -167,57 +167,87 @@ public sealed partial class AssistentMemory } } + 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) { - EnsureOpen(); - 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(); - 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 }; + 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) { - EnsureOpen(); + EnsureTrainingReady(); using SqliteCommand cmd = _conn.CreateCommand(); cmd.CommandText = "DELETE FROM train_samples WHERE id = $id"; cmd.Parameters.AddWithValue("$id", id ?? ""); @@ -229,7 +259,7 @@ public sealed partial class AssistentMemory { lock (_lock) { - EnsureOpen(); + EnsureTrainingReady(); using SqliteCommand cmd = _conn.CreateCommand(); if (string.IsNullOrWhiteSpace(status) || string.Equals(status, "all", StringComparison.OrdinalIgnoreCase)) { @@ -288,7 +318,7 @@ public sealed partial class AssistentMemory { lock (_lock) { - EnsureOpen(); + 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 ?? ""); @@ -305,7 +335,7 @@ public sealed partial class AssistentMemory { lock (_lock) { - EnsureOpen(); + 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 " @@ -341,7 +371,7 @@ public sealed partial class AssistentMemory { lock (_lock) { - EnsureOpen(); + 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(); diff --git a/AssistentMemory.cs b/AssistentMemory.cs index 315db90..0142e7f 100644 --- a/AssistentMemory.cs +++ b/AssistentMemory.cs @@ -149,7 +149,7 @@ public sealed partial class AssistentMemory : IDisposable } catch (Exception ex) { - Logs.Debug($"AssistentMemory training schema: {ex.Message}"); + Logs.Error($"AssistentMemory training schema failed: {ex.Message}"); } try { @@ -166,6 +166,10 @@ public sealed partial class AssistentMemory : IDisposable bool HasColumn(string table, string column) { + if (!HasTable(table)) + { + return false; + } using SqliteCommand cmd = _conn.CreateCommand(); cmd.CommandText = $"PRAGMA table_info({table})"; using SqliteDataReader reader = cmd.ExecuteReader(); @@ -179,6 +183,29 @@ public sealed partial class AssistentMemory : IDisposable return false; } + bool HasTable(string table) + { + using SqliteCommand cmd = _conn.CreateCommand(); + cmd.CommandText = "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = $n LIMIT 1"; + cmd.Parameters.AddWithValue("$n", table ?? ""); + return cmd.ExecuteScalar() != null; + } + + internal void EnsureTrainingReady() + { + 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() { if (HasColumn("memories", "persona")) diff --git a/AssistentTraining.Agent.cs b/AssistentTraining.Agent.cs index 8ffec7e..3506a03 100644 --- a/AssistentTraining.Agent.cs +++ b/AssistentTraining.Agent.cs @@ -25,14 +25,21 @@ public partial class SwarmAssistentExtension public async Task AssistentGetDatasetAgentSettings(Session session) { await Task.CompletedTask; - JObject settings = Config.LoadTrainingAgent(); - return new JObject + try { - ["success"] = true, - ["settings"] = settings, - ["linked"] = Memory.CountAgentLinkedTrainSamples(), - ["approved"] = Memory.CountTrainSamples("approved"), - }; + 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) diff --git a/SwarmAssistentExtension.cs b/SwarmAssistentExtension.cs index 094e58f..73c04d0 100644 --- a/SwarmAssistentExtension.cs +++ b/SwarmAssistentExtension.cs @@ -33,7 +33,7 @@ public partial class SwarmAssistentExtension : Extension ExtensionAuthor = "mrleo1nid"; Description = "Collaborative Krea 2 assistant: Ollama chat, persona presets, vector memory, model cards, Generate loop."; License = "MIT"; - Version = "0.15.11"; + Version = "0.15.12"; Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory", "training", "heard", "qlora"]; } diff --git a/scripts/_forbid014.json b/scripts/_forbid014.json new file mode 100644 index 0000000..9040bc5 --- /dev/null +++ b/scripts/_forbid014.json @@ -0,0 +1,17 @@ +[ + "catalog_card", + "fromCards", + "AssistentGetCard", + "AssistentSaveCard", + "AssistentSearchCivitai", + "AssistentListWanted", + "AssistentEnqueueWanted", + "buildCivitaiCards", + "maybeWriteCardAfterDownload", + "sa_tab_cards", + "modelCards", + "extractCardJson", + "userImpliesGenerate", + "fromEmptyPatchRetry", + "synthesizePatchAfterEmptyFence" +] \ No newline at end of file diff --git a/src/training.js b/src/training.js index 404afdc..2772bf4 100644 --- a/src/training.js +++ b/src/training.js @@ -50,7 +50,7 @@ export function attachTraining(SA) { setAgentHeardStats(state.agentLinked); } catch (e) { const msg = String(e.message || e); - setTrainStatus(isSqliteError(msg) ? `${msg} — ${sqliteHint()}` : msg); + setTrainStatus(formatTrainError(msg)); console.warn('loadAgentHeardSettings', e); } } @@ -66,7 +66,7 @@ export function attachTraining(SA) { state.agentSettings = data?.settings || settings; setTrainStatus('Настройки «услышанного» сохранены'); } catch (e) { - setTrainStatus(String(e.message || e)); + setTrainStatus(formatTrainError(e.message || e)); } } @@ -80,7 +80,7 @@ export function attachTraining(SA) { setTrainStatus(`Подключено: +${data?.linked_now ?? 0}, всего ${data?.total_linked ?? '—'}`); await refreshSamples(); } catch (e) { - setTrainStatus(String(e.message || e)); + setTrainStatus(formatTrainError(e.message || e)); } } @@ -91,7 +91,25 @@ export function attachTraining(SA) { function isSqliteError(msg) { const s = String(msg || '').toLowerCase(); - return s.includes('sqlite') || s.includes('sqlconnection'); + 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} — ${sqliteHint()}`; + } + if (isGenericServerError(s)) { + return `${s} — часто SQLite/импорт HF: gpu-rent seed-extensions, restart SwarmUI, HF token для gated.`; + } + return s; } function sqliteHint() { @@ -167,7 +185,7 @@ export function attachTraining(SA) { const data = await SA.request('AssistentListTrainSamples', { status, persona, limit: 300 }); if (data?.error) { const msg = data.error; - setTrainStatus(isSqliteError(msg) ? `${msg} — ${sqliteHint()}` : msg); + setTrainStatus(formatTrainError(msg)); const stats = $('sa_train_stats'); if (stats) stats.textContent = 'Датасет недоступен (SQLite)'; state.samples = []; @@ -196,7 +214,7 @@ export function attachTraining(SA) { } renderSamples(); } catch (e) { - setTrainStatus(String(e.message || e)); + setTrainStatus(formatTrainError(e.message || e)); } } @@ -441,7 +459,7 @@ export function attachTraining(SA) { renderHfList(); setTrainStatus(`Найдено: ${state.hfResults.length}`); } catch (e) { - setTrainStatus(String(e.message || e)); + setTrainStatus(formatTrainError(e.message || e)); } } @@ -522,7 +540,7 @@ export function attachTraining(SA) { $('sa_train_samples')?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } catch (e) { const err = String(e.message || e); - const show = isSqliteError(err) ? `${err} — ${sqliteHint()}` : err; + const show = formatTrainError(err); setTrainStatus(show); if (hfSt) hfSt.textContent = show; } finally { @@ -578,7 +596,7 @@ export function attachTraining(SA) { setTrainStatus(`Готово: ${data.name}`); SA.app?.refreshModels?.(); } catch (e) { - setTrainStatus(String(e.message || e)); + setTrainStatus(formatTrainError(e.message || e)); } finally { btn?.removeAttribute('disabled'); } @@ -712,7 +730,7 @@ export function attachTraining(SA) { setTrainingTab('train'); setTrainStatus('QLoRA запущена — прогресс ниже'); } catch (e) { - setTrainStatus(String(e.message || e)); + setTrainStatus(formatTrainError(e.message || e)); } } @@ -722,7 +740,7 @@ export function attachTraining(SA) { setTrainingLock(false); setTrainStatus('Отменено'); } catch (e) { - setTrainStatus(String(e.message || e)); + setTrainStatus(formatTrainError(e.message || e)); } } @@ -760,7 +778,7 @@ export function attachTraining(SA) { }); setTrainStatus('Раннер сохранён'); } catch (e) { - setTrainStatus(String(e.message || e)); + setTrainStatus(formatTrainError(e.message || e)); } } @@ -805,7 +823,7 @@ export function attachTraining(SA) { const data = await SA.request('AssistentImportDataset', { format: 'auto', content: text }); setTrainStatus(`Импорт: ${data.imported}`); await refreshSamples(); - } catch (err) { setTrainStatus(String(err.message || err)); } + } catch (err) { setTrainStatus(formatTrainError(err.message || err)); } e.target.value = ''; }); $('sa_btn_train_export')?.addEventListener('click', async () => { @@ -842,7 +860,7 @@ export function attachTraining(SA) { setAgentHeardStats(state.agentLinked); setTrainStatus('Пример подключён к агенту'); await refreshSamples(); - } catch (err) { setTrainStatus(String(err.message || err)); } + } catch (err) { setTrainStatus(formatTrainError(err.message || err)); } } else if (e.target.closest('[data-unlink]')) { try { const data = await SA.request('AssistentUnlinkTrainSampleFromAgent', { id }); @@ -850,7 +868,7 @@ export function attachTraining(SA) { setAgentHeardStats(state.agentLinked); setTrainStatus('Пример отключён от агента'); await refreshSamples(); - } catch (err) { setTrainStatus(String(err.message || err)); } + } catch (err) { setTrainStatus(formatTrainError(err.message || err)); } } else if (e.target.closest('[data-del]')) { if (window.confirm('Удалить пример?')) { await SA.request('AssistentDeleteTrainSample', { id });