@barivia/barsom-mcp 0.15.3 → 0.17.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 +14 -7
- package/dist/index.js +1 -1
- package/dist/job_status_format.js +62 -6
- package/dist/shared.js +25 -4
- package/dist/tools/datasets.js +24 -1
- package/dist/tools/explore_map.js +1 -1
- package/dist/tools/inference.js +43 -4
- package/dist/tools/jobs.js +16 -56
- package/dist/tools/results.js +34 -62
- package/dist/tools/train.js +73 -133
- package/dist/tools/training_core.js +126 -9
- package/dist/tools/training_guidance.js +1 -1
- package/dist/tools/training_monitor.js +65 -6
- package/dist/tools/training_prep.js +211 -21
- package/dist/views/src/views/training-monitor/index.html +30 -16
- package/dist/views/src/views/training-prep/index.html +25 -11
- package/dist/viz-server.js +19 -0
- package/package.json +1 -1
|
@@ -10,6 +10,16 @@ export async function fetchTrainingPresets() {
|
|
|
10
10
|
const configData = (await apiCall("GET", "/v1/training/config"));
|
|
11
11
|
return configData?.presets || {};
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Fetch presets + the plan-scoped allowed_job_types in one /v1/training/config call.
|
|
15
|
+
* Used by training_prep to filter the train-action selector to entitled modes.
|
|
16
|
+
*/
|
|
17
|
+
export async function fetchTrainingConfig() {
|
|
18
|
+
const configData = (await apiCall("GET", "/v1/training/config"));
|
|
19
|
+
const presets = configData?.presets || {};
|
|
20
|
+
const allowedJobTypes = configData?.guidance_scope?.allowed_job_types || [];
|
|
21
|
+
return { presets, allowedJobTypes };
|
|
22
|
+
}
|
|
13
23
|
/**
|
|
14
24
|
* Zod raw shape for all training parameters (core SOM + impute + SIOM + FLooP).
|
|
15
25
|
* Spread into the `train` tool input schema and the deprecated `jobs` wrapper
|
|
@@ -48,7 +58,8 @@ export const TRAINING_INPUT_SCHEMA = {
|
|
|
48
58
|
separator: z.string().optional(),
|
|
49
59
|
})).optional(),
|
|
50
60
|
feature_weights: z.record(z.number()).optional(),
|
|
51
|
-
transforms: z.record(z.enum(["log", "log1p", "log10", "sqrt", "square", "abs", "invert", "rank", "none"])).optional()
|
|
61
|
+
transforms: z.record(z.enum(["log", "log1p", "log10", "sqrt", "square", "abs", "invert", "rank", "none"])).optional()
|
|
62
|
+
.describe("Per-column transform before normalization. rank is train-only (no batch predict/project_columns). Not for train(impute)."),
|
|
52
63
|
auto_log_transforms: z.boolean().optional().default(false),
|
|
53
64
|
time_delay_embeddings: z.array(z.object({
|
|
54
65
|
feature: z.string(),
|
|
@@ -59,7 +70,9 @@ export const TRAINING_INPUT_SCHEMA = {
|
|
|
59
70
|
categories: z.array(z.string()),
|
|
60
71
|
weight: z.number().positive()
|
|
61
72
|
})).optional(),
|
|
62
|
-
normalize: z.union([z.enum(["all", "auto", "mad", "sigmoidal"]), z.array(z.string())]).optional().default("auto"),
|
|
73
|
+
normalize: z.union([z.enum(["all", "auto", "mad", "sigmoidal", "sepd"]), z.array(z.string())]).optional().default("auto"),
|
|
74
|
+
normalization_methods: z.record(z.enum(["zscore", "mad", "sigmoidal", "sepd", "none"])).optional()
|
|
75
|
+
.describe("Per-feature normalization override. Use MAD for heavy tails; SEPD for skewed financial/policy columns (see training_guidance). Default auto=zscore on non-cyclics. train(impute) rejects sepd."),
|
|
63
76
|
sigma_f: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
|
|
64
77
|
learning_rate: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.union([z.number(), z.object({
|
|
65
78
|
ordering: z.tuple([z.number(), z.number()]),
|
|
@@ -80,12 +93,18 @@ export const TRAINING_INPUT_SCHEMA = {
|
|
|
80
93
|
.describe("With viz_mode=summary_plus_top: upload top-N component maps by variance (default 8)"),
|
|
81
94
|
emit_cell_uncertainty: z.boolean().optional()
|
|
82
95
|
.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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
96
|
+
gamma: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional())
|
|
97
|
+
.describe("siom_map / impute (SIOM or auto on som_siom+): self-interaction strength γ (default 0.5 on worker)"),
|
|
98
|
+
gamma_f: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional())
|
|
99
|
+
.describe("siom_map / impute SIOM: final γ for exponential decay schedule (0 = constant γ)"),
|
|
100
|
+
siom_decay: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional())
|
|
101
|
+
.describe("siom_map / impute SIOM: occupation EMA decay λ (default 0.01; 0 = cumulative counting)"),
|
|
102
|
+
siom_penalty: z.enum(["linear", "log", "exp"]).optional()
|
|
103
|
+
.describe("siom_map / impute SIOM: occupation penalty shape"),
|
|
104
|
+
penalty_alpha: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional())
|
|
105
|
+
.describe("siom_map / impute SIOM: scaling α inside log/exp penalty"),
|
|
106
|
+
reset_per_epoch: z.boolean().optional()
|
|
107
|
+
.describe("siom_map / impute SIOM: reset occupation μ at each epoch"),
|
|
89
108
|
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
109
|
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
110
|
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)"),
|
|
@@ -109,14 +128,112 @@ export const TRAINING_INPUT_SCHEMA = {
|
|
|
109
128
|
elastic_anchor: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
|
|
110
129
|
anchor_percentile: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
|
|
111
130
|
};
|
|
131
|
+
/**
|
|
132
|
+
* Build the FLooP-SIOM job params + summary from raw args. Shared single source for
|
|
133
|
+
* the `train` tool and `training_prep` so the growing-manifold knobs never drift.
|
|
134
|
+
* (FLooP has no fixed grid/epochs; it uses topology/max_nodes/effort + growth knobs.)
|
|
135
|
+
*/
|
|
136
|
+
export function buildFloopParams(args) {
|
|
137
|
+
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, } = args;
|
|
138
|
+
const params = {
|
|
139
|
+
_job_type: "train_floop_siom",
|
|
140
|
+
topology: topology ?? "free",
|
|
141
|
+
effort: effort ?? "standard",
|
|
142
|
+
normalize: normalize ?? "auto",
|
|
143
|
+
output_format: output_format ?? "png",
|
|
144
|
+
};
|
|
145
|
+
if (max_nodes !== undefined)
|
|
146
|
+
params.max_nodes = max_nodes;
|
|
147
|
+
if (columns?.length)
|
|
148
|
+
params.columns = columns;
|
|
149
|
+
if (cyclic_features?.length)
|
|
150
|
+
params.cyclic_features = cyclic_features;
|
|
151
|
+
if (temporal_features?.length)
|
|
152
|
+
params.temporal_features = temporal_features;
|
|
153
|
+
if (feature_weights && Object.keys(feature_weights).length > 0)
|
|
154
|
+
params.feature_weights = feature_weights;
|
|
155
|
+
if (transforms && Object.keys(transforms).length > 0)
|
|
156
|
+
params.transforms = transforms;
|
|
157
|
+
if (auto_log_transforms)
|
|
158
|
+
params.auto_log_transforms = auto_log_transforms;
|
|
159
|
+
if (row_range && row_range.length >= 2 && row_range[0] <= row_range[1])
|
|
160
|
+
params.row_range = row_range;
|
|
161
|
+
const dpiMap = { standard: 1, retina: 2, print: 4 };
|
|
162
|
+
if (output_dpi && output_dpi !== "retina")
|
|
163
|
+
params.output_dpi = dpiMap[output_dpi] ?? 2;
|
|
164
|
+
if (colormap)
|
|
165
|
+
params.colormap = colormap;
|
|
166
|
+
if (n_initial !== undefined)
|
|
167
|
+
params.n_initial = n_initial;
|
|
168
|
+
if (n_passes !== undefined)
|
|
169
|
+
params.n_passes = n_passes;
|
|
170
|
+
if (growth_interval !== undefined)
|
|
171
|
+
params.growth_interval = growth_interval;
|
|
172
|
+
if (neighborhood_size !== undefined)
|
|
173
|
+
params.neighborhood_size = neighborhood_size;
|
|
174
|
+
if (edge_max_age !== undefined)
|
|
175
|
+
params.edge_max_age = edge_max_age;
|
|
176
|
+
if (max_degree !== undefined)
|
|
177
|
+
params.max_degree = max_degree;
|
|
178
|
+
if (gamma !== undefined)
|
|
179
|
+
params.gamma = gamma;
|
|
180
|
+
if (siom_decay !== undefined)
|
|
181
|
+
params.siom_decay = siom_decay;
|
|
182
|
+
if (siom_penalty !== undefined)
|
|
183
|
+
params.siom_penalty = siom_penalty;
|
|
184
|
+
if (penalty_alpha !== undefined)
|
|
185
|
+
params.penalty_alpha = penalty_alpha;
|
|
186
|
+
if (error_threshold !== undefined)
|
|
187
|
+
params.error_threshold = error_threshold;
|
|
188
|
+
if (error_decay !== undefined)
|
|
189
|
+
params.error_decay = error_decay;
|
|
190
|
+
if (ring_close_threshold !== undefined)
|
|
191
|
+
params.ring_close_threshold = ring_close_threshold;
|
|
192
|
+
if (sigma_0 !== undefined)
|
|
193
|
+
params.sigma_0 = sigma_0;
|
|
194
|
+
if (sigma_f !== undefined)
|
|
195
|
+
params.sigma_f = sigma_f;
|
|
196
|
+
if (eta_0 !== undefined)
|
|
197
|
+
params.eta_0 = eta_0;
|
|
198
|
+
if (eta_f !== undefined)
|
|
199
|
+
params.eta_f = eta_f;
|
|
200
|
+
if (elastic_lambda !== undefined)
|
|
201
|
+
params.elastic_lambda = elastic_lambda;
|
|
202
|
+
if (elastic_mu !== undefined)
|
|
203
|
+
params.elastic_mu = elastic_mu;
|
|
204
|
+
if (elastic_anchor !== undefined)
|
|
205
|
+
params.elastic_anchor = elastic_anchor;
|
|
206
|
+
if (anchor_percentile !== undefined)
|
|
207
|
+
params.anchor_percentile = anchor_percentile;
|
|
208
|
+
const maxNodeSummary = max_nodes === undefined ? "max_nodes=auto(~2*sqrt(n_samples))" : `max_nodes=${max_nodes}`;
|
|
209
|
+
const paramSummary = [
|
|
210
|
+
"variant=floop",
|
|
211
|
+
`topology=${topology ?? "free"}`,
|
|
212
|
+
maxNodeSummary,
|
|
213
|
+
`effort=${effort ?? "standard"}`,
|
|
214
|
+
n_initial !== undefined ? `n_initial=${n_initial}` : "",
|
|
215
|
+
growth_interval !== undefined ? `growth_interval=${growth_interval}` : "",
|
|
216
|
+
n_passes !== undefined ? `n_passes=${n_passes}` : "",
|
|
217
|
+
gamma !== undefined ? `gamma=${gamma}` : "",
|
|
218
|
+
siom_decay !== undefined ? `siom_decay=${siom_decay}` : "",
|
|
219
|
+
siom_penalty !== undefined ? `siom_penalty=${siom_penalty}` : "",
|
|
220
|
+
elastic_lambda !== undefined ? `elastic_lambda=${elastic_lambda}` : "",
|
|
221
|
+
elastic_mu !== undefined ? `elastic_mu=${elastic_mu}` : "",
|
|
222
|
+
elastic_anchor !== undefined ? `elastic_anchor=${elastic_anchor}` : "",
|
|
223
|
+
].filter(Boolean).join(", ");
|
|
224
|
+
return { params, paramSummary };
|
|
225
|
+
}
|
|
112
226
|
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;
|
|
227
|
+
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, normalization_methods, 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
228
|
const p = preset ? presets[preset] : undefined;
|
|
115
229
|
const params = {
|
|
116
230
|
model: model ?? "SOM",
|
|
117
231
|
periodic: periodic ?? true,
|
|
118
232
|
normalize: normalize ?? "auto",
|
|
119
233
|
};
|
|
234
|
+
if (normalization_methods && Object.keys(normalization_methods).length > 0) {
|
|
235
|
+
params.normalization_methods = normalization_methods;
|
|
236
|
+
}
|
|
120
237
|
if (grid_x !== undefined && grid_y !== undefined)
|
|
121
238
|
params.grid = [grid_x, grid_y];
|
|
122
239
|
else if (p)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { registerAuditedTool } from "../audit.js";
|
|
2
2
|
import { fetchTrainingGuidanceFromApi } from "../shared.js";
|
|
3
3
|
export function registerTrainingGuidanceTool(server) {
|
|
4
|
-
registerAuditedTool(server, "training_guidance", "Structured parameter guidance for training (presets, grid/epochs/batch, model, periodic, SIOM/FLooP fields where your plan allows, categorical baselines). Served from the API and filtered to allowed_job_types. Prep ladder: prepare_training prompt = narrative checklist; this tool = JSON/hints; training_prep = interactive UI + submit_prepared_training. For full orientation and SOP, call guide_barsom_workflow first. Optional UIs: training_prep, results_explorer—not required; jobs + results suffice.", {}, async () => {
|
|
4
|
+
registerAuditedTool(server, "training_guidance", "Structured parameter guidance for training (presets, grid/epochs/batch, model, periodic, SIOM/FLooP fields where your plan allows, categorical baselines, normalization MAD vs SEPD guide). Served from the API and filtered to allowed_job_types. Prep ladder: prepare_training prompt = narrative checklist; this tool = JSON/hints + normalization decision ladder; training_prep = interactive UI + submit_prepared_training. For full orientation and SOP, call guide_barsom_workflow first. Optional UIs: training_prep, results_explorer—not required; jobs + results suffice.", {}, async () => {
|
|
5
5
|
const text = await fetchTrainingGuidanceFromApi();
|
|
6
6
|
return { content: [{ type: "text", text }] };
|
|
7
7
|
});
|
|
@@ -3,6 +3,16 @@ import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
|
|
|
3
3
|
import { runMcpToolAudit } from "../audit.js";
|
|
4
4
|
import { apiCall, getClientSupportsMcpApps, getVizPort, structuredTextResult, } from "../shared.js";
|
|
5
5
|
export const TRAINING_MONITOR_URI = "ui://barsom/training-monitor";
|
|
6
|
+
export const TRAINING_MONITOR_REFRESH_MS = 5000;
|
|
7
|
+
function hasCurveArrays(data) {
|
|
8
|
+
const keys = [
|
|
9
|
+
"ordering_errors",
|
|
10
|
+
"convergence_errors",
|
|
11
|
+
"ordering_topographic_errors",
|
|
12
|
+
"convergence_topographic_errors",
|
|
13
|
+
];
|
|
14
|
+
return keys.some((k) => Array.isArray(data[k]) && data[k].length > 0);
|
|
15
|
+
}
|
|
6
16
|
function buildStructuredContent(job_id, data) {
|
|
7
17
|
const id = String(data.id ?? job_id);
|
|
8
18
|
return {
|
|
@@ -11,24 +21,73 @@ function buildStructuredContent(job_id, data) {
|
|
|
11
21
|
jobId: id,
|
|
12
22
|
id,
|
|
13
23
|
job_id: id,
|
|
24
|
+
refresh_interval_ms: TRAINING_MONITOR_REFRESH_MS,
|
|
14
25
|
};
|
|
15
26
|
}
|
|
27
|
+
async function enrichWithTrainingLog(job_id, data) {
|
|
28
|
+
const status = String(data.status ?? "");
|
|
29
|
+
if (status !== "completed" && status !== "failed")
|
|
30
|
+
return data;
|
|
31
|
+
if (hasCurveArrays(data))
|
|
32
|
+
return data;
|
|
33
|
+
try {
|
|
34
|
+
const log = (await apiCall("GET", `/v1/results/${job_id}/training-log`));
|
|
35
|
+
return {
|
|
36
|
+
...data,
|
|
37
|
+
ordering_errors: log.ordering_errors ?? data.ordering_errors,
|
|
38
|
+
convergence_errors: log.convergence_errors ?? data.convergence_errors,
|
|
39
|
+
ordering_topographic_errors: log.ordering_topographic_errors ?? data.ordering_topographic_errors,
|
|
40
|
+
convergence_topographic_errors: log.convergence_topographic_errors ?? data.convergence_topographic_errors,
|
|
41
|
+
quantization_error: log.quantization_error ?? data.quantization_error,
|
|
42
|
+
topographic_error: log.topographic_error ?? data.topographic_error,
|
|
43
|
+
grid: log.grid ?? data.grid,
|
|
44
|
+
model: log.model ?? data.model,
|
|
45
|
+
epochs: log.epochs ?? data.epochs,
|
|
46
|
+
n_samples: log.n_samples ?? data.n_samples,
|
|
47
|
+
n_features: log.n_features ?? data.n_features,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return data;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
16
54
|
export function registerTrainingMonitorTool(server) {
|
|
17
55
|
registerAppTool(server, "training_monitor", {
|
|
18
56
|
title: "Training Monitor",
|
|
19
|
-
description: "Optional embedded or standalone view of training progress for a job_id.
|
|
57
|
+
description: "Optional embedded or standalone view of training progress for a job_id. When embedded in a client that supports MCP Apps, the panel auto-refreshes every 5s until the job completes: live QE/TE learning curves plus a per-epoch hit-distribution heatmap (sampled BMU counts across the map, showing where observations are landing as training proceeds). A standalone browser URL is also available. The same scalar fields are available via jobs(action=status)—not required to complete training or read results.",
|
|
20
58
|
inputSchema: {
|
|
21
59
|
job_id: z.string().describe("Training job ID to monitor"),
|
|
60
|
+
fetch_training_log: z
|
|
61
|
+
.boolean()
|
|
62
|
+
.optional()
|
|
63
|
+
.describe("Internal: merge completed-job training-log arrays when live progress is empty"),
|
|
22
64
|
},
|
|
23
65
|
_meta: { ui: { resourceUri: TRAINING_MONITOR_URI } },
|
|
24
66
|
}, async (args) => runMcpToolAudit("training_monitor", "default", args, async () => {
|
|
25
|
-
const job_id =
|
|
26
|
-
|
|
67
|
+
const { job_id, fetch_training_log } = args;
|
|
68
|
+
let data = (await apiCall("GET", `/v1/jobs/${job_id}`));
|
|
69
|
+
const jobStatus = String(data.status ?? "");
|
|
70
|
+
const terminal = jobStatus === "completed" || jobStatus === "failed";
|
|
71
|
+
if (fetch_training_log || (terminal && !hasCurveArrays(data))) {
|
|
72
|
+
data = await enrichWithTrainingLog(job_id, data);
|
|
73
|
+
}
|
|
27
74
|
const structuredContent = buildStructuredContent(job_id, data);
|
|
28
|
-
const status =
|
|
75
|
+
const status = jobStatus;
|
|
29
76
|
const progress = (data.progress ?? 0) * 100;
|
|
30
|
-
const
|
|
31
|
-
|
|
77
|
+
const etaSec = data.training_eta_sec != null ? Number(data.training_eta_sec) : null;
|
|
78
|
+
const elapsedSec = data.training_elapsed_sec != null ? Number(data.training_elapsed_sec) : null;
|
|
79
|
+
const epoch = data.epoch != null ? Number(data.epoch) : null;
|
|
80
|
+
const totalEpochs = data.total_epochs != null ? Number(data.total_epochs) : null;
|
|
81
|
+
const timingParts = [];
|
|
82
|
+
if (elapsedSec != null && elapsedSec >= 0)
|
|
83
|
+
timingParts.push(`elapsed ${Math.round(elapsedSec)}s`);
|
|
84
|
+
if (etaSec != null && etaSec > 0)
|
|
85
|
+
timingParts.push(`ETA ~${Math.round(etaSec)}s`);
|
|
86
|
+
if (epoch != null && totalEpochs != null)
|
|
87
|
+
timingParts.push(`epoch ${epoch}/${totalEpochs}`);
|
|
88
|
+
const timingNote = timingParts.length > 0 ? ` ${timingParts.join(", ")}.` : "";
|
|
89
|
+
const text = `Training monitor (live when embedded, refreshes every ${TRAINING_MONITOR_REFRESH_MS / 1000}s): job ${job_id} — ${status} (${progress.toFixed(1)}%).${timingNote} ` +
|
|
90
|
+
`Optional UI; jobs(action=status) is enough to finish the workflow.`;
|
|
32
91
|
const content = [{ type: "text", text }];
|
|
33
92
|
const port = getVizPort();
|
|
34
93
|
const standaloneUrl = port
|
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
|
|
3
3
|
import { registerAuditedTool, runMcpToolAudit } from "../audit.js";
|
|
4
4
|
import { apiCall, fetchTrainingGuidanceFromApi, getClientSupportsMcpApps, getVizPort, pollUntilComplete, POLL_DERIVE_MAX_MS, structuredTextResult, } from "../shared.js";
|
|
5
|
-
import { buildTrainMapParams,
|
|
5
|
+
import { buildTrainMapParams, buildFloopParams, fetchTrainingConfig, } 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
8
|
/**
|
|
@@ -162,20 +162,131 @@ function buildGuidanceLines(guidanceText) {
|
|
|
162
162
|
}
|
|
163
163
|
return [...new Set(lines)];
|
|
164
164
|
}
|
|
165
|
-
|
|
166
|
-
const
|
|
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([
|
|
167
271
|
apiCall("GET", `/v1/datasets/${datasetId}/preview?n_rows=5`),
|
|
168
272
|
fetchAnalyzeData(datasetId),
|
|
169
|
-
|
|
273
|
+
fetchTrainingConfig().catch(() => ({ presets: {}, allowedJobTypes: [] })),
|
|
170
274
|
fetchTrainingGuidanceFromApi().catch(() => "No training guidance available."),
|
|
171
275
|
fetchPreparePrompt(datasetId),
|
|
172
276
|
]);
|
|
277
|
+
const presets = config.presets;
|
|
173
278
|
const selectedColumns = input.columns?.length
|
|
174
279
|
? input.columns
|
|
175
280
|
: getRecommendedTrainColumns(preview, analyze);
|
|
176
281
|
const resolvedInput = { ...input, columns: selectedColumns };
|
|
177
|
-
const
|
|
178
|
-
const
|
|
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);
|
|
179
290
|
const recommendations = analyze.recommendations ?? {};
|
|
180
291
|
const projectLater = recommendations.project_later ?? [];
|
|
181
292
|
const transforms = Object.entries(params.transforms ?? {})
|
|
@@ -194,7 +305,25 @@ async function buildTrainingPrepPayload(datasetId, input) {
|
|
|
194
305
|
const epochs = getEpochFields(params);
|
|
195
306
|
const grid = params.grid ?? [];
|
|
196
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);
|
|
197
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
|
+
}
|
|
198
327
|
const guidanceLines = buildGuidanceLines(guidanceText);
|
|
199
328
|
const datasetName = String(preview.name ?? datasetId);
|
|
200
329
|
const sections = [
|
|
@@ -208,6 +337,15 @@ async function buildTrainingPrepPayload(datasetId, input) {
|
|
|
208
337
|
? [...projectLater, "(These columns are good candidates for post-training projection onto the map.)"]
|
|
209
338
|
: ["None"],
|
|
210
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
|
+
},
|
|
211
349
|
{
|
|
212
350
|
label: "Transformations",
|
|
213
351
|
items: transforms.length > 0 ? transforms : ["None"],
|
|
@@ -218,14 +356,25 @@ async function buildTrainingPrepPayload(datasetId, input) {
|
|
|
218
356
|
},
|
|
219
357
|
{
|
|
220
358
|
label: "Hyperparameters",
|
|
221
|
-
items:
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
+
],
|
|
229
378
|
},
|
|
230
379
|
];
|
|
231
380
|
const port = getVizPort();
|
|
@@ -237,8 +386,11 @@ async function buildTrainingPrepPayload(datasetId, input) {
|
|
|
237
386
|
reviewId: review.reviewId,
|
|
238
387
|
reviewToken: review.reviewToken,
|
|
239
388
|
reviewExpiresAt: new Date(review.expiresAt).toISOString(),
|
|
389
|
+
trainAction,
|
|
240
390
|
selectedColumns,
|
|
241
391
|
suggestedProjectionColumns: projectLater,
|
|
392
|
+
normalizationPlan,
|
|
393
|
+
suggestedNormalizationMethods,
|
|
242
394
|
warnings,
|
|
243
395
|
recommendations: recommendationLines,
|
|
244
396
|
guidance: guidanceLines,
|
|
@@ -253,6 +405,12 @@ async function buildTrainingPrepPayload(datasetId, input) {
|
|
|
253
405
|
gridY: grid.length >= 2 ? String(grid[1]) : "",
|
|
254
406
|
epochsOrdering: epochs.ordering,
|
|
255
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) : "",
|
|
256
414
|
},
|
|
257
415
|
options: {
|
|
258
416
|
presetOptions: Object.keys(presets).map((value) => ({ value, label: value.replace("_", " ") })),
|
|
@@ -273,6 +431,7 @@ async function buildTrainingPrepPayload(datasetId, input) {
|
|
|
273
431
|
{ value: "SOM-SOFT", label: "SOM-SOFT" },
|
|
274
432
|
{ value: "RSOM-SOFT", label: "RSOM-SOFT" },
|
|
275
433
|
],
|
|
434
|
+
trainActionOptions: buildTrainActionOptions(config.allowedJobTypes),
|
|
276
435
|
},
|
|
277
436
|
},
|
|
278
437
|
draftArgs: {
|
|
@@ -284,9 +443,12 @@ async function buildTrainingPrepPayload(datasetId, input) {
|
|
|
284
443
|
},
|
|
285
444
|
summaryLines: [
|
|
286
445
|
`Dataset: ${datasetName}`,
|
|
446
|
+
`Action: ${PREP_ACTION_META[trainAction].label}`,
|
|
287
447
|
`Variables: ${selectedColumns.length > 0 ? selectedColumns.join(", ") : "Not selected"}`,
|
|
288
|
-
|
|
289
|
-
|
|
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"}`,
|
|
290
452
|
],
|
|
291
453
|
standaloneUrl,
|
|
292
454
|
};
|
|
@@ -294,14 +456,25 @@ async function buildTrainingPrepPayload(datasetId, input) {
|
|
|
294
456
|
return payload;
|
|
295
457
|
}
|
|
296
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
|
+
: [];
|
|
297
467
|
const content = [
|
|
298
468
|
{
|
|
299
469
|
type: "text",
|
|
300
470
|
text: [
|
|
301
471
|
`Training prep ready for ${payload.datasetName} (${payload.datasetId}).`,
|
|
302
|
-
`Review ID: ${payload.reviewId}`,
|
|
472
|
+
`Review ID: ${payload.reviewId} | Review token: ${payload.reviewToken ?? "(none)"}`,
|
|
303
473
|
...payload.summaryLines,
|
|
304
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).`,
|
|
305
478
|
].filter(Boolean).join("\n"),
|
|
306
479
|
},
|
|
307
480
|
];
|
|
@@ -316,6 +489,8 @@ function buildTrainingPrepContent(payload) {
|
|
|
316
489
|
export function registerTrainingPrepTools(server) {
|
|
317
490
|
const trainPrepSchema = {
|
|
318
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."),
|
|
319
494
|
preset: z.enum(["quick", "standard", "refined", "high_res"]).optional(),
|
|
320
495
|
grid_x: z.number().int().optional(),
|
|
321
496
|
grid_y: z.number().int().optional(),
|
|
@@ -337,7 +512,8 @@ export function registerTrainingPrepTools(server) {
|
|
|
337
512
|
feature_weights: z.record(z.number()).optional(),
|
|
338
513
|
transforms: z.record(z.enum(["log", "log1p", "log10", "sqrt", "square", "abs", "invert", "rank", "none"])).optional(),
|
|
339
514
|
auto_log_transforms: z.boolean().optional().default(false),
|
|
340
|
-
normalize: z.union([z.enum(["all", "auto", "mad", "sigmoidal"]), z.array(z.string())]).optional().default("auto"),
|
|
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(),
|
|
341
517
|
batch_size: z.number().int().optional(),
|
|
342
518
|
backend: z.enum(["auto", "cpu", "gpu", "gpu_graphs"]).optional().default("auto"),
|
|
343
519
|
output_format: z.enum(["png", "pdf", "svg"]).optional().default("png"),
|
|
@@ -345,14 +521,28 @@ export function registerTrainingPrepTools(server) {
|
|
|
345
521
|
siom_feature_geometry: z.enum(["l2", "mixed", "auto"]).optional(),
|
|
346
522
|
siom_qe_backend: z.enum(["cpu", "cuda", "auto"]).optional(),
|
|
347
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(),
|
|
348
538
|
};
|
|
349
539
|
registerAppTool(server, "training_prep", {
|
|
350
540
|
title: "Training Preparation",
|
|
351
|
-
description: "Interactive training prep UI + guarded submit (submit_prepared_training). Prep ladder: prepare_training prompt = narrative checklist; training_guidance tool = JSON/presets; this tool = visual review
|
|
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.",
|
|
352
542
|
inputSchema: trainPrepSchema,
|
|
353
543
|
_meta: { ui: { resourceUri: TRAINING_PREP_URI } },
|
|
354
|
-
}, async ({ dataset_id, ...args }) => runMcpToolAudit("training_prep", "default", { dataset_id, ...args }, async () => {
|
|
355
|
-
const payload = await buildTrainingPrepPayload(dataset_id, args);
|
|
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"));
|
|
356
546
|
return structuredTextResult(payload, undefined, buildTrainingPrepContent(payload));
|
|
357
547
|
}));
|
|
358
548
|
registerAuditedTool(server, "submit_prepared_training", "Submit a previously reviewed training-prep configuration. Requires an unexpired review token and explicit confirmation.", {
|