@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.
- package/README.md +24 -15
- package/dist/index.js +1 -1
- package/dist/prepare_training_prompt.js +1 -1
- package/dist/shared.js +1 -1
- package/dist/tools/account.js +1 -1
- package/dist/tools/datasets.js +65 -18
- package/dist/tools/explore_map.js +6 -7
- package/dist/tools/inference.js +38 -9
- package/dist/tools/jobs.js +58 -453
- package/dist/tools/results.js +1 -1
- package/dist/tools/train.js +262 -0
- package/dist/tools/training_core.js +185 -0
- package/dist/tools/training_prep.js +29 -3
- package/package.json +1 -1
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { registerAuditedTool } from "../audit.js";
|
|
3
|
+
import { apiCall, textResult } from "../shared.js";
|
|
4
|
+
import { TRAINING_INPUT_SCHEMA, buildTrainMapParams, fetchTrainingPresets, } from "./training_core.js";
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
| Action | Use when |
|
|
8
|
+
|--------|----------|
|
|
9
|
+
| map | Standard SOM on a fixed hex grid — complete-case numeric data (no NaNs in training columns). |
|
|
10
|
+
| siom_map | Self-interacting map — same grid flow plus SIOM coverage control (gamma, siom_decay, penalty) to avoid dead nodes. |
|
|
11
|
+
| impute | Sparse training data: trains a missing-tolerant map (accelerated missSOM) AND returns dense imputed.csv in one job. Plain numeric columns only. |
|
|
12
|
+
| 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). |
|
|
13
|
+
| baseline_study | One-call heuristic baseline SOM (grid ~5·√n, MAD normalization) for a quick first map. |
|
|
14
|
+
|
|
15
|
+
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.
|
|
16
|
+
|
|
17
|
+
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=...).
|
|
18
|
+
|
|
19
|
+
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. Defaults model=auto, cv_folds=5 → quality.csv with two labeled numbers per column (kfold_holdout_pool = honest held-out; resubstitution_optimistic = in-sample, optimistic). Artifacts: imputed.csv, imputation_mask.csv, optional imputation_uncertainty.csv. A completed impute job_id is a valid parent for inference(impute_column).
|
|
20
|
+
action=floop_siom: max_nodes is a TOTAL node budget (not a grid width/area); if coverage collapses, reduce max_nodes before increasing effort.
|
|
21
|
+
NOT FOR: lifecycle (status/list/compare/cancel/delete) → use jobs; scoring data → use inference.`;
|
|
22
|
+
/**
|
|
23
|
+
* Core training dispatcher. Shared by the `train` tool and the deprecated
|
|
24
|
+
* `jobs(train_*)` wrappers. Every branch posts to the same /v1/jobs route.
|
|
25
|
+
*/
|
|
26
|
+
export async function runTrain(action, args) {
|
|
27
|
+
const dataset_id = args.dataset_id;
|
|
28
|
+
if (action === "baseline_study") {
|
|
29
|
+
if (!dataset_id)
|
|
30
|
+
throw new Error("train(baseline_study) requires dataset_id");
|
|
31
|
+
const dsInfo = (await apiCall("GET", `/v1/datasets/${dataset_id}/preview?n_rows=1`));
|
|
32
|
+
const nSamples = dsInfo.total_rows || 1000;
|
|
33
|
+
const totalNodes = Math.round(5 * Math.sqrt(nSamples));
|
|
34
|
+
const side = Math.max(5, Math.round(Math.sqrt(totalNodes)));
|
|
35
|
+
const params = {
|
|
36
|
+
model: "SOM",
|
|
37
|
+
periodic: false,
|
|
38
|
+
normalize: "mad",
|
|
39
|
+
grid: [side, side],
|
|
40
|
+
epochs: [10, 20],
|
|
41
|
+
batch_size: 32,
|
|
42
|
+
quality_metrics: "standard",
|
|
43
|
+
output_format: "png",
|
|
44
|
+
output_dpi: 2,
|
|
45
|
+
colormap: "coolwarm",
|
|
46
|
+
};
|
|
47
|
+
const data = (await apiCall("POST", "/v1/jobs", { dataset_id, params }));
|
|
48
|
+
const jid = String(data.id ?? "");
|
|
49
|
+
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.` }] };
|
|
50
|
+
}
|
|
51
|
+
if (action === "map" || action === "siom_map" || action === "impute") {
|
|
52
|
+
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;
|
|
53
|
+
let PRESETS = {};
|
|
54
|
+
try {
|
|
55
|
+
PRESETS = await fetchTrainingPresets();
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
if (e.httpStatus === 401 || e.httpStatus === 403)
|
|
59
|
+
throw e;
|
|
60
|
+
if (preset && grid_x === undefined && epochs === undefined) {
|
|
61
|
+
throw new Error("Could not fetch training config from server, and missing explicit grid/epochs.");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (!dataset_id)
|
|
65
|
+
throw new Error(`train(${action}) requires dataset_id`);
|
|
66
|
+
const { params, paramSummary, effectiveGrid } = buildTrainMapParams({
|
|
67
|
+
preset, grid_x, grid_y, epochs, model, periodic, columns, cyclic_features,
|
|
68
|
+
temporal_features, feature_weights, transforms, auto_log_transforms,
|
|
69
|
+
time_delay_embeddings, categorical_features,
|
|
70
|
+
normalize, sigma_f, learning_rate, batch_size, quality_metrics, backend,
|
|
71
|
+
output_format, output_dpi, colormap, row_range,
|
|
72
|
+
}, PRESETS);
|
|
73
|
+
if (action === "impute") {
|
|
74
|
+
params._job_type = "train_impute";
|
|
75
|
+
params.model = model ?? "auto";
|
|
76
|
+
params.cv_folds = cv_folds ?? 5;
|
|
77
|
+
if (viz_mode !== undefined)
|
|
78
|
+
params.viz_mode = viz_mode;
|
|
79
|
+
if (viz_top_components !== undefined)
|
|
80
|
+
params.viz_top_components = viz_top_components;
|
|
81
|
+
if (emit_cell_uncertainty !== undefined)
|
|
82
|
+
params.emit_cell_uncertainty = emit_cell_uncertainty;
|
|
83
|
+
// Plain numeric path only — strip unsupported keys if caller passed them
|
|
84
|
+
delete params.cyclic_features;
|
|
85
|
+
delete params.temporal_features;
|
|
86
|
+
delete params.categorical_features;
|
|
87
|
+
delete params.transforms;
|
|
88
|
+
delete params.auto_log_transforms;
|
|
89
|
+
delete params.time_delay_embeddings;
|
|
90
|
+
}
|
|
91
|
+
if (action === "siom_map") {
|
|
92
|
+
params._job_type = "train_siom";
|
|
93
|
+
if (gamma !== undefined)
|
|
94
|
+
params.gamma = gamma;
|
|
95
|
+
if (gamma_f !== undefined)
|
|
96
|
+
params.gamma_f = gamma_f;
|
|
97
|
+
if (siom_decay !== undefined)
|
|
98
|
+
params.siom_decay = siom_decay;
|
|
99
|
+
if (siom_penalty !== undefined)
|
|
100
|
+
params.siom_penalty = siom_penalty;
|
|
101
|
+
if (penalty_alpha !== undefined)
|
|
102
|
+
params.penalty_alpha = penalty_alpha;
|
|
103
|
+
if (reset_per_epoch !== undefined)
|
|
104
|
+
params.reset_per_epoch = reset_per_epoch;
|
|
105
|
+
if (siom_feature_geometry !== undefined)
|
|
106
|
+
params.siom_feature_geometry = siom_feature_geometry;
|
|
107
|
+
if (siom_qe_backend !== undefined)
|
|
108
|
+
params.siom_qe_backend = siom_qe_backend;
|
|
109
|
+
if (siom_qe_batch_size !== undefined)
|
|
110
|
+
params.siom_qe_batch_size = siom_qe_batch_size;
|
|
111
|
+
}
|
|
112
|
+
let totalRows = 0;
|
|
113
|
+
try {
|
|
114
|
+
const preview = (await apiCall("GET", `/v1/datasets/${dataset_id}/preview?n_rows=1`));
|
|
115
|
+
totalRows = Number(preview?.total_rows ?? 0);
|
|
116
|
+
}
|
|
117
|
+
catch { /* ignore */ }
|
|
118
|
+
const submitBody = { dataset_id, params };
|
|
119
|
+
if (label && label.trim() !== "")
|
|
120
|
+
submitBody.label = label;
|
|
121
|
+
const data = (await apiCall("POST", "/v1/jobs", submitBody));
|
|
122
|
+
const newJobId = data.id;
|
|
123
|
+
const variantPrefix = action === "siom_map" ? "variant=siom"
|
|
124
|
+
: action === "impute" ? "variant=train_impute"
|
|
125
|
+
: "variant=som";
|
|
126
|
+
data.effective_params = `${variantPrefix}, ${paramSummary}`;
|
|
127
|
+
try {
|
|
128
|
+
const sys = (await apiCall("GET", "/v1/system/info"));
|
|
129
|
+
const pending = Number(sys.status?.pending_jobs ?? sys.pending_jobs ?? 0);
|
|
130
|
+
const totalEta = Number(sys.training_time_estimates_seconds?.total ??
|
|
131
|
+
(sys.gpu_available ? 45 : 120));
|
|
132
|
+
const waitMinutes = Math.round((pending * totalEta) / 60);
|
|
133
|
+
let msg = `Job submitted (${variantPrefix}, ${paramSummary}). `;
|
|
134
|
+
if (waitMinutes > 1)
|
|
135
|
+
msg += `You are #${pending + 1} in queue. Estimated wait: ~${waitMinutes} min. `;
|
|
136
|
+
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 === "impute" ? " and imputed.csv" : ""} and metrics.`;
|
|
137
|
+
msg += ` Optional: training_monitor(job_id="${newJobId}") for charts—not required.`;
|
|
138
|
+
if (effectiveGrid && totalRows > 0 && effectiveGrid[0] * effectiveGrid[1] > totalRows * 0.75) {
|
|
139
|
+
msg += ` Note: Grid may be large for ${totalRows} rows (consider grid=auto for fewer dead nodes).`;
|
|
140
|
+
}
|
|
141
|
+
data.message = msg;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
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 === "impute" ? " and imputed.csv" : ""} and metrics.`;
|
|
145
|
+
msg += ` Optional: training_monitor(job_id="${newJobId}") for charts—not required.`;
|
|
146
|
+
if (effectiveGrid && totalRows > 0 && effectiveGrid[0] * effectiveGrid[1] > totalRows * 0.75) {
|
|
147
|
+
msg += ` Note: Grid may be large for ${totalRows} rows (consider grid=auto for fewer dead nodes).`;
|
|
148
|
+
}
|
|
149
|
+
data.message = msg;
|
|
150
|
+
}
|
|
151
|
+
return textResult(data);
|
|
152
|
+
}
|
|
153
|
+
if (action === "floop_siom") {
|
|
154
|
+
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;
|
|
155
|
+
if (!dataset_id)
|
|
156
|
+
throw new Error("train(floop_siom) requires dataset_id");
|
|
157
|
+
const params = {
|
|
158
|
+
_job_type: "train_floop_siom",
|
|
159
|
+
topology,
|
|
160
|
+
effort,
|
|
161
|
+
normalize,
|
|
162
|
+
output_format: output_format ?? "png",
|
|
163
|
+
};
|
|
164
|
+
if (max_nodes !== undefined)
|
|
165
|
+
params.max_nodes = max_nodes;
|
|
166
|
+
if (columns?.length)
|
|
167
|
+
params.columns = columns;
|
|
168
|
+
if (cyclic_features?.length)
|
|
169
|
+
params.cyclic_features = cyclic_features;
|
|
170
|
+
if (temporal_features?.length)
|
|
171
|
+
params.temporal_features = temporal_features;
|
|
172
|
+
if (feature_weights && Object.keys(feature_weights).length > 0)
|
|
173
|
+
params.feature_weights = feature_weights;
|
|
174
|
+
if (transforms && Object.keys(transforms).length > 0)
|
|
175
|
+
params.transforms = transforms;
|
|
176
|
+
if (auto_log_transforms)
|
|
177
|
+
params.auto_log_transforms = auto_log_transforms;
|
|
178
|
+
if (row_range && row_range.length >= 2 && row_range[0] <= row_range[1])
|
|
179
|
+
params.row_range = row_range;
|
|
180
|
+
const dpiMap = { standard: 1, retina: 2, print: 4 };
|
|
181
|
+
if (output_dpi && output_dpi !== "retina")
|
|
182
|
+
params.output_dpi = dpiMap[output_dpi] ?? 2;
|
|
183
|
+
if (colormap)
|
|
184
|
+
params.colormap = colormap;
|
|
185
|
+
if (n_initial !== undefined)
|
|
186
|
+
params.n_initial = n_initial;
|
|
187
|
+
if (n_passes !== undefined)
|
|
188
|
+
params.n_passes = n_passes;
|
|
189
|
+
if (growth_interval !== undefined)
|
|
190
|
+
params.growth_interval = growth_interval;
|
|
191
|
+
if (neighborhood_size !== undefined)
|
|
192
|
+
params.neighborhood_size = neighborhood_size;
|
|
193
|
+
if (edge_max_age !== undefined)
|
|
194
|
+
params.edge_max_age = edge_max_age;
|
|
195
|
+
if (max_degree !== undefined)
|
|
196
|
+
params.max_degree = max_degree;
|
|
197
|
+
if (gamma !== undefined)
|
|
198
|
+
params.gamma = gamma;
|
|
199
|
+
if (siom_decay !== undefined)
|
|
200
|
+
params.siom_decay = siom_decay;
|
|
201
|
+
if (siom_penalty !== undefined)
|
|
202
|
+
params.siom_penalty = siom_penalty;
|
|
203
|
+
if (penalty_alpha !== undefined)
|
|
204
|
+
params.penalty_alpha = penalty_alpha;
|
|
205
|
+
if (error_threshold !== undefined)
|
|
206
|
+
params.error_threshold = error_threshold;
|
|
207
|
+
if (error_decay !== undefined)
|
|
208
|
+
params.error_decay = error_decay;
|
|
209
|
+
if (ring_close_threshold !== undefined)
|
|
210
|
+
params.ring_close_threshold = ring_close_threshold;
|
|
211
|
+
if (sigma_0 !== undefined)
|
|
212
|
+
params.sigma_0 = sigma_0;
|
|
213
|
+
if (sigma_f !== undefined)
|
|
214
|
+
params.sigma_f = sigma_f;
|
|
215
|
+
if (eta_0 !== undefined)
|
|
216
|
+
params.eta_0 = eta_0;
|
|
217
|
+
if (eta_f !== undefined)
|
|
218
|
+
params.eta_f = eta_f;
|
|
219
|
+
if (elastic_lambda !== undefined)
|
|
220
|
+
params.elastic_lambda = elastic_lambda;
|
|
221
|
+
if (elastic_mu !== undefined)
|
|
222
|
+
params.elastic_mu = elastic_mu;
|
|
223
|
+
if (elastic_anchor !== undefined)
|
|
224
|
+
params.elastic_anchor = elastic_anchor;
|
|
225
|
+
if (anchor_percentile !== undefined)
|
|
226
|
+
params.anchor_percentile = anchor_percentile;
|
|
227
|
+
const submitBody = { dataset_id, params };
|
|
228
|
+
if (label && label.trim() !== "")
|
|
229
|
+
submitBody.label = label;
|
|
230
|
+
const data = (await apiCall("POST", "/v1/jobs", submitBody));
|
|
231
|
+
const newJobId = data.id;
|
|
232
|
+
const maxNodeSummary = max_nodes === undefined ? "max_nodes=auto(~2*sqrt(n_samples))" : `max_nodes=${max_nodes}`;
|
|
233
|
+
const paramSummary = [
|
|
234
|
+
"variant=floop",
|
|
235
|
+
`topology=${topology ?? "free"}`,
|
|
236
|
+
maxNodeSummary,
|
|
237
|
+
`effort=${effort ?? "standard"}`,
|
|
238
|
+
gamma !== undefined ? `gamma=${gamma}` : "",
|
|
239
|
+
].filter(Boolean).join(", ");
|
|
240
|
+
data.effective_params = paramSummary;
|
|
241
|
+
data.message =
|
|
242
|
+
`Job submitted (${paramSummary}). Poll with jobs(action=status, job_id="${newJobId}"). ` +
|
|
243
|
+
`When status is completed, use results(action=get, job_id="${newJobId}") to view FLooP-SIOM figures and metrics. ` +
|
|
244
|
+
`Optional: training_monitor(job_id="${newJobId}") for charts—not required.`;
|
|
245
|
+
return textResult(data);
|
|
246
|
+
}
|
|
247
|
+
throw new Error("Invalid train action");
|
|
248
|
+
}
|
|
249
|
+
export function registerTrainTool(server) {
|
|
250
|
+
registerAuditedTool(server, "train", TRAIN_DESCRIPTION, {
|
|
251
|
+
action: z
|
|
252
|
+
.enum(["map", "siom_map", "impute", "floop_siom", "baseline_study"])
|
|
253
|
+
.describe("map: standard SOM; siom_map: self-interacting map; impute: missing-tolerant map + imputed.csv; floop_siom: growing manifold (Premium/Enterprise); baseline_study: one-call heuristic baseline SOM"),
|
|
254
|
+
dataset_id: z.string().optional().describe("Dataset ID to train on (required for all actions)."),
|
|
255
|
+
label: z
|
|
256
|
+
.string()
|
|
257
|
+
.max(120)
|
|
258
|
+
.optional()
|
|
259
|
+
.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\")."),
|
|
260
|
+
...TRAINING_INPUT_SCHEMA,
|
|
261
|
+
}, async (args) => runTrain(args.action, args));
|
|
262
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared training surface for the barsom proxy: the Zod input schema for training
|
|
3
|
+
* parameters, the param-building helper, and preset fetch. Declared once here so
|
|
4
|
+
* the `train` tool, the deprecated `jobs(train_*)` wrappers, and `training_prep`
|
|
5
|
+
* all reuse the same definitions (no drift).
|
|
6
|
+
*/
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { apiCall } from "../shared.js";
|
|
9
|
+
export async function fetchTrainingPresets() {
|
|
10
|
+
const configData = (await apiCall("GET", "/v1/training/config"));
|
|
11
|
+
return configData?.presets || {};
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Zod raw shape for all training parameters (core SOM + impute + SIOM + FLooP).
|
|
15
|
+
* Spread into the `train` tool input schema and the deprecated `jobs` wrapper
|
|
16
|
+
* schema. Does NOT include tool-level keys (action, dataset_id, label, job_id).
|
|
17
|
+
*/
|
|
18
|
+
export const TRAINING_INPUT_SCHEMA = {
|
|
19
|
+
preset: z.enum(["quick", "standard", "refined", "high_res"]).optional(),
|
|
20
|
+
grid_x: z.number().int().optional()
|
|
21
|
+
.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."),
|
|
22
|
+
grid_y: z.number().int().optional().describe("Grid height. See grid_x for auto-sizing."),
|
|
23
|
+
epochs: z.preprocess((v) => {
|
|
24
|
+
if (v === undefined || v === null)
|
|
25
|
+
return v;
|
|
26
|
+
if (typeof v === "string") {
|
|
27
|
+
const n = parseInt(v, 10);
|
|
28
|
+
if (!Number.isNaN(n))
|
|
29
|
+
return n;
|
|
30
|
+
const m = v.match(/^\[\s*(\d+)\s*,\s*(\d+)\s*\]$/);
|
|
31
|
+
if (m)
|
|
32
|
+
return [parseInt(m[1], 10), parseInt(m[2], 10)];
|
|
33
|
+
}
|
|
34
|
+
return v;
|
|
35
|
+
}, z.union([z.number().int(), z.array(z.number().int()).length(2)]).optional()),
|
|
36
|
+
model: z.enum(["auto", "SOM", "SIOM", "RSOM", "SOM-SOFT", "RSOM-SOFT"]).optional(),
|
|
37
|
+
periodic: z.boolean().optional().default(true),
|
|
38
|
+
columns: z.array(z.string()).optional(),
|
|
39
|
+
cyclic_features: z.array(z.object({
|
|
40
|
+
feature: z.string(),
|
|
41
|
+
period: z.number(),
|
|
42
|
+
})).optional(),
|
|
43
|
+
temporal_features: z.array(z.object({
|
|
44
|
+
columns: z.array(z.string()),
|
|
45
|
+
format: z.string(),
|
|
46
|
+
extract: z.array(z.enum(["hour_of_day", "day_of_year", "month", "day_of_week", "minute_of_hour"])),
|
|
47
|
+
cyclic: z.boolean().default(true),
|
|
48
|
+
separator: z.string().optional(),
|
|
49
|
+
})).optional(),
|
|
50
|
+
feature_weights: z.record(z.number()).optional(),
|
|
51
|
+
transforms: z.record(z.enum(["log", "log1p", "log10", "sqrt", "square", "abs", "invert", "rank", "none"])).optional(),
|
|
52
|
+
auto_log_transforms: z.boolean().optional().default(false),
|
|
53
|
+
time_delay_embeddings: z.array(z.object({
|
|
54
|
+
feature: z.string(),
|
|
55
|
+
lags: z.array(z.number().int().min(1))
|
|
56
|
+
})).optional(),
|
|
57
|
+
categorical_features: z.array(z.object({
|
|
58
|
+
feature: z.string(),
|
|
59
|
+
categories: z.array(z.string()),
|
|
60
|
+
weight: z.number().positive()
|
|
61
|
+
})).optional(),
|
|
62
|
+
normalize: z.union([z.enum(["all", "auto", "mad", "sigmoidal"]), z.array(z.string())]).optional().default("auto"),
|
|
63
|
+
sigma_f: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
|
|
64
|
+
learning_rate: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.union([z.number(), z.object({
|
|
65
|
+
ordering: z.tuple([z.number(), z.number()]),
|
|
66
|
+
convergence: z.tuple([z.number(), z.number()]),
|
|
67
|
+
})]).optional()),
|
|
68
|
+
batch_size: z.number().int().optional(),
|
|
69
|
+
quality_metrics: z.union([z.enum(["fast", "standard", "full"]), z.array(z.string())]).optional(),
|
|
70
|
+
backend: z.enum(["auto", "cpu", "gpu", "gpu_graphs"]).optional().default("auto"),
|
|
71
|
+
output_format: z.enum(["png", "pdf", "svg"]).optional().default("png"),
|
|
72
|
+
output_dpi: z.enum(["standard", "retina", "print"]).optional().default("retina"),
|
|
73
|
+
colormap: z.string().optional(),
|
|
74
|
+
row_range: z.tuple([z.number().int().min(1), z.number().int().min(1)]).optional(),
|
|
75
|
+
cv_folds: z.number().int().min(0).max(20).optional()
|
|
76
|
+
.describe("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."),
|
|
77
|
+
viz_mode: z.enum(["full", "summary", "summary_plus_top"]).optional()
|
|
78
|
+
.describe("Visualization density; auto-capped when feature count > 40"),
|
|
79
|
+
viz_top_components: z.number().int().min(0).max(64).optional()
|
|
80
|
+
.describe("With viz_mode=summary_plus_top: upload top-N component maps by variance (default 8)"),
|
|
81
|
+
emit_cell_uncertainty: z.boolean().optional()
|
|
82
|
+
.describe("impute: write imputation_uncertainty.csv (pool_std per imputed cell)"),
|
|
83
|
+
gamma: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
|
|
84
|
+
gamma_f: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
|
|
85
|
+
siom_decay: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
|
|
86
|
+
siom_penalty: z.enum(["linear", "log", "exp"]).optional(),
|
|
87
|
+
penalty_alpha: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
|
|
88
|
+
reset_per_epoch: z.boolean().optional(),
|
|
89
|
+
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"),
|
|
90
|
+
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"),
|
|
91
|
+
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)"),
|
|
92
|
+
topology: z.enum(["free", "chain"]).optional().default("free"),
|
|
93
|
+
max_nodes: z.number().int().min(2).optional(),
|
|
94
|
+
effort: z.enum(["quick", "standard", "thorough"]).optional().default("standard"),
|
|
95
|
+
n_initial: z.number().int().min(2).optional(),
|
|
96
|
+
n_passes: z.number().int().min(1).optional(),
|
|
97
|
+
growth_interval: z.number().int().min(1).optional(),
|
|
98
|
+
neighborhood_size: z.number().int().min(1).optional(),
|
|
99
|
+
edge_max_age: z.number().int().min(1).optional(),
|
|
100
|
+
max_degree: z.number().int().min(0).optional(),
|
|
101
|
+
error_threshold: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
|
|
102
|
+
error_decay: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
|
|
103
|
+
ring_close_threshold: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
|
|
104
|
+
sigma_0: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
|
|
105
|
+
eta_0: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
|
|
106
|
+
eta_f: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
|
|
107
|
+
elastic_lambda: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
|
|
108
|
+
elastic_mu: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
|
|
109
|
+
elastic_anchor: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
|
|
110
|
+
anchor_percentile: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
|
|
111
|
+
};
|
|
112
|
+
export function buildTrainMapParams(args, presets) {
|
|
113
|
+
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;
|
|
114
|
+
const p = preset ? presets[preset] : undefined;
|
|
115
|
+
const params = {
|
|
116
|
+
model: model ?? "SOM",
|
|
117
|
+
periodic: periodic ?? true,
|
|
118
|
+
normalize: normalize ?? "auto",
|
|
119
|
+
};
|
|
120
|
+
if (grid_x !== undefined && grid_y !== undefined)
|
|
121
|
+
params.grid = [grid_x, grid_y];
|
|
122
|
+
else if (p)
|
|
123
|
+
params.grid = p.grid;
|
|
124
|
+
if (epochs !== undefined)
|
|
125
|
+
params.epochs = epochs;
|
|
126
|
+
else if (p)
|
|
127
|
+
params.epochs = p.epochs;
|
|
128
|
+
if (cyclic_features?.length)
|
|
129
|
+
params.cyclic_features = cyclic_features;
|
|
130
|
+
if (columns?.length)
|
|
131
|
+
params.columns = columns;
|
|
132
|
+
if (auto_log_transforms)
|
|
133
|
+
params.auto_log_transforms = auto_log_transforms;
|
|
134
|
+
if (time_delay_embeddings?.length)
|
|
135
|
+
params.time_delay_embeddings = time_delay_embeddings;
|
|
136
|
+
if (categorical_features?.length)
|
|
137
|
+
params.categorical_features = categorical_features;
|
|
138
|
+
if (transforms && Object.keys(transforms).length > 0)
|
|
139
|
+
params.transforms = transforms;
|
|
140
|
+
if (temporal_features?.length)
|
|
141
|
+
params.temporal_features = temporal_features;
|
|
142
|
+
if (feature_weights && Object.keys(feature_weights).length > 0)
|
|
143
|
+
params.feature_weights = feature_weights;
|
|
144
|
+
if (sigma_f !== undefined)
|
|
145
|
+
params.sigma_f = sigma_f;
|
|
146
|
+
if (learning_rate !== undefined)
|
|
147
|
+
params.learning_rate = learning_rate;
|
|
148
|
+
if (batch_size !== undefined)
|
|
149
|
+
params.batch_size = batch_size;
|
|
150
|
+
else if (p)
|
|
151
|
+
params.batch_size = p.batch_size;
|
|
152
|
+
if (quality_metrics !== undefined)
|
|
153
|
+
params.quality_metrics = quality_metrics;
|
|
154
|
+
if (backend !== undefined && backend !== "auto")
|
|
155
|
+
params.backend = backend;
|
|
156
|
+
else if (p?.backend)
|
|
157
|
+
params.backend = p.backend;
|
|
158
|
+
params.output_format = output_format ?? "png";
|
|
159
|
+
const dpiMap = { standard: 1, retina: 2, print: 4 };
|
|
160
|
+
if (output_dpi && output_dpi !== "retina")
|
|
161
|
+
params.output_dpi = dpiMap[output_dpi] ?? 2;
|
|
162
|
+
if (colormap)
|
|
163
|
+
params.colormap = colormap;
|
|
164
|
+
if (row_range && row_range.length >= 2 && row_range[0] <= row_range[1])
|
|
165
|
+
params.row_range = row_range;
|
|
166
|
+
if (siom_feature_geometry !== undefined)
|
|
167
|
+
params.siom_feature_geometry = siom_feature_geometry;
|
|
168
|
+
if (siom_qe_backend !== undefined)
|
|
169
|
+
params.siom_qe_backend = siom_qe_backend;
|
|
170
|
+
if (siom_qe_batch_size !== undefined)
|
|
171
|
+
params.siom_qe_batch_size = siom_qe_batch_size;
|
|
172
|
+
const effectiveGrid = params.grid;
|
|
173
|
+
const effectiveEpochs = params.epochs;
|
|
174
|
+
const effectiveBatch = params.batch_size;
|
|
175
|
+
const effectiveBackend = params.backend;
|
|
176
|
+
const paramSummary = [
|
|
177
|
+
effectiveGrid ? `grid=${effectiveGrid[0]}×${effectiveGrid[1]}` : "grid=auto",
|
|
178
|
+
effectiveEpochs ? `epochs=${Array.isArray(effectiveEpochs) ? effectiveEpochs.join("+") : effectiveEpochs}` : "epochs=auto",
|
|
179
|
+
effectiveBatch ? `batch=${effectiveBatch}` : "batch=auto",
|
|
180
|
+
`model=${params.model}`,
|
|
181
|
+
effectiveBackend ? `backend=${effectiveBackend}` : "",
|
|
182
|
+
preset ? `(from preset: ${preset})` : "",
|
|
183
|
+
].filter(Boolean).join(", ");
|
|
184
|
+
return { params, paramSummary, effectiveGrid };
|
|
185
|
+
}
|
|
@@ -1,10 +1,36 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
|
|
3
3
|
import { registerAuditedTool, runMcpToolAudit } from "../audit.js";
|
|
4
|
-
import { apiCall, fetchTrainingGuidanceFromApi, getClientSupportsMcpApps, getVizPort, structuredTextResult, } from "../shared.js";
|
|
5
|
-
import { buildTrainMapParams, fetchTrainingPresets, } from "./
|
|
4
|
+
import { apiCall, fetchTrainingGuidanceFromApi, getClientSupportsMcpApps, getVizPort, pollUntilComplete, POLL_DERIVE_MAX_MS, structuredTextResult, } from "../shared.js";
|
|
5
|
+
import { buildTrainMapParams, fetchTrainingPresets, } from "./training_core.js";
|
|
6
6
|
import { attachTrainingReviewPayload, consumeTrainingReview, getTrainingReview, storeTrainingReview, } from "./training_review_store.js";
|
|
7
7
|
export const TRAINING_PREP_URI = "ui://barsom/training-prep";
|
|
8
|
+
/**
|
|
9
|
+
* Fetch column analysis the same way datasets(action=analyze) does: enqueue the
|
|
10
|
+
* async sampled analyze job, poll, then read the summary. Falls back to the legacy
|
|
11
|
+
* synchronous GET on older APIs (404). Analysis is advisory in prep, so a job that
|
|
12
|
+
* is still running / failed degrades to an empty analysis (preview + presets still
|
|
13
|
+
* render) rather than failing the whole prep call.
|
|
14
|
+
*/
|
|
15
|
+
async function fetchAnalyzeData(datasetId) {
|
|
16
|
+
try {
|
|
17
|
+
const submit = (await apiCall("POST", `/v1/datasets/${datasetId}/analyze`));
|
|
18
|
+
const jobId = (submit.id ?? submit.job_id);
|
|
19
|
+
if (!jobId)
|
|
20
|
+
return {};
|
|
21
|
+
const poll = await pollUntilComplete(jobId, POLL_DERIVE_MAX_MS);
|
|
22
|
+
if (poll.status !== "completed")
|
|
23
|
+
return {};
|
|
24
|
+
const results = (await apiCall("GET", `/v1/results/${jobId}`));
|
|
25
|
+
return (results.summary ?? results);
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
if (err?.httpStatus === 404) {
|
|
29
|
+
return (await apiCall("GET", `/v1/datasets/${datasetId}/analyze`));
|
|
30
|
+
}
|
|
31
|
+
throw err;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
8
34
|
function getNumericColumns(preview) {
|
|
9
35
|
const stats = preview.column_stats ?? [];
|
|
10
36
|
return stats
|
|
@@ -139,7 +165,7 @@ function buildGuidanceLines(guidanceText) {
|
|
|
139
165
|
async function buildTrainingPrepPayload(datasetId, input) {
|
|
140
166
|
const [preview, analyze, presets, guidanceText, preparePrompt] = await Promise.all([
|
|
141
167
|
apiCall("GET", `/v1/datasets/${datasetId}/preview?n_rows=5`),
|
|
142
|
-
|
|
168
|
+
fetchAnalyzeData(datasetId),
|
|
143
169
|
fetchTrainingPresets().catch(() => ({})),
|
|
144
170
|
fetchTrainingGuidanceFromApi().catch(() => "No training guidance available."),
|
|
145
171
|
fetchPreparePrompt(datasetId),
|