@barivia/barsom-mcp 0.22.4 → 0.23.2

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.
@@ -4,6 +4,7 @@ import { apiCall } from "../shared.js";
4
4
  import { buildTrainSubmitExtras } from "../train_submit_message.js";
5
5
  import { TRAINING_MONITOR_URI, buildTrainingMonitorResult, } from "./training_monitor.js";
6
6
  import { TRAINING_INPUT_SCHEMA, buildTrainMapParams, buildFloopParams, fetchTrainingPresets, assertValidNormalizationArgs, } from "./training_core.js";
7
+ import { SMALL_N_SOFT, buildSmallNGuardrailLines, suggestedAutoGridSide, } from "../training_scale_guidance.js";
7
8
  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.
8
9
 
9
10
  | Action | Use when |
@@ -13,11 +14,11 @@ export const TRAIN_DESCRIPTION = `Submit a self-organizing map training job. Ret
13
14
  | impute | Sparse training data: trains a missing-tolerant map (accelerated missSOM; SIOM missSOM when model=auto on som_siom+ or model=SIOM) AND returns dense imputed.csv in one job. Plain numeric columns only. Optional SIOM knobs: gamma, siom_decay, siom_penalty, penalty_alpha, reset_per_epoch. |
14
15
  | floop_siom | FLooP-SIOM — a growing node-budget manifold instead of a fixed grid; default topology=free (CHL). Requires a plan with all_algorithms (Premium/Enterprise). |
15
16
 
16
- First quick map (no separate baseline action): use \`train(action=map, preset=quick)\` (or \`normalize=mad\` with a grid sized from the preview row count); see the \`baseline_explore\` preset in **training_guidance**.
17
+ First quick map (no separate baseline action): use \`train(action=map, preset=quick)\` when n is large enough (hundreds+ rows); for n<1000 prefer omitting grid_x/grid_y for auto (~sqrt(5·√n)) or an explicit small grid see **training_guidance** / analyze next_steps. On CPU, preset=quick 15×15 is typically very fast up to ~100k rows — do not subsample first.
17
18
 
18
19
  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 guessing. Backend strings are "cpu" | "gpu" | "gpu_graphs" (no colon); on CPU-only hosts pass backend=cpu.
19
20
 
20
- 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=...).
21
+ 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: preset=quick 15×15 often tens of seconds even at ~30k–100k rows on CPU | 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=...).
21
22
 
22
23
  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).
23
24
  action=floop_siom: max_nodes is a TOTAL node budget (not a grid width/area); if coverage collapses, reduce max_nodes before increasing effort.
@@ -55,6 +56,10 @@ async function submitTrainJob(opts) {
55
56
  const submitBody = { dataset_id: opts.dataset_id, params: opts.params };
56
57
  if (opts.label && opts.label.trim() !== "")
57
58
  submitBody.label = opts.label;
59
+ if (opts.description !== undefined)
60
+ submitBody.description = opts.description;
61
+ if (opts.tags !== undefined)
62
+ submitBody.tags = opts.tags;
58
63
  const data = (await apiCall("POST", "/v1/jobs", submitBody));
59
64
  const newJobId = data.id;
60
65
  data.effective_params = `${opts.variantPrefix}, ${opts.paramSummary}`;
@@ -62,6 +67,11 @@ async function submitTrainJob(opts) {
62
67
  if (opts.gridLargeForRows) {
63
68
  data.grid_auto_warning = true;
64
69
  }
70
+ const guardrails = opts.guardrailLines ?? [];
71
+ if (guardrails.length > 0) {
72
+ data.training_guardrails = guardrails;
73
+ data.small_n_warning = opts.totalRows > 0 && opts.totalRows < SMALL_N_SOFT;
74
+ }
65
75
  const extras = buildTrainSubmitExtras({
66
76
  newJobId,
67
77
  totalRows: opts.totalRows,
@@ -85,8 +95,12 @@ async function submitTrainJob(opts) {
85
95
  catch {
86
96
  /* ignore queue enrichment */
87
97
  }
88
- if (opts.gridLargeForRows) {
89
- msg += ` Note: Grid may be large for ${opts.totalRows} rows (consider grid=auto for fewer dead nodes).`;
98
+ if (guardrails.length > 0) {
99
+ msg += ` ${guardrails.join(" ")}`;
100
+ }
101
+ else if (opts.gridLargeForRows) {
102
+ const autoSide = suggestedAutoGridSide(opts.totalRows);
103
+ msg += ` Note: Grid may be large for ${opts.totalRows} rows — prefer omitting grid_x/grid_y for auto (~${autoSide}×${autoSide}) to reduce dead nodes.`;
90
104
  }
91
105
  data.message = msg;
92
106
  data.suggested_next_step = extras.suggested_next_step;
@@ -104,7 +118,7 @@ async function submitTrainJob(opts) {
104
118
  export async function runTrain(action, args) {
105
119
  const dataset_id = args.dataset_id;
106
120
  if (action === "map" || action === "siom_map" || action === "impute") {
107
- 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, viz_upload_cyclic_components, emit_cell_uncertainty, } = args;
121
+ 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, description, tags, cv_folds, viz_mode, viz_top_components, viz_upload_cyclic_components, emit_cell_uncertainty, } = args;
108
122
  let PRESETS = {};
109
123
  try {
110
124
  PRESETS = await fetchTrainingPresets();
@@ -191,6 +205,16 @@ export async function runTrain(action, args) {
191
205
  typeof transforms === "object" &&
192
206
  Object.keys(transforms).length > 0;
193
207
  const gridLargeForRows = effectiveGrid && totalRows > 0 && effectiveGrid[0] * effectiveGrid[1] > totalRows * 0.75;
208
+ const nFeatures = Array.isArray(columns) ? columns.length
209
+ : Array.isArray(params.columns) ? params.columns.length
210
+ : undefined;
211
+ const guardrailLines = buildSmallNGuardrailLines({
212
+ n: totalRows,
213
+ nFeatures,
214
+ preset: typeof preset === "string" ? preset : undefined,
215
+ gridX: effectiveGrid?.[0],
216
+ gridY: effectiveGrid?.[1],
217
+ });
194
218
  const resultsHint = action === "impute" ? "view the map and imputed.csv"
195
219
  : "view the map and metrics";
196
220
  const variantPrefix = action === "siom_map" ? "variant=siom"
@@ -200,6 +224,8 @@ export async function runTrain(action, args) {
200
224
  dataset_id,
201
225
  params,
202
226
  label,
227
+ description,
228
+ tags,
203
229
  totalRows,
204
230
  hasTransforms,
205
231
  variantPrefix,
@@ -207,13 +233,14 @@ export async function runTrain(action, args) {
207
233
  resultsHint,
208
234
  impute: action === "impute",
209
235
  gridLargeForRows: !!gridLargeForRows,
236
+ guardrailLines,
210
237
  });
211
238
  }
212
239
  if (action === "floop_siom") {
213
240
  if (!dataset_id)
214
241
  throw new Error("train(floop_siom) requires dataset_id");
215
242
  assertValidNormalizationArgs(args);
216
- const { transforms, label } = args;
243
+ const { transforms, label, description, tags } = args;
217
244
  const { params, paramSummary } = buildFloopParams(args);
218
245
  let totalRows = 0;
219
246
  try {
@@ -226,6 +253,8 @@ export async function runTrain(action, args) {
226
253
  dataset_id,
227
254
  params,
228
255
  label,
256
+ description,
257
+ tags,
229
258
  totalRows,
230
259
  hasTransforms,
231
260
  variantPrefix: "variant=floop",
@@ -246,6 +275,14 @@ export function registerTrainTool(server) {
246
275
  .max(120)
247
276
  .optional()
248
277
  .describe("Optional run label (≤120 chars); appears in jobs(list) and the jobs(compare) table; sanitized server-side. Useful for sweeps (e.g. label=\"sweep_periodic_true\")."),
278
+ description: z
279
+ .string()
280
+ .optional()
281
+ .describe("Optional free-text description for inventory/navigation (shown in jobs(list) and account(inventory))."),
282
+ tags: z
283
+ .array(z.string())
284
+ .optional()
285
+ .describe("Optional tags for inventory/navigation (e.g. [\"sweep\",\"baseline\"])."),
249
286
  ...TRAINING_INPUT_SCHEMA,
250
287
  }, async (args) => runTrain(args.action, args));
251
288
  }
@@ -46,7 +46,7 @@ export async function fetchTrainingConfig() {
46
46
  export const TRAINING_INPUT_SCHEMA = {
47
47
  preset: z.enum(["quick", "standard", "refined", "high_res"]).optional(),
48
48
  grid_x: z.number().int().optional()
49
- .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."),
49
+ .describe("Grid width. Omit grid_x AND grid_y (and preset) to auto-size via sqrt(5·√n) per side (clamped 5–50; ~6×6 at n≈45). Pass grid_x/grid_y — not a bare `grid` field. Results report hit_stats.active_node_fraction and grid_suggestion when many nodes are dead."),
50
50
  grid_y: z.number().int().optional().describe("Grid height. See grid_x for auto-sizing."),
51
51
  epochs: z.preprocess((v) => {
52
52
  if (v === undefined || v === null)
@@ -118,17 +118,17 @@ export const TRAINING_INPUT_SCHEMA = {
118
118
  emit_cell_uncertainty: z.boolean().optional()
119
119
  .describe("impute: write imputation_uncertainty.csv (pool_std = prototype-neighbourhood dispersion, not calibrated SD; pool_n = contributing nodes)"),
120
120
  gamma: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional())
121
- .describe("siom_map / impute (SIOM or auto on som_siom+): self-interaction strength γ (default 0.5 on worker)"),
121
+ .describe("siom_map / impute (SIOM or auto on som_siom+): self-interaction strength γ (default 0.5). Prefer γ∈[0.5,1.5] on large n; γ>1.5 often diminishing returns."),
122
122
  gamma_f: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional())
123
- .describe("siom_map / impute SIOM: final γ for exponential decay schedule (0 = constant γ)"),
123
+ .describe("siom_map / impute SIOM: final γ for exponential decay (0 = constant γ). Pattern: start strict, relax late (e.g. 1.5→0.2)."),
124
124
  siom_decay: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional())
125
- .describe("siom_map / impute SIOM: occupation EMA decay λ (default 0.01; 0 = cumulative counting)"),
125
+ .describe("siom_map / impute SIOM: occupation EMA decay λ (default 0.01; 0 = cumulative). Avoid pairing high γ with aggressive decay (e.g. 0.05)."),
126
126
  siom_penalty: z.enum(["linear", "log", "exp"]).optional()
127
- .describe("siom_map / impute SIOM: occupation penalty shape"),
127
+ .describe("siom_map / impute SIOM: occupation penalty shape (linear default; prefer exp over log — log often worsens TE)"),
128
128
  penalty_alpha: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional())
129
129
  .describe("siom_map / impute SIOM: scaling α inside log/exp penalty"),
130
130
  reset_per_epoch: z.boolean().optional()
131
- .describe("siom_map / impute SIOM: reset occupation μ at each epoch"),
131
+ .describe("siom_map / impute SIOM: reset occupation μ each epoch (default false; avoid with high γ — can collapse EV)"),
132
132
  siom_feature_geometry: z.enum(["l2", "mixed", "auto"]).optional().describe("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"),
133
133
  siom_qe_backend: z.enum(["cpu", "cuda", "auto"]).optional().describe("siom_map: backend for metric-aligned siom_qe when using mixed geometry; auto uses GPU when available"),
134
134
  siom_qe_batch_size: z.number().int().min(1).max(1_000_000).optional().describe("siom_map: batch size for GPU SIOM QE (default 256 on worker)"),
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Scale / small-n guidance shared by datasets(preview|analyze) and train submit.
3
+ * Auto-grid formula matches API/worker: clamp(round(sqrt(5*sqrt(n))), 5, 50).
4
+ */
5
+ export const SMALL_N_SOFT = 1000;
6
+ export const SMALL_N_HARD = 100;
7
+ /** Agents should not suggest sample_n below this unless the user asks. */
8
+ export const SAMPLE_N_SUGGEST_THRESHOLD = 100_000;
9
+ /** Worker/API auto grid side length. */
10
+ export function suggestedAutoGridSide(n) {
11
+ if (!Number.isFinite(n) || n < 1)
12
+ return 5;
13
+ return Math.max(5, Math.min(50, Math.round(Math.sqrt(5 * Math.sqrt(n)))));
14
+ }
15
+ export function presetGridCells(preset) {
16
+ switch (preset) {
17
+ case "quick":
18
+ return 15 * 15;
19
+ case "standard":
20
+ return 25 * 25;
21
+ case "refined":
22
+ return 40 * 40;
23
+ case "high_res":
24
+ return 60 * 60;
25
+ default:
26
+ return null;
27
+ }
28
+ }
29
+ /**
30
+ * Build prominent small-n / oversize-grid warnings for agents.
31
+ * Empty when n is healthy and the chosen grid is not oversized.
32
+ */
33
+ export function buildSmallNGuardrailLines(opts) {
34
+ const { n, nFeatures, preset, gridX, gridY } = opts;
35
+ if (!Number.isFinite(n) || n <= 0)
36
+ return [];
37
+ const lines = [];
38
+ const autoSide = suggestedAutoGridSide(n);
39
+ const cells = gridX !== undefined && gridY !== undefined
40
+ ? gridX * gridY
41
+ : presetGridCells(preset);
42
+ if (n < SMALL_N_HARD) {
43
+ lines.push(`JUMPBACK — small-n (${n} rows): maps are fragile below ~100 rows. Prefer grid_x/grid_y ≈ ${autoSide}×${autoSide} (omit both for auto ≈ sqrt(5·√n)), not preset=quick/standard (15×15/25×25 → mostly dead nodes). Drop redundant geo features (keep distance OR lat/lon, not all three when |r|>0.9). Check hit_stats.active_node_fraction / TE before narrating insights.`);
44
+ }
45
+ else if (n < SMALL_N_SOFT) {
46
+ lines.push(`Warning — small-n (${n} rows < 1000): oversized grids leave dead nodes. Auto grid ≈ ${autoSide}×${autoSide}; omit grid_x/grid_y for auto, or size so |features|×grid_cells is not ≫ n. preset=quick (15×15) is often too large below a few hundred rows — verify active_node_fraction after train.`);
47
+ }
48
+ if (cells !== null && cells > n * 0.75) {
49
+ lines.push(`Grid risk: ${cells} cells vs ${n} rows (cells ≫ n) → expect dead nodes / TE≈1. Use auto (~${autoSide}×${autoSide}) or a smaller explicit grid_x/grid_y.`);
50
+ }
51
+ if (nFeatures !== undefined &&
52
+ nFeatures > 0 &&
53
+ cells !== null &&
54
+ nFeatures * cells > n * 2) {
55
+ lines.push(`Capacity risk: ${nFeatures} features × ${cells} cells ≫ ${n} rows. Drop highly correlated columns (see analyze recommendations) and/or shrink the grid before train.`);
56
+ }
57
+ return lines;
58
+ }
59
+ /** CPU scale guidance: do not subsample by default below 100k. */
60
+ export function buildCpuScaleGuidanceLines(n) {
61
+ if (!Number.isFinite(n) || n <= 0)
62
+ return [];
63
+ if (n <= SAMPLE_N_SUGGEST_THRESHOLD) {
64
+ return [
65
+ `Scale: on CPU, preset=quick (15×15) is typically very fast up to ~100k rows (tens of seconds). Do NOT call datasets(subset)/sample_n for n=${n.toLocaleString()} — train the full dataset_id unless the user asks to downsample. Only suggest sample_n when n>100k or the user requests it.`,
66
+ ];
67
+ }
68
+ return [
69
+ `Scale: n=${n.toLocaleString()} > 100k — full-table train is still supported; suggest sample_n only if the user wants a faster exploratory pass or ops runbook says so. Never auto-subset silently.`,
70
+ ];
71
+ }
72
+ /** Periodicity caveat for analyze next_steps (row-order dependent). */
73
+ export const PERIODICITY_ROW_ORDER_NOTE = "Periodicity assumes meaningful row order (time/sequence). If the CSV is unordered or shuffled, ignore lag-based suggestions; only use cyclic_features when the column is genuinely cyclic (e.g. hour, month, angle).";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barivia/barsom-mcp",
3
- "version": "0.22.4",
3
+ "version": "0.23.2",
4
4
  "description": "barSOM MCP proxy — connect any MCP client to the barSOM cloud API for Self-Organizing Map analytics",
5
5
  "keywords": [
6
6
  "mcp",