@barivia/barsom-mcp 0.15.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.
- package/README.md +3 -2
- package/dist/index.js +1 -1
- package/dist/inference_prepare.js +18 -0
- package/dist/job_status_format.js +75 -0
- package/dist/shared.js +1 -1
- package/dist/tools/datasets.js +59 -9
- package/dist/tools/inference.js +5 -0
- package/dist/tools/jobs.js +17 -18
- package/dist/tools/results.js +2 -2
- package/dist/tools/train.js +37 -10
- package/dist/train_finalize.js +25 -0
- package/dist/train_submit_message.js +52 -0
- package/dist/views/src/views/training-monitor/index.html +18 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -86,10 +86,11 @@ Call at the **start of mapping work** (or when the user asks what the MCP can do
|
|
|
86
86
|
|
|
87
87
|
| Action | Use when |
|
|
88
88
|
|--------|----------|
|
|
89
|
-
| `upload` | Adding a new CSV — returns dataset_id. Accepts `.csv`/`.tsv` **and pre-gzipped `.csv.gz`/`.tsv.gz`** (compress big tables locally to transfer ~3× less). Large files stream directly to object storage; multi-GB uploads are supported. |
|
|
89
|
+
| `upload` | Adding a new CSV — returns dataset_id and **`stage_job_id`** when staging is enqueued. Accepts `.csv`/`.tsv` **and pre-gzipped `.csv.gz`/`.tsv.gz`** (compress big tables locally to transfer ~3× less). Large files stream directly to object storage; multi-GB uploads are supported. |
|
|
90
90
|
| `preview` | Before training — inspect columns, stats, cyclic/datetime hints |
|
|
91
91
|
| `analyze` | Pre-training recommendations (correlation, columns to consider dropping, etc.) |
|
|
92
92
|
| `list` | Finding dataset IDs |
|
|
93
|
+
| `get` | One dataset by id — status, staging fields, `stage_job_id`, ingest errors |
|
|
93
94
|
| `subset` | Creating a filtered/sliced copy (row_range, filter conditions) |
|
|
94
95
|
| `add_expression` | Add a derived column from an expression (formula → new column on the dataset) |
|
|
95
96
|
| `reduce_spectral` | Pre-training reducer for long ordered numeric blocks (spectra, time series, sensor fingerprints, gene panels). Methods: **pca** (top-k principal components), **log_sample** (k columns at log-spaced indices — scattering, audio bands), **uniform_sample** (k columns at evenly-spaced indices — regularly-sampled time series), **stats** (6 fixed per-row summary columns). All produce one feature vector per row; appends derived columns to the dataset. |
|
|
@@ -214,7 +215,7 @@ MCP Client (Cursor/Claude) ←stdio→ @barivia/barsom-mcp ←HTTPS→ api.bariv
|
|
|
214
215
|
| `401` / invalid key | `BARIVIA_API_KEY` in MCP config; check your email or manage your key in the [account dashboard](https://barivia.se/dashboard). Error text includes a **request id** for support. |
|
|
215
216
|
| Request timed out | Raise `BARIVIA_FETCH_TIMEOUT_MS` (e.g. `120000`). Large uploads already use an extended timeout; `datasets(analyze)` is async (auto-polled). Timeouts are not auto-retried, so re-running a timed-out upload is safe — an idempotency key reconciles it to the original dataset instead of duplicating. |
|
|
216
217
|
| `Path must be within the workspace` / upload can’t find file | Set `BARIVIA_WORKSPACE_ROOT` to your project directory, or use an absolute path / `file:///...` URI. |
|
|
217
|
-
| Job stuck “running” | Poll `jobs(action=status)` every 10–15s; large grids or FLooP-SIOM can take several minutes—not an MCP error. |
|
|
218
|
+
| Job stuck “running” | Poll `jobs(action=status)` every 10–15s; large grids or FLooP-SIOM can take several minutes—not an MCP error. Staged datasets enqueue **`prepare_training_matrix`** (or impute prepare) first — train submit returns `prepare_job_id`; MCP auto-polls prepare when present. CFD mesh submit may return `prepare_job_id` for **`cfd_prepare`** (barmesh auto-polls). |
|
|
218
219
|
| `429` | Rate limit—wait and retry. |
|
|
219
220
|
| Malformed MCP / client errors | Ensure nothing writes to **stdout** except MCP JSON-RPC (the proxy logs API traffic to **stderr** only). |
|
|
220
221
|
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{McpServer as e}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as t}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as o}from"zod";import{getUiCapability as r,registerAppResource as n,RESOURCE_MIME_TYPE as s}from"@modelcontextprotocol/ext-apps/server";import{startVizServer as a}from"./viz-server.js";import{API_KEY as i,apiCall as l,apiRawCall as p,loadViewHtml as c,setVizPort as m,setClientSupportsMcpApps as d,CLIENT_VERSION as u}from"./shared.js";import{registerDatasetsTool as f}from"./tools/datasets.js";import{registerTrainTool as g}from"./tools/train.js";import{registerJobsTool as _,JOBS_DESCRIPTION_BASE as b}from"./tools/jobs.js";import{registerResultsTool as y}from"./tools/results.js";import{registerExploreMapTool as h,RESULTS_EXPLORER_URI as w}from"./tools/explore_map.js";import{registerAccountTool as j}from"./tools/account.js";import{registerInferenceTool as v}from"./tools/inference.js";import{registerGuideBarsomTool as P}from"./tools/guide_barsom.js";import{registerTrainingGuidanceTool as x}from"./tools/training_guidance.js";import{registerFeedbackTool as I}from"./tools/feedback.js";import{registerTrainingPrepTools as k,TRAINING_PREP_URI as T}from"./tools/training_prep.js";import{registerTrainingMonitorTool as M,TRAINING_MONITOR_URI as O}from"./tools/training_monitor.js";import{resolvePrepareTrainingPromptText as A}from"./prepare_training_prompt.js";i||(console.error("Error: BARIVIA_API_KEY not set. Set it in your MCP client config."),process.exit(1));const S=new e({name:"analytics-engine",version:u,instructions:'# Barivia Mapping Analytics Engine\n\nSelf-organizing map (SOM) analytics: project high-dimensional data to a 2D grid for clusters, gradients, and anomalies.\n\n## Workflow (short)\n\nUpload (`datasets(upload)`) → `datasets(preview)` and `datasets(analyze)` before train → submit one of `train(action=map)`, `train(action=siom_map)`, or `train(action=floop_siom)` (only if plan allows FLooP) → poll `jobs(status)` every 10–15s until `completed` → `results(get)` for metrics and figures (there is no separate analyze tool). Then `jobs(compare)`, `results(download/recolor/transition_flow)`, or `inference` as needed.\n\n**Full detail:** Call `guide_barsom_workflow` for plan-scoped tool map, training modes, async rules, optional MCP App UIs, and step-by-step SOP (from the Barivia API when online).\n\n## Tool map (compact)\n\n| Area | Tool | Notes |\n|------|------|--------|\n| Data | `datasets` | upload, preview, analyze, list, subset, add_expression, reduce_spectral
|
|
2
|
+
import{McpServer as e}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as t}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as o}from"zod";import{getUiCapability as r,registerAppResource as n,RESOURCE_MIME_TYPE as s}from"@modelcontextprotocol/ext-apps/server";import{startVizServer as a}from"./viz-server.js";import{API_KEY as i,apiCall as l,apiRawCall as p,loadViewHtml as c,setVizPort as m,setClientSupportsMcpApps as d,CLIENT_VERSION as u}from"./shared.js";import{registerDatasetsTool as f}from"./tools/datasets.js";import{registerTrainTool as g}from"./tools/train.js";import{registerJobsTool as _,JOBS_DESCRIPTION_BASE as b}from"./tools/jobs.js";import{registerResultsTool as y}from"./tools/results.js";import{registerExploreMapTool as h,RESULTS_EXPLORER_URI as w}from"./tools/explore_map.js";import{registerAccountTool as j}from"./tools/account.js";import{registerInferenceTool as v}from"./tools/inference.js";import{registerGuideBarsomTool as P}from"./tools/guide_barsom.js";import{registerTrainingGuidanceTool as x}from"./tools/training_guidance.js";import{registerFeedbackTool as I}from"./tools/feedback.js";import{registerTrainingPrepTools as k,TRAINING_PREP_URI as T}from"./tools/training_prep.js";import{registerTrainingMonitorTool as M,TRAINING_MONITOR_URI as O}from"./tools/training_monitor.js";import{resolvePrepareTrainingPromptText as A}from"./prepare_training_prompt.js";i||(console.error("Error: BARIVIA_API_KEY not set. Set it in your MCP client config."),process.exit(1));const S=new e({name:"analytics-engine",version:u,instructions:'# Barivia Mapping Analytics Engine\n\nSelf-organizing map (SOM) analytics: project high-dimensional data to a 2D grid for clusters, gradients, and anomalies.\n\n## Workflow (short)\n\nUpload (`datasets(upload)`) → `datasets(preview)` and `datasets(analyze)` before train → submit one of `train(action=map)`, `train(action=siom_map)`, or `train(action=floop_siom)` (only if plan allows FLooP) → poll `jobs(status)` every 10–15s until `completed` → `results(get)` for metrics and figures (there is no separate analyze tool). Then `jobs(compare)`, `results(download/recolor/transition_flow)`, or `inference` as needed.\n\n**Full detail:** Call `guide_barsom_workflow` for plan-scoped tool map, training modes, async rules, optional MCP App UIs, and step-by-step SOP (from the Barivia API when online).\n\n## Tool map (compact)\n\n| Area | Tool | Notes |\n|------|------|--------|\n| Data | `datasets` | upload, preview, analyze, list, **get** (status/staging), subset, add_expression, reduce_spectral, delete |\n| Train | `train` | map, siom_map, impute (sparse data: map + dense imputed.csv), floop_siom (entitled), baseline_study — all async, returns job_id |\n| Jobs | `jobs` | status, list, compare, cancel, delete (lifecycle). jobs(train_*/run_baseline_study/batch_predict) still work as deprecated forwarders → use train / inference |\n| Results | `results` | get (figures="none" for metrics-only), export, download, recolor (async), transition_flow (async; time-ordered rows only) |\n| Inference | `inference` | predict (regime-aware; output="compact"|"annotated"), batch_predict (fan out many predict jobs), impute_column (neighbor-pool fill for a non-training column), compare, project_columns, report |\n| Account | `account` | status, burst/compute actions, history, add_funds |\n| Bootstrap | `guide_barsom_workflow` | orientation + SOP |\n| Parameters | `training_guidance` | presets and field hints (API-scoped) |\n| Prep | `prepare_training` prompt, `training_prep` + `submit_prepared_training` | checklist / interactive UI |\n| Explore | `results_explorer`, `training_monitor` | optional MCP Apps; `jobs(status)` and `results(get)` suffice without them |\n| Other | `send_feedback` | only after user agrees |\n\n## Async pattern\n\n- **Manual poll:** Training submits return `job_id` immediately — poll `jobs(status)` every 10–15s. **Running is not failed**; large grids or FLooP-SIOM can take many minutes. `max_nodes` (FLooP) is a total node budget, not grid side length.\n- **Often auto-polled:** `inference` actions, `results(recolor)`, `results(transition_flow)` may wait in-proxy; if you get a `job_id`, poll `jobs(status)` the same way.\n\nCredits: jobs consume compute credits; check `account(status)` before big runs. Per-request HTTP timeout defaults to 60s (`datasets(analyze)` is async + auto-polled, so it is not bound by it); on slow networks raise `BARIVIA_FETCH_TIMEOUT_MS`. Timeouts are NOT auto-retried (avoids duplicating slow/expensive work).\n\n## Constraints\n\n- Prep ladder: `prepare_training` prompt = narrative checklist; `training_guidance` = structured hints; `training_prep` = UI + guarded submit. Do not guess tiers or FLooP entitlement.\n- `inference(predict)`: prefer `dataset_id` for batch and for SIOM/irregular maps; single-row `rows` uses a fast path that can fail on some topologies — retry with `dataset_id`. FLooP-SIOM: if predict jobs fail while grid SIOM works, capture errors + `job_id`.\n- Column names are case-sensitive — match `datasets(preview)`.\n- Default training path is numeric/cyclic/temporal; use explicit `categorical_features` for baseline categoricals. `predict` must match the model contract.\n- After `recolor`, `transition_flow`, or `project_columns`, use the **new** `job_id` returned for follow-up `results` if applicable.'});n(S,w,w,{mimeType:s},async()=>{const e=await c("results-explorer");return{contents:[{uri:w,mimeType:s,text:e??"<html><body>Results Explorer view not built yet. Run: npm run build:views</body></html>"}]}}),n(S,T,T,{mimeType:s},async()=>{const e=await c("training-prep");return{contents:[{uri:T,mimeType:s,text:e??"<html><body>Training Preparation view not built yet.</body></html>"}]}}),n(S,O,O,{mimeType:s},async()=>{const e=await c("training-monitor");return{contents:[{uri:O,mimeType:s,text:e??"<html><body>Training Monitor view not built yet.</body></html>"}]}}),P(S),h(S),k(S),M(S),f(S),g(S),_(S,b),y(S),j(S),v(S),x(S),I(S),S.prompt("info","Short orientation for the Barivia Mapping MCP. For full plan-scoped workflow, tool map, and SOP, the model should call guide_barsom_workflow. Use when the user asks what this MCP can do or how to get started.",{},()=>({messages:[{role:"user",content:{type:"text",text:["Give a concise, scannable answer (headers + bullets):","","**What it is:** MCP client to the Barivia mapping engine (2D SOM / SIOM / FLooP-SIOM when entitled) over HTTPS.","","**First step:** Call `guide_barsom_workflow` for plan-scoped bootstrap (full tool list, async rules, training modes, optional MCP Apps, SOP).","","**Core path:** `datasets(upload)` → `datasets(preview)` + `datasets(analyze)` → choose training action → poll `jobs(status)` every 10–15s until completed → `results(get)` (all main figures/metrics; no separate analyze tool).","",'**Key tools:** `datasets` (data; reduce_spectral for spectra/long blocks), `train` (map/siom_map/impute/floop_siom/baseline_study; pass `label` for readable compare rows), `jobs` (status/list/compare/cancel/delete), `results` (get/download/export/recolor/transition_flow; figures="none" for metrics-only), `inference` (predict; batch_predict; impute_column for topology-neighbor pool fill; compare; project_columns; report), `account` (status/credits/queue).',"","**Prep help:** `prepare_training` prompt (checklist) · `training_guidance` (presets/JSON hints) · `training_prep` + `submit_prepared_training` (interactive UI).","","**Optional UI:** `results_explorer`, `training_monitor` — nice for browsing; not required if you use `results` + `jobs(status)`.","","**After training:** `jobs(compare)` across runs, `results(recolor)`, `inference(project_columns)` for variables not in training, `transition_flow` only if rows are time-ordered.","","**Rules:** Running ≠ failed. Column names must match `datasets(preview)` exactly. Do not call `_fetch_figure` from chat (host/UI only); use `results(get)` or `results_explorer`.","","Offer `send_feedback` only after asking the user."].join("\n")}}]})),S.prompt("prepare_training","Narrative pre-training checklist (prompt). Use after upload and before train. Content is tier-scoped from the API when online. Prep ladder: this prompt = story checklist; training_guidance tool = JSON presets/parameter hints; training_prep tool = interactive UI + submit_prepared_training.",{dataset_id:o.string().describe("Dataset ID to prepare for training")},async({dataset_id:e})=>({messages:[{role:"user",content:{type:"text",text:await A(e)}}]}));const C=new t;(async function(){try{const e=await a(l,p,c);m(e)}catch(e){process.env.BARIVIA_VIZ_PORT&&console.error("Barivia viz server failed to start:",e)}const e=S.server;e.oninitialized=()=>{const t=e.getClientCapabilities(),o=r(t);d(!!o?.mimeTypes?.includes(s))},await S.connect(C)})().catch(console.error);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { pollUntilComplete } from "./shared.js";
|
|
2
|
+
/**
|
|
3
|
+
* When the API enqueues prepare_training_matrix for cross-dataset scoring,
|
|
4
|
+
* poll it to completion before polling the main inference job.
|
|
5
|
+
*/
|
|
6
|
+
export async function pollPrepareIfPresent(data, label, timeoutMs = 600_000) {
|
|
7
|
+
const prepareJobId = data.prepare_job_id;
|
|
8
|
+
if (!prepareJobId)
|
|
9
|
+
return null;
|
|
10
|
+
const poll = await pollUntilComplete(prepareJobId, timeoutMs);
|
|
11
|
+
if (poll.status === "failed") {
|
|
12
|
+
throw new Error(`${label}: prepare_training_matrix job ${prepareJobId} failed: ${poll.error ?? "unknown error"}`);
|
|
13
|
+
}
|
|
14
|
+
if (poll.status !== "completed") {
|
|
15
|
+
throw new Error(`${label}: prepare_training_matrix job ${prepareJobId} did not complete (status=${poll.status})`);
|
|
16
|
+
}
|
|
17
|
+
return prepareJobId;
|
|
18
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/** Format jobs(status) text from GET /v1/jobs/:id payload. Exported for tests. */
|
|
2
|
+
export function formatJobStatusText(job_id, data) {
|
|
3
|
+
const status = String(data.status ?? "unknown");
|
|
4
|
+
const progress = (data.progress ?? 0) * 100;
|
|
5
|
+
const label = data.label != null && data.label !== "" ? String(data.label) : null;
|
|
6
|
+
const jobDesc = label ? `Job ${label} (id: ${job_id})` : `Job ${job_id}`;
|
|
7
|
+
const parts = [`${jobDesc}: ${status} (${progress.toFixed(1)}%)`];
|
|
8
|
+
const attempt = data.attempt != null ? Number(data.attempt) : null;
|
|
9
|
+
if (attempt != null && attempt > 1) {
|
|
10
|
+
parts.push(`attempt ${attempt} (job was requeued after worker timeout — not stuck from scratch)`);
|
|
11
|
+
}
|
|
12
|
+
const phase = data.progress_phase != null && String(data.progress_phase) !== ""
|
|
13
|
+
? String(data.progress_phase)
|
|
14
|
+
: null;
|
|
15
|
+
if (status === "running") {
|
|
16
|
+
if (phase) {
|
|
17
|
+
parts.push(`phase: ${phase}`);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
parts.push(`phase: preprocessing (not set yet — often download/parse before first progress tick)`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const startedAt = data.started_at != null ? String(data.started_at) : null;
|
|
24
|
+
if (startedAt) {
|
|
25
|
+
parts.push(`started_at: ${startedAt}`);
|
|
26
|
+
const startedMs = Date.parse(startedAt);
|
|
27
|
+
if (!Number.isNaN(startedMs)) {
|
|
28
|
+
const elapsedSec = Math.max(0, Math.round((Date.now() - startedMs) / 1000));
|
|
29
|
+
parts.push(`elapsed: ${elapsedSec}s`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const datasetRows = data.dataset_rows != null ? Number(data.dataset_rows) : null;
|
|
33
|
+
const datasetName = data.dataset_name != null ? String(data.dataset_name) : null;
|
|
34
|
+
if (datasetRows != null && datasetRows > 0) {
|
|
35
|
+
const nameBit = datasetName ? ` (${datasetName})` : "";
|
|
36
|
+
parts.push(`dataset: ${datasetRows.toLocaleString()} rows${nameBit}`);
|
|
37
|
+
}
|
|
38
|
+
if (status === "running" &&
|
|
39
|
+
progress < 5 &&
|
|
40
|
+
datasetRows != null &&
|
|
41
|
+
datasetRows >= 1_000_000) {
|
|
42
|
+
parts.push("Advisory: still in early preprocessing (download/parse/normalize). Progress may stay near 0% for a long time on large tables; epoch counter starts after ingest.");
|
|
43
|
+
}
|
|
44
|
+
else if (status === "running" && progress < 5 && !phase) {
|
|
45
|
+
parts.push("Advisory: still in early preprocessing — epoch counter starts after ingest completes.");
|
|
46
|
+
}
|
|
47
|
+
if (status === "completed") {
|
|
48
|
+
const finalizeId = data.finalize_job_id != null && String(data.finalize_job_id) !== ""
|
|
49
|
+
? String(data.finalize_job_id)
|
|
50
|
+
: null;
|
|
51
|
+
if (finalizeId) {
|
|
52
|
+
parts.push(`Training kernel done; finalize_training still running (finalize_job_id=${finalizeId}). Poll jobs(action=status) again to auto-wait, or poll that job directly before results(get).`);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
parts.push(`Results ready. Use results(action=get, job_id="${job_id}") to retrieve.`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else if (status === "failed") {
|
|
59
|
+
const failureStage = data.failure_stage != null && String(data.failure_stage) !== ""
|
|
60
|
+
? ` [${data.failure_stage}]`
|
|
61
|
+
: "";
|
|
62
|
+
parts.push(`Error${failureStage}: ${data.error ?? "unknown"}`);
|
|
63
|
+
}
|
|
64
|
+
else if (status === "cancelled") {
|
|
65
|
+
parts.push(`Cancelled. Poll again to confirm; dataset and partial worker state are unchanged.`);
|
|
66
|
+
}
|
|
67
|
+
return parts.join(" | ");
|
|
68
|
+
}
|
|
69
|
+
export function formatJobCancelText(job_id, data) {
|
|
70
|
+
const status = String(data.status ?? "cancelled");
|
|
71
|
+
const ts = new Date().toISOString();
|
|
72
|
+
return (`Cancel accepted at ${ts} for job ${job_id}: status=${status}. ` +
|
|
73
|
+
`Poll jobs(action=status, job_id="${job_id}") to confirm; training stops between phases (may take up to ~30s).`);
|
|
74
|
+
}
|
|
75
|
+
export const GZIP_UPLOAD_HINT = "Try gzip: save as .csv.gz (often 2–3× smaller). The upload limit applies to the compressed file size on presigned/large uploads.";
|
package/dist/shared.js
CHANGED
|
@@ -26,7 +26,7 @@ export const RETRYABLE_STATUS = new Set([502, 503, 504]);
|
|
|
26
26
|
* X-Barsom-Client-Version so the server can annotate tool guidance with the
|
|
27
27
|
* wrapper version each action requires. Keep in sync with package.json on bump.
|
|
28
28
|
*/
|
|
29
|
-
export const CLIENT_VERSION = "0.15.
|
|
29
|
+
export const CLIENT_VERSION = "0.15.3";
|
|
30
30
|
/** User-facing links; keep aligned with barivia.se / api.barivia.se. */
|
|
31
31
|
export const PUBLIC_SITE_ORIGIN = "https://barivia.se";
|
|
32
32
|
/** Self-serve account dashboard (manage plan, billing, and API keys). */
|
package/dist/tools/datasets.js
CHANGED
|
@@ -5,6 +5,7 @@ import { createHash } from "node:crypto";
|
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { registerAuditedTool } from "../audit.js";
|
|
7
7
|
import { apiCall, getWorkspaceRootAsync, resolveFilePathForUpload, textResult, pollUntilComplete, POLL_DERIVE_MAX_MS, UPLOAD_DATASET_TIMEOUT_MS, LARGE_UPLOAD_BYTES, PRESIGNED_PUT_TIMEOUT_MS, POLL_STAGE_MAX_MS, streamFileSha256, putPresignedStream, } from "../shared.js";
|
|
8
|
+
import { GZIP_UPLOAD_HINT } from "../job_status_format.js";
|
|
8
9
|
/**
|
|
9
10
|
* Format a dataset analyze result (from either the async job summary or the
|
|
10
11
|
* legacy sync GET response) into the human-readable text block.
|
|
@@ -95,18 +96,21 @@ function formatAnalyzeResult(data, dataset_id) {
|
|
|
95
96
|
export function registerDatasetsTool(server) {
|
|
96
97
|
registerAuditedTool(server, "datasets", `Manage datasets: upload, preview, list, subset, add_expression, or delete.
|
|
97
98
|
|
|
99
|
+
Formats: plain CSV/TSV or gzip (.csv.gz / .tsv.gz). For files above ~100 MB, prefer .csv.gz (often 2–3× smaller); large uploads use presigned direct-to-storage PUT and accept gzip bodies.
|
|
100
|
+
|
|
98
101
|
| Action | Use when |
|
|
99
102
|
|--------|----------|
|
|
100
|
-
| upload | You have a CSV file to add — do this first |
|
|
103
|
+
| upload | You have a CSV or .csv.gz file to add — do this first |
|
|
101
104
|
| preview | Before train(action=map) — always preview an unfamiliar dataset to spot cyclics, nulls, column types |
|
|
102
105
|
| analyze | Pre-training column analysis: correlation matrix, periodicity ranking, column recommendations (train / drop / project later). Run after preview, before train_map. |
|
|
103
106
|
| list | Finding dataset IDs for train_map, preview, or subset — see all available datasets |
|
|
107
|
+
| get | Fetch one dataset by id — status, staging fields, ingest_error (use after upload or when staging is slow) |
|
|
104
108
|
| subset | Creating a filtered/sliced view without re-uploading the full CSV |
|
|
105
109
|
| add_expression | Add a computed column from an expression (dataset_id + name + expression). With project_onto_job=<training_job_id>, instead projects the computed expression onto that map as a component plane in one step (no column added). |
|
|
106
110
|
| reduce_spectral | Collapse a long ordered numeric block (e.g. 1000-point spectrum, time series, sensor fingerprint) into a small per-row feature set so the SOM can train on signal-dense features. Methods: pca, log_sample, uniform_sample, stats. |
|
|
107
111
|
| delete | Cleaning up after experiments or freeing the dataset slot |
|
|
108
112
|
|
|
109
|
-
action=upload: PREFER file_path — server reads from workspace root (token-efficient; no file content in context). Use csv_data only for small inline pastes (e.g. <10KB). Returns dataset ID. Then use datasets(action=preview) before train(action=map).
|
|
113
|
+
action=upload: PREFER file_path — server reads from workspace root (token-efficient; no file content in context). Accepts .csv, .tsv, .csv.gz, .tsv.gz. Use csv_data only for small inline pastes (e.g. <10KB). Returns dataset ID. Then use datasets(action=preview) before train(action=map). If plain CSV exceeds the 5 GB upload cap, gzip it first (.csv.gz).
|
|
110
114
|
|
|
111
115
|
Step 0 (before upload): (1) CSV with header; one row per observation. (2) Columns you will use for training must be numeric (or datetime if you will use temporal extraction). (3) Complete-case path (train action=map): no NaNs in training columns. Sparse path (train action=impute): missing cells in training columns are OK — the job trains missSOM and returns dense imputed.csv; plain numeric columns only. (4) Categoricals: encode (e.g. one-hot, label) or exclude — do not use raw categorical columns as training features; preview shows which columns are non-numeric. (5) Optional: if you plan to use transition_flow, sort rows chronologically before upload.
|
|
112
116
|
Step 1 (after upload): always datasets(action=preview) to verify column types and spot cyclics/datetime, then choose train_map (complete-case) or train_impute (sparse) before submit.
|
|
@@ -114,6 +118,7 @@ Step 1 (after upload): always datasets(action=preview) to verify column types an
|
|
|
114
118
|
action=preview: Show columns, stats, sample rows, cyclic/datetime detections. ALWAYS preview before train(action=map) on an unfamiliar dataset.
|
|
115
119
|
action=analyze: Pre-training analysis on numeric columns — Pearson correlation, autocorrelation periodicity scores, and column recommendations (train, consider_dropping, project_later, low_variance). Use to choose which columns to include in training and which to project onto the map after training.
|
|
116
120
|
action=list: List all datasets belonging to the organisation with id, name, rows, cols, created_at (use created_at or id to distinguish datasets with the same name).
|
|
121
|
+
action=get: Fetch one dataset by dataset_id — returns status, staged_prefix, staged_version, ingest_error, and stage_job_id when staging is pending.
|
|
117
122
|
action=subset: Create a new dataset from a subset of an existing one. Requires name and at least one of row_range, filters, or sample_n.
|
|
118
123
|
- row_range: [start, end] 1-based inclusive (e.g. [1, 2000] for first 2000 rows)
|
|
119
124
|
- filters: array of conditions, ALL must match (AND logic). Each: { column, op, value }.
|
|
@@ -134,12 +139,12 @@ BEST FOR: Tabular numeric data. CSV with header required.
|
|
|
134
139
|
NOT FOR: Real-time data streams or binary files — upload a snapshot CSV instead.
|
|
135
140
|
ESCALATION: If upload fails with column errors, open the file locally and verify the header row. If preview shows nulls in training columns: use train(action=impute) for sparse data, or clean/subset for train(action=map).`, {
|
|
136
141
|
action: z
|
|
137
|
-
.enum(["upload", "preview", "analyze", "list", "subset", "delete", "add_expression", "reduce_spectral"])
|
|
138
|
-
.describe("upload: add CSV; preview: inspect columns/stats; analyze: pre-training correlation and periodicity; list: see all datasets; subset: create filtered subset; delete: remove dataset; add_expression: add derived column from expression (or, with project_onto_job, project the expression onto a trained map as a component plane); reduce_spectral: collapse a long ordered numeric block (e.g. spectrum, time series) into a small per-row feature set via PCA / log_sample / uniform_sample / stats"),
|
|
142
|
+
.enum(["upload", "preview", "analyze", "list", "get", "subset", "delete", "add_expression", "reduce_spectral"])
|
|
143
|
+
.describe("upload: add CSV or .csv.gz; preview: inspect columns/stats; analyze: pre-training correlation and periodicity; list: see all datasets; get: fetch one dataset metadata (status/staging); subset: create filtered subset; delete: remove dataset; add_expression: add derived column from expression (or, with project_onto_job, project the expression onto a trained map as a component plane); reduce_spectral: collapse a long ordered numeric block (e.g. spectrum, time series) into a small per-row feature set via PCA / log_sample / uniform_sample / stats"),
|
|
139
144
|
name: z.string().optional().describe("Dataset name (required for action=upload and subset)"),
|
|
140
|
-
file_path: z.string().optional().describe("Path to local CSV (PREFERRED): absolute path, file:// URI, or relative to workspace root. Token-efficient; server reads file."),
|
|
145
|
+
file_path: z.string().optional().describe("Path to local CSV or .csv.gz (PREFERRED): absolute path, file:// URI, or relative to workspace root. Token-efficient; server reads file. Use .csv.gz for large scientific tables."),
|
|
141
146
|
csv_data: z.string().optional().describe("Inline CSV string for small pastes only (<10KB). Avoid for large files — use file_path instead to avoid token explosion."),
|
|
142
|
-
dataset_id: z.string().optional().describe("Dataset ID (required for preview, subset, and delete)"),
|
|
147
|
+
dataset_id: z.string().optional().describe("Dataset ID (required for preview, get, subset, and delete)"),
|
|
143
148
|
n_rows: z.number().int().optional().default(5).describe("Sample rows to return (preview only)"),
|
|
144
149
|
row_range: z
|
|
145
150
|
.tuple([z.number().int(), z.number().int()])
|
|
@@ -243,13 +248,24 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
243
248
|
`Set BARIVIA_WORKSPACE_ROOT in your MCP config env if needed (current: ${await getWorkspaceRootAsync(server)}).`);
|
|
244
249
|
}
|
|
245
250
|
if (stat.size > HARD_MAX_BYTES) {
|
|
246
|
-
|
|
251
|
+
const gzipHint = isGzipInput ? "" : ` ${GZIP_UPLOAD_HINT}`;
|
|
252
|
+
throw new Error(`File too large (${(stat.size / 1024 / 1024 / 1024).toFixed(2)} GB). Maximum upload size is 5 GB.${gzipHint}`);
|
|
247
253
|
}
|
|
248
254
|
// Large files: stream gzip directly to R2 (presigned PUT), then stage async.
|
|
249
255
|
// Never reads the file into memory or sends it through the API/Cloudflare.
|
|
250
256
|
if (stat.size >= LARGE_UPLOAD_BYTES) {
|
|
251
257
|
const idem = await streamFileSha256(resolved);
|
|
252
|
-
|
|
258
|
+
let init;
|
|
259
|
+
try {
|
|
260
|
+
init = (await apiCall("POST", "/v1/datasets/upload-url", { name, size_bytes: stat.size }, { "Idempotency-Key": idem }));
|
|
261
|
+
}
|
|
262
|
+
catch (e) {
|
|
263
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
264
|
+
if (msg.includes("dataset_too_large") && !isGzipInput) {
|
|
265
|
+
throw new Error(`${msg} ${GZIP_UPLOAD_HINT}`);
|
|
266
|
+
}
|
|
267
|
+
throw e;
|
|
268
|
+
}
|
|
253
269
|
const datasetId = (init.dataset_id ?? init.id);
|
|
254
270
|
// Only short-circuit when the prior upload actually completed. A stale
|
|
255
271
|
// `pending_upload` row (a previous PUT that never landed) comes back with
|
|
@@ -584,9 +600,43 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
584
600
|
return textResult(data);
|
|
585
601
|
}
|
|
586
602
|
if (action === "list") {
|
|
587
|
-
const data = await apiCall("GET", "/v1/datasets");
|
|
603
|
+
const data = (await apiCall("GET", "/v1/datasets"));
|
|
604
|
+
if (Array.isArray(data)) {
|
|
605
|
+
const lines = data.map((ds) => {
|
|
606
|
+
const id = String(ds.id ?? "");
|
|
607
|
+
const name = String(ds.name ?? "");
|
|
608
|
+
const rows = ds.rows != null ? Number(ds.rows) : "?";
|
|
609
|
+
const cols = ds.cols != null ? Number(ds.cols) : "?";
|
|
610
|
+
const st = ds.status != null ? String(ds.status) : "ready";
|
|
611
|
+
const statusBit = st !== "ready" ? ` | status=${st}` : "";
|
|
612
|
+
const err = ds.ingest_error != null && String(ds.ingest_error) !== ""
|
|
613
|
+
? ` | ingest_error=${String(ds.ingest_error)}`
|
|
614
|
+
: "";
|
|
615
|
+
return `${name} (${id}) — ${rows}×${cols}${statusBit}${err}`;
|
|
616
|
+
});
|
|
617
|
+
return { content: [{ type: "text", text: lines.length > 0 ? lines.join("\n") : "No datasets." }] };
|
|
618
|
+
}
|
|
588
619
|
return textResult(data);
|
|
589
620
|
}
|
|
621
|
+
if (action === "get") {
|
|
622
|
+
if (!dataset_id)
|
|
623
|
+
throw new Error("datasets(get) requires dataset_id");
|
|
624
|
+
const ds = (await apiCall("GET", `/v1/datasets/${dataset_id}`));
|
|
625
|
+
const lines = [
|
|
626
|
+
`Dataset: ${ds.name ?? "?"} (${ds.id ?? dataset_id})`,
|
|
627
|
+
`Status: ${ds.status ?? "ready"}`,
|
|
628
|
+
`Rows × cols: ${ds.rows ?? "?"} × ${ds.cols ?? "?"}`,
|
|
629
|
+
ds.size_bytes != null ? `Size: ${Number(ds.size_bytes).toLocaleString()} bytes` : "",
|
|
630
|
+
ds.staged_prefix != null ? `Staged prefix: ${String(ds.staged_prefix)}` : "",
|
|
631
|
+
ds.staged_version != null ? `Staged version: ${String(ds.staged_version)}` : "",
|
|
632
|
+
ds.stage_job_id != null ? `Stage job: ${String(ds.stage_job_id)} (poll jobs(action=status))` : "",
|
|
633
|
+
ds.ingest_error != null && String(ds.ingest_error) !== ""
|
|
634
|
+
? `Ingest error: ${String(ds.ingest_error)}`
|
|
635
|
+
: "",
|
|
636
|
+
ds.created_at != null ? `Created: ${String(ds.created_at)}` : "",
|
|
637
|
+
].filter(Boolean);
|
|
638
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
639
|
+
}
|
|
590
640
|
if (action === "delete") {
|
|
591
641
|
if (!dataset_id)
|
|
592
642
|
throw new Error("datasets(delete) requires dataset_id");
|
package/dist/tools/inference.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { registerAuditedTool } from "../audit.js";
|
|
3
3
|
import { apiCall, pollUntilComplete, tryAttachImage } from "../shared.js";
|
|
4
|
+
import { pollPrepareIfPresent } from "../inference_prepare.js";
|
|
4
5
|
const PREDICT_PREVIEW_ROW_CAP = 10;
|
|
5
6
|
/** One line per scored row when worker embedded `predictions_preview` (n_rows ≤ cap). Exported for tests. */
|
|
6
7
|
export function formatPredictPreviewLines(summary) {
|
|
@@ -153,6 +154,7 @@ action=report: Returns a report manifest for the given job_id (job must be compl
|
|
|
153
154
|
body.rows = rows;
|
|
154
155
|
const data = (await apiCall("POST", `/v1/results/${job_id}/predict`, body));
|
|
155
156
|
const predictJobId = data.id;
|
|
157
|
+
const prepareId = await pollPrepareIfPresent(data, "inference(predict)");
|
|
156
158
|
// annotated runs over the full dataset; allow up to 120s like compact
|
|
157
159
|
const poll = await pollUntilComplete(predictJobId, 120_000);
|
|
158
160
|
if (poll.status === "completed") {
|
|
@@ -180,6 +182,7 @@ action=report: Returns a report manifest for the given job_id (job must be compl
|
|
|
180
182
|
: `Output: predictions.csv (row_id, bmu_x, bmu_y, bmu_node_index, cluster_id, quantization_error, potential_anomaly). Summary includes mean_qe, max_qe, qe_p95. Clusters: ${Object.keys(summary.cluster_counts ?? {}).length}.`);
|
|
181
183
|
return { content: [{ type: "text", text: [
|
|
182
184
|
headerLine,
|
|
185
|
+
prepareId ? `(prepare_training_matrix ${prepareId} completed before scoring)` : "",
|
|
183
186
|
`Regime: ${regime}${regime === "training" ? " (scored against the training set)" : ""} | Style: ${effectiveStyle}`,
|
|
184
187
|
`Rows scored: ${summary.n_rows ?? "?"}`,
|
|
185
188
|
metricsLine,
|
|
@@ -271,6 +274,7 @@ action=report: Returns a report manifest for the given job_id (job must be compl
|
|
|
271
274
|
body.colormap = colormap;
|
|
272
275
|
const data = (await apiCall("POST", `/v1/results/${job_id}/project_columns`, body));
|
|
273
276
|
const projJobId = data.id;
|
|
277
|
+
const prepareId = await pollPrepareIfPresent(data, "inference(project_columns)");
|
|
274
278
|
const poll = await pollUntilComplete(projJobId, 90_000);
|
|
275
279
|
if (poll.status === "completed") {
|
|
276
280
|
const results = (await apiCall("GET", `/v1/results/${projJobId}`));
|
|
@@ -280,6 +284,7 @@ action=report: Returns a report manifest for the given job_id (job must be compl
|
|
|
280
284
|
const fmtStat = (v) => v !== null && v !== undefined ? Number(v).toFixed(3) : "—";
|
|
281
285
|
const lines = [
|
|
282
286
|
`Project columns complete — job: ${projJobId}`,
|
|
287
|
+
prepareId ? `(prepare_training_matrix ${prepareId} completed before projection)` : "",
|
|
283
288
|
`Columns: ${summary.columns?.join(", ") ?? columns.join(", ")} | Samples: ${summary.n_samples ?? "?"}`,
|
|
284
289
|
...Object.entries(columnStats).map(([col, s]) => {
|
|
285
290
|
const parts = [`${col}: nodes=${s.n_nodes_with_data ?? "?"}`];
|
package/dist/tools/jobs.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { registerAuditedTool } from "../audit.js";
|
|
3
3
|
import { apiCall, textResult } from "../shared.js";
|
|
4
|
+
import { formatJobCancelText, formatJobStatusText } from "../job_status_format.js";
|
|
5
|
+
import { pollFinalizeIfPresent, refreshJobAfterFinalize } from "../train_finalize.js";
|
|
4
6
|
import { runTrain } from "./train.js";
|
|
5
7
|
import { runBatchPredict } from "./inference.js";
|
|
6
8
|
import { TRAINING_INPUT_SCHEMA } from "./training_core.js";
|
|
@@ -195,23 +197,20 @@ export function registerJobsTool(server, description) {
|
|
|
195
197
|
if (action === "status") {
|
|
196
198
|
if (!job_id)
|
|
197
199
|
throw new Error("jobs(status) requires job_id");
|
|
198
|
-
|
|
199
|
-
const status = data.status;
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
200
|
+
let data = (await apiCall("GET", `/v1/jobs/${job_id}`));
|
|
201
|
+
const status = String(data.status ?? "");
|
|
202
|
+
if (status === "completed" && data.finalize_job_id) {
|
|
203
|
+
const { note } = await pollFinalizeIfPresent(job_id, data);
|
|
204
|
+
data = await refreshJobAfterFinalize(job_id);
|
|
205
|
+
const text = formatJobStatusText(job_id, data);
|
|
206
|
+
return {
|
|
207
|
+
content: [{
|
|
208
|
+
type: "text",
|
|
209
|
+
text: note ? `${text}\n${note}` : text,
|
|
210
|
+
}],
|
|
211
|
+
};
|
|
207
212
|
}
|
|
208
|
-
|
|
209
|
-
text += ` | Results ready. Use results(action=get, job_id="${job_id}") to retrieve.`;
|
|
210
|
-
}
|
|
211
|
-
else if (status === "failed") {
|
|
212
|
-
text += ` | Error: ${data.error ?? "unknown"}`;
|
|
213
|
-
}
|
|
214
|
-
return { content: [{ type: "text", text }] };
|
|
213
|
+
return { content: [{ type: "text", text: formatJobStatusText(job_id, data) }] };
|
|
215
214
|
}
|
|
216
215
|
if (action === "list") {
|
|
217
216
|
const listPath = dataset_id ? `/v1/jobs?dataset_id=${dataset_id}` : "/v1/jobs";
|
|
@@ -238,8 +237,8 @@ export function registerJobsTool(server, description) {
|
|
|
238
237
|
if (action === "cancel") {
|
|
239
238
|
if (!job_id)
|
|
240
239
|
throw new Error("jobs(cancel) requires job_id");
|
|
241
|
-
const data = await apiCall("POST", `/v1/jobs/${job_id}/cancel`);
|
|
242
|
-
return
|
|
240
|
+
const data = (await apiCall("POST", `/v1/jobs/${job_id}/cancel`));
|
|
241
|
+
return { content: [{ type: "text", text: formatJobCancelText(job_id, data) }] };
|
|
243
242
|
}
|
|
244
243
|
if (action === "delete") {
|
|
245
244
|
if (!job_id)
|
package/dist/tools/results.js
CHANGED
|
@@ -30,9 +30,9 @@ action=export: Structured data exports. Use export_type= to choose what to expor
|
|
|
30
30
|
- export_type=nodes: per-node hit count + feature stats. Profile clusters and operating modes.
|
|
31
31
|
|
|
32
32
|
action=download: Save figures to disk. Use so user can open, share, or version files locally.
|
|
33
|
-
- folder: e.g. "." or "./results".
|
|
33
|
+
- folder: e.g. "." or "./results". Relative to the client's working directory (or MCP workspace). Each job lands in a dedicated subfolder under folder: {job_type}_{label} when label was set at train time, otherwise {job_type}_{job_id_prefix} — so parallel downloads never overwrite summary.json or figure names.
|
|
34
34
|
- figures: "all" (default) or array of filenames.
|
|
35
|
-
- include_json: also
|
|
35
|
+
- include_json: when true, also saves summary.json (and adds it to the download list if figures omitted).
|
|
36
36
|
|
|
37
37
|
action=recolor: Change colormap or output format — no retraining. Returns a new job_id; auto-polls 60s.
|
|
38
38
|
AFTER: use results(action=get, job_id=NEW_JOB_ID).
|
package/dist/tools/train.js
CHANGED
|
@@ -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 =
|
|
147
|
+
let msg = extras.message;
|
|
134
148
|
if (waitMinutes > 1)
|
|
135
|
-
msg
|
|
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 =
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
+
}
|