@barivia/barsom-mcp 0.14.0 → 0.15.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { registerAuditedTool } from "../audit.js";
3
3
  import { apiCall, textResult } from "../shared.js";
4
+ import { buildTrainSubmitExtras } from "../train_submit_message.js";
4
5
  import { TRAINING_INPUT_SCHEMA, buildTrainMapParams, fetchTrainingPresets, } from "./training_core.js";
5
6
  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
 
@@ -124,29 +125,41 @@ export async function runTrain(action, args) {
124
125
  : action === "impute" ? "variant=train_impute"
125
126
  : "variant=som";
126
127
  data.effective_params = `${variantPrefix}, ${paramSummary}`;
128
+ const hasTransforms = action !== "impute" &&
129
+ transforms != null &&
130
+ typeof transforms === "object" &&
131
+ Object.keys(transforms).length > 0;
132
+ const extras = buildTrainSubmitExtras({
133
+ newJobId,
134
+ totalRows,
135
+ hasTransforms,
136
+ prepareJobId: data.prepare_job_id ?? null,
137
+ variantPrefix,
138
+ paramSummary,
139
+ impute: action === "impute",
140
+ });
127
141
  try {
128
142
  const sys = (await apiCall("GET", "/v1/system/info"));
129
143
  const pending = Number(sys.status?.pending_jobs ?? sys.pending_jobs ?? 0);
130
144
  const totalEta = Number(sys.training_time_estimates_seconds?.total ??
131
145
  (sys.gpu_available ? 45 : 120));
132
146
  const waitMinutes = Math.round((pending * totalEta) / 60);
133
- let msg = `Job submitted (${variantPrefix}, ${paramSummary}). `;
147
+ let msg = extras.message;
134
148
  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.`;
149
+ msg = `You are #${pending + 1} in queue. Estimated wait: ~${waitMinutes} min. ${msg}`;
138
150
  if (effectiveGrid && totalRows > 0 && effectiveGrid[0] * effectiveGrid[1] > totalRows * 0.75) {
139
151
  msg += ` Note: Grid may be large for ${totalRows} rows (consider grid=auto for fewer dead nodes).`;
140
152
  }
141
153
  data.message = msg;
154
+ data.suggested_next_step = extras.suggested_next_step;
142
155
  }
143
156
  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.`;
157
+ let msg = extras.message;
146
158
  if (effectiveGrid && totalRows > 0 && effectiveGrid[0] * effectiveGrid[1] > totalRows * 0.75) {
147
159
  msg += ` Note: Grid may be large for ${totalRows} rows (consider grid=auto for fewer dead nodes).`;
148
160
  }
149
161
  data.message = msg;
162
+ data.suggested_next_step = extras.suggested_next_step;
150
163
  }
151
164
  return textResult(data);
152
165
  }
@@ -229,6 +242,13 @@ export async function runTrain(action, args) {
229
242
  submitBody.label = label;
230
243
  const data = (await apiCall("POST", "/v1/jobs", submitBody));
231
244
  const newJobId = data.id;
245
+ let totalRows = 0;
246
+ try {
247
+ const preview = (await apiCall("GET", `/v1/datasets/${dataset_id}/preview?n_rows=1`));
248
+ totalRows = Number(preview?.total_rows ?? 0);
249
+ }
250
+ catch { /* ignore */ }
251
+ const hasTransforms = transforms != null && typeof transforms === "object" && Object.keys(transforms).length > 0;
232
252
  const maxNodeSummary = max_nodes === undefined ? "max_nodes=auto(~2*sqrt(n_samples))" : `max_nodes=${max_nodes}`;
233
253
  const paramSummary = [
234
254
  "variant=floop",
@@ -238,10 +258,17 @@ export async function runTrain(action, args) {
238
258
  gamma !== undefined ? `gamma=${gamma}` : "",
239
259
  ].filter(Boolean).join(", ");
240
260
  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.`;
261
+ const extras = buildTrainSubmitExtras({
262
+ newJobId,
263
+ totalRows,
264
+ hasTransforms,
265
+ prepareJobId: data.prepare_job_id ?? null,
266
+ variantPrefix: "variant=floop",
267
+ paramSummary,
268
+ resultsHint: "view FLooP-SIOM figures and metrics",
269
+ });
270
+ data.message = extras.message;
271
+ data.suggested_next_step = extras.suggested_next_step;
245
272
  return textResult(data);
246
273
  }
247
274
  throw new Error("Invalid train action");
@@ -0,0 +1,25 @@
1
+ import { apiCall, pollUntilComplete } from "./shared.js";
2
+ /**
3
+ * When train compute finishes, finalize_training may still be rendering figures on worker-io.
4
+ * Poll it before fetching results.
5
+ */
6
+ export async function pollFinalizeIfPresent(jobId, data, timeoutMs = 600_000) {
7
+ const finalizeJobId = data.finalize_job_id;
8
+ if (!finalizeJobId)
9
+ return { finalizeJobId: null, note: null };
10
+ const poll = await pollUntilComplete(finalizeJobId, timeoutMs);
11
+ if (poll.status === "failed") {
12
+ throw new Error(`Training job ${jobId}: finalize_training ${finalizeJobId} failed: ${poll.error ?? "unknown error"}`);
13
+ }
14
+ if (poll.status !== "completed") {
15
+ throw new Error(`Training job ${jobId}: finalize_training ${finalizeJobId} did not complete (status=${poll.status})`);
16
+ }
17
+ return {
18
+ finalizeJobId,
19
+ note: `Finalize job ${finalizeJobId} completed (figures and summary uploaded).`,
20
+ };
21
+ }
22
+ /** Re-fetch GET /v1/jobs/:id after finalize so callers see cleared finalize_job_id. */
23
+ export async function refreshJobAfterFinalize(jobId) {
24
+ return (await apiCall("GET", `/v1/jobs/${jobId}`));
25
+ }
@@ -0,0 +1,52 @@
1
+ import { getVizPort } from "./shared.js";
2
+ export function buildTrainSubmitExtras(opts) {
3
+ const { newJobId, totalRows, hasTransforms, prepareJobId, variantPrefix, paramSummary, impute, resultsHint } = opts;
4
+ const monitorUrl = getVizPort() > 0
5
+ ? `http://localhost:${getVizPort()}/viz/training-monitor?mode=standalone&job_id=${encodeURIComponent(newJobId)}`
6
+ : null;
7
+ let msg = `Job submitted (${variantPrefix}, ${paramSummary}). `;
8
+ if (prepareJobId) {
9
+ msg += `Preprocessing job prepare_training_matrix (${prepareJobId}) runs on worker-io first; train job ${newJobId} starts after it completes. Poll jobs(action=status, job_id="${prepareJobId}") until completed, then poll the train job. `;
10
+ }
11
+ msg += `Recommended: training_monitor(job_id="${newJobId}")`;
12
+ if (monitorUrl)
13
+ msg += ` or open ${monitorUrl}`;
14
+ msg += ` while polling jobs(action=status, job_id="${newJobId}") every 60–120s for large jobs. `;
15
+ msg += `When status is completed, check jobs(action=status) for finalize_job_id — if present, poll that finalize_training job before results(action=get). `;
16
+ msg += `Then use results(action=get, job_id="${newJobId}")`;
17
+ if (resultsHint) {
18
+ msg += ` to ${resultsHint}`;
19
+ }
20
+ else if (impute) {
21
+ msg += ` for the map and imputed.csv`;
22
+ }
23
+ else {
24
+ msg += ` to view the map and metrics`;
25
+ }
26
+ msg += `.`;
27
+ if (totalRows >= 1_000_000 || hasTransforms) {
28
+ msg += ` Large-table note: progress may stay low during staging preprocess before epochs start.`;
29
+ if (hasTransforms && totalRows >= 1_000_000) {
30
+ if (prepareJobId) {
31
+ msg += ` Transforms run in prepare_training_matrix on worker-io (full dataset, no CSV re-download on GPU).`;
32
+ }
33
+ else {
34
+ msg += ` Transforms require prepare_training_matrix on worker-io for staged datasets; the API enqueues it automatically when needed.`;
35
+ }
36
+ }
37
+ }
38
+ const pollSteps = prepareJobId
39
+ ? [
40
+ `1) jobs(action=status, job_id="${prepareJobId}") until prepare completes`,
41
+ `2) jobs(action=status, job_id="${newJobId}") every 60–120s until completed`,
42
+ `3) if finalize_job_id is set, poll it until completed`,
43
+ `4) results(action=download, job_id="${newJobId}", folder=".", include_json=true)`,
44
+ ]
45
+ : [
46
+ `1) training_monitor(job_id="${newJobId}")`,
47
+ `2) jobs(action=status, job_id="${newJobId}") every 60–120s until completed`,
48
+ `3) if finalize_job_id is set, poll it until completed`,
49
+ `4) results(action=download, job_id="${newJobId}", folder=".", include_json=true)`,
50
+ ];
51
+ return { message: msg, suggested_next_step: pollSteps.join(" → ") };
52
+ }