@barivia/barsom-mcp 0.18.0 → 0.19.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 +7 -6
- package/dist/index.js +1 -1
- package/dist/shared.js +3 -2
- package/dist/tools/datasets.js +4 -2
- package/dist/tools/explore_map.js +2 -2
- package/dist/tools/guide_barsom.js +1 -1
- package/dist/tools/training_core.js +4 -5
- package/dist/tools/training_guidance.js +1 -1
- package/dist/tools/training_monitor.js +2 -1
- package/dist/views/src/views/results-explorer/index.html +28 -28
- package/dist/views/src/views/training-monitor/index.html +14 -13
- package/dist/viz-server.js +1 -71
- package/package.json +2 -2
- package/dist/tools/training_prep.js +0 -572
- package/dist/tools/training_review_store.js +0 -53
- package/dist/views/src/views/training-prep/index.html +0 -387
|
@@ -1,572 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
|
|
3
|
-
import { registerAuditedTool, runMcpToolAudit } from "../audit.js";
|
|
4
|
-
import { apiCall, fetchTrainingGuidanceFromApi, getClientSupportsMcpApps, getVizPort, pollUntilComplete, POLL_DERIVE_MAX_MS, structuredTextResult, } from "../shared.js";
|
|
5
|
-
import { buildTrainMapParams, buildFloopParams, fetchTrainingConfig, } from "./training_core.js";
|
|
6
|
-
import { attachTrainingReviewPayload, consumeTrainingReview, getTrainingReview, storeTrainingReview, } from "./training_review_store.js";
|
|
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
|
-
}
|
|
34
|
-
function getNumericColumns(preview) {
|
|
35
|
-
const stats = preview.column_stats ?? [];
|
|
36
|
-
return stats
|
|
37
|
-
.filter((stat) => stat.is_numeric !== false)
|
|
38
|
-
.map((stat) => String(stat.column ?? ""))
|
|
39
|
-
.filter(Boolean);
|
|
40
|
-
}
|
|
41
|
-
function getLikelyIdColumns(preview) {
|
|
42
|
-
const stats = preview.column_stats ?? [];
|
|
43
|
-
const totalRows = Number(preview.total_rows ?? 0);
|
|
44
|
-
if (totalRows <= 0)
|
|
45
|
-
return [];
|
|
46
|
-
return stats
|
|
47
|
-
.filter((stat) => stat.is_numeric !== false)
|
|
48
|
-
.filter((stat) => {
|
|
49
|
-
const mn = Number(stat.min);
|
|
50
|
-
const mx = Number(stat.max);
|
|
51
|
-
const colName = String(stat.column ?? "").toLowerCase();
|
|
52
|
-
const nameHint = /^(id|index|instant|row_?num|serial|record|obs)$/i.test(colName)
|
|
53
|
-
|| colName.endsWith("_id")
|
|
54
|
-
|| colName.startsWith("id_");
|
|
55
|
-
return ((mn === 0 || mn === 1) &&
|
|
56
|
-
mx >= totalRows * 0.9 &&
|
|
57
|
-
mx <= totalRows * 1.1 &&
|
|
58
|
-
Number(stat.null_count ?? 0) === 0 &&
|
|
59
|
-
nameHint);
|
|
60
|
-
})
|
|
61
|
-
.map((stat) => String(stat.column ?? ""))
|
|
62
|
-
.filter(Boolean);
|
|
63
|
-
}
|
|
64
|
-
function getRecommendedTrainColumns(preview, analyze) {
|
|
65
|
-
const recommendations = analyze.recommendations ?? {};
|
|
66
|
-
const recommended = (recommendations.train ?? []).filter(Boolean);
|
|
67
|
-
if (recommended.length > 0)
|
|
68
|
-
return recommended;
|
|
69
|
-
const numeric = getNumericColumns(preview);
|
|
70
|
-
const likelyId = new Set(getLikelyIdColumns(preview));
|
|
71
|
-
return numeric.filter((column) => !likelyId.has(column));
|
|
72
|
-
}
|
|
73
|
-
function getEpochFields(params) {
|
|
74
|
-
const epochs = params.epochs;
|
|
75
|
-
if (Array.isArray(epochs) && epochs.length >= 2) {
|
|
76
|
-
return {
|
|
77
|
-
ordering: String(epochs[0] ?? ""),
|
|
78
|
-
convergence: String(epochs[1] ?? ""),
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
if (typeof epochs === "number") {
|
|
82
|
-
return {
|
|
83
|
-
ordering: String(epochs),
|
|
84
|
-
convergence: "",
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
return { ordering: "", convergence: "" };
|
|
88
|
-
}
|
|
89
|
-
async function fetchPreparePrompt(datasetId) {
|
|
90
|
-
try {
|
|
91
|
-
const data = (await apiCall("GET", `/v1/docs/prepare_training?dataset_id=${datasetId}`));
|
|
92
|
-
if (typeof data.prompt === "string" && data.prompt.trim()) {
|
|
93
|
-
return data.prompt.trim();
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
catch {
|
|
97
|
-
// Best effort only.
|
|
98
|
-
}
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
const TOOL_SYNTAX_RE = /\(action=|dataset_id=|job_id=|datasets\(|jobs\(/;
|
|
102
|
-
function buildWarnings(preview, selectedColumns, params, analyze) {
|
|
103
|
-
const warnings = [];
|
|
104
|
-
const stats = preview.column_stats ?? [];
|
|
105
|
-
const statByColumn = new Map(stats.map((stat) => [String(stat.column ?? ""), stat]));
|
|
106
|
-
for (const column of selectedColumns) {
|
|
107
|
-
const stat = statByColumn.get(column);
|
|
108
|
-
if (!stat)
|
|
109
|
-
continue;
|
|
110
|
-
if (stat.is_numeric === false) {
|
|
111
|
-
warnings.push(`${column} is not numeric in preview and should not be used directly for training.`);
|
|
112
|
-
}
|
|
113
|
-
if (Number(stat.null_count ?? 0) > 0) {
|
|
114
|
-
warnings.push(`${column} has ${stat.null_count} null values. Training columns must not contain missing values.`);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
const totalRows = Number(preview.total_rows ?? 0);
|
|
118
|
-
const grid = params.grid;
|
|
119
|
-
if (grid && totalRows > 0 && grid[0] * grid[1] > totalRows * 0.75) {
|
|
120
|
-
warnings.push(`Grid ${grid[0]}×${grid[1]} may be large for ${totalRows} rows, which can create many dead nodes.`);
|
|
121
|
-
}
|
|
122
|
-
const recommendations = analyze.recommendations ?? {};
|
|
123
|
-
const consideredDrop = recommendations.consider_dropping ?? [];
|
|
124
|
-
const overlappingDrop = consideredDrop.filter((column) => selectedColumns.includes(column));
|
|
125
|
-
if (overlappingDrop.length > 0) {
|
|
126
|
-
warnings.push(`Some selected columns are highly correlated and may be redundant: ${overlappingDrop.join(", ")}.`);
|
|
127
|
-
}
|
|
128
|
-
const rawCategoricals = stats
|
|
129
|
-
.filter((stat) => stat.is_numeric === false)
|
|
130
|
-
.map((stat) => String(stat.column ?? ""))
|
|
131
|
-
.filter(Boolean);
|
|
132
|
-
if (rawCategoricals.length > 0) {
|
|
133
|
-
const shown = rawCategoricals.slice(0, 5).join(", ");
|
|
134
|
-
const suffix = rawCategoricals.length > 5 ? ", ..." : "";
|
|
135
|
-
warnings.push(`Raw categorical/text columns detected: ${shown}${suffix}. Exclude them by default or use baseline categorical_features explicitly.`);
|
|
136
|
-
}
|
|
137
|
-
return warnings;
|
|
138
|
-
}
|
|
139
|
-
function buildDataRecommendations(analyze) {
|
|
140
|
-
const recommendations = [];
|
|
141
|
-
const dataRec = analyze.recommendations ?? {};
|
|
142
|
-
if ((dataRec.project_later ?? []).length > 0) {
|
|
143
|
-
recommendations.push(`Project later after training: ${(dataRec.project_later ?? []).join(", ")}.`);
|
|
144
|
-
}
|
|
145
|
-
if ((dataRec.low_variance ?? []).length > 0) {
|
|
146
|
-
recommendations.push(`Low-variance columns can usually be excluded: ${(dataRec.low_variance ?? []).join(", ")}.`);
|
|
147
|
-
}
|
|
148
|
-
return recommendations;
|
|
149
|
-
}
|
|
150
|
-
function buildGuidanceLines(guidanceText) {
|
|
151
|
-
const lines = [];
|
|
152
|
-
for (const line of guidanceText.split("\n")) {
|
|
153
|
-
const trimmed = line.replace(/^-\s*/, "").trim();
|
|
154
|
-
if (trimmed &&
|
|
155
|
-
!trimmed.startsWith("Parameter guidance") &&
|
|
156
|
-
!trimmed.startsWith("Presets:") &&
|
|
157
|
-
!TOOL_SYNTAX_RE.test(trimmed)) {
|
|
158
|
-
lines.push(trimmed);
|
|
159
|
-
}
|
|
160
|
-
if (lines.length >= 4)
|
|
161
|
-
break;
|
|
162
|
-
}
|
|
163
|
-
return [...new Set(lines)];
|
|
164
|
-
}
|
|
165
|
-
function buildSuggestedNormalizationMethods(analyze) {
|
|
166
|
-
const hints = analyze.normalization_hints;
|
|
167
|
-
if (!hints || typeof hints !== "object")
|
|
168
|
-
return undefined;
|
|
169
|
-
const out = {};
|
|
170
|
-
for (const [col, hint] of Object.entries(hints)) {
|
|
171
|
-
if (hint === "mad" || hint === "sepd") {
|
|
172
|
-
out[col] = hint;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
return Object.keys(out).length > 0 ? out : undefined;
|
|
176
|
-
}
|
|
177
|
-
function buildSuggestedTransforms(analyze) {
|
|
178
|
-
const hints = analyze.normalization_hints;
|
|
179
|
-
if (!hints || typeof hints !== "object")
|
|
180
|
-
return undefined;
|
|
181
|
-
const out = {};
|
|
182
|
-
for (const [col, hint] of Object.entries(hints)) {
|
|
183
|
-
if (hint === "log_then_zscore") {
|
|
184
|
-
out[col] = "log";
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
return Object.keys(out).length > 0 ? out : undefined;
|
|
188
|
-
}
|
|
189
|
-
/** Prep action -> (entitlement job type, review store action, display label). */
|
|
190
|
-
const PREP_ACTION_META = {
|
|
191
|
-
map: { jobType: "train_som", storeAction: "train_map", label: "Map (standard SOM)" },
|
|
192
|
-
siom_map: { jobType: "train_siom", storeAction: "train_siom", label: "SIOM map" },
|
|
193
|
-
impute: { jobType: "train_impute", storeAction: "train_impute", label: "Impute (sparse → map + imputed.csv)" },
|
|
194
|
-
floop_siom: { jobType: "train_floop_siom", storeAction: "train_floop_siom", label: "FLooP-SIOM (growing manifold)" },
|
|
195
|
-
};
|
|
196
|
-
function buildTrainActionOptions(allowedJobTypes) {
|
|
197
|
-
const allowed = new Set(allowedJobTypes);
|
|
198
|
-
// map is always available (train_som is the baseline entitlement); gate siom/impute.
|
|
199
|
-
return Object.keys(PREP_ACTION_META)
|
|
200
|
-
.filter((action) => action === "map" || allowed.size === 0 || allowed.has(PREP_ACTION_META[action].jobType))
|
|
201
|
-
.map((action) => ({ value: action, label: PREP_ACTION_META[action].label }));
|
|
202
|
-
}
|
|
203
|
-
/**
|
|
204
|
-
* Apply the action-specific job-type + knobs onto the params built by
|
|
205
|
-
* buildTrainMapParams (mirrors the train tool's runTrain for map/siom/impute).
|
|
206
|
-
*/
|
|
207
|
-
function applyPrepActionParams(action, params, input) {
|
|
208
|
-
if (action === "siom_map") {
|
|
209
|
-
params._job_type = "train_siom";
|
|
210
|
-
}
|
|
211
|
-
else if (action === "impute") {
|
|
212
|
-
params._job_type = "train_impute";
|
|
213
|
-
params.model = input.model ?? "auto";
|
|
214
|
-
params.cv_folds = 5;
|
|
215
|
-
// impute does not accept these (mirror train.ts) and rejects sepd (warned separately).
|
|
216
|
-
delete params.categorical_features;
|
|
217
|
-
delete params.auto_log_transforms;
|
|
218
|
-
delete params.time_delay_embeddings;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
/** Effective per-column normalization method, mirroring the worker/auto policy. */
|
|
222
|
-
function resolveColumnNormalization(column, normalize, methods, isCyclic) {
|
|
223
|
-
if (isCyclic)
|
|
224
|
-
return "cyclic_encoding";
|
|
225
|
-
if (methods && methods[column])
|
|
226
|
-
return methods[column];
|
|
227
|
-
if (typeof normalize === "string") {
|
|
228
|
-
if (normalize === "mad" || normalize === "sigmoidal" || normalize === "sepd")
|
|
229
|
-
return normalize;
|
|
230
|
-
return "zscore"; // auto / all -> z-score on non-cyclic numerics
|
|
231
|
-
}
|
|
232
|
-
return "zscore";
|
|
233
|
-
}
|
|
234
|
-
function normalizationDetail(method, stat, staging) {
|
|
235
|
-
const fmt = (v) => (v === null || v === undefined ? "?" : Number(v).toFixed(3));
|
|
236
|
-
const nulls = staging?.null_count ?? stat?.null_count;
|
|
237
|
-
const nullBit = nulls !== undefined && nulls !== null ? ` [nulls=${nulls}]` : "";
|
|
238
|
-
switch (method) {
|
|
239
|
-
case "zscore":
|
|
240
|
-
return `z-score · mean=${fmt(stat?.mean)}, std=${fmt(stat?.std)}${nullBit}`;
|
|
241
|
-
case "mad":
|
|
242
|
-
return `MAD (robust to heavy tails) · min=${fmt(stat?.min)}, max=${fmt(stat?.max)}${nullBit}`;
|
|
243
|
-
case "sigmoidal":
|
|
244
|
-
return `sigmoidal (logistic squashing of mean=${fmt(stat?.mean)}, std=${fmt(stat?.std)})${nullBit}`;
|
|
245
|
-
case "sepd":
|
|
246
|
-
return `SEPD (skewed exponential power; for skewed financial/policy columns)${nullBit}`;
|
|
247
|
-
case "none":
|
|
248
|
-
return `none (raw values)${nullBit}`;
|
|
249
|
-
case "cyclic_encoding":
|
|
250
|
-
return `cyclic (encoded as cos/sin, not separately normalized)`;
|
|
251
|
-
default:
|
|
252
|
-
return `${method}${nullBit}`;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
function buildNormalizationPlan(selectedColumns, params, preview, analyze) {
|
|
256
|
-
const statByColumn = new Map((preview.column_stats ?? []).map((s) => [String(s.column ?? ""), s]));
|
|
257
|
-
const stagingStats = analyze.column_staging_stats ?? {};
|
|
258
|
-
const methods = params.normalization_methods;
|
|
259
|
-
const cyclicNames = new Set((params.cyclic_features ?? []).map((c) => c.feature));
|
|
260
|
-
return selectedColumns.map((column) => {
|
|
261
|
-
const method = resolveColumnNormalization(column, params.normalize, methods, cyclicNames.has(column));
|
|
262
|
-
return {
|
|
263
|
-
column,
|
|
264
|
-
method,
|
|
265
|
-
detail: normalizationDetail(method, statByColumn.get(column), stagingStats[column]),
|
|
266
|
-
};
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
async function buildTrainingPrepPayload(datasetId, input, trainAction) {
|
|
270
|
-
const [preview, analyze, config, guidanceText, preparePrompt] = await Promise.all([
|
|
271
|
-
apiCall("GET", `/v1/datasets/${datasetId}/preview?n_rows=5`),
|
|
272
|
-
fetchAnalyzeData(datasetId),
|
|
273
|
-
fetchTrainingConfig().catch(() => ({ presets: {}, allowedJobTypes: [] })),
|
|
274
|
-
fetchTrainingGuidanceFromApi().catch(() => "No training guidance available."),
|
|
275
|
-
fetchPreparePrompt(datasetId),
|
|
276
|
-
]);
|
|
277
|
-
const presets = config.presets;
|
|
278
|
-
const selectedColumns = input.columns?.length
|
|
279
|
-
? input.columns
|
|
280
|
-
: getRecommendedTrainColumns(preview, analyze);
|
|
281
|
-
const resolvedInput = { ...input, columns: selectedColumns };
|
|
282
|
-
const isFloop = trainAction === "floop_siom";
|
|
283
|
-
const params = isFloop
|
|
284
|
-
? buildFloopParams(resolvedInput).params
|
|
285
|
-
: buildTrainMapParams(resolvedInput, presets).params;
|
|
286
|
-
if (!isFloop)
|
|
287
|
-
applyPrepActionParams(trainAction, params, resolvedInput);
|
|
288
|
-
const normalizationPlan = buildNormalizationPlan(selectedColumns, params, preview, analyze);
|
|
289
|
-
const review = storeTrainingReview(datasetId, params, PREP_ACTION_META[trainAction].storeAction);
|
|
290
|
-
const recommendations = analyze.recommendations ?? {};
|
|
291
|
-
const projectLater = recommendations.project_later ?? [];
|
|
292
|
-
const transforms = Object.entries(params.transforms ?? {})
|
|
293
|
-
.map(([feature, transform]) => `${feature}: ${transform}`);
|
|
294
|
-
if (params.auto_log_transforms === true) {
|
|
295
|
-
transforms.push("Auto log transforms: enabled");
|
|
296
|
-
}
|
|
297
|
-
const cyclic = (params.cyclic_features ?? [])
|
|
298
|
-
.map((feature) => `${feature.feature} (period ${feature.period})`);
|
|
299
|
-
const temporal = (params.temporal_features ?? [])
|
|
300
|
-
.map((feature) => {
|
|
301
|
-
const columns = feature.columns ?? [];
|
|
302
|
-
const extract = feature.extract ?? [];
|
|
303
|
-
return `${columns.join(" + ")} -> ${extract.join(", ")}`;
|
|
304
|
-
});
|
|
305
|
-
const epochs = getEpochFields(params);
|
|
306
|
-
const grid = params.grid ?? [];
|
|
307
|
-
const warnings = buildWarnings(preview, selectedColumns, params, analyze);
|
|
308
|
-
if (trainAction === "impute") {
|
|
309
|
-
const sepdCols = normalizationPlan.filter((n) => n.method === "sepd").map((n) => n.column);
|
|
310
|
-
if (params.normalize === "sepd" || sepdCols.length > 0) {
|
|
311
|
-
warnings.push(`SEPD normalization is not allowed for impute (train(impute) rejects sepd)${sepdCols.length ? `: ${sepdCols.join(", ")}` : ""}. Switch these columns to mad/zscore before submit.`);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
const suggestedNormalizationMethods = buildSuggestedNormalizationMethods(analyze);
|
|
315
|
-
const suggestedTransforms = buildSuggestedTransforms(analyze);
|
|
316
|
-
const recommendationLines = buildDataRecommendations(analyze);
|
|
317
|
-
if (suggestedNormalizationMethods) {
|
|
318
|
-
recommendationLines.push("Suggested normalization_methods (confirm before submit): " +
|
|
319
|
-
JSON.stringify(suggestedNormalizationMethods) +
|
|
320
|
-
" — see training_guidance for MAD vs SEPD ladder. train(impute) rejects sepd.");
|
|
321
|
-
}
|
|
322
|
-
if (suggestedTransforms) {
|
|
323
|
-
recommendationLines.push("Suggested transforms before normalization (confirm before submit): " +
|
|
324
|
-
JSON.stringify(suggestedTransforms) +
|
|
325
|
-
" — rank is train-only (no inference); not for train(impute).");
|
|
326
|
-
}
|
|
327
|
-
const guidanceLines = buildGuidanceLines(guidanceText);
|
|
328
|
-
const datasetName = String(preview.name ?? datasetId);
|
|
329
|
-
const sections = [
|
|
330
|
-
{
|
|
331
|
-
label: "Variables",
|
|
332
|
-
items: selectedColumns.length > 0 ? selectedColumns : ["No variables selected yet."],
|
|
333
|
-
},
|
|
334
|
-
{
|
|
335
|
-
label: "Projection Candidates",
|
|
336
|
-
items: projectLater.length > 0
|
|
337
|
-
? [...projectLater, "(These columns are good candidates for post-training projection onto the map.)"]
|
|
338
|
-
: ["None"],
|
|
339
|
-
},
|
|
340
|
-
{
|
|
341
|
-
label: "Preprocessing / Normalization (per column)",
|
|
342
|
-
items: normalizationPlan.length > 0
|
|
343
|
-
? [
|
|
344
|
-
...normalizationPlan.map((n) => `${n.column}: ${n.method} — ${n.detail}`),
|
|
345
|
-
"Tweak via normalization_methods (per column) or a global normalize; changing these reruns preprocessing (new recipe) on the next submit.",
|
|
346
|
-
]
|
|
347
|
-
: ["No training columns resolved yet."],
|
|
348
|
-
},
|
|
349
|
-
{
|
|
350
|
-
label: "Transformations",
|
|
351
|
-
items: transforms.length > 0 ? transforms : ["None"],
|
|
352
|
-
},
|
|
353
|
-
{
|
|
354
|
-
label: "Encodings",
|
|
355
|
-
items: [...cyclic, ...temporal].length > 0 ? [...cyclic, ...temporal] : ["None"],
|
|
356
|
-
},
|
|
357
|
-
{
|
|
358
|
-
label: "Hyperparameters",
|
|
359
|
-
items: isFloop
|
|
360
|
-
? [
|
|
361
|
-
`Variant: FLooP-SIOM (growing manifold — no fixed grid/epochs)`,
|
|
362
|
-
`Topology: ${String(params.topology ?? "free")}`,
|
|
363
|
-
`Max nodes: ${params.max_nodes !== undefined ? String(params.max_nodes) : "auto (~2·√n_samples)"}`,
|
|
364
|
-
`Effort: ${String(params.effort ?? "standard")}`,
|
|
365
|
-
params.gamma !== undefined ? `Gamma: ${String(params.gamma)}` : "",
|
|
366
|
-
params.siom_decay !== undefined ? `SIOM decay: ${String(params.siom_decay)}` : "",
|
|
367
|
-
`Normalize: ${Array.isArray(params.normalize) ? params.normalize.join(", ") : String(params.normalize ?? "auto")}`,
|
|
368
|
-
`Output: ${String(params.output_format ?? "png")}`,
|
|
369
|
-
].filter(Boolean)
|
|
370
|
-
: [
|
|
371
|
-
`Model: ${String(params.model ?? "SOM")}`,
|
|
372
|
-
`Grid: ${grid.length >= 2 ? `${grid[0]}×${grid[1]}` : "auto"}`,
|
|
373
|
-
`Epochs: ${epochs.ordering || "auto"}${epochs.convergence ? ` + ${epochs.convergence}` : ""}`,
|
|
374
|
-
`Backend: ${String(params.backend ?? "auto")}`,
|
|
375
|
-
`Normalize: ${Array.isArray(params.normalize) ? params.normalize.join(", ") : String(params.normalize ?? "auto")}`,
|
|
376
|
-
`Output: ${String(params.output_format ?? "png")}`,
|
|
377
|
-
],
|
|
378
|
-
},
|
|
379
|
-
];
|
|
380
|
-
const port = getVizPort();
|
|
381
|
-
const standaloneUrl = port ? `http://localhost:${port}/viz/training-prep?mode=standalone&review_token=${review.reviewToken}` : undefined;
|
|
382
|
-
const payload = {
|
|
383
|
-
type: "training-prep",
|
|
384
|
-
datasetId,
|
|
385
|
-
datasetName,
|
|
386
|
-
reviewId: review.reviewId,
|
|
387
|
-
reviewToken: review.reviewToken,
|
|
388
|
-
reviewExpiresAt: new Date(review.expiresAt).toISOString(),
|
|
389
|
-
trainAction,
|
|
390
|
-
selectedColumns,
|
|
391
|
-
suggestedProjectionColumns: projectLater,
|
|
392
|
-
normalizationPlan,
|
|
393
|
-
suggestedNormalizationMethods,
|
|
394
|
-
warnings,
|
|
395
|
-
recommendations: recommendationLines,
|
|
396
|
-
guidance: guidanceLines,
|
|
397
|
-
sections,
|
|
398
|
-
editable: {
|
|
399
|
-
values: {
|
|
400
|
-
preset: input.preset,
|
|
401
|
-
backend: String(params.backend ?? input.backend ?? "auto"),
|
|
402
|
-
outputFormat: String(params.output_format ?? input.output_format ?? "png"),
|
|
403
|
-
model: String(params.model ?? input.model ?? "SOM"),
|
|
404
|
-
gridX: grid.length >= 2 ? String(grid[0]) : "",
|
|
405
|
-
gridY: grid.length >= 2 ? String(grid[1]) : "",
|
|
406
|
-
epochsOrdering: epochs.ordering,
|
|
407
|
-
epochsConvergence: epochs.convergence,
|
|
408
|
-
trainAction,
|
|
409
|
-
columns: selectedColumns.join(", "),
|
|
410
|
-
normalizationMethodsJson: params.normalization_methods
|
|
411
|
-
? JSON.stringify(params.normalization_methods)
|
|
412
|
-
: "",
|
|
413
|
-
transformsJson: params.transforms ? JSON.stringify(params.transforms) : "",
|
|
414
|
-
},
|
|
415
|
-
options: {
|
|
416
|
-
presetOptions: Object.keys(presets).map((value) => ({ value, label: value.replace("_", " ") })),
|
|
417
|
-
backendOptions: [
|
|
418
|
-
{ value: "auto", label: "Auto" },
|
|
419
|
-
{ value: "cpu", label: "CPU" },
|
|
420
|
-
{ value: "gpu", label: "GPU" },
|
|
421
|
-
{ value: "gpu_graphs", label: "GPU Graphs" },
|
|
422
|
-
],
|
|
423
|
-
outputFormatOptions: [
|
|
424
|
-
{ value: "png", label: "PNG" },
|
|
425
|
-
{ value: "pdf", label: "PDF" },
|
|
426
|
-
{ value: "svg", label: "SVG" },
|
|
427
|
-
],
|
|
428
|
-
modelOptions: [
|
|
429
|
-
{ value: "SOM", label: "SOM" },
|
|
430
|
-
{ value: "RSOM", label: "RSOM" },
|
|
431
|
-
{ value: "SOM-SOFT", label: "SOM-SOFT" },
|
|
432
|
-
{ value: "RSOM-SOFT", label: "RSOM-SOFT" },
|
|
433
|
-
],
|
|
434
|
-
trainActionOptions: buildTrainActionOptions(config.allowedJobTypes),
|
|
435
|
-
},
|
|
436
|
-
},
|
|
437
|
-
draftArgs: {
|
|
438
|
-
...resolvedInput,
|
|
439
|
-
periodic: params.periodic,
|
|
440
|
-
normalize: params.normalize,
|
|
441
|
-
columns: selectedColumns,
|
|
442
|
-
output_format: params.output_format,
|
|
443
|
-
},
|
|
444
|
-
summaryLines: [
|
|
445
|
-
`Dataset: ${datasetName}`,
|
|
446
|
-
`Action: ${PREP_ACTION_META[trainAction].label}`,
|
|
447
|
-
`Variables: ${selectedColumns.length > 0 ? selectedColumns.join(", ") : "Not selected"}`,
|
|
448
|
-
isFloop
|
|
449
|
-
? `Topology: ${String(params.topology ?? "free")} | Max nodes: ${params.max_nodes !== undefined ? String(params.max_nodes) : "auto"} | Effort: ${String(params.effort ?? "standard")}`
|
|
450
|
-
: `Grid: ${grid.length >= 2 ? `${grid[0]}×${grid[1]}` : "auto"} | Model: ${String(params.model ?? "SOM")} | Backend: ${String(params.backend ?? "auto")} | Output: ${String(params.output_format ?? "png")}`,
|
|
451
|
-
`Normalization: ${normalizationPlan.length > 0 ? normalizationPlan.map((n) => `${n.column}→${n.method}`).join(", ") : "auto"}`,
|
|
452
|
-
],
|
|
453
|
-
standaloneUrl,
|
|
454
|
-
};
|
|
455
|
-
attachTrainingReviewPayload(review.reviewToken, payload);
|
|
456
|
-
return payload;
|
|
457
|
-
}
|
|
458
|
-
function buildTrainingPrepContent(payload) {
|
|
459
|
-
const normalizationBlock = payload.normalizationPlan.length > 0
|
|
460
|
-
? [
|
|
461
|
-
"",
|
|
462
|
-
"Preprocessing / normalization plan (per column — review before submit):",
|
|
463
|
-
...payload.normalizationPlan.map((n) => ` • ${n.column}: ${n.method} — ${n.detail}`),
|
|
464
|
-
"To change: re-call training_prep with normalization_methods (per column) and/or normalize/transforms; that reruns preprocessing on submit. Show this to the user and confirm before submit_prepared_training.",
|
|
465
|
-
]
|
|
466
|
-
: [];
|
|
467
|
-
const content = [
|
|
468
|
-
{
|
|
469
|
-
type: "text",
|
|
470
|
-
text: [
|
|
471
|
-
`Training prep ready for ${payload.datasetName} (${payload.datasetId}).`,
|
|
472
|
-
`Review ID: ${payload.reviewId} | Review token: ${payload.reviewToken ?? "(none)"}`,
|
|
473
|
-
...payload.summaryLines,
|
|
474
|
-
payload.warnings.length > 0 ? `Warnings: ${payload.warnings.join(" | ")}` : "",
|
|
475
|
-
...normalizationBlock,
|
|
476
|
-
"",
|
|
477
|
-
`When the user has confirmed, submit with submit_prepared_training(review_token="${payload.reviewToken ?? ""}", explicit_confirm=true).`,
|
|
478
|
-
].filter(Boolean).join("\n"),
|
|
479
|
-
},
|
|
480
|
-
];
|
|
481
|
-
if (!getClientSupportsMcpApps() && payload.standaloneUrl) {
|
|
482
|
-
content.push({
|
|
483
|
-
type: "text",
|
|
484
|
-
text: `Interactive review: ${payload.standaloneUrl}\nOpen this URL in your browser to review and submit the prepared job.`,
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
return content;
|
|
488
|
-
}
|
|
489
|
-
export function registerTrainingPrepTools(server) {
|
|
490
|
-
const trainPrepSchema = {
|
|
491
|
-
dataset_id: z.string().describe("Dataset ID to prepare for training"),
|
|
492
|
-
train_action: z.enum(["map", "siom_map", "impute", "floop_siom"]).optional().default("map")
|
|
493
|
-
.describe("Which training mode to prepare: map (standard SOM), siom_map (self-interacting), impute (sparse → map + imputed.csv), or floop_siom (growing node-budget manifold). Filtered to your plan's allowed_job_types."),
|
|
494
|
-
preset: z.enum(["quick", "standard", "refined", "high_res"]).optional(),
|
|
495
|
-
grid_x: z.number().int().optional(),
|
|
496
|
-
grid_y: z.number().int().optional(),
|
|
497
|
-
epochs: z.union([z.number().int(), z.array(z.number().int()).length(2)]).optional(),
|
|
498
|
-
model: z.enum(["SOM", "RSOM", "SOM-SOFT", "RSOM-SOFT"]).optional(),
|
|
499
|
-
periodic: z.boolean().optional().default(true),
|
|
500
|
-
columns: z.array(z.string()).optional(),
|
|
501
|
-
cyclic_features: z.array(z.object({
|
|
502
|
-
feature: z.string(),
|
|
503
|
-
period: z.number(),
|
|
504
|
-
})).optional(),
|
|
505
|
-
temporal_features: z.array(z.object({
|
|
506
|
-
columns: z.array(z.string()),
|
|
507
|
-
format: z.string(),
|
|
508
|
-
extract: z.array(z.enum(["hour_of_day", "day_of_year", "month", "day_of_week", "minute_of_hour"])),
|
|
509
|
-
cyclic: z.boolean().default(true),
|
|
510
|
-
separator: z.string().optional(),
|
|
511
|
-
})).optional(),
|
|
512
|
-
feature_weights: z.record(z.number()).optional(),
|
|
513
|
-
transforms: z.record(z.enum(["log", "log1p", "log10", "sqrt", "square", "abs", "invert", "rank", "none"])).optional(),
|
|
514
|
-
auto_log_transforms: z.boolean().optional().default(false),
|
|
515
|
-
normalize: z.union([z.enum(["all", "auto", "mad", "sigmoidal", "sepd"]), z.array(z.string())]).optional().default("auto"),
|
|
516
|
-
normalization_methods: z.record(z.enum(["zscore", "mad", "sigmoidal", "sepd", "none"])).optional(),
|
|
517
|
-
batch_size: z.number().int().optional(),
|
|
518
|
-
backend: z.enum(["auto", "cpu", "gpu", "gpu_graphs"]).optional().default("auto"),
|
|
519
|
-
output_format: z.enum(["png", "pdf", "svg"]).optional().default("png"),
|
|
520
|
-
row_range: z.tuple([z.number().int().min(1), z.number().int().min(1)]).optional(),
|
|
521
|
-
siom_feature_geometry: z.enum(["l2", "mixed", "auto"]).optional(),
|
|
522
|
-
siom_qe_backend: z.enum(["cpu", "cuda", "auto"]).optional(),
|
|
523
|
-
siom_qe_batch_size: z.number().int().min(1).max(1_000_000).optional(),
|
|
524
|
-
// FLooP-SIOM (train_action=floop_siom) — growing manifold; no fixed grid/epochs.
|
|
525
|
-
topology: z.enum(["free", "chain"]).optional(),
|
|
526
|
-
max_nodes: z.number().int().min(2).optional(),
|
|
527
|
-
effort: z.enum(["quick", "standard", "thorough"]).optional(),
|
|
528
|
-
n_initial: z.number().int().min(2).optional(),
|
|
529
|
-
n_passes: z.number().int().min(1).optional(),
|
|
530
|
-
growth_interval: z.number().int().min(1).optional(),
|
|
531
|
-
gamma: z.number().optional(),
|
|
532
|
-
siom_decay: z.number().optional(),
|
|
533
|
-
siom_penalty: z.enum(["linear", "log", "exp"]).optional(),
|
|
534
|
-
penalty_alpha: z.number().optional(),
|
|
535
|
-
elastic_lambda: z.number().optional(),
|
|
536
|
-
elastic_mu: z.number().optional(),
|
|
537
|
-
elastic_anchor: z.number().optional(),
|
|
538
|
-
};
|
|
539
|
-
registerAppTool(server, "training_prep", {
|
|
540
|
-
title: "Training Preparation",
|
|
541
|
-
description: "Interactive training prep UI + guarded submit (submit_prepared_training). Shows the RESOLVED decisions before a job is committed: per-column normalization (z-score / MAD / SEPD / sigmoidal) with its stat, transforms, encodings, grid/epochs/model. Tweak normalization_methods, transforms, columns, or train_action and re-call to iterate preprocessing before submitting. Prep ladder: prepare_training prompt = narrative checklist; training_guidance tool = JSON/presets; this tool = visual review + guarded submit.",
|
|
542
|
-
inputSchema: trainPrepSchema,
|
|
543
|
-
_meta: { ui: { resourceUri: TRAINING_PREP_URI } },
|
|
544
|
-
}, async ({ dataset_id, train_action, ...args }) => runMcpToolAudit("training_prep", "default", { dataset_id, train_action, ...args }, async () => {
|
|
545
|
-
const payload = await buildTrainingPrepPayload(dataset_id, args, (train_action ?? "map"));
|
|
546
|
-
return structuredTextResult(payload, undefined, buildTrainingPrepContent(payload));
|
|
547
|
-
}));
|
|
548
|
-
registerAuditedTool(server, "submit_prepared_training", "Submit a previously reviewed training-prep configuration. Requires an unexpired review token and explicit confirmation.", {
|
|
549
|
-
review_token: z.string().describe("Review token returned inside training_prep structured content."),
|
|
550
|
-
explicit_confirm: z.boolean().describe("Must be true to submit the reviewed training job."),
|
|
551
|
-
}, async ({ review_token, explicit_confirm }) => {
|
|
552
|
-
if (!explicit_confirm) {
|
|
553
|
-
throw new Error("submit_prepared_training requires explicit_confirm=true.");
|
|
554
|
-
}
|
|
555
|
-
const review = getTrainingReview(review_token);
|
|
556
|
-
if (!review) {
|
|
557
|
-
throw new Error("Review token is missing, expired, or already used. Re-open training_prep and confirm again.");
|
|
558
|
-
}
|
|
559
|
-
const data = (await apiCall("POST", "/v1/jobs", {
|
|
560
|
-
dataset_id: review.datasetId,
|
|
561
|
-
params: review.params,
|
|
562
|
-
}));
|
|
563
|
-
consumeTrainingReview(review_token);
|
|
564
|
-
const jobId = String(data.id ?? "");
|
|
565
|
-
return structuredTextResult({
|
|
566
|
-
type: "prepared-training-submitted",
|
|
567
|
-
datasetId: review.datasetId,
|
|
568
|
-
jobId,
|
|
569
|
-
message: `Training job submitted from reviewed prep. Poll with jobs(action=status, job_id="${jobId}").`,
|
|
570
|
-
}, `Training job submitted from reviewed prep. Job ID: ${jobId}\nPoll with jobs(action=status, job_id="${jobId}") until complete, then use results(action=get, job_id="${jobId}").`);
|
|
571
|
-
});
|
|
572
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
const REVIEW_TTL_MS = 15 * 60 * 1000;
|
|
2
|
-
const reviews = new Map();
|
|
3
|
-
function createToken() {
|
|
4
|
-
return `${Math.random().toString(36).slice(2, 10)}${Math.random().toString(36).slice(2, 10)}`;
|
|
5
|
-
}
|
|
6
|
-
function createReviewId() {
|
|
7
|
-
return Math.random().toString(36).slice(2, 10);
|
|
8
|
-
}
|
|
9
|
-
function pruneExpired() {
|
|
10
|
-
const now = Date.now();
|
|
11
|
-
for (const [token, review] of reviews.entries()) {
|
|
12
|
-
if (review.expiresAt <= now) {
|
|
13
|
-
reviews.delete(token);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
export function storeTrainingReview(datasetId, params, trainAction = "train_map") {
|
|
18
|
-
pruneExpired();
|
|
19
|
-
const createdAt = Date.now();
|
|
20
|
-
const review = {
|
|
21
|
-
reviewId: createReviewId(),
|
|
22
|
-
reviewToken: createToken(),
|
|
23
|
-
datasetId,
|
|
24
|
-
trainAction,
|
|
25
|
-
params,
|
|
26
|
-
createdAt,
|
|
27
|
-
expiresAt: createdAt + REVIEW_TTL_MS,
|
|
28
|
-
};
|
|
29
|
-
reviews.set(review.reviewToken, review);
|
|
30
|
-
return review;
|
|
31
|
-
}
|
|
32
|
-
export function attachTrainingReviewPayload(reviewToken, payload) {
|
|
33
|
-
pruneExpired();
|
|
34
|
-
const review = reviews.get(reviewToken);
|
|
35
|
-
if (!review)
|
|
36
|
-
return;
|
|
37
|
-
review.payload = payload;
|
|
38
|
-
}
|
|
39
|
-
export function getTrainingReview(reviewToken) {
|
|
40
|
-
pruneExpired();
|
|
41
|
-
const review = reviews.get(reviewToken);
|
|
42
|
-
if (!review)
|
|
43
|
-
return null;
|
|
44
|
-
return review;
|
|
45
|
-
}
|
|
46
|
-
export function consumeTrainingReview(reviewToken) {
|
|
47
|
-
pruneExpired();
|
|
48
|
-
const review = reviews.get(reviewToken);
|
|
49
|
-
if (!review)
|
|
50
|
-
return null;
|
|
51
|
-
reviews.delete(reviewToken);
|
|
52
|
-
return review;
|
|
53
|
-
}
|