@barivia/barsom-mcp 0.20.4 → 0.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -3
- package/dist/audit.js +4 -1
- package/dist/index.js +1 -1
- package/dist/inference_prepare.js +2 -2
- package/dist/job_monitor.js +38 -2
- package/dist/prepare_training_prompt.js +10 -2
- package/dist/shared.js +147 -21
- package/dist/tools/datasets.js +14 -11
- package/dist/tools/guide_barsom.js +1 -1
- package/dist/tools/inference.js +10 -12
- package/dist/tools/results.js +52 -61
- package/dist/tools/train.js +68 -53
- package/dist/tools/training_core.js +29 -8
- package/dist/tools/training_guidance.js +1 -1
- package/dist/train_finalize.js +2 -2
- package/dist/training_monitor_curve.js +1 -1
- package/dist/views/src/views/training-monitor/index.html +28 -28
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@ const OFFLINE_STUB = `## Offline / API unavailable
|
|
|
5
5
|
|
|
6
6
|
Configure \`BARIVIA_API_KEY\` and optional \`BARIVIA_API_URL\`, then call **guide_barsom_workflow** again. Full tool map, async rules, training modes, and step-by-step SOP are loaded from the Barivia API (authenticated) and scoped to your plan.
|
|
7
7
|
|
|
8
|
-
**Core tools:** \`datasets\`, \`
|
|
8
|
+
**Core tools:** \`datasets\`, \`train\` (map, siom_map, impute, floop_siom where entitled), \`jobs\` (status/list/compare — lifecycle only), \`results\`, \`inference\`, \`account\`, \`training_guidance\`, \`guide_barsom_workflow\`.
|
|
9
9
|
|
|
10
10
|
**Parameter hints:** call \`training_guidance\` (also API-scoped). **Async:** poll \`jobs(action=status)\` every 10–15s after submit.`;
|
|
11
11
|
export function registerGuideBarsomTool(server) {
|
package/dist/tools/inference.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { registerAuditedTool } from "../audit.js";
|
|
3
|
-
import { apiCall, pollUntilComplete, tryAttachImage } from "../shared.js";
|
|
3
|
+
import { apiCall, pollUntilComplete, tryAttachImage, POLL_INFERENCE_MAX_MS, POLL_PROJECT_MAX_MS, resolveOutputDpi, fmtDensityDiffNode } from "../shared.js";
|
|
4
4
|
import { pollPrepareIfPresent } from "../inference_prepare.js";
|
|
5
5
|
const PREDICT_PREVIEW_ROW_CAP = 10;
|
|
6
6
|
/** One line per scored row when worker embedded `predictions_preview` (n_rows ≤ cap). Exported for tests. */
|
|
@@ -123,8 +123,7 @@ action=report: Returns a report manifest for the given job_id (job must be compl
|
|
|
123
123
|
top_k: z.number().int().min(1).optional().default(10)
|
|
124
124
|
.describe("action=transition_flow: number of top-flow nodes in the statistics summary (default 10)."),
|
|
125
125
|
}, async ({ action, job_id, dataset_id, columns, rows, output, colormap, output_format, output_dpi, top_n, target_column, only_missing, impute_aggregation, cv_folds, target_column_kind, weighting, inputs, lag, min_transitions, top_k }) => {
|
|
126
|
-
const
|
|
127
|
-
const numericDpi = dpiMap[output_dpi ?? "retina"] ?? 2;
|
|
126
|
+
const numericDpi = resolveOutputDpi(output_dpi);
|
|
128
127
|
if (action === "batch_predict")
|
|
129
128
|
return runBatchPredict({ job_id, inputs });
|
|
130
129
|
if (action === "transition_flow") {
|
|
@@ -136,10 +135,10 @@ action=report: Returns a report manifest for the given job_id (job must be compl
|
|
|
136
135
|
if (colormap !== undefined)
|
|
137
136
|
body.colormap = colormap;
|
|
138
137
|
if (output_dpi && output_dpi !== "retina")
|
|
139
|
-
body.output_dpi =
|
|
138
|
+
body.output_dpi = resolveOutputDpi(output_dpi);
|
|
140
139
|
const data = (await apiCall("POST", `/v1/results/${job_id}/transition-flow`, body));
|
|
141
140
|
const flowJobId = data.id;
|
|
142
|
-
const poll = await pollUntilComplete(flowJobId,
|
|
141
|
+
const poll = await pollUntilComplete(flowJobId, POLL_INFERENCE_MAX_MS);
|
|
143
142
|
if (poll.status === "completed") {
|
|
144
143
|
const res = (await apiCall("GET", `/v1/results/${flowJobId}`));
|
|
145
144
|
const summary = (res.summary ?? {});
|
|
@@ -195,7 +194,7 @@ action=report: Returns a report manifest for the given job_id (job must be compl
|
|
|
195
194
|
const predictJobId = data.id;
|
|
196
195
|
const prepareId = await pollPrepareIfPresent(data, "inference(predict)");
|
|
197
196
|
// annotated runs over the full dataset; allow up to 120s like compact
|
|
198
|
-
const poll = await pollUntilComplete(predictJobId,
|
|
197
|
+
const poll = await pollUntilComplete(predictJobId, POLL_INFERENCE_MAX_MS);
|
|
199
198
|
if (poll.status === "completed") {
|
|
200
199
|
const results = (await apiCall("GET", `/v1/results/${predictJobId}`));
|
|
201
200
|
const summary = (results.summary ?? {});
|
|
@@ -254,7 +253,7 @@ action=report: Returns a report manifest for the given job_id (job must be compl
|
|
|
254
253
|
body.weighting = weighting;
|
|
255
254
|
const data = (await apiCall("POST", `/v1/results/${job_id}/impute_column`, body));
|
|
256
255
|
const imputeJobId = data.id;
|
|
257
|
-
const poll = await pollUntilComplete(imputeJobId,
|
|
256
|
+
const poll = await pollUntilComplete(imputeJobId, POLL_INFERENCE_MAX_MS);
|
|
258
257
|
if (poll.status === "completed") {
|
|
259
258
|
const results = (await apiCall("GET", `/v1/results/${imputeJobId}`));
|
|
260
259
|
const summary = (results.summary ?? {});
|
|
@@ -281,19 +280,18 @@ action=report: Returns a report manifest for the given job_id (job must be compl
|
|
|
281
280
|
body.colormap = colormap;
|
|
282
281
|
const data = (await apiCall("POST", `/v1/results/${job_id}/compare_datasets`, body));
|
|
283
282
|
const compareJobId = data.id;
|
|
284
|
-
const poll = await pollUntilComplete(compareJobId,
|
|
283
|
+
const poll = await pollUntilComplete(compareJobId, POLL_INFERENCE_MAX_MS);
|
|
285
284
|
if (poll.status === "completed") {
|
|
286
285
|
const results = (await apiCall("GET", `/v1/results/${compareJobId}`));
|
|
287
286
|
const summary = (results.summary ?? {});
|
|
288
287
|
const gained = summary.top_gained_nodes ?? [];
|
|
289
288
|
const lost = summary.top_lost_nodes ?? [];
|
|
290
|
-
const fmtNode = (n) => ` node ${n.node_index ?? "?"} [${(n.coords ?? [0, 0]).map(v => Number(v).toFixed(1)).join(",")}] Δ=${Number(n.density_diff ?? 0).toFixed(4)}`;
|
|
291
289
|
const n = top_n ?? 10;
|
|
292
290
|
const content = [{ type: "text", text: [
|
|
293
291
|
`Dataset comparison — job: ${compareJobId}`,
|
|
294
292
|
`Dataset A rows: ${summary.n_rows_a ?? "?"} | Dataset B rows: ${summary.n_rows_b ?? "?"}`,
|
|
295
|
-
`Top ${Math.min(n, gained.length)} gained (B > A):`, ...gained.slice(0, n).map(
|
|
296
|
-
`Top ${Math.min(n, lost.length)} lost (A > B):`, ...lost.slice(0, n).map(
|
|
293
|
+
`Top ${Math.min(n, gained.length)} gained (B > A):`, ...gained.slice(0, n).map(fmtDensityDiffNode),
|
|
294
|
+
`Top ${Math.min(n, lost.length)} lost (A > B):`, ...lost.slice(0, n).map(fmtDensityDiffNode),
|
|
297
295
|
].join("\n") }];
|
|
298
296
|
content.push({ type: "text", text: "Density difference — Positive (red) = Dataset B gained density; Negative (blue) = Dataset A had more density." });
|
|
299
297
|
await tryAttachImage(content, compareJobId, `density_diff.${summary.output_format ?? output_format ?? "png"}`);
|
|
@@ -314,7 +312,7 @@ action=report: Returns a report manifest for the given job_id (job must be compl
|
|
|
314
312
|
const data = (await apiCall("POST", `/v1/results/${job_id}/project_columns`, body));
|
|
315
313
|
const projJobId = data.id;
|
|
316
314
|
const prepareId = await pollPrepareIfPresent(data, "inference(project_columns)");
|
|
317
|
-
const poll = await pollUntilComplete(projJobId,
|
|
315
|
+
const poll = await pollUntilComplete(projJobId, POLL_PROJECT_MAX_MS);
|
|
318
316
|
if (poll.status === "completed") {
|
|
319
317
|
const results = (await apiCall("GET", `/v1/results/${projJobId}`));
|
|
320
318
|
const summary = (results.summary ?? {});
|
package/dist/tools/results.js
CHANGED
|
@@ -2,7 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { registerAuditedTool } from "../audit.js";
|
|
5
|
-
import { apiCall, apiRawCall, getWorkspaceRootAsync, sandboxPath, pollUntilComplete, tryAttachImage, resetInlineAttachBudget,
|
|
5
|
+
import { apiCall, apiRawCall, getWorkspaceRootAsync, sandboxPath, pollUntilComplete, tryAttachImage, resetInlineAttachBudget, attachResultsImages, resolveOutputDpi, POLL_RECOLOR_MAX_MS, fmtDensityDiffNode, getCaptionForImage, shouldFetchAllRemainingFigures, mimeForFilename, structuredTextResult, } from "../shared.js";
|
|
6
6
|
export function registerResultsTool(server) {
|
|
7
7
|
registerAuditedTool(server, "results", `Retrieve, recolor, download, or export figures and metrics for a completed map job. Presentation and artifact access only — operations on the frozen map (predict, compare, project, impute_column, transition_flow) live in **inference**.
|
|
8
8
|
|
|
@@ -85,7 +85,7 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
|
|
|
85
85
|
.optional()
|
|
86
86
|
.describe("action=export: training_log=learning curve; weights=weight matrix; nodes=per-node stats; clusters=regime boundaries; transition_matrix=Markov transition matrix"),
|
|
87
87
|
}, async ({ action, job_id, figures, include_individual, include_json, folder, colormap, output_format, output_dpi, recolor_figures, export_type: exportType }) => {
|
|
88
|
-
const
|
|
88
|
+
const figuresArg = figures;
|
|
89
89
|
if (action === "get") {
|
|
90
90
|
resetInlineAttachBudget();
|
|
91
91
|
const data = (await apiCall("GET", `/v1/results/${job_id}`));
|
|
@@ -119,13 +119,7 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
|
|
|
119
119
|
``, `Arrows show net directional drift. Long/bright = frequent transitions. Short = stable states.`,
|
|
120
120
|
`Background = U-matrix. Use results(action=transition_flow, lag=N) with larger N for longer-term structure.`,
|
|
121
121
|
].join("\n") });
|
|
122
|
-
|
|
123
|
-
const cap = getCaptionForImage(name);
|
|
124
|
-
if (cap)
|
|
125
|
-
content.push({ type: "text", text: cap });
|
|
126
|
-
await tryAttachImage(content, job_id, name);
|
|
127
|
-
inlinedImages.add(name);
|
|
128
|
-
}
|
|
122
|
+
await attachResultsImages(content, job_id, jobType, summary, figuresArg, include_individual, inlinedImages);
|
|
129
123
|
}
|
|
130
124
|
else if (jobType === "project_variable") {
|
|
131
125
|
const varName = summary.variable_name ?? "variable";
|
|
@@ -140,13 +134,7 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
|
|
|
140
134
|
` Mean: ${stats.mean !== undefined ? Number(stats.mean).toFixed(3) : "N/A"}`,
|
|
141
135
|
` Nodes with data: ${stats.n_nodes_with_data ?? "N/A"}`,
|
|
142
136
|
].join("\n") });
|
|
143
|
-
|
|
144
|
-
const cap = getCaptionForImage(name);
|
|
145
|
-
if (cap)
|
|
146
|
-
content.push({ type: "text", text: cap });
|
|
147
|
-
await tryAttachImage(content, job_id, name);
|
|
148
|
-
inlinedImages.add(name);
|
|
149
|
-
}
|
|
137
|
+
await attachResultsImages(content, job_id, jobType, summary, figuresArg, include_individual, inlinedImages);
|
|
150
138
|
}
|
|
151
139
|
else if (jobType === "project_columns") {
|
|
152
140
|
const cols = summary.columns ?? [];
|
|
@@ -158,18 +146,12 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
|
|
|
158
146
|
...Object.entries(colStats).map(([col, s]) => ` ${col}: nodes=${s.n_nodes_with_data ?? "?"} | min=${s.min !== undefined ? Number(s.min).toFixed(3) : "—"} | max=${s.max !== undefined ? Number(s.max).toFixed(3) : "—"}`),
|
|
159
147
|
];
|
|
160
148
|
content.push({ type: "text", text: lines.join("\n") });
|
|
161
|
-
|
|
162
|
-
const cap = getCaptionForImage(name);
|
|
163
|
-
if (cap)
|
|
164
|
-
content.push({ type: "text", text: cap });
|
|
165
|
-
await tryAttachImage(content, job_id, name);
|
|
166
|
-
inlinedImages.add(name);
|
|
167
|
-
}
|
|
149
|
+
await attachResultsImages(content, job_id, jobType, summary, figuresArg, include_individual, inlinedImages);
|
|
168
150
|
}
|
|
169
151
|
else if (jobType === "compare_datasets") {
|
|
170
152
|
const gained = summary.top_gained_nodes ?? [];
|
|
171
153
|
const lost = summary.top_lost_nodes ?? [];
|
|
172
|
-
const fmtNode =
|
|
154
|
+
const fmtNode = fmtDensityDiffNode;
|
|
173
155
|
const lines = [
|
|
174
156
|
`Dataset Comparison — ${resultsHeader}`,
|
|
175
157
|
`Parent map job: ${summary.parent_job_id ?? "N/A"} | Dataset A: ${summary.n_rows_a ?? "?"} rows | Dataset B: ${summary.n_rows_b ?? "?"} rows`,
|
|
@@ -179,13 +161,7 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
|
|
|
179
161
|
...lost.slice(0, 5).map(fmtNode),
|
|
180
162
|
];
|
|
181
163
|
content.push({ type: "text", text: lines.join("\n") });
|
|
182
|
-
|
|
183
|
-
const cap = getCaptionForImage(name);
|
|
184
|
-
if (cap)
|
|
185
|
-
content.push({ type: "text", text: cap });
|
|
186
|
-
await tryAttachImage(content, job_id, name);
|
|
187
|
-
inlinedImages.add(name);
|
|
188
|
-
}
|
|
164
|
+
await attachResultsImages(content, job_id, jobType, summary, figuresArg, include_individual, inlinedImages);
|
|
189
165
|
}
|
|
190
166
|
else if (jobType === "enrich_dataset") {
|
|
191
167
|
// Older exports: summary.job_type from stored results (read-only display).
|
|
@@ -256,23 +232,22 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
|
|
|
256
232
|
: []),
|
|
257
233
|
cv ? [
|
|
258
234
|
"",
|
|
259
|
-
`Held-out imputation accuracy (cv_folds=${cv.cv_folds ?? "?"}, method=${cv.cv_method ?? "
|
|
235
|
+
`Held-out imputation accuracy (cv_folds=${cv.cv_folds ?? "?"}, method=${cv.cv_method ?? "kfold_holdout_pool"}):`,
|
|
260
236
|
cv.columns_sampled ? ` (sampled ${cv.columns_evaluated ?? "?"} columns for speed)` : "",
|
|
261
|
-
cv.aggregate ? ` Aggregate MAE: ${fmt(cv.aggregate.mae)} |
|
|
262
|
-
|
|
237
|
+
cv.aggregate ? ` Aggregate MAE/RMSE (non-cyclic columns): ${fmt(cv.aggregate.mae)} | ${fmt(cv.aggregate.rmse)}` : "",
|
|
238
|
+
cv.aggregate?.excluded_cyclic_columns
|
|
239
|
+
? ` Excluded cyclic columns from aggregate: ${cv.aggregate.excluded_cyclic_columns}`
|
|
240
|
+
: "",
|
|
241
|
+
" See quality.csv — linear: mae/rmse/r2; cyclic_pairs: circ_mae_rad + mean_resultant_length only.",
|
|
242
|
+
" k-fold is relative column difficulty, not external missing-cell ground truth.",
|
|
263
243
|
].filter(Boolean).join("\n") : "\nHeld-out accuracy: skipped (cv_folds=0). Set cv_folds=5 to validate imputation.",
|
|
264
244
|
"",
|
|
265
245
|
`Artifacts: ${(summary.files ?? []).filter((f) => f !== "summary.json").join(", ")}`,
|
|
266
246
|
"Next: results(action=download) for imputed.csv / quality.csv; inference(impute_column) with this job_id as parent for held-out columns.",
|
|
247
|
+
"Optional imputation_uncertainty.csv pool_std is prototype-neighbourhood dispersion (not calibrated predictive SD).",
|
|
267
248
|
].filter((l) => l !== "");
|
|
268
249
|
content.push({ type: "text", text: lines.join("\n") });
|
|
269
|
-
|
|
270
|
-
const cap = getCaptionForImage(name);
|
|
271
|
-
if (cap)
|
|
272
|
-
content.push({ type: "text", text: cap });
|
|
273
|
-
await tryAttachImage(content, job_id, name);
|
|
274
|
-
inlinedImages.add(name);
|
|
275
|
-
}
|
|
250
|
+
await attachResultsImages(content, job_id, jobType, summary, figuresArg, include_individual, inlinedImages);
|
|
276
251
|
}
|
|
277
252
|
else if (jobType === "reduce_spectral") {
|
|
278
253
|
const method = String(summary.method ?? "?");
|
|
@@ -349,13 +324,7 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
|
|
|
349
324
|
files.length > 0 ? `Artifacts: ${files.join(", ")}` : "",
|
|
350
325
|
].filter((l) => l !== "");
|
|
351
326
|
content.push({ type: "text", text: lines.join("\n") });
|
|
352
|
-
|
|
353
|
-
const cap = getCaptionForImage(name);
|
|
354
|
-
if (cap)
|
|
355
|
-
content.push({ type: "text", text: cap });
|
|
356
|
-
await tryAttachImage(content, job_id, name);
|
|
357
|
-
inlinedImages.add(name);
|
|
358
|
-
}
|
|
327
|
+
await attachResultsImages(content, job_id, jobType, summary, figuresArg, include_individual, inlinedImages);
|
|
359
328
|
}
|
|
360
329
|
else {
|
|
361
330
|
const grid = summary.grid ?? [0, 0];
|
|
@@ -438,14 +407,7 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
|
|
|
438
407
|
...((jobType === "train_som" || jobType === "train_siom" || jobType === "train_impute") ? ["", `Next: results_explorer(job_id="${job_id}") to browse all figures interactively; results(action=export, export_type=training_log) for learning curve; results(action=download) to save figures${jobType === "train_impute" ? " (includes imputed.csv)" : ""}; jobs(action=compare, job_ids=[...]) to compare runs; inference(action=predict) to score new data; inference(action=project_columns) to project other variables onto the map.`] : []),
|
|
439
408
|
].filter((l) => l !== "").join("\n");
|
|
440
409
|
content.push({ type: "text", text: textSummary });
|
|
441
|
-
|
|
442
|
-
for (const name of imagesToFetch) {
|
|
443
|
-
const cap = getCaptionForImage(name);
|
|
444
|
-
if (cap)
|
|
445
|
-
content.push({ type: "text", text: cap });
|
|
446
|
-
await tryAttachImage(content, job_id, name);
|
|
447
|
-
inlinedImages.add(name);
|
|
448
|
-
}
|
|
410
|
+
await attachResultsImages(content, job_id, jobType, summary, figuresArg, include_individual, inlinedImages);
|
|
449
411
|
}
|
|
450
412
|
const files = summary.files ?? [];
|
|
451
413
|
const jobType2 = summary.job_type ?? "train_som";
|
|
@@ -476,7 +438,16 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
|
|
|
476
438
|
: "";
|
|
477
439
|
content.push({ type: "text", text: `Available: ${files.join(", ")}. ${logicalNames}Use results(action=get, figures=[...]) for specific plots, figures=all for everything, or figures="none" for metrics-only.` });
|
|
478
440
|
}
|
|
479
|
-
|
|
441
|
+
const structuredPayload = {
|
|
442
|
+
job_id,
|
|
443
|
+
job_type: jobType2,
|
|
444
|
+
label: jobLabel,
|
|
445
|
+
summary,
|
|
446
|
+
files,
|
|
447
|
+
figures_inlined: [...inlinedImages],
|
|
448
|
+
};
|
|
449
|
+
const summaryText = content.filter((c) => c.type === "text").map((c) => c.text).join("\n\n");
|
|
450
|
+
return structuredTextResult(structuredPayload, summaryText, content);
|
|
480
451
|
}
|
|
481
452
|
if (action === "export") {
|
|
482
453
|
if (!exportType)
|
|
@@ -633,16 +604,36 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
|
|
|
633
604
|
}
|
|
634
605
|
await fs.mkdir(resolvedDir, { recursive: true });
|
|
635
606
|
const saved = [];
|
|
607
|
+
const missing = [];
|
|
636
608
|
for (const filename of toDownload) {
|
|
637
609
|
try {
|
|
638
610
|
const { data: buf } = await apiRawCall(`/v1/results/${job_id}/image/${filename}`);
|
|
639
611
|
await fs.writeFile(path.join(resolvedDir, filename), buf);
|
|
640
612
|
saved.push(filename);
|
|
641
613
|
}
|
|
642
|
-
catch {
|
|
614
|
+
catch {
|
|
615
|
+
missing.push(filename);
|
|
616
|
+
}
|
|
643
617
|
}
|
|
644
618
|
const savedDir = path.join(folder, jobSubfolder);
|
|
645
|
-
|
|
619
|
+
if (saved.length === 0) {
|
|
620
|
+
const missNote = missing.length > 0 ? ` Missing/failed: ${missing.join(", ")}.` : "";
|
|
621
|
+
return {
|
|
622
|
+
content: [{
|
|
623
|
+
type: "text",
|
|
624
|
+
text: `No files saved.${missNote} Check job_id and that the job is completed.`,
|
|
625
|
+
}],
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
const missNote = missing.length > 0
|
|
629
|
+
? ` Missing/failed (${missing.length}): ${missing.join(", ")}.`
|
|
630
|
+
: "";
|
|
631
|
+
return {
|
|
632
|
+
content: [{
|
|
633
|
+
type: "text",
|
|
634
|
+
text: `Saved ${saved.length} file(s) to ${savedDir}: ${saved.join(", ")}.${missNote}`,
|
|
635
|
+
}],
|
|
636
|
+
};
|
|
646
637
|
}
|
|
647
638
|
if (action === "recolor") {
|
|
648
639
|
if (!colormap)
|
|
@@ -651,11 +642,11 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
|
|
|
651
642
|
colormap,
|
|
652
643
|
figures: recolor_figures ?? ["combined"],
|
|
653
644
|
output_format: output_format ?? "png",
|
|
654
|
-
output_dpi:
|
|
645
|
+
output_dpi: resolveOutputDpi(output_dpi),
|
|
655
646
|
};
|
|
656
647
|
const data = (await apiCall("POST", `/v1/results/${job_id}/render`, body));
|
|
657
648
|
const newJobId = data.id;
|
|
658
|
-
const poll = await pollUntilComplete(newJobId,
|
|
649
|
+
const poll = await pollUntilComplete(newJobId, POLL_RECOLOR_MAX_MS);
|
|
659
650
|
if (poll.status === "completed") {
|
|
660
651
|
const content = [{ type: "text", text: `Re-rendered with colormap "${colormap}". New job_id: ${newJobId}.` }];
|
|
661
652
|
for (const fig of recolor_figures ?? ["combined"]) {
|
package/dist/tools/train.js
CHANGED
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { registerAuditedTool } from "../audit.js";
|
|
3
3
|
import { apiCall, textResult } from "../shared.js";
|
|
4
4
|
import { buildTrainSubmitExtras } from "../train_submit_message.js";
|
|
5
|
-
import { TRAINING_INPUT_SCHEMA, buildTrainMapParams, buildFloopParams, fetchTrainingPresets, } from "./training_core.js";
|
|
5
|
+
import { TRAINING_INPUT_SCHEMA, buildTrainMapParams, buildFloopParams, fetchTrainingPresets, assertValidNormalizationArgs, } from "./training_core.js";
|
|
6
6
|
export const TRAIN_DESCRIPTION = `Submit a self-organizing map training job. Returns a job_id; poll with jobs(action=status) and then results(action=get). All actions are async.
|
|
7
7
|
|
|
8
8
|
| Action | Use when |
|
|
@@ -18,12 +18,13 @@ Parameter details (grid, epochs, batch, model, presets, normalize, categorical_f
|
|
|
18
18
|
|
|
19
19
|
ASYNC: every action returns a job_id immediately. Poll jobs(action=status, job_id=...) every 10–15s (do NOT poll faster). Map training typical times: 10×10 ~30s | 20×20 ~3–5 min | 40×40 ~15–30 min — do not assume failure before 3 minutes on CPU. When status is completed, call results(action=get, job_id=...).
|
|
20
20
|
|
|
21
|
-
action=impute: use when many cells are missing across training columns; use inference(impute_column) instead when you already have a complete-case map and need to fill one held-out column. Supports transforms (log/sqrt/etc., not rank), temporal_features, cyclic_features, and normalization_methods (zscore/mad/sigmoidal/none — not sepd). Does not support auto_log_transforms, rank transforms, or normalize=sepd. Defaults model=auto (SIOM missSOM on som_siom+ plans, else plain missSOM), cv_folds=5 → quality.csv
|
|
21
|
+
action=impute: use when many cells are missing across training columns; use inference(impute_column) instead when you already have a complete-case map and need to fill one held-out column. Supports transforms (log/sqrt/etc., not rank), temporal_features, cyclic_features, optional cyclic_pairs=[[cos,sin],...] (circular prototype refresh + atomic chord BMU; those columns get circular quality metrics only — never quote Euclidean R² on cos/sin), and normalization_methods (zscore/mad/sigmoidal/none — not sepd). Does not support auto_log_transforms, rank transforms, or normalize=sepd. Defaults model=auto (SIOM missSOM on som_siom+ plans, else plain missSOM), cv_folds=5 → quality.csv (linear: kfold_holdout_pool + resubstitution_optimistic; cyclic pairs: kfold_holdout_circular). Aggregate MAE/RMSE excludes cyclic columns. Do not treat k-fold alone as true missing-cell ground truth. Optional SIOM knobs: gamma, gamma_f, siom_decay, siom_penalty, penalty_alpha, reset_per_epoch. Artifacts: imputed.csv, imputation_mask.csv, optional imputation_uncertainty.csv (pool_std = neighbourhood dispersion). A completed impute job_id is a valid parent for inference(impute_column).
|
|
22
22
|
action=floop_siom: max_nodes is a TOTAL node budget (not a grid width/area); if coverage collapses, reduce max_nodes before increasing effort.
|
|
23
23
|
NOT FOR: lifecycle (status/list/compare/cancel/delete) → use jobs; scoring data → use inference.
|
|
24
24
|
|
|
25
25
|
Rank transform (map/siom/floop only): training-only — batch predict, project_columns, and impute_column cannot score rank-transformed columns.`;
|
|
26
26
|
function assertImputeTrainingArgs(args) {
|
|
27
|
+
assertValidNormalizationArgs(args);
|
|
27
28
|
const normalize = args.normalize;
|
|
28
29
|
if (normalize === "sepd") {
|
|
29
30
|
throw new Error("train(impute) does not support normalize=sepd");
|
|
@@ -48,6 +49,48 @@ function assertImputeTrainingArgs(args) {
|
|
|
48
49
|
throw new Error("train(impute) does not support auto_log_transforms yet.");
|
|
49
50
|
}
|
|
50
51
|
}
|
|
52
|
+
/** Shared POST /v1/jobs + train submit message extras (map/siom/impute and floop). */
|
|
53
|
+
async function submitTrainJob(opts) {
|
|
54
|
+
const submitBody = { dataset_id: opts.dataset_id, params: opts.params };
|
|
55
|
+
if (opts.label && opts.label.trim() !== "")
|
|
56
|
+
submitBody.label = opts.label;
|
|
57
|
+
const data = (await apiCall("POST", "/v1/jobs", submitBody));
|
|
58
|
+
const newJobId = data.id;
|
|
59
|
+
data.effective_params = `${opts.variantPrefix}, ${opts.paramSummary}`;
|
|
60
|
+
data.results_hint = opts.resultsHint;
|
|
61
|
+
if (opts.gridLargeForRows) {
|
|
62
|
+
data.grid_auto_warning = true;
|
|
63
|
+
}
|
|
64
|
+
const extras = buildTrainSubmitExtras({
|
|
65
|
+
newJobId,
|
|
66
|
+
totalRows: opts.totalRows,
|
|
67
|
+
hasTransforms: opts.hasTransforms,
|
|
68
|
+
prepareJobId: data.prepare_job_id ?? null,
|
|
69
|
+
variantPrefix: opts.variantPrefix,
|
|
70
|
+
paramSummary: opts.paramSummary,
|
|
71
|
+
impute: opts.impute,
|
|
72
|
+
resultsHint: opts.resultsHint,
|
|
73
|
+
});
|
|
74
|
+
let msg = extras.message;
|
|
75
|
+
try {
|
|
76
|
+
const sys = (await apiCall("GET", "/v1/system/info"));
|
|
77
|
+
const pending = Number(sys.status?.pending_jobs ?? sys.pending_jobs ?? 0);
|
|
78
|
+
const totalEta = Number(sys.training_time_estimates_seconds?.total ??
|
|
79
|
+
(sys.gpu_available ? 45 : 120));
|
|
80
|
+
const waitMinutes = Math.round((pending * totalEta) / 60);
|
|
81
|
+
if (waitMinutes > 1)
|
|
82
|
+
msg = `You are #${pending + 1} in queue. Estimated wait: ~${waitMinutes} min. ${msg}`;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
/* ignore queue enrichment */
|
|
86
|
+
}
|
|
87
|
+
if (opts.gridLargeForRows) {
|
|
88
|
+
msg += ` Note: Grid may be large for ${opts.totalRows} rows (consider grid=auto for fewer dead nodes).`;
|
|
89
|
+
}
|
|
90
|
+
data.message = msg;
|
|
91
|
+
data.suggested_next_step = extras.suggested_next_step;
|
|
92
|
+
return textResult(data);
|
|
93
|
+
}
|
|
51
94
|
/**
|
|
52
95
|
* Core training dispatcher. Shared by the `train` tool and the deprecated
|
|
53
96
|
* `jobs(train_*)` wrappers. Every branch posts to the same /v1/jobs route.
|
|
@@ -55,7 +98,7 @@ function assertImputeTrainingArgs(args) {
|
|
|
55
98
|
export async function runTrain(action, args) {
|
|
56
99
|
const dataset_id = args.dataset_id;
|
|
57
100
|
if (action === "map" || action === "siom_map" || action === "impute") {
|
|
58
|
-
const { preset, grid_x, grid_y, epochs, model, periodic, columns, cyclic_features, temporal_features, feature_weights, transforms, auto_log_transforms, time_delay_embeddings, categorical_features, normalize, sigma_f, learning_rate, batch_size, quality_metrics, backend, output_format, output_dpi, colormap, row_range, gamma, gamma_f, siom_decay, siom_penalty, penalty_alpha, reset_per_epoch, siom_feature_geometry, siom_qe_backend, siom_qe_batch_size, label, cv_folds, viz_mode, viz_top_components, emit_cell_uncertainty, } = args;
|
|
101
|
+
const { preset, grid_x, grid_y, epochs, model, periodic, columns, cyclic_features, cyclic_pairs, temporal_features, feature_weights, transforms, auto_log_transforms, time_delay_embeddings, categorical_features, normalize, normalization_methods, sigma_f, learning_rate, batch_size, quality_metrics, backend, output_format, output_dpi, colormap, row_range, gamma, gamma_f, siom_decay, siom_penalty, penalty_alpha, reset_per_epoch, siom_feature_geometry, siom_qe_backend, siom_qe_batch_size, label, cv_folds, viz_mode, viz_top_components, emit_cell_uncertainty, } = args;
|
|
59
102
|
let PRESETS = {};
|
|
60
103
|
try {
|
|
61
104
|
PRESETS = await fetchTrainingPresets();
|
|
@@ -72,11 +115,14 @@ export async function runTrain(action, args) {
|
|
|
72
115
|
if (action === "impute") {
|
|
73
116
|
assertImputeTrainingArgs(args);
|
|
74
117
|
}
|
|
118
|
+
else {
|
|
119
|
+
assertValidNormalizationArgs(args);
|
|
120
|
+
}
|
|
75
121
|
const { params, paramSummary, effectiveGrid } = buildTrainMapParams({
|
|
76
|
-
preset, grid_x, grid_y, epochs, model, periodic, columns, cyclic_features,
|
|
122
|
+
preset, grid_x, grid_y, epochs, model, periodic, columns, cyclic_features, cyclic_pairs,
|
|
77
123
|
temporal_features, feature_weights, transforms, auto_log_transforms,
|
|
78
124
|
time_delay_embeddings, categorical_features,
|
|
79
|
-
normalize, sigma_f, learning_rate, batch_size, quality_metrics, backend,
|
|
125
|
+
normalize, normalization_methods, sigma_f, learning_rate, batch_size, quality_metrics, backend,
|
|
80
126
|
output_format, output_dpi, colormap, row_range,
|
|
81
127
|
}, PRESETS);
|
|
82
128
|
if (action === "impute") {
|
|
@@ -134,62 +180,34 @@ export async function runTrain(action, args) {
|
|
|
134
180
|
totalRows = Number(preview?.total_rows ?? 0);
|
|
135
181
|
}
|
|
136
182
|
catch { /* ignore */ }
|
|
137
|
-
const submitBody = { dataset_id, params };
|
|
138
|
-
if (label && label.trim() !== "")
|
|
139
|
-
submitBody.label = label;
|
|
140
|
-
const data = (await apiCall("POST", "/v1/jobs", submitBody));
|
|
141
|
-
const newJobId = data.id;
|
|
142
|
-
const variantPrefix = action === "siom_map" ? "variant=siom"
|
|
143
|
-
: action === "impute" ? "variant=train_impute"
|
|
144
|
-
: "variant=som";
|
|
145
|
-
data.effective_params = `${variantPrefix}, ${paramSummary}`;
|
|
146
183
|
const hasTransforms = transforms != null &&
|
|
147
184
|
typeof transforms === "object" &&
|
|
148
185
|
Object.keys(transforms).length > 0;
|
|
149
|
-
const
|
|
150
|
-
|
|
186
|
+
const gridLargeForRows = effectiveGrid && totalRows > 0 && effectiveGrid[0] * effectiveGrid[1] > totalRows * 0.75;
|
|
187
|
+
const resultsHint = action === "impute" ? "view the map and imputed.csv"
|
|
188
|
+
: "view the map and metrics";
|
|
189
|
+
const variantPrefix = action === "siom_map" ? "variant=siom"
|
|
190
|
+
: action === "impute" ? "variant=train_impute"
|
|
191
|
+
: "variant=som";
|
|
192
|
+
return submitTrainJob({
|
|
193
|
+
dataset_id,
|
|
194
|
+
params,
|
|
195
|
+
label,
|
|
151
196
|
totalRows,
|
|
152
197
|
hasTransforms,
|
|
153
|
-
prepareJobId: data.prepare_job_id ?? null,
|
|
154
198
|
variantPrefix,
|
|
155
199
|
paramSummary,
|
|
200
|
+
resultsHint,
|
|
156
201
|
impute: action === "impute",
|
|
202
|
+
gridLargeForRows: !!gridLargeForRows,
|
|
157
203
|
});
|
|
158
|
-
try {
|
|
159
|
-
const sys = (await apiCall("GET", "/v1/system/info"));
|
|
160
|
-
const pending = Number(sys.status?.pending_jobs ?? sys.pending_jobs ?? 0);
|
|
161
|
-
const totalEta = Number(sys.training_time_estimates_seconds?.total ??
|
|
162
|
-
(sys.gpu_available ? 45 : 120));
|
|
163
|
-
const waitMinutes = Math.round((pending * totalEta) / 60);
|
|
164
|
-
let msg = extras.message;
|
|
165
|
-
if (waitMinutes > 1)
|
|
166
|
-
msg = `You are #${pending + 1} in queue. Estimated wait: ~${waitMinutes} min. ${msg}`;
|
|
167
|
-
if (effectiveGrid && totalRows > 0 && effectiveGrid[0] * effectiveGrid[1] > totalRows * 0.75) {
|
|
168
|
-
msg += ` Note: Grid may be large for ${totalRows} rows (consider grid=auto for fewer dead nodes).`;
|
|
169
|
-
}
|
|
170
|
-
data.message = msg;
|
|
171
|
-
data.suggested_next_step = extras.suggested_next_step;
|
|
172
|
-
}
|
|
173
|
-
catch {
|
|
174
|
-
let msg = extras.message;
|
|
175
|
-
if (effectiveGrid && totalRows > 0 && effectiveGrid[0] * effectiveGrid[1] > totalRows * 0.75) {
|
|
176
|
-
msg += ` Note: Grid may be large for ${totalRows} rows (consider grid=auto for fewer dead nodes).`;
|
|
177
|
-
}
|
|
178
|
-
data.message = msg;
|
|
179
|
-
data.suggested_next_step = extras.suggested_next_step;
|
|
180
|
-
}
|
|
181
|
-
return textResult(data);
|
|
182
204
|
}
|
|
183
205
|
if (action === "floop_siom") {
|
|
184
206
|
if (!dataset_id)
|
|
185
207
|
throw new Error("train(floop_siom) requires dataset_id");
|
|
208
|
+
assertValidNormalizationArgs(args);
|
|
186
209
|
const { transforms, label } = args;
|
|
187
210
|
const { params, paramSummary } = buildFloopParams(args);
|
|
188
|
-
const submitBody = { dataset_id, params };
|
|
189
|
-
if (label && label.trim() !== "")
|
|
190
|
-
submitBody.label = label;
|
|
191
|
-
const data = (await apiCall("POST", "/v1/jobs", submitBody));
|
|
192
|
-
const newJobId = data.id;
|
|
193
211
|
let totalRows = 0;
|
|
194
212
|
try {
|
|
195
213
|
const preview = (await apiCall("GET", `/v1/datasets/${dataset_id}/preview?n_rows=1`));
|
|
@@ -197,19 +215,16 @@ export async function runTrain(action, args) {
|
|
|
197
215
|
}
|
|
198
216
|
catch { /* ignore */ }
|
|
199
217
|
const hasTransforms = transforms != null && typeof transforms === "object" && Object.keys(transforms).length > 0;
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
218
|
+
return submitTrainJob({
|
|
219
|
+
dataset_id,
|
|
220
|
+
params,
|
|
221
|
+
label,
|
|
203
222
|
totalRows,
|
|
204
223
|
hasTransforms,
|
|
205
|
-
prepareJobId: data.prepare_job_id ?? null,
|
|
206
224
|
variantPrefix: "variant=floop",
|
|
207
225
|
paramSummary,
|
|
208
226
|
resultsHint: "view FLooP-SIOM figures and metrics",
|
|
209
227
|
});
|
|
210
|
-
data.message = extras.message;
|
|
211
|
-
data.suggested_next_step = extras.suggested_next_step;
|
|
212
|
-
return textResult(data);
|
|
213
228
|
}
|
|
214
229
|
throw new Error("Invalid train action");
|
|
215
230
|
}
|
|
@@ -4,11 +4,30 @@
|
|
|
4
4
|
* the `train` tool reuses a single set of definitions (no drift).
|
|
5
5
|
*/
|
|
6
6
|
import { z } from "zod";
|
|
7
|
-
import { apiCall } from "../shared.js";
|
|
7
|
+
import { apiCall, resolveOutputDpi } from "../shared.js";
|
|
8
8
|
export async function fetchTrainingPresets() {
|
|
9
9
|
const configData = (await apiCall("GET", "/v1/training/config"));
|
|
10
10
|
return configData?.presets || {};
|
|
11
11
|
}
|
|
12
|
+
const ALLOWED_NORM_METHODS = new Set(["zscore", "mad", "sigmoidal", "sepd", "none"]);
|
|
13
|
+
const ALLOWED_NORMALIZE_GLOBALS = new Set(["all", "auto", "mad", "sigmoidal", "sepd", "zscore"]);
|
|
14
|
+
/**
|
|
15
|
+
* Fail closed on unknown normalize / normalization_methods values before POST /v1/jobs.
|
|
16
|
+
*/
|
|
17
|
+
export function assertValidNormalizationArgs(args) {
|
|
18
|
+
const normalize = args.normalize;
|
|
19
|
+
if (typeof normalize === "string" && normalize.length > 0 && !ALLOWED_NORMALIZE_GLOBALS.has(normalize)) {
|
|
20
|
+
throw new Error(`Unknown normalize=${normalize}. Use auto|mad|sigmoidal|sepd|zscore|all, or pass a string[] of feature names.`);
|
|
21
|
+
}
|
|
22
|
+
const nm = args.normalization_methods;
|
|
23
|
+
if (nm && typeof nm === "object" && !Array.isArray(nm)) {
|
|
24
|
+
for (const [col, method] of Object.entries(nm)) {
|
|
25
|
+
if (!ALLOWED_NORM_METHODS.has(String(method))) {
|
|
26
|
+
throw new Error(`Unknown normalization_methods.${col}=${method}. Allowed: zscore|mad|sigmoidal|sepd|none.`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
12
31
|
/**
|
|
13
32
|
* Fetch presets + the plan-scoped allowed_job_types in one /v1/training/config call.
|
|
14
33
|
* Used to scope train-action selection to entitled modes.
|
|
@@ -49,6 +68,8 @@ export const TRAINING_INPUT_SCHEMA = {
|
|
|
49
68
|
feature: z.string(),
|
|
50
69
|
period: z.number(),
|
|
51
70
|
})).optional(),
|
|
71
|
+
cyclic_pairs: z.array(z.array(z.string()).min(2).max(2)).optional()
|
|
72
|
+
.describe("impute only: [[cos_col, sin_col], ...] for circular prototype means + atomic chord BMU. Those columns get circular quality metrics only (no Euclidean R²)."),
|
|
52
73
|
temporal_features: z.array(z.object({
|
|
53
74
|
columns: z.array(z.string()),
|
|
54
75
|
format: z.string(),
|
|
@@ -87,13 +108,13 @@ export const TRAINING_INPUT_SCHEMA = {
|
|
|
87
108
|
colormap: z.string().optional(),
|
|
88
109
|
row_range: z.tuple([z.number().int().min(1), z.number().int().min(1)]).optional(),
|
|
89
110
|
cv_folds: z.number().int().min(0).max(20).optional()
|
|
90
|
-
.describe("impute only:
|
|
111
|
+
.describe("impute only: quality.csv — linear cols: kfold_holdout_pool + resubstitution_optimistic; cyclic_pairs cols: circular metrics only (no Euclidean R²). Aggregate MAE/RMSE excludes cyclic columns. 0=off, default 5."),
|
|
91
112
|
viz_mode: z.enum(["full", "summary", "summary_plus_top"]).optional()
|
|
92
113
|
.describe("Visualization density; auto-capped when feature count > 40"),
|
|
93
114
|
viz_top_components: z.number().int().min(0).max(64).optional()
|
|
94
115
|
.describe("With viz_mode=summary_plus_top: upload top-N component maps by variance (default 8)"),
|
|
95
116
|
emit_cell_uncertainty: z.boolean().optional()
|
|
96
|
-
.describe("impute: write imputation_uncertainty.csv (pool_std
|
|
117
|
+
.describe("impute: write imputation_uncertainty.csv (pool_std = prototype-neighbourhood dispersion, not calibrated SD; pool_n = contributing nodes)"),
|
|
97
118
|
gamma: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional())
|
|
98
119
|
.describe("siom_map / impute (SIOM or auto on som_siom+): self-interaction strength γ (default 0.5 on worker)"),
|
|
99
120
|
gamma_f: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional())
|
|
@@ -159,9 +180,8 @@ export function buildFloopParams(args) {
|
|
|
159
180
|
params.auto_log_transforms = auto_log_transforms;
|
|
160
181
|
if (row_range && row_range.length >= 2 && row_range[0] <= row_range[1])
|
|
161
182
|
params.row_range = row_range;
|
|
162
|
-
const dpiMap = { standard: 1, retina: 2, print: 4 };
|
|
163
183
|
if (output_dpi && output_dpi !== "retina")
|
|
164
|
-
params.output_dpi =
|
|
184
|
+
params.output_dpi = resolveOutputDpi(output_dpi);
|
|
165
185
|
if (colormap)
|
|
166
186
|
params.colormap = colormap;
|
|
167
187
|
if (n_initial !== undefined)
|
|
@@ -225,7 +245,7 @@ export function buildFloopParams(args) {
|
|
|
225
245
|
return { params, paramSummary };
|
|
226
246
|
}
|
|
227
247
|
export function buildTrainMapParams(args, presets) {
|
|
228
|
-
const { preset, grid_x, grid_y, epochs, model, periodic, columns, cyclic_features, temporal_features, feature_weights, transforms, auto_log_transforms, time_delay_embeddings, categorical_features, normalize, normalization_methods, sigma_f, learning_rate, batch_size, quality_metrics, backend, output_format, output_dpi, colormap, row_range, siom_feature_geometry, siom_qe_backend, siom_qe_batch_size, } = args;
|
|
248
|
+
const { preset, grid_x, grid_y, epochs, model, periodic, columns, cyclic_features, cyclic_pairs, temporal_features, feature_weights, transforms, auto_log_transforms, time_delay_embeddings, categorical_features, normalize, normalization_methods, sigma_f, learning_rate, batch_size, quality_metrics, backend, output_format, output_dpi, colormap, row_range, siom_feature_geometry, siom_qe_backend, siom_qe_batch_size, } = args;
|
|
229
249
|
const p = preset ? presets[preset] : undefined;
|
|
230
250
|
const params = {
|
|
231
251
|
model: model ?? "SOM",
|
|
@@ -245,6 +265,8 @@ export function buildTrainMapParams(args, presets) {
|
|
|
245
265
|
params.epochs = p.epochs;
|
|
246
266
|
if (cyclic_features?.length)
|
|
247
267
|
params.cyclic_features = cyclic_features;
|
|
268
|
+
if (cyclic_pairs?.length)
|
|
269
|
+
params.cyclic_pairs = cyclic_pairs;
|
|
248
270
|
if (columns?.length)
|
|
249
271
|
params.columns = columns;
|
|
250
272
|
if (auto_log_transforms)
|
|
@@ -274,9 +296,8 @@ export function buildTrainMapParams(args, presets) {
|
|
|
274
296
|
else if (p?.backend)
|
|
275
297
|
params.backend = p.backend;
|
|
276
298
|
params.output_format = output_format ?? "png";
|
|
277
|
-
const dpiMap = { standard: 1, retina: 2, print: 4 };
|
|
278
299
|
if (output_dpi && output_dpi !== "retina")
|
|
279
|
-
params.output_dpi =
|
|
300
|
+
params.output_dpi = resolveOutputDpi(output_dpi);
|
|
280
301
|
if (colormap)
|
|
281
302
|
params.colormap = colormap;
|
|
282
303
|
if (row_range && row_range.length >= 2 && row_range[0] <= row_range[1])
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { registerAuditedTool } from "../audit.js";
|
|
2
2
|
import { fetchTrainingGuidanceFromApi } from "../shared.js";
|
|
3
3
|
export function registerTrainingGuidanceTool(server) {
|
|
4
|
-
registerAuditedTool(server, "training_guidance", "Structured
|
|
4
|
+
registerAuditedTool(server, "training_guidance", "Structured presets, parameter hints, and normalization decisions for train. Served from the API, filtered to your plan. Review, then submit with train.", {}, async () => {
|
|
5
5
|
const text = await fetchTrainingGuidanceFromApi();
|
|
6
6
|
return { content: [{ type: "text", text }] };
|
|
7
7
|
});
|
package/dist/train_finalize.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { apiCall, pollUntilComplete } from "./shared.js";
|
|
1
|
+
import { apiCall, pollUntilComplete, POLL_FINALIZE_MAX_MS } from "./shared.js";
|
|
2
2
|
/**
|
|
3
3
|
* When train compute finishes, finalize_training may still be rendering figures on worker-io.
|
|
4
4
|
* Poll it before fetching results.
|
|
5
5
|
*/
|
|
6
|
-
export async function pollFinalizeIfPresent(jobId, data, timeoutMs =
|
|
6
|
+
export async function pollFinalizeIfPresent(jobId, data, timeoutMs = POLL_FINALIZE_MAX_MS) {
|
|
7
7
|
const finalizeJobId = data.finalize_job_id;
|
|
8
8
|
if (!finalizeJobId)
|
|
9
9
|
return { finalizeJobId: null, note: null };
|