@barivia/barsom-mcp 0.11.1 → 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,56 +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 |
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) |
15
- | train_floop_chain | Deprecated alias for train_floop_siom — same behavior; prefer train_floop_siom |
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. |
16
14
  | cancel | Stopping a running or pending job to free the worker |
17
15
  | delete | Permanently removing a job and all its S3 result files |
18
16
 
19
- 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.
20
18
 
21
19
  ASYNC POLLING PROTOCOL (action=status):
22
20
  - Poll every 10-15 seconds. Do NOT poll faster — it wastes context.
23
- - 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.
24
22
  - For large grids (40×40+), do not assume failure before 3 minutes on CPU.
25
- - Wait for status "completed" before calling results(action=get).
26
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.
27
25
 
28
- ESCALATION (action=status):
29
- - completed → call results(action=get) to retrieve the map and metrics
30
- - failed → error message and optional failure_stage (e.g. preprocessing, training, metrics, visualization, upload) indicate which phase broke:
31
- - memory/allocation error: reduce batch_size or grid size and retrain
32
- - column missing: verify with datasets(action=preview)
33
- - NaN error on train_map: use jobs(action=train_impute) for sparse training columns, or clean the CSV for complete-case train_map
34
-
35
- action=train_map / train_siom_map: Submits a grid-map training job. Returns job_id — poll with jobs(action=status, job_id=...).
36
- action=train_impute: Submits missing-tolerant map training (accelerated missSOM / SIOM missSOM). 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 (SIOM on som_siom+ plans), cv_folds=5 → quality.csv. quality.csv reports two clearly-labeled numbers per column: kfold_holdout_pool (honest held-out) and resubstitution_optimistic (in-sample, optimistic). status returns progress_phase (e.g. ordering/convergence/cv/upload). High-dimensional datasets automatically adjust visualization density, metric set, and backend (call training_guidance for specifics). Params: viz_mode, viz_top_components, emit_cell_uncertainty, quality_metrics. Artifacts: imputed.csv, imputation_mask.csv, optional imputation_uncertainty.csv. Completed train_impute job_id is a valid parent for inference(impute_column).
37
- Presets: quick | standard | refined | high_res — use preset=... for grid/epochs/batch defaults; call training_guidance for details.
38
- Presets refined/high_res may use GPU. On CPU-only hosts pass backend=cpu. API expects strings "cpu" | "gpu" | "gpu_graphs" (no colon). Future backends (e.g. non-CUDA) may be added under the same contract.
39
- normalize: "auto" (default) = scale only non-cyclic features; "all" = scale every feature. Use "auto" when using cyclic_features.
40
- categorical_features: optional baseline categorical support using explicit weighted one-hot encoding. Provide the raw column name, allowed categories, and a weight. Advanced categorical embeddings are intentionally outside this default tool surface.
41
- label (optional, ≤120 chars): user-supplied run name; appears in jobs(list) and jobs(compare) output. Strongly recommended for sweeps so compare rows stay readable.
42
- train_siom_map only: siom_feature_geometry l2 (default) | mixed | auto — torus distance on cyclic (cos,sin) pairs when mixed or auto with cyclic encodings. siom_qe_backend cpu|cuda|auto and siom_qe_batch_size align siom_qe with training geometry when mixed.
43
- action=train_floop_siom (preferred) or train_floop_chain (deprecated alias): Submits FLooP-SIOM — a growing node-budget manifold instead of a fixed hex grid. **Only if the API key’s plan includes all_algorithms** (Premium or Enterprise); otherwise the API returns a clear upgrade message. Default topology is free (CHL dynamic graph); topology=chain is optional for a strict 1D linked-list backbone.
44
- Key params: topology (default free), max_nodes, effort, gamma, siom_decay.
45
- max_nodes is a total node budget, not a grid width or area. If omitted, the backend uses a dataset-size heuristic (roughly 2*sqrt(n_samples), capped for stability).
46
- effort controls passes only: quick≈15, standard≈30, thorough≈60. If coverage collapses, reduce max_nodes before increasing effort.
47
- action=compare: Returns a metrics table (QE, TE, explained variance, silhouette) for 2+ jobs. The table only shows hyperparameter columns that actually differ across the compared runs (preset, backend, batch_size, normalize, periodic, n_features, etc.) so multi-run sweeps stay readable. Tip: pass label="sweep_a" / "sweep_b" on train_map to make rows self-explanatory.
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.
48
27
  action=cancel: Not instant — worker checks between phases. Expect up to 30s delay.
49
28
  action=delete: WARNING — job ID will no longer work with results or any other tool.`;
50
- export async function fetchTrainingPresets() {
51
- const configData = (await apiCall("GET", "/v1/training/config"));
52
- return configData?.presets || {};
53
- }
54
29
  /**
55
30
  * Render the jobs(compare) markdown table. Strategy: always show job_id (or label
56
31
  * if present) + the four core metrics (QE, TE, Expl.Var, Silhouette); add any
@@ -142,435 +117,81 @@ export function renderCompareTable(comparisons) {
142
117
  }
143
118
  if (varying.length === 0 && ok.length >= 2) {
144
119
  lines.push("");
145
- 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.)");
146
121
  }
147
122
  else if (!anyLabel && ok.length >= 2) {
148
123
  lines.push("");
149
- 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.");
150
125
  }
151
126
  return lines.join("\n");
152
127
  }
153
- export function buildTrainMapParams(args, presets) {
154
- 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;
155
- const p = preset ? presets[preset] : undefined;
156
- const params = {
157
- model: model ?? "SOM",
158
- periodic: periodic ?? true,
159
- normalize: normalize ?? "auto",
160
- };
161
- if (grid_x !== undefined && grid_y !== undefined)
162
- params.grid = [grid_x, grid_y];
163
- else if (p)
164
- params.grid = p.grid;
165
- if (epochs !== undefined)
166
- params.epochs = epochs;
167
- else if (p)
168
- params.epochs = p.epochs;
169
- if (cyclic_features?.length)
170
- params.cyclic_features = cyclic_features;
171
- if (columns?.length)
172
- params.columns = columns;
173
- if (auto_log_transforms)
174
- params.auto_log_transforms = auto_log_transforms;
175
- if (time_delay_embeddings?.length)
176
- params.time_delay_embeddings = time_delay_embeddings;
177
- if (categorical_features?.length)
178
- params.categorical_features = categorical_features;
179
- if (transforms && Object.keys(transforms).length > 0)
180
- params.transforms = transforms;
181
- if (temporal_features?.length)
182
- params.temporal_features = temporal_features;
183
- if (feature_weights && Object.keys(feature_weights).length > 0)
184
- params.feature_weights = feature_weights;
185
- if (sigma_f !== undefined)
186
- params.sigma_f = sigma_f;
187
- if (learning_rate !== undefined)
188
- params.learning_rate = learning_rate;
189
- if (batch_size !== undefined)
190
- params.batch_size = batch_size;
191
- else if (p)
192
- params.batch_size = p.batch_size;
193
- if (quality_metrics !== undefined)
194
- params.quality_metrics = quality_metrics;
195
- if (backend !== undefined && backend !== "auto")
196
- params.backend = backend;
197
- else if (p?.backend)
198
- params.backend = p.backend;
199
- params.output_format = output_format ?? "png";
200
- const dpiMap = { standard: 1, retina: 2, print: 4 };
201
- if (output_dpi && output_dpi !== "retina")
202
- params.output_dpi = dpiMap[output_dpi] ?? 2;
203
- if (colormap)
204
- params.colormap = colormap;
205
- if (row_range && row_range.length >= 2 && row_range[0] <= row_range[1])
206
- params.row_range = row_range;
207
- if (siom_feature_geometry !== undefined)
208
- params.siom_feature_geometry = siom_feature_geometry;
209
- if (siom_qe_backend !== undefined)
210
- params.siom_qe_backend = siom_qe_backend;
211
- if (siom_qe_batch_size !== undefined)
212
- params.siom_qe_batch_size = siom_qe_batch_size;
213
- const effectiveGrid = params.grid;
214
- const effectiveEpochs = params.epochs;
215
- const effectiveBatch = params.batch_size;
216
- const effectiveBackend = params.backend;
217
- const paramSummary = [
218
- effectiveGrid ? `grid=${effectiveGrid[0]}×${effectiveGrid[1]}` : "grid=auto",
219
- effectiveEpochs ? `epochs=${Array.isArray(effectiveEpochs) ? effectiveEpochs.join("+") : effectiveEpochs}` : "epochs=auto",
220
- effectiveBatch ? `batch=${effectiveBatch}` : "batch=auto",
221
- `model=${params.model}`,
222
- effectiveBackend ? `backend=${effectiveBackend}` : "",
223
- preset ? `(from preset: ${preset})` : "",
224
- ].filter(Boolean).join(", ");
225
- return { params, paramSummary, effectiveGrid };
226
- }
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
+ };
227
136
  export function registerJobsTool(server, description) {
228
137
  registerAuditedTool(server, "jobs", description, {
229
138
  action: z
230
- .enum(["status", "list", "compare", "cancel", "delete", "train_map", "train_impute", "train_siom_map", "train_floop_siom", "train_floop_chain", "batch_predict", "run_baseline_study"])
231
- .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 (preferred); train_floop_chain: deprecated alias for train_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)."),
232
145
  job_id: z
233
146
  .string()
234
147
  .optional()
235
- .describe("Job ID — required for action=status, cancel, delete, batch_predict (parent training job)"),
236
- inputs: z
237
- .array(z.object({
238
- dataset_id: z.string().optional(),
239
- rows: z.array(z.record(z.string(), z.number())).optional(),
240
- }))
241
- .optional()
242
- .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)."),
243
149
  job_ids: z
244
150
  .array(z.string())
245
151
  .optional()
246
- .describe("Array of job IDs — required for action=compare (minimum 2)"),
152
+ .describe("Array of job IDs — required for action=compare (minimum 2)."),
247
153
  dataset_id: z
248
154
  .string()
249
155
  .optional()
250
- .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."),
251
157
  label: z
252
158
  .string()
253
159
  .max(120)
254
160
  .optional()
255
- .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\")."),
256
- preset: z.enum(["quick", "standard", "refined", "high_res"]).optional(),
257
- grid_x: z.number().int().optional()
258
- .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."),
259
- grid_y: z.number().int().optional().describe("Grid height. See grid_x for auto-sizing."),
260
- epochs: z.preprocess((v) => {
261
- if (v === undefined || v === null)
262
- return v;
263
- if (typeof v === "string") {
264
- const n = parseInt(v, 10);
265
- if (!Number.isNaN(n))
266
- return n;
267
- const m = v.match(/^\[\s*(\d+)\s*,\s*(\d+)\s*\]$/);
268
- if (m)
269
- return [parseInt(m[1], 10), parseInt(m[2], 10)];
270
- }
271
- return v;
272
- }, z.union([z.number().int(), z.array(z.number().int()).length(2)]).optional()),
273
- model: z.enum(["auto", "SOM", "SIOM", "RSOM", "SOM-SOFT", "RSOM-SOFT"]).optional(),
274
- periodic: z.boolean().optional().default(true),
275
- columns: z.array(z.string()).optional(),
276
- cyclic_features: z.array(z.object({
277
- feature: z.string(),
278
- period: z.number(),
279
- })).optional(),
280
- temporal_features: z.array(z.object({
281
- columns: z.array(z.string()),
282
- format: z.string(),
283
- extract: z.array(z.enum(["hour_of_day", "day_of_year", "month", "day_of_week", "minute_of_hour"])),
284
- cyclic: z.boolean().default(true),
285
- separator: z.string().optional(),
286
- })).optional(),
287
- feature_weights: z.record(z.number()).optional(),
288
- transforms: z.record(z.enum(["log", "log1p", "log10", "sqrt", "square", "abs", "invert", "rank", "none"])).optional(),
289
- auto_log_transforms: z.boolean().optional().default(false),
290
- time_delay_embeddings: z.array(z.object({
291
- feature: z.string(),
292
- lags: z.array(z.number().int().min(1))
293
- })).optional(),
294
- categorical_features: z.array(z.object({
295
- feature: z.string(),
296
- categories: z.array(z.string()),
297
- weight: z.number().positive()
298
- })).optional(),
299
- normalize: z.union([z.enum(["all", "auto", "mad", "sigmoidal"]), z.array(z.string())]).optional().default("auto"),
300
- sigma_f: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
301
- learning_rate: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.union([z.number(), z.object({
302
- ordering: z.tuple([z.number(), z.number()]),
303
- convergence: z.tuple([z.number(), z.number()]),
304
- })]).optional()),
305
- batch_size: z.number().int().optional(),
306
- quality_metrics: z.union([z.enum(["fast", "standard", "full"]), z.array(z.string())]).optional(),
307
- backend: z.enum(["auto", "cpu", "gpu", "gpu_graphs"]).optional().default("auto"),
308
- output_format: z.enum(["png", "pdf", "svg"]).optional().default("png"),
309
- output_dpi: z.enum(["standard", "retina", "print"]).optional().default("retina"),
310
- colormap: z.string().optional(),
311
- row_range: z.tuple([z.number().int().min(1), z.number().int().min(1)]).optional(),
312
- cv_folds: z.number().int().min(0).max(20).optional()
313
- .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."),
314
- viz_mode: z.enum(["full", "summary", "summary_plus_top"]).optional()
315
- .describe("Visualization density; auto-capped when feature count > 40"),
316
- viz_top_components: z.number().int().min(0).max(64).optional()
317
- .describe("With viz_mode=summary_plus_top: upload top-N component maps by variance (default 8)"),
318
- emit_cell_uncertainty: z.boolean().optional()
319
- .describe("train_impute: write imputation_uncertainty.csv (pool_std per imputed cell)"),
320
- gamma: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
321
- gamma_f: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
322
- siom_decay: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
323
- siom_penalty: z.enum(["linear", "log", "exp"]).optional(),
324
- penalty_alpha: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
325
- reset_per_epoch: z.boolean().optional(),
326
- 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"),
327
- 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"),
328
- 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)"),
329
- topology: z.enum(["free", "chain"]).optional().default("free"),
330
- max_nodes: z.number().int().min(2).optional(),
331
- effort: z.enum(["quick", "standard", "thorough"]).optional().default("standard"),
332
- n_initial: z.number().int().min(2).optional(),
333
- n_passes: z.number().int().min(1).optional(),
334
- growth_interval: z.number().int().min(1).optional(),
335
- neighborhood_size: z.number().int().min(1).optional(),
336
- edge_max_age: z.number().int().min(1).optional(),
337
- max_degree: z.number().int().min(0).optional(),
338
- error_threshold: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
339
- error_decay: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
340
- ring_close_threshold: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
341
- sigma_0: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
342
- eta_0: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
343
- eta_f: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
344
- elastic_lambda: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
345
- elastic_mu: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
346
- elastic_anchor: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
347
- 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,
348
172
  }, async (args) => {
349
173
  const { action, job_id, job_ids, dataset_id } = args;
350
- if (action === "run_baseline_study") {
351
- if (!dataset_id)
352
- throw new Error("jobs(run_baseline_study) requires dataset_id");
353
- // Fetch dataset preview to get row count
354
- const dsInfo = await apiCall("GET", `/v1/datasets/${dataset_id}/preview?n_rows=1`);
355
- const nSamples = dsInfo.total_rows || 1000;
356
- // Heuristic grid size: 5 * sqrt(N)
357
- const totalNodes = Math.round(5 * Math.sqrt(nSamples));
358
- const side = Math.max(5, Math.round(Math.sqrt(totalNodes)));
359
- const params = {
360
- model: "SOM",
361
- periodic: false,
362
- normalize: "mad", // robust normalization
363
- grid: [side, side],
364
- epochs: [10, 20],
365
- batch_size: 32,
366
- quality_metrics: "standard",
367
- output_format: "png",
368
- output_dpi: 2,
369
- 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
+ ],
370
183
  };
371
- const data = (await apiCall("POST", "/v1/jobs", { dataset_id, params }));
372
- const jid = String(data.id ?? "");
373
- 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.` }] };
374
184
  }
375
- if (action === "train_map" || action === "train_siom_map" || action === "train_impute") {
376
- 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;
377
- let PRESETS = {};
378
- try {
379
- PRESETS = await fetchTrainingPresets();
380
- }
381
- catch (e) {
382
- if (e.httpStatus === 401 || e.httpStatus === 403)
383
- throw e;
384
- if (preset && grid_x === undefined && epochs === undefined) {
385
- throw new Error("Could not fetch training config from server, and missing explicit grid/epochs.");
386
- }
387
- }
388
- if (!dataset_id)
389
- throw new Error(`jobs(${action}) requires dataset_id`);
390
- const { params, paramSummary, effectiveGrid } = buildTrainMapParams({
391
- preset, grid_x, grid_y, epochs, model, periodic, columns, cyclic_features,
392
- temporal_features, feature_weights, transforms, auto_log_transforms,
393
- time_delay_embeddings, categorical_features,
394
- normalize, sigma_f, learning_rate, batch_size, quality_metrics, backend,
395
- output_format, output_dpi, colormap, row_range,
396
- }, PRESETS);
397
- if (action === "train_impute") {
398
- params._job_type = "train_impute";
399
- params.model = model ?? "auto";
400
- params.cv_folds = cv_folds ?? 5;
401
- if (viz_mode !== undefined)
402
- params.viz_mode = viz_mode;
403
- if (viz_top_components !== undefined)
404
- params.viz_top_components = viz_top_components;
405
- if (emit_cell_uncertainty !== undefined)
406
- params.emit_cell_uncertainty = emit_cell_uncertainty;
407
- // Plain numeric path only — strip unsupported keys if caller passed them
408
- delete params.cyclic_features;
409
- delete params.temporal_features;
410
- delete params.categorical_features;
411
- delete params.transforms;
412
- delete params.auto_log_transforms;
413
- delete params.time_delay_embeddings;
414
- }
415
- if (action === "train_siom_map") {
416
- params._job_type = "train_siom";
417
- if (gamma !== undefined)
418
- params.gamma = gamma;
419
- if (gamma_f !== undefined)
420
- params.gamma_f = gamma_f;
421
- if (siom_decay !== undefined)
422
- params.siom_decay = siom_decay;
423
- if (siom_penalty !== undefined)
424
- params.siom_penalty = siom_penalty;
425
- if (penalty_alpha !== undefined)
426
- params.penalty_alpha = penalty_alpha;
427
- if (reset_per_epoch !== undefined)
428
- params.reset_per_epoch = reset_per_epoch;
429
- if (siom_feature_geometry !== undefined)
430
- params.siom_feature_geometry = siom_feature_geometry;
431
- if (siom_qe_backend !== undefined)
432
- params.siom_qe_backend = siom_qe_backend;
433
- if (siom_qe_batch_size !== undefined)
434
- params.siom_qe_batch_size = siom_qe_batch_size;
435
- }
436
- let totalRows = 0;
437
- try {
438
- const preview = (await apiCall("GET", `/v1/datasets/${dataset_id}/preview?n_rows=1`));
439
- totalRows = Number(preview?.total_rows ?? 0);
440
- }
441
- catch { /* ignore */ }
442
- const submitBody = { dataset_id, params };
443
- if (label && label.trim() !== "")
444
- submitBody.label = label;
445
- const data = (await apiCall("POST", "/v1/jobs", submitBody));
446
- const newJobId = data.id;
447
- const variantPrefix = action === "train_siom_map" ? "variant=siom"
448
- : action === "train_impute" ? "variant=train_impute"
449
- : "variant=som";
450
- data.effective_params = `${variantPrefix}, ${paramSummary}`;
451
- try {
452
- const sys = (await apiCall("GET", "/v1/system/info"));
453
- const pending = Number(sys.status?.pending_jobs ?? sys.pending_jobs ?? 0);
454
- const totalEta = Number(sys.training_time_estimates_seconds?.total ??
455
- (sys.gpu_available ? 45 : 120));
456
- const waitMinutes = Math.round((pending * totalEta) / 60);
457
- let msg = `Job submitted (${variantPrefix}, ${paramSummary}). `;
458
- if (waitMinutes > 1)
459
- msg += `You are #${pending + 1} in queue. Estimated wait: ~${waitMinutes} min. `;
460
- 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.`;
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
- catch {
468
- 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.`;
469
- msg += ` Optional: training_monitor(job_id="${newJobId}") for charts—not required.`;
470
- if (effectiveGrid && totalRows > 0 && effectiveGrid[0] * effectiveGrid[1] > totalRows * 0.75) {
471
- msg += ` Note: Grid may be large for ${totalRows} rows (consider grid=auto for fewer dead nodes).`;
472
- }
473
- data.message = msg;
474
- }
475
- return textResult(data);
476
- }
477
- if (action === "train_floop_siom" || action === "train_floop_chain") {
478
- 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;
479
- if (!dataset_id)
480
- throw new Error("jobs(train_floop_siom) requires dataset_id");
481
- const params = {
482
- _job_type: "train_floop_siom",
483
- topology,
484
- effort,
485
- normalize,
486
- 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
+ ],
487
192
  };
488
- if (max_nodes !== undefined)
489
- params.max_nodes = max_nodes;
490
- if (columns?.length)
491
- params.columns = columns;
492
- if (cyclic_features?.length)
493
- params.cyclic_features = cyclic_features;
494
- if (temporal_features?.length)
495
- params.temporal_features = temporal_features;
496
- if (feature_weights && Object.keys(feature_weights).length > 0)
497
- params.feature_weights = feature_weights;
498
- if (transforms && Object.keys(transforms).length > 0)
499
- params.transforms = transforms;
500
- if (auto_log_transforms)
501
- params.auto_log_transforms = auto_log_transforms;
502
- if (row_range && row_range.length >= 2 && row_range[0] <= row_range[1])
503
- params.row_range = row_range;
504
- const dpiMap = { standard: 1, retina: 2, print: 4 };
505
- if (output_dpi && output_dpi !== "retina")
506
- params.output_dpi = dpiMap[output_dpi] ?? 2;
507
- if (colormap)
508
- params.colormap = colormap;
509
- if (n_initial !== undefined)
510
- params.n_initial = n_initial;
511
- if (n_passes !== undefined)
512
- params.n_passes = n_passes;
513
- if (growth_interval !== undefined)
514
- params.growth_interval = growth_interval;
515
- if (neighborhood_size !== undefined)
516
- params.neighborhood_size = neighborhood_size;
517
- if (edge_max_age !== undefined)
518
- params.edge_max_age = edge_max_age;
519
- if (max_degree !== undefined)
520
- params.max_degree = max_degree;
521
- if (gamma !== undefined)
522
- params.gamma = gamma;
523
- if (siom_decay !== undefined)
524
- params.siom_decay = siom_decay;
525
- if (siom_penalty !== undefined)
526
- params.siom_penalty = siom_penalty;
527
- if (penalty_alpha !== undefined)
528
- params.penalty_alpha = penalty_alpha;
529
- if (error_threshold !== undefined)
530
- params.error_threshold = error_threshold;
531
- if (error_decay !== undefined)
532
- params.error_decay = error_decay;
533
- if (ring_close_threshold !== undefined)
534
- params.ring_close_threshold = ring_close_threshold;
535
- if (sigma_0 !== undefined)
536
- params.sigma_0 = sigma_0;
537
- if (sigma_f !== undefined)
538
- params.sigma_f = sigma_f;
539
- if (eta_0 !== undefined)
540
- params.eta_0 = eta_0;
541
- if (eta_f !== undefined)
542
- params.eta_f = eta_f;
543
- if (elastic_lambda !== undefined)
544
- params.elastic_lambda = elastic_lambda;
545
- if (elastic_mu !== undefined)
546
- params.elastic_mu = elastic_mu;
547
- if (elastic_anchor !== undefined)
548
- params.elastic_anchor = elastic_anchor;
549
- if (anchor_percentile !== undefined)
550
- params.anchor_percentile = anchor_percentile;
551
- const submitBody = { dataset_id, params };
552
- if (label && label.trim() !== "")
553
- submitBody.label = label;
554
- const data = (await apiCall("POST", "/v1/jobs", submitBody));
555
- const newJobId = data.id;
556
- const maxNodeSummary = max_nodes === undefined ? "max_nodes=auto(~2*sqrt(n_samples))" : `max_nodes=${max_nodes}`;
557
- const paramSummary = [
558
- "variant=floop",
559
- `topology=${topology ?? "free"}`,
560
- maxNodeSummary,
561
- `effort=${effort ?? "standard"}`,
562
- gamma !== undefined ? `gamma=${gamma}` : "",
563
- ].filter(Boolean).join(", ");
564
- data.effective_params = paramSummary;
565
- data.message =
566
- `Job submitted (${paramSummary}). Poll with jobs(action=status, job_id="${newJobId}"). ` +
567
- `When status is completed, use results(action=get, job_id="${newJobId}") to view FLooP-SIOM figures and metrics. ` +
568
- `Optional: training_monitor(job_id="${newJobId}") for charts—not required.` +
569
- (action === "train_floop_chain"
570
- ? " Note: action=train_floop_chain is deprecated; prefer train_floop_siom."
571
- : "");
572
- return textResult(data);
573
193
  }
194
+ // ---- Lifecycle ----
574
195
  if (action === "status") {
575
196
  if (!job_id)
576
197
  throw new Error("jobs(status) requires job_id");
@@ -626,22 +247,6 @@ export function registerJobsTool(server, description) {
626
247
  const data = await apiCall("DELETE", `/v1/jobs/${job_id}`);
627
248
  return textResult(data);
628
249
  }
629
- if (action === "batch_predict") {
630
- if (!job_id)
631
- throw new Error("jobs(batch_predict) requires job_id (parent training job)");
632
- if (!args.inputs || !Array.isArray(args.inputs) || args.inputs.length === 0) {
633
- throw new Error("jobs(batch_predict) requires inputs (non-empty array of { dataset_id? or rows? })");
634
- }
635
- const body = { inputs: args.inputs };
636
- const data = (await apiCall("POST", `/v1/results/${job_id}/batch_predict`, body));
637
- const ids = (data.ids ?? []);
638
- return {
639
- content: [{
640
- type: "text",
641
- text: `Batch predict: ${ids.length} job(s) submitted. Parent: ${job_id}. Poll each with jobs(action=status, job_id="<id>"). IDs: ${ids.join(", ")}`,
642
- }],
643
- };
644
- }
645
250
  throw new Error("Invalid action");
646
251
  });
647
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") {