@barivia/barsom-mcp 0.13.0 → 0.14.0

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.
@@ -1,48 +1,31 @@
1
1
  import { z } from "zod";
2
2
  import { registerAuditedTool } from "../audit.js";
3
3
  import { apiCall, textResult } from "../shared.js";
4
- export const JOBS_DESCRIPTION_BASE = `Manage and inspect jobs.
4
+ import { runTrain } from "./train.js";
5
+ import { runBatchPredict } from "./inference.js";
6
+ import { TRAINING_INPUT_SCHEMA } from "./training_core.js";
7
+ export const JOBS_DESCRIPTION_BASE = `Manage and inspect jobs (lifecycle).
5
8
 
6
9
  | Action | Use when |
7
10
  |--------|----------|
8
- | status | Polling after any async job submission — call every 10–15s |
9
- | list | Finding job IDs, checking what is pending/completed, reviewing hyperparameters. Response includes job_type (train_map, report, recolor, project, transition_flow, compare, predict, impute_column, annotated_dataset, reduce_spectral) to filter or display. |
10
- | compare | Picking the best TRAINING run from a set of completed jobs (metrics table). NOT inference(compare), which diffs one dataset's distribution against the training set. |
11
- | train_map | Submitting a new map training job — returns job_id for polling |
12
- | train_impute | Sparse training data: train a map AND impute missing cells in one job (accelerated missSOM) — returns map + imputed.csv |
13
- | train_siom_map | Submitting a self-interacting map training job — same map flow with SIOM coverage control |
14
- | train_floop_siom | Submitting a FLooP-SIOM job (growing manifold; default topology=free / CHL) — requires Premium or Enterprise plan (all_algorithms) |
11
+ | status | Polling after any async submission — call every 10–15s |
12
+ | list | Finding job IDs / reviewing pipeline state (optionally filter by dataset_id) |
13
+ | compare | Picking the best TRAINING run from a set (metrics table). NOT inference(compare), which diffs one dataset's distribution against the training set. |
15
14
  | cancel | Stopping a running or pending job to free the worker |
16
15
  | delete | Permanently removing a job and all its S3 result files |
17
16
 
18
- For parameter hints (grid, epochs, batch, model, periodic, FLooP max_nodes/effort), call the **training_guidance** tool or use the **prepare_training** prompt before training.
17
+ To SUBMIT training, use the **train** tool (train(action=map | siom_map | impute | floop_siom | baseline_study)); to score data, use **inference**. The jobs(action=train_map | train_siom_map | train_impute | train_floop_siom | run_baseline_study | batch_predict) actions still work as **deprecated** forwarders for one release — migrate to train / inference.
19
18
 
20
19
  ASYNC POLLING PROTOCOL (action=status):
21
20
  - Poll every 10-15 seconds. Do NOT poll faster — it wastes context.
22
- - Progress increases through phases (ordering then convergence for map training). Other job types may use different phase logic.
21
+ - Progress increases through phases (ordering then convergence for map training); other job types differ.
23
22
  - For large grids (40×40+), do not assume failure before 3 minutes on CPU.
24
- - Wait for status "completed" before calling results(action=get).
25
23
  - Map training typical times: 10×10 ~30s | 20×20 ~3–5 min | 40×40 ~15–30 min.
24
+ - completed → results(action=get); failed → read error + optional failure_stage (preprocessing/training/metrics/visualization/upload): memory error → smaller grid/batch and retrain; column missing → datasets(action=preview); NaN on a standard map → train(action=impute) for sparse columns or clean the CSV.
26
25
 
27
- ESCALATION (action=status):
28
- - completed → call results(action=get) to retrieve the map and metrics
29
- - failed → error message and optional failure_stage (e.g. preprocessing, training, metrics, visualization, upload) indicate which phase broke:
30
- - memory/allocation error: reduce batch_size or grid size and retrain
31
- - column missing: verify with datasets(action=preview)
32
- - NaN error on train_map: use jobs(action=train_impute) for sparse training columns, or clean the CSV for complete-case train_map
33
-
34
- Parameter details (grid, epochs, batch, model, presets, normalize, categorical_features, backend, FLooP max_nodes/effort, SIOM geometry) live in the **training_guidance** tool / **prepare_training** prompt — call them instead of inlining here. Backend strings are "cpu" | "gpu" | "gpu_graphs" (no colon); on CPU-only hosts pass backend=cpu.
35
-
36
- action=train_map / train_siom_map: Submits a grid-map training job. Returns job_id — poll with jobs(action=status, job_id=...). train_siom_map adds SIOM coverage control.
37
- action=train_impute: Missing-tolerant map training (accelerated missSOM / SIOM). Use when many cells are missing across training columns; use inference(impute_column) when you already have a complete-case map and need to fill one held-out column. Plain numeric columns only (no cyclic/temporal/categorical). Defaults model=auto, cv_folds=5 → quality.csv with two labeled numbers per column: kfold_holdout_pool (honest held-out) and resubstitution_optimistic (in-sample, optimistic). status returns progress_phase (ordering/convergence/cv/upload). Artifacts: imputed.csv, imputation_mask.csv, optional imputation_uncertainty.csv. A completed train_impute job_id is a valid parent for inference(impute_column).
38
- action=train_floop_siom: growing node-budget manifold instead of a fixed hex grid. Requires a plan with all_algorithms (Premium/Enterprise); otherwise the API returns a clear upgrade message. Default topology=free (CHL); topology=chain for a strict 1D backbone. max_nodes is a TOTAL node budget (not grid width/area). If coverage collapses, reduce max_nodes before increasing effort.
39
- action=compare: metrics table (QE, TE, explained variance, silhouette) for 2+ COMPLETED TRAINING jobs — pick the best run in a sweep. NOT inference(action=compare) (single trained job + second dataset → density-diff heatmap). Only hyperparameter columns that differ across runs are shown; pass label="sweep_a"/"sweep_b" on train_map for readable rows.
26
+ action=compare: metrics table (QE, TE, explained variance, silhouette) for 2+ COMPLETED TRAINING jobs — pick the best run in a sweep. NOT inference(action=compare) (single trained job + second dataset → density-diff heatmap). Only hyperparameter columns that differ across runs are shown; pass label="…" at train time for readable rows.
40
27
  action=cancel: Not instant — worker checks between phases. Expect up to 30s delay.
41
28
  action=delete: WARNING — job ID will no longer work with results or any other tool.`;
42
- export async function fetchTrainingPresets() {
43
- const configData = (await apiCall("GET", "/v1/training/config"));
44
- return configData?.presets || {};
45
- }
46
29
  /**
47
30
  * Render the jobs(compare) markdown table. Strategy: always show job_id (or label
48
31
  * if present) + the four core metrics (QE, TE, Expl.Var, Silhouette); add any
@@ -134,432 +117,81 @@ export function renderCompareTable(comparisons) {
134
117
  }
135
118
  if (varying.length === 0 && ok.length >= 2) {
136
119
  lines.push("");
137
- lines.push("(All hyperparameters identical across compared jobs — only metrics shown. Pass label=\"…\" on train_map for readable run names.)");
120
+ lines.push("(All hyperparameters identical across compared jobs — only metrics shown. Pass label=\"…\" at train time for readable run names.)");
138
121
  }
139
122
  else if (!anyLabel && ok.length >= 2) {
140
123
  lines.push("");
141
- lines.push("Tip: pass label=\"sweep_xyz\" on train_map / train_siom_map / train_floop_siom to make compare rows readable.");
124
+ lines.push("Tip: pass label=\"sweep_xyz\" on train(action=map | siom_map | floop_siom) to make compare rows readable.");
142
125
  }
143
126
  return lines.join("\n");
144
127
  }
145
- export function buildTrainMapParams(args, presets) {
146
- 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, siom_feature_geometry, siom_qe_backend, siom_qe_batch_size, } = args;
147
- const p = preset ? presets[preset] : undefined;
148
- const params = {
149
- model: model ?? "SOM",
150
- periodic: periodic ?? true,
151
- normalize: normalize ?? "auto",
152
- };
153
- if (grid_x !== undefined && grid_y !== undefined)
154
- params.grid = [grid_x, grid_y];
155
- else if (p)
156
- params.grid = p.grid;
157
- if (epochs !== undefined)
158
- params.epochs = epochs;
159
- else if (p)
160
- params.epochs = p.epochs;
161
- if (cyclic_features?.length)
162
- params.cyclic_features = cyclic_features;
163
- if (columns?.length)
164
- params.columns = columns;
165
- if (auto_log_transforms)
166
- params.auto_log_transforms = auto_log_transforms;
167
- if (time_delay_embeddings?.length)
168
- params.time_delay_embeddings = time_delay_embeddings;
169
- if (categorical_features?.length)
170
- params.categorical_features = categorical_features;
171
- if (transforms && Object.keys(transforms).length > 0)
172
- params.transforms = transforms;
173
- if (temporal_features?.length)
174
- params.temporal_features = temporal_features;
175
- if (feature_weights && Object.keys(feature_weights).length > 0)
176
- params.feature_weights = feature_weights;
177
- if (sigma_f !== undefined)
178
- params.sigma_f = sigma_f;
179
- if (learning_rate !== undefined)
180
- params.learning_rate = learning_rate;
181
- if (batch_size !== undefined)
182
- params.batch_size = batch_size;
183
- else if (p)
184
- params.batch_size = p.batch_size;
185
- if (quality_metrics !== undefined)
186
- params.quality_metrics = quality_metrics;
187
- if (backend !== undefined && backend !== "auto")
188
- params.backend = backend;
189
- else if (p?.backend)
190
- params.backend = p.backend;
191
- params.output_format = output_format ?? "png";
192
- const dpiMap = { standard: 1, retina: 2, print: 4 };
193
- if (output_dpi && output_dpi !== "retina")
194
- params.output_dpi = dpiMap[output_dpi] ?? 2;
195
- if (colormap)
196
- params.colormap = colormap;
197
- if (row_range && row_range.length >= 2 && row_range[0] <= row_range[1])
198
- params.row_range = row_range;
199
- if (siom_feature_geometry !== undefined)
200
- params.siom_feature_geometry = siom_feature_geometry;
201
- if (siom_qe_backend !== undefined)
202
- params.siom_qe_backend = siom_qe_backend;
203
- if (siom_qe_batch_size !== undefined)
204
- params.siom_qe_batch_size = siom_qe_batch_size;
205
- const effectiveGrid = params.grid;
206
- const effectiveEpochs = params.epochs;
207
- const effectiveBatch = params.batch_size;
208
- const effectiveBackend = params.backend;
209
- const paramSummary = [
210
- effectiveGrid ? `grid=${effectiveGrid[0]}×${effectiveGrid[1]}` : "grid=auto",
211
- effectiveEpochs ? `epochs=${Array.isArray(effectiveEpochs) ? effectiveEpochs.join("+") : effectiveEpochs}` : "epochs=auto",
212
- effectiveBatch ? `batch=${effectiveBatch}` : "batch=auto",
213
- `model=${params.model}`,
214
- effectiveBackend ? `backend=${effectiveBackend}` : "",
215
- preset ? `(from preset: ${preset})` : "",
216
- ].filter(Boolean).join(", ");
217
- return { params, paramSummary, effectiveGrid };
218
- }
128
+ /** Deprecated jobs(train_*) / run_baseline_study actions -> the train tool's concise actions. */
129
+ const TRAIN_ALIAS = {
130
+ train_map: "map",
131
+ train_siom_map: "siom_map",
132
+ train_impute: "impute",
133
+ train_floop_siom: "floop_siom",
134
+ run_baseline_study: "baseline_study",
135
+ };
219
136
  export function registerJobsTool(server, description) {
220
137
  registerAuditedTool(server, "jobs", description, {
221
138
  action: z
222
- .enum(["status", "list", "compare", "cancel", "delete", "train_map", "train_impute", "train_siom_map", "train_floop_siom", "batch_predict", "run_baseline_study"])
223
- .describe("status: check progress; list: see all jobs; compare: metrics table; cancel: stop job; delete: remove job + files; train_map: submit standard map training; train_siom_map: submit SIOM map training; train_floop_siom: submit FLooP-SIOM; batch_predict: submit multiple predict jobs at once; run_baseline_study: auto-configure and train a baseline SOM"),
139
+ .enum([
140
+ "status", "list", "compare", "cancel", "delete",
141
+ // Deprecated forwarders (kept for one release; use train / inference).
142
+ "train_map", "train_siom_map", "train_impute", "train_floop_siom", "run_baseline_study", "batch_predict",
143
+ ])
144
+ .describe("status: check progress; list: see all jobs; compare: metrics table; cancel: stop job; delete: remove job + files. DEPRECATED forwarders: train_map/train_siom_map/train_impute/train_floop_siom/run_baseline_study (use the train tool); batch_predict (use inference)."),
224
145
  job_id: z
225
146
  .string()
226
147
  .optional()
227
- .describe("Job ID — required for action=status, cancel, delete, batch_predict (parent training job)"),
228
- inputs: z
229
- .array(z.object({
230
- dataset_id: z.string().optional(),
231
- rows: z.array(z.record(z.string(), z.number())).optional(),
232
- }))
233
- .optional()
234
- .describe("action=batch_predict: array of inputs; each item has dataset_id and/or rows (same as inference predict)."),
148
+ .describe("Job ID — required for action=status, cancel, delete, and the deprecated batch_predict (parent training job)."),
235
149
  job_ids: z
236
150
  .array(z.string())
237
151
  .optional()
238
- .describe("Array of job IDs — required for action=compare (minimum 2)"),
152
+ .describe("Array of job IDs — required for action=compare (minimum 2)."),
239
153
  dataset_id: z
240
154
  .string()
241
155
  .optional()
242
- .describe("Required for action=train_map. For action=list, filter jobs by this dataset ID."),
156
+ .describe("action=list: filter jobs by this dataset ID. Also required by the deprecated train_* forwarders."),
243
157
  label: z
244
158
  .string()
245
159
  .max(120)
246
160
  .optional()
247
- .describe("Optional run label (≤120 chars) for train_map / train_siom_map / train_floop_siom appears in jobs(list) and the jobs(compare) table; sanitized server-side. Useful for sweeps (e.g. label=\"sweep_periodic_true\")."),
248
- preset: z.enum(["quick", "standard", "refined", "high_res"]).optional(),
249
- grid_x: z.number().int().optional()
250
- .describe("Grid width. Omit grid_x AND grid_y (and preset) to auto-size the map (~5·√√n per side); the result reports hit_stats.active_node_fraction and a grid_suggestion when too many nodes are dead."),
251
- grid_y: z.number().int().optional().describe("Grid height. See grid_x for auto-sizing."),
252
- epochs: z.preprocess((v) => {
253
- if (v === undefined || v === null)
254
- return v;
255
- if (typeof v === "string") {
256
- const n = parseInt(v, 10);
257
- if (!Number.isNaN(n))
258
- return n;
259
- const m = v.match(/^\[\s*(\d+)\s*,\s*(\d+)\s*\]$/);
260
- if (m)
261
- return [parseInt(m[1], 10), parseInt(m[2], 10)];
262
- }
263
- return v;
264
- }, z.union([z.number().int(), z.array(z.number().int()).length(2)]).optional()),
265
- model: z.enum(["auto", "SOM", "SIOM", "RSOM", "SOM-SOFT", "RSOM-SOFT"]).optional(),
266
- periodic: z.boolean().optional().default(true),
267
- columns: z.array(z.string()).optional(),
268
- cyclic_features: z.array(z.object({
269
- feature: z.string(),
270
- period: z.number(),
271
- })).optional(),
272
- temporal_features: z.array(z.object({
273
- columns: z.array(z.string()),
274
- format: z.string(),
275
- extract: z.array(z.enum(["hour_of_day", "day_of_year", "month", "day_of_week", "minute_of_hour"])),
276
- cyclic: z.boolean().default(true),
277
- separator: z.string().optional(),
278
- })).optional(),
279
- feature_weights: z.record(z.number()).optional(),
280
- transforms: z.record(z.enum(["log", "log1p", "log10", "sqrt", "square", "abs", "invert", "rank", "none"])).optional(),
281
- auto_log_transforms: z.boolean().optional().default(false),
282
- time_delay_embeddings: z.array(z.object({
283
- feature: z.string(),
284
- lags: z.array(z.number().int().min(1))
285
- })).optional(),
286
- categorical_features: z.array(z.object({
287
- feature: z.string(),
288
- categories: z.array(z.string()),
289
- weight: z.number().positive()
290
- })).optional(),
291
- normalize: z.union([z.enum(["all", "auto", "mad", "sigmoidal"]), z.array(z.string())]).optional().default("auto"),
292
- sigma_f: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
293
- learning_rate: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.union([z.number(), z.object({
294
- ordering: z.tuple([z.number(), z.number()]),
295
- convergence: z.tuple([z.number(), z.number()]),
296
- })]).optional()),
297
- batch_size: z.number().int().optional(),
298
- quality_metrics: z.union([z.enum(["fast", "standard", "full"]), z.array(z.string())]).optional(),
299
- backend: z.enum(["auto", "cpu", "gpu", "gpu_graphs"]).optional().default("auto"),
300
- output_format: z.enum(["png", "pdf", "svg"]).optional().default("png"),
301
- output_dpi: z.enum(["standard", "retina", "print"]).optional().default("retina"),
302
- colormap: z.string().optional(),
303
- row_range: z.tuple([z.number().int().min(1), z.number().int().min(1)]).optional(),
304
- cv_folds: z.number().int().min(0).max(20).optional()
305
- .describe("train_impute only: per-column imputation quality → quality.csv with two labeled methods (kfold_holdout_pool = held-out; resubstitution_optimistic = in-sample). 0=off, default 5."),
306
- viz_mode: z.enum(["full", "summary", "summary_plus_top"]).optional()
307
- .describe("Visualization density; auto-capped when feature count > 40"),
308
- viz_top_components: z.number().int().min(0).max(64).optional()
309
- .describe("With viz_mode=summary_plus_top: upload top-N component maps by variance (default 8)"),
310
- emit_cell_uncertainty: z.boolean().optional()
311
- .describe("train_impute: write imputation_uncertainty.csv (pool_std per imputed cell)"),
312
- gamma: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
313
- gamma_f: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
314
- siom_decay: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
315
- siom_penalty: z.enum(["linear", "log", "exp"]).optional(),
316
- penalty_alpha: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
317
- reset_per_epoch: z.boolean().optional(),
318
- siom_feature_geometry: z.enum(["l2", "mixed", "auto"]).optional().describe("train_siom_map: l2 (default) = legacy L2 on cos/sin columns; mixed = cyclic torus distance when cyclic pairs exist; auto = mixed only when cyclic encodings are present"),
319
- siom_qe_backend: z.enum(["cpu", "cuda", "auto"]).optional().describe("train_siom_map: backend for metric-aligned siom_qe when using mixed geometry; auto uses GPU when available"),
320
- siom_qe_batch_size: z.number().int().min(1).max(1_000_000).optional().describe("train_siom_map: batch size for GPU SIOM QE (default 256 on worker)"),
321
- topology: z.enum(["free", "chain"]).optional().default("free"),
322
- max_nodes: z.number().int().min(2).optional(),
323
- effort: z.enum(["quick", "standard", "thorough"]).optional().default("standard"),
324
- n_initial: z.number().int().min(2).optional(),
325
- n_passes: z.number().int().min(1).optional(),
326
- growth_interval: z.number().int().min(1).optional(),
327
- neighborhood_size: z.number().int().min(1).optional(),
328
- edge_max_age: z.number().int().min(1).optional(),
329
- max_degree: z.number().int().min(0).optional(),
330
- error_threshold: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
331
- error_decay: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
332
- ring_close_threshold: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
333
- sigma_0: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
334
- eta_0: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
335
- eta_f: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
336
- elastic_lambda: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
337
- elastic_mu: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
338
- elastic_anchor: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
339
- anchor_percentile: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
161
+ .describe("Optional run label for the deprecated train_* forwarders (≤120 chars; appears in jobs(list) and the jobs(compare) table)."),
162
+ inputs: z
163
+ .array(z.object({
164
+ dataset_id: z.string().optional(),
165
+ rows: z.array(z.record(z.string(), z.number())).optional(),
166
+ }))
167
+ .optional()
168
+ .describe("Deprecated batch_predict forwarder: array of inputs; each item has dataset_id and/or rows."),
169
+ // Deprecated: training parameters accepted only by the jobs(train_*) forwarders.
170
+ // Submit training via the `train` tool instead; these are removed in a later release.
171
+ ...TRAINING_INPUT_SCHEMA,
340
172
  }, async (args) => {
341
173
  const { action, job_id, job_ids, dataset_id } = args;
342
- if (action === "run_baseline_study") {
343
- if (!dataset_id)
344
- throw new Error("jobs(run_baseline_study) requires dataset_id");
345
- // Fetch dataset preview to get row count
346
- const dsInfo = await apiCall("GET", `/v1/datasets/${dataset_id}/preview?n_rows=1`);
347
- const nSamples = dsInfo.total_rows || 1000;
348
- // Heuristic grid size: 5 * sqrt(N)
349
- const totalNodes = Math.round(5 * Math.sqrt(nSamples));
350
- const side = Math.max(5, Math.round(Math.sqrt(totalNodes)));
351
- const params = {
352
- model: "SOM",
353
- periodic: false,
354
- normalize: "mad", // robust normalization
355
- grid: [side, side],
356
- epochs: [10, 20],
357
- batch_size: 32,
358
- quality_metrics: "standard",
359
- output_format: "png",
360
- output_dpi: 2,
361
- colormap: "coolwarm"
174
+ // ---- Deprecated forwarders -> train / inference ----
175
+ const mapped = TRAIN_ALIAS[action];
176
+ if (mapped) {
177
+ const result = await runTrain(mapped, args);
178
+ return {
179
+ content: [
180
+ ...result.content,
181
+ { type: "text", text: `Note: jobs(action=${action}) is deprecated; use train(action=${mapped}).` },
182
+ ],
362
183
  };
363
- const data = (await apiCall("POST", "/v1/jobs", { dataset_id, params }));
364
- const jid = String(data.id ?? "");
365
- return { content: [{ type: "text", text: `Baseline study submitted. Job ID: ${jid}\nGrid size: ${side}x${side}\nNormalization: MAD\n\nPoll with jobs(action=status, job_id="${jid}") until complete, then retrieve with results(action=get, job_id="${jid}"). Optional: training_monitor(job_id="${jid}") for a visual panel—not required.` }] };
366
184
  }
367
- if (action === "train_map" || action === "train_siom_map" || action === "train_impute") {
368
- 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;
369
- let PRESETS = {};
370
- try {
371
- PRESETS = await fetchTrainingPresets();
372
- }
373
- catch (e) {
374
- if (e.httpStatus === 401 || e.httpStatus === 403)
375
- throw e;
376
- if (preset && grid_x === undefined && epochs === undefined) {
377
- throw new Error("Could not fetch training config from server, and missing explicit grid/epochs.");
378
- }
379
- }
380
- if (!dataset_id)
381
- throw new Error(`jobs(${action}) requires dataset_id`);
382
- const { params, paramSummary, effectiveGrid } = buildTrainMapParams({
383
- preset, grid_x, grid_y, epochs, model, periodic, columns, cyclic_features,
384
- temporal_features, feature_weights, transforms, auto_log_transforms,
385
- time_delay_embeddings, categorical_features,
386
- normalize, sigma_f, learning_rate, batch_size, quality_metrics, backend,
387
- output_format, output_dpi, colormap, row_range,
388
- }, PRESETS);
389
- if (action === "train_impute") {
390
- params._job_type = "train_impute";
391
- params.model = model ?? "auto";
392
- params.cv_folds = cv_folds ?? 5;
393
- if (viz_mode !== undefined)
394
- params.viz_mode = viz_mode;
395
- if (viz_top_components !== undefined)
396
- params.viz_top_components = viz_top_components;
397
- if (emit_cell_uncertainty !== undefined)
398
- params.emit_cell_uncertainty = emit_cell_uncertainty;
399
- // Plain numeric path only — strip unsupported keys if caller passed them
400
- delete params.cyclic_features;
401
- delete params.temporal_features;
402
- delete params.categorical_features;
403
- delete params.transforms;
404
- delete params.auto_log_transforms;
405
- delete params.time_delay_embeddings;
406
- }
407
- if (action === "train_siom_map") {
408
- params._job_type = "train_siom";
409
- if (gamma !== undefined)
410
- params.gamma = gamma;
411
- if (gamma_f !== undefined)
412
- params.gamma_f = gamma_f;
413
- if (siom_decay !== undefined)
414
- params.siom_decay = siom_decay;
415
- if (siom_penalty !== undefined)
416
- params.siom_penalty = siom_penalty;
417
- if (penalty_alpha !== undefined)
418
- params.penalty_alpha = penalty_alpha;
419
- if (reset_per_epoch !== undefined)
420
- params.reset_per_epoch = reset_per_epoch;
421
- if (siom_feature_geometry !== undefined)
422
- params.siom_feature_geometry = siom_feature_geometry;
423
- if (siom_qe_backend !== undefined)
424
- params.siom_qe_backend = siom_qe_backend;
425
- if (siom_qe_batch_size !== undefined)
426
- params.siom_qe_batch_size = siom_qe_batch_size;
427
- }
428
- let totalRows = 0;
429
- try {
430
- const preview = (await apiCall("GET", `/v1/datasets/${dataset_id}/preview?n_rows=1`));
431
- totalRows = Number(preview?.total_rows ?? 0);
432
- }
433
- catch { /* ignore */ }
434
- const submitBody = { dataset_id, params };
435
- if (label && label.trim() !== "")
436
- submitBody.label = label;
437
- const data = (await apiCall("POST", "/v1/jobs", submitBody));
438
- const newJobId = data.id;
439
- const variantPrefix = action === "train_siom_map" ? "variant=siom"
440
- : action === "train_impute" ? "variant=train_impute"
441
- : "variant=som";
442
- data.effective_params = `${variantPrefix}, ${paramSummary}`;
443
- try {
444
- const sys = (await apiCall("GET", "/v1/system/info"));
445
- const pending = Number(sys.status?.pending_jobs ?? sys.pending_jobs ?? 0);
446
- const totalEta = Number(sys.training_time_estimates_seconds?.total ??
447
- (sys.gpu_available ? 45 : 120));
448
- const waitMinutes = Math.round((pending * totalEta) / 60);
449
- let msg = `Job submitted (${variantPrefix}, ${paramSummary}). `;
450
- if (waitMinutes > 1)
451
- msg += `You are #${pending + 1} in queue. Estimated wait: ~${waitMinutes} min. `;
452
- msg += `Poll with jobs(action=status, job_id="${newJobId}"). When status is completed, use results(action=get, job_id="${newJobId}") to view the map${action === "train_impute" ? " and imputed.csv" : ""} and metrics.`;
453
- msg += ` Optional: training_monitor(job_id="${newJobId}") for charts—not required.`;
454
- if (effectiveGrid && totalRows > 0 && effectiveGrid[0] * effectiveGrid[1] > totalRows * 0.75) {
455
- msg += ` Note: Grid may be large for ${totalRows} rows (consider grid=auto for fewer dead nodes).`;
456
- }
457
- data.message = msg;
458
- }
459
- catch {
460
- let msg = `Job submitted (${variantPrefix}, ${paramSummary}). Poll with jobs(action=status, job_id="${newJobId}"). When status is completed, use results(action=get, job_id="${newJobId}") to view the map${action === "train_impute" ? " and imputed.csv" : ""} and metrics.`;
461
- msg += ` Optional: training_monitor(job_id="${newJobId}") for charts—not required.`;
462
- if (effectiveGrid && totalRows > 0 && effectiveGrid[0] * effectiveGrid[1] > totalRows * 0.75) {
463
- msg += ` Note: Grid may be large for ${totalRows} rows (consider grid=auto for fewer dead nodes).`;
464
- }
465
- data.message = msg;
466
- }
467
- return textResult(data);
468
- }
469
- if (action === "train_floop_siom") {
470
- const { columns, cyclic_features, temporal_features, feature_weights, transforms, auto_log_transforms, normalize, row_range, output_format, output_dpi, colormap, topology, max_nodes, effort, n_initial, n_passes, growth_interval, neighborhood_size, edge_max_age, max_degree, gamma, siom_decay, siom_penalty, penalty_alpha, error_threshold, error_decay, ring_close_threshold, sigma_0, sigma_f, eta_0, eta_f, elastic_lambda, elastic_mu, elastic_anchor, anchor_percentile, label, } = args;
471
- if (!dataset_id)
472
- throw new Error("jobs(train_floop_siom) requires dataset_id");
473
- const params = {
474
- _job_type: "train_floop_siom",
475
- topology,
476
- effort,
477
- normalize,
478
- output_format: output_format ?? "png",
185
+ if (action === "batch_predict") {
186
+ const result = await runBatchPredict({ job_id, inputs: args.inputs });
187
+ return {
188
+ content: [
189
+ ...result.content,
190
+ { type: "text", text: "Note: jobs(action=batch_predict) is deprecated; use inference(action=batch_predict)." },
191
+ ],
479
192
  };
480
- if (max_nodes !== undefined)
481
- params.max_nodes = max_nodes;
482
- if (columns?.length)
483
- params.columns = columns;
484
- if (cyclic_features?.length)
485
- params.cyclic_features = cyclic_features;
486
- if (temporal_features?.length)
487
- params.temporal_features = temporal_features;
488
- if (feature_weights && Object.keys(feature_weights).length > 0)
489
- params.feature_weights = feature_weights;
490
- if (transforms && Object.keys(transforms).length > 0)
491
- params.transforms = transforms;
492
- if (auto_log_transforms)
493
- params.auto_log_transforms = auto_log_transforms;
494
- if (row_range && row_range.length >= 2 && row_range[0] <= row_range[1])
495
- params.row_range = row_range;
496
- const dpiMap = { standard: 1, retina: 2, print: 4 };
497
- if (output_dpi && output_dpi !== "retina")
498
- params.output_dpi = dpiMap[output_dpi] ?? 2;
499
- if (colormap)
500
- params.colormap = colormap;
501
- if (n_initial !== undefined)
502
- params.n_initial = n_initial;
503
- if (n_passes !== undefined)
504
- params.n_passes = n_passes;
505
- if (growth_interval !== undefined)
506
- params.growth_interval = growth_interval;
507
- if (neighborhood_size !== undefined)
508
- params.neighborhood_size = neighborhood_size;
509
- if (edge_max_age !== undefined)
510
- params.edge_max_age = edge_max_age;
511
- if (max_degree !== undefined)
512
- params.max_degree = max_degree;
513
- if (gamma !== undefined)
514
- params.gamma = gamma;
515
- if (siom_decay !== undefined)
516
- params.siom_decay = siom_decay;
517
- if (siom_penalty !== undefined)
518
- params.siom_penalty = siom_penalty;
519
- if (penalty_alpha !== undefined)
520
- params.penalty_alpha = penalty_alpha;
521
- if (error_threshold !== undefined)
522
- params.error_threshold = error_threshold;
523
- if (error_decay !== undefined)
524
- params.error_decay = error_decay;
525
- if (ring_close_threshold !== undefined)
526
- params.ring_close_threshold = ring_close_threshold;
527
- if (sigma_0 !== undefined)
528
- params.sigma_0 = sigma_0;
529
- if (sigma_f !== undefined)
530
- params.sigma_f = sigma_f;
531
- if (eta_0 !== undefined)
532
- params.eta_0 = eta_0;
533
- if (eta_f !== undefined)
534
- params.eta_f = eta_f;
535
- if (elastic_lambda !== undefined)
536
- params.elastic_lambda = elastic_lambda;
537
- if (elastic_mu !== undefined)
538
- params.elastic_mu = elastic_mu;
539
- if (elastic_anchor !== undefined)
540
- params.elastic_anchor = elastic_anchor;
541
- if (anchor_percentile !== undefined)
542
- params.anchor_percentile = anchor_percentile;
543
- const submitBody = { dataset_id, params };
544
- if (label && label.trim() !== "")
545
- submitBody.label = label;
546
- const data = (await apiCall("POST", "/v1/jobs", submitBody));
547
- const newJobId = data.id;
548
- const maxNodeSummary = max_nodes === undefined ? "max_nodes=auto(~2*sqrt(n_samples))" : `max_nodes=${max_nodes}`;
549
- const paramSummary = [
550
- "variant=floop",
551
- `topology=${topology ?? "free"}`,
552
- maxNodeSummary,
553
- `effort=${effort ?? "standard"}`,
554
- gamma !== undefined ? `gamma=${gamma}` : "",
555
- ].filter(Boolean).join(", ");
556
- data.effective_params = paramSummary;
557
- data.message =
558
- `Job submitted (${paramSummary}). Poll with jobs(action=status, job_id="${newJobId}"). ` +
559
- `When status is completed, use results(action=get, job_id="${newJobId}") to view FLooP-SIOM figures and metrics. ` +
560
- `Optional: training_monitor(job_id="${newJobId}") for charts—not required.`;
561
- return textResult(data);
562
193
  }
194
+ // ---- Lifecycle ----
563
195
  if (action === "status") {
564
196
  if (!job_id)
565
197
  throw new Error("jobs(status) requires job_id");
@@ -615,22 +247,6 @@ export function registerJobsTool(server, description) {
615
247
  const data = await apiCall("DELETE", `/v1/jobs/${job_id}`);
616
248
  return textResult(data);
617
249
  }
618
- if (action === "batch_predict") {
619
- if (!job_id)
620
- throw new Error("jobs(batch_predict) requires job_id (parent training job)");
621
- if (!args.inputs || !Array.isArray(args.inputs) || args.inputs.length === 0) {
622
- throw new Error("jobs(batch_predict) requires inputs (non-empty array of { dataset_id? or rows? })");
623
- }
624
- const body = { inputs: args.inputs };
625
- const data = (await apiCall("POST", `/v1/results/${job_id}/batch_predict`, body));
626
- const ids = (data.ids ?? []);
627
- return {
628
- content: [{
629
- type: "text",
630
- text: `Batch predict: ${ids.length} job(s) submitted. Parent: ${job_id}. Poll each with jobs(action=status, job_id="<id>"). IDs: ${ids.join(", ")}`,
631
- }],
632
- };
633
- }
634
250
  throw new Error("Invalid action");
635
251
  });
636
252
  }
@@ -307,7 +307,7 @@ NOT FOR: Jobs that haven't completed. Use jobs(action=status) to check first.`,
307
307
  if (idxs)
308
308
  lines.push(`Selected indices: [${idxs.join(", ")}]`);
309
309
  }
310
- lines.push(`The new columns are appended to the source dataset and ready to use in jobs(action=train_map, columns=[..., "${outCols[0] ?? "feat_1"}", …]).`);
310
+ lines.push(`The new columns are appended to the source dataset and ready to use in train(action=map, columns=[..., "${outCols[0] ?? "feat_1"}", …]).`);
311
311
  content.push({ type: "text", text: lines.join("\n") });
312
312
  }
313
313
  else if (jobType === "train_floop_siom") {