@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.
- 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 +45 -10
- package/dist/tools/datasets.js +87 -14
- 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 |
|
|
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
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
* Shared configuration, helpers, types, and state for the MCP proxy.
|
|
3
3
|
*/
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
|
-
import { createReadStream } from "node:fs";
|
|
5
|
+
import { createReadStream, createWriteStream } from "node:fs";
|
|
6
6
|
import { createGzip } from "node:zlib";
|
|
7
|
-
import { createHash } from "node:crypto";
|
|
7
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
8
8
|
import { Readable } from "node:stream";
|
|
9
|
+
import { pipeline } from "node:stream/promises";
|
|
10
|
+
import os from "node:os";
|
|
9
11
|
import path from "node:path";
|
|
10
12
|
import { fileURLToPath } from "node:url";
|
|
11
13
|
import { logInfo } from "./logger.js";
|
|
@@ -24,7 +26,7 @@ export const RETRYABLE_STATUS = new Set([502, 503, 504]);
|
|
|
24
26
|
* X-Barsom-Client-Version so the server can annotate tool guidance with the
|
|
25
27
|
* wrapper version each action requires. Keep in sync with package.json on bump.
|
|
26
28
|
*/
|
|
27
|
-
export const CLIENT_VERSION = "0.
|
|
29
|
+
export const CLIENT_VERSION = "0.15.3";
|
|
28
30
|
/** User-facing links; keep aligned with barivia.se / api.barivia.se. */
|
|
29
31
|
export const PUBLIC_SITE_ORIGIN = "https://barivia.se";
|
|
30
32
|
/** Self-serve account dashboard (manage plan, billing, and API keys). */
|
|
@@ -80,31 +82,64 @@ export async function streamFileSha256(srcPath) {
|
|
|
80
82
|
s.on("error", reject);
|
|
81
83
|
});
|
|
82
84
|
}
|
|
85
|
+
/** Turn a raw S3/R2 presigned-PUT error into a clean, actionable message. */
|
|
86
|
+
function presignedPutError(status, bodyText) {
|
|
87
|
+
const snippet = bodyText.slice(0, 200);
|
|
88
|
+
if (status === 411 || /MissingContentLength/i.test(bodyText)) {
|
|
89
|
+
// Should no longer happen now that we always send Content-Length, but keep a
|
|
90
|
+
// clear message instead of leaking raw S3 XML if a CDN strips the header.
|
|
91
|
+
return new Error("Upload rejected by storage: the Content-Length header was missing. " +
|
|
92
|
+
"This is a client bug — please update @barivia/barsom-mcp to the latest version.");
|
|
93
|
+
}
|
|
94
|
+
if (status === 413 || /EntityTooLarge|entity is too large/i.test(bodyText)) {
|
|
95
|
+
return new Error("Upload rejected by storage: file exceeds the maximum upload size.");
|
|
96
|
+
}
|
|
97
|
+
if (status === 403) {
|
|
98
|
+
return new Error("Upload rejected by storage: the presigned URL expired or was invalid; retry datasets(action=upload).");
|
|
99
|
+
}
|
|
100
|
+
return new Error(`Presigned upload failed: HTTP ${status} ${snippet}`);
|
|
101
|
+
}
|
|
83
102
|
/**
|
|
84
|
-
* Stream a local file
|
|
85
|
-
*
|
|
103
|
+
* Stream a local file directly to a presigned PUT URL (e.g. R2), gzip-compressing
|
|
104
|
+
* on the way unless the source is already gzipped. Never materializes the payload
|
|
105
|
+
* in process memory: when compression is needed we gzip to a temp file first, then
|
|
106
|
+
* PUT that file with an explicit Content-Length.
|
|
107
|
+
*
|
|
108
|
+
* R2/S3 presigned PUT requires Content-Length and rejects chunked transfer-encoding
|
|
109
|
+
* with HTTP 411 (MissingContentLength). Node's fetch sends a streaming body as
|
|
110
|
+
* chunked unless an explicit Content-Length is set, and gzip output length is not
|
|
111
|
+
* known until compression finishes — hence the gzip-to-temp-file step.
|
|
86
112
|
*/
|
|
87
|
-
export async function putPresignedStream(url, srcPath, contentType, timeoutMs = PRESIGNED_PUT_TIMEOUT_MS) {
|
|
88
|
-
|
|
89
|
-
|
|
113
|
+
export async function putPresignedStream(url, srcPath, contentType, timeoutMs = PRESIGNED_PUT_TIMEOUT_MS, alreadyGzipped = false) {
|
|
114
|
+
let putPath = srcPath;
|
|
115
|
+
let tempPath;
|
|
116
|
+
if (!alreadyGzipped) {
|
|
117
|
+
tempPath = path.join(os.tmpdir(), `barsom-upload-${randomUUID()}.csv.gz`);
|
|
118
|
+
await pipeline(createReadStream(srcPath), createGzip(), createWriteStream(tempPath));
|
|
119
|
+
putPath = tempPath;
|
|
120
|
+
}
|
|
90
121
|
const controller = new AbortController();
|
|
91
122
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
92
123
|
try {
|
|
124
|
+
const contentLength = (await fs.stat(putPath)).size;
|
|
125
|
+
const webStream = Readable.toWeb(createReadStream(putPath));
|
|
93
126
|
const resp = await fetch(url, {
|
|
94
127
|
method: "PUT",
|
|
95
128
|
body: webStream,
|
|
96
|
-
headers: { "Content-Type": contentType },
|
|
129
|
+
headers: { "Content-Type": contentType, "Content-Length": String(contentLength) },
|
|
97
130
|
// Required by Node's fetch to send a streaming request body.
|
|
98
131
|
duplex: "half",
|
|
99
132
|
signal: controller.signal,
|
|
100
133
|
});
|
|
101
134
|
if (!resp.ok) {
|
|
102
135
|
const t = await resp.text().catch(() => "");
|
|
103
|
-
throw
|
|
136
|
+
throw presignedPutError(resp.status, t);
|
|
104
137
|
}
|
|
105
138
|
}
|
|
106
139
|
finally {
|
|
107
140
|
clearTimeout(timer);
|
|
141
|
+
if (tempPath)
|
|
142
|
+
await fs.rm(tempPath, { force: true }).catch(() => { });
|
|
108
143
|
}
|
|
109
144
|
}
|
|
110
145
|
export async function fetchWithTimeout(url, init, timeoutMs = FETCH_TIMEOUT_MS) {
|
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()])
|
|
@@ -224,9 +229,14 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
224
229
|
if (file_path) {
|
|
225
230
|
await apiCall("GET", "/v1/system/info");
|
|
226
231
|
const resolved = await resolveFilePathForUpload(file_path, server);
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
232
|
+
const lower = resolved.toLowerCase();
|
|
233
|
+
// Accept pre-gzipped CSV/TSV (.csv.gz / .tsv.gz) so large tables transfer
|
|
234
|
+
// ~3x smaller. Already-gzipped files are stored as-is (the platform stores
|
|
235
|
+
// every dataset gzipped anyway); plain files are gzipped on the way up.
|
|
236
|
+
const isGzipInput = lower.endsWith(".gz");
|
|
237
|
+
const baseExt = path.extname(isGzipInput ? lower.slice(0, -3) : lower);
|
|
238
|
+
if (baseExt !== ".csv" && baseExt !== ".tsv") {
|
|
239
|
+
throw new Error("Only .csv, .tsv, .csv.gz, or .tsv.gz files can be uploaded as datasets.");
|
|
230
240
|
}
|
|
231
241
|
const HARD_MAX_BYTES = 5 * 1024 * 1024 * 1024; // 5 GB (R2 single-PUT limit)
|
|
232
242
|
let stat;
|
|
@@ -238,19 +248,33 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
238
248
|
`Set BARIVIA_WORKSPACE_ROOT in your MCP config env if needed (current: ${await getWorkspaceRootAsync(server)}).`);
|
|
239
249
|
}
|
|
240
250
|
if (stat.size > HARD_MAX_BYTES) {
|
|
241
|
-
|
|
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}`);
|
|
242
253
|
}
|
|
243
254
|
// Large files: stream gzip directly to R2 (presigned PUT), then stage async.
|
|
244
255
|
// Never reads the file into memory or sends it through the API/Cloudflare.
|
|
245
256
|
if (stat.size >= LARGE_UPLOAD_BYTES) {
|
|
246
257
|
const idem = await streamFileSha256(resolved);
|
|
247
|
-
|
|
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
|
+
}
|
|
248
269
|
const datasetId = (init.dataset_id ?? init.id);
|
|
249
|
-
|
|
270
|
+
// Only short-circuit when the prior upload actually completed. A stale
|
|
271
|
+
// `pending_upload` row (a previous PUT that never landed) comes back with
|
|
272
|
+
// a fresh upload_url and no idempotent_replay flag, so we re-PUT below.
|
|
273
|
+
if (init.idempotent_replay && !init.upload_url) {
|
|
250
274
|
return textResult({ id: datasetId, status: init.status, idempotent_replay: true,
|
|
251
275
|
suggested_next_step: `datasets(action=preview, dataset_id=${datasetId})` });
|
|
252
276
|
}
|
|
253
|
-
await putPresignedStream(init.upload_url, resolved, init.content_type ?? "application/octet-stream", PRESIGNED_PUT_TIMEOUT_MS);
|
|
277
|
+
await putPresignedStream(init.upload_url, resolved, init.content_type ?? "application/octet-stream", PRESIGNED_PUT_TIMEOUT_MS, isGzipInput);
|
|
254
278
|
const fin = (await apiCall("POST", `/v1/datasets/${datasetId}/finalize`, {}));
|
|
255
279
|
const jobId = (fin.id ?? fin.job_id);
|
|
256
280
|
const poll = await pollUntilComplete(jobId, POLL_STAGE_MAX_MS);
|
|
@@ -267,6 +291,21 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
267
291
|
: `Still staging; poll jobs(action=status, job_id="${jobId}") then datasets(action=preview, dataset_id=${datasetId}).`,
|
|
268
292
|
});
|
|
269
293
|
}
|
|
294
|
+
// Small pre-gzipped files: send the bytes as-is (the API decompresses
|
|
295
|
+
// Content-Encoding: gzip), never decoding them as UTF-8 text.
|
|
296
|
+
if (isGzipInput) {
|
|
297
|
+
const gzBytes = await fs.readFile(resolved);
|
|
298
|
+
const data = (await apiCall("POST", "/v1/datasets", gzBytes, {
|
|
299
|
+
"X-Dataset-Name": name,
|
|
300
|
+
"Content-Type": "text/csv",
|
|
301
|
+
"Content-Encoding": "gzip",
|
|
302
|
+
"Idempotency-Key": createHash("sha256").update(`${name}\n`).update(gzBytes).digest("hex"),
|
|
303
|
+
}, UPLOAD_DATASET_TIMEOUT_MS));
|
|
304
|
+
const gid = data.id ?? data.dataset_id;
|
|
305
|
+
if (gid != null)
|
|
306
|
+
data.suggested_next_step = `Suggested next step: datasets(action=preview, dataset_id=${gid}) to inspect columns before training.`;
|
|
307
|
+
return textResult(data);
|
|
308
|
+
}
|
|
270
309
|
body = await fs.readFile(resolved, "utf-8");
|
|
271
310
|
}
|
|
272
311
|
else if (csv_data && csv_data.length > 0) {
|
|
@@ -561,9 +600,43 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
561
600
|
return textResult(data);
|
|
562
601
|
}
|
|
563
602
|
if (action === "list") {
|
|
564
|
-
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
|
+
}
|
|
565
619
|
return textResult(data);
|
|
566
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
|
+
}
|
|
567
640
|
if (action === "delete") {
|
|
568
641
|
if (!dataset_id)
|
|
569
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).
|