@barivia/barsom-mcp 0.20.4 → 0.22.1
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 +15 -3
- package/dist/audit.js +4 -1
- package/dist/index.js +1 -1
- package/dist/inference_prepare.js +2 -2
- package/dist/job_monitor.js +38 -2
- package/dist/prepare_training_prompt.js +10 -2
- package/dist/shared.js +147 -21
- package/dist/tools/datasets.js +14 -11
- package/dist/tools/guide_barsom.js +1 -1
- package/dist/tools/inference.js +10 -12
- package/dist/tools/results.js +52 -61
- package/dist/tools/train.js +68 -53
- package/dist/tools/training_core.js +29 -8
- package/dist/tools/training_guidance.js +1 -1
- package/dist/train_finalize.js +2 -2
- package/dist/training_monitor_curve.js +1 -1
- package/dist/views/src/views/training-monitor/index.html +28 -28
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,6 +27,17 @@ MCP clients typically run it with **`npx`** (downloads on first use):
|
|
|
27
27
|
|
|
28
28
|
**Verify:** `npm view @barivia/barsom-mcp version`
|
|
29
29
|
|
|
30
|
+
## 2-minute quickstart
|
|
31
|
+
|
|
32
|
+
1. **Get an API key** — Sign up at [barivia.se/dashboard](https://barivia.se/dashboard) (self-serve) or ask your admin for a `bv_…` key.
|
|
33
|
+
2. **Add the MCP server** — Paste the JSON block above into your client’s MCP config (`mcp.json` in Cursor). Set `BARIVIA_API_KEY` and keep `BARIVIA_API_URL=https://api.barivia.se`.
|
|
34
|
+
3. **Orient** — In chat, invoke **`guide_barsom_workflow`** (no parameters). It returns your plan-scoped tool map and async rules.
|
|
35
|
+
4. **Upload data** — `datasets(action=upload, file_path="your.csv")` → note `dataset_id` (and `stage_job_id` if staging is enqueued; poll until complete).
|
|
36
|
+
5. **Train** — `train(action=map, dataset_id=…, preset=quick)` → returns `job_id`. Poll with `jobs(action=status, job_id=…)` every 60–120s, or use **`training_monitor(job_id=…)`** for live curves.
|
|
37
|
+
6. **Results** — When status is `completed`, `results(action=get, job_id=…)` for metrics + headline figure; **`results_explorer(job_id=…)`** to browse all figures.
|
|
38
|
+
|
|
39
|
+
**Troubleshooting:** Upload fails with “paths outside the workspace” → set `BARIVIA_WORKSPACE_ROOT` to your project folder. Timeouts on slow calls → raise `BARIVIA_FETCH_TIMEOUT_MS`. Tool “server does not exist” in Cursor → use the resolved server id from the MCP tool list, not necessarily the key in `mcp.json`. Hosted HTTP MCP at `mcp.barivia.se` is paused — use this npm package only.
|
|
40
|
+
|
|
30
41
|
**Audit logging (0.10.3+):** Each tool invocation emits a structured **`mcp_tool_call`** line on stderr for support correlation — not sent to the Barivia API. Fields: `tool`, `action`, `duration_ms`, `outcome`, **`rid`** (matches API `X-Request-ID`), **`job_id`** / **`dataset_id`** when the action returns or accepts them, scale hints **`grid_x`** / **`grid_y`** on training submits, and **`timeout_hit`** / **`output_truncated`** when proxy fetch/output limits apply. No API keys or file paths. Grep client stderr alongside API/worker logs by `rid`; see `docs/OPERATIONS_MANUAL.md` §5b.
|
|
31
42
|
|
|
32
43
|
**Hosted HTTP MCP paused (2026-06):** `https://mcp.barivia.se/mcp` returns **503** (`hosted_mcp_paused`). Use this npm package only.
|
|
@@ -62,7 +73,7 @@ Legacy `BARSOM_API_KEY` / `BARSOM_API_URL` / `BARSOM_WORKSPACE_ROOT` are also ac
|
|
|
62
73
|
|
|
63
74
|
All multi-action tools follow the `datasets` / `jobs` / `results` / `inference` / `account` pattern: a required `action` enum routes to the correct operation.
|
|
64
75
|
|
|
65
|
-
**Prompts (not tools):** `info` (short orientation
|
|
76
|
+
**Bootstrap:** `guide_barsom_workflow` (no params; plan-scoped orientation from the API — call first). **Prompts (not tools):** `info` (short orientation), `prepare_training` (requires `dataset_id`; checklist text from API when online).
|
|
66
77
|
|
|
67
78
|
### Flattened catalog (optional grouping in UIs)
|
|
68
79
|
|
|
@@ -70,12 +81,12 @@ All multi-action tools follow the `datasets` / `jobs` / `results` / `inference`
|
|
|
70
81
|
|--------|--------|
|
|
71
82
|
| Bootstrap | `guide_barsom_workflow`, prompts `info`, `prepare_training` |
|
|
72
83
|
| Data | `datasets` |
|
|
84
|
+
| Train prep | `training_guidance` |
|
|
73
85
|
| Train & poll | `train`, `jobs`, `training_monitor` (optional) |
|
|
74
86
|
| Results | `results`, `results_explorer` (optional) |
|
|
75
87
|
| Inference | `inference` |
|
|
76
88
|
| Account | `account` |
|
|
77
89
|
| Optional UI | `training_monitor`, `results_explorer` |
|
|
78
|
-
| Advanced | `training_guidance` |
|
|
79
90
|
|
|
80
91
|
Agents should not call **`_fetch_figure`** from chat; it exists for the Results Explorer MCP App host. Use `results(action=get)` or `results_explorer` instead.
|
|
81
92
|
|
|
@@ -162,7 +173,7 @@ All actions use a frozen trained map — no retraining. Derived columns use **`d
|
|
|
162
173
|
|
|
163
174
|
| Tool | Role |
|
|
164
175
|
|------|------|
|
|
165
|
-
| `training_monitor` | Live QE + topographic-error curves (ordering/convergence, dual y-axis)
|
|
176
|
+
| `training_monitor` | Live QE + **panel** topographic-error curves (ordering/convergence, dual y-axis); **Panel TE** and **Map TE** scalars at kernel end; FLooP step progress when applicable; optional — `jobs(action=status)` is enough |
|
|
166
177
|
| `results_explorer` | Browse metrics and figures after training completes |
|
|
167
178
|
| `_fetch_figure` | **Host / App only** — Results Explorer invokes this for one raster figure; not for agent chat |
|
|
168
179
|
|
|
@@ -180,6 +191,7 @@ The right viewer depends on **(MCP App support)** **and** **(can the human reach
|
|
|
180
191
|
|
|
181
192
|
### Migration notes
|
|
182
193
|
|
|
194
|
+
- **Fixed-panel live TE (0.20.4):** mid-training TE curves use a fixed evaluation panel (`te_panel_size` on `train`). Monitors show **Panel TE** and **Map TE** separately; curve tail no longer snaps to map TE.
|
|
183
195
|
- **Features (0.20.0, non-breaking):**
|
|
184
196
|
- **FLooP-SIOM maps are now projectable.** `inference(action=project_columns)`, `inference(action=predict)`, and `datasets(action=add_expression, project_onto_job=<floop_job>)` work on FLooP-SIOM (free/chain) maps — values render onto the FLooP Voronoi layout instead of failing. Grid-only ops (`inference(action=transition_flow | impute_column | render_variant)`) return a clear `unsupported_topology_for_inference` message; use a fixed-grid SOM/SIOM for those. No client change required.
|
|
185
197
|
- **`datasets(action=reduce_spectral)` column selectors.** New optional `columns_range` (`[first, last]`, the server expands the inclusive numeric block — preferred for wide spectra over sending thousands of names) and `columns_except` (drop non-frequency columns like id/label/target from the block, or use alone for "all numeric except these"). `columns_except` composes with `columns_block` or `columns_range`; `columns_block` and `columns_range` are mutually exclusive. Existing `columns_block` callers are unaffected.
|
package/dist/audit.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* MCP tool audit wrapper — tool name, action, latency, outcome; no secrets.
|
|
3
3
|
*/
|
|
4
4
|
import { logAudit } from "./logger.js";
|
|
5
|
+
import { runWithTrace } from "./shared.js";
|
|
5
6
|
/** Keys safe to include in audit param summaries (no paths, keys, or bodies). */
|
|
6
7
|
const AUDIT_PARAM_KEYS = new Set([
|
|
7
8
|
"action",
|
|
@@ -160,7 +161,9 @@ export function registerAuditedTool(server, name, description, schema, handler)
|
|
|
160
161
|
server.tool(name, description, schema, (async (args) => {
|
|
161
162
|
const rec = args;
|
|
162
163
|
const action = typeof rec.action === "string" && rec.action.length > 0 ? rec.action : "default";
|
|
163
|
-
|
|
164
|
+
// One trace per tool invocation: every apiCall inside this handler shares one
|
|
165
|
+
// trace_id, so the API + job chain reconstruct the whole logical action.
|
|
166
|
+
return runWithTrace(() => runMcpToolAudit(name, action, rec, () => handler(args)));
|
|
164
167
|
}));
|
|
165
168
|
}
|
|
166
169
|
/** Attach audit flags to a tool result (stripped before returning to MCP client). */
|
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 s,RESOURCE_MIME_TYPE as n}from"@modelcontextprotocol/ext-apps/server";import{startVizServer as i}from"./viz-server.js";import{API_KEY as a,apiCall as l,apiRawCall as c,loadViewHtml as p,setVizPort as m,setClientSupportsMcpApps as u,CLIENT_VERSION as d}from"./shared.js";import{registerDatasetsTool as f}from"./tools/datasets.js";import{registerTrainTool as g}from"./tools/train.js";import{registerJobsTool as h,JOBS_DESCRIPTION_BASE as _}from"./tools/jobs.js";import{registerResultsTool as b}from"./tools/results.js";import{registerExploreMapTool as w,RESULTS_EXPLORER_URI as y}from"./tools/explore_map.js";import{registerAccountTool as j}from"./tools/account.js";import{registerInferenceTool as v}from"./tools/inference.js";import{registerGuideBarsomTool as x}from"./tools/guide_barsom.js";import{registerTrainingGuidanceTool as P}from"./tools/training_guidance.js";import{registerFeedbackTool as k}from"./tools/feedback.js";import{registerTrainingMonitorTool as I,TRAINING_MONITOR_URI as A}from"./tools/training_monitor.js";import{resolvePrepareTrainingPromptText as M}from"./prepare_training_prompt.js";a||(console.error("Error: BARIVIA_API_KEY not set. Set it in your MCP client config."),process.exit(1));const T=new e({name:"analytics-engine",version:d,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 `results_explorer(job_id)` to browse the figures interactively. Then `jobs(compare)`, `results(download/recolor)`, or `inference` (predict / compare / project_columns / impute_column / transition_flow) 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 taxonomy (one tool per stage — do not mix)\n\n| Stage | Tool(s) | Contract |\n|-------|---------|----------|\n| Data | `datasets` | upload, preview, analyze, list, **get** (status/staging), subset, add_expression, reduce_spectral, delete — ingest + feature engineering, no map |\n| Train submit | `train` | map, siom_map, impute (sparse → map + dense imputed.csv), floop_siom (entitled). Async, returns job_id. Submit only |\n| Lifecycle | `jobs` | status, list, compare (**training runs**), cancel, delete. No training/scoring here |\n| Artifacts | `results` | get (default=combined only; figures="none" for metrics-only sweeps; figures="all" or results_explorer/download for every plot), export, download, recolor (async). Read/render completed jobs |\n| Frozen-map ops | `inference` | predict (regime-aware; "compact"|"annotated"), batch_predict, impute_column (neighbor-pool fill for a non-training column), compare (cohort density-diff), project_columns, transition_flow (time-ordered rows only), report. No weight updates |\n| Account | `account` | status, burst/compute actions, history, add_funds |\n| Bootstrap | `guide_barsom_workflow`, `training_guidance`, `prepare_training` prompt | orientation, parameter hints, narrative checklist (API/tier-scoped) |\n| Explore (UI) | `results_explorer`, `training_monitor` | richer UX over the same API; `jobs(status)`+`results(get)` still suffice headless |\n| Other | `send_feedback` | only after user agrees |\n\n### Avoid these mis-routes\n- `jobs(compare)` = metrics table across **training runs** (pick best in a sweep). `inference(compare)` = density-diff of one map vs a **second dataset** (drift/cohort). Different tools.\n- Post-training map analysis (predict, compare, project_columns, impute_column, **transition_flow**) is all **inference** — not `results`. `results` is presentation/artifacts only (`recolor` stays here: it re-renders, it doesn\'t touch data).\n- Project a quantity onto the map: existing columns → `inference(project_columns)`; a formula → `datasets(add_expression, project_onto_job=<job>)`.\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 (incl. `transition_flow`) and `results(recolor)` 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 (presets, resolved normalization/transforms, grid/epochs). Review these, then submit with `train`. 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 `results(recolor)`, `inference(transition_flow)`, or `inference(project_columns)`, use the **new** `job_id` returned for follow-up `results` if applicable.\n- **Large results / MCP timeouts:** For sweeps, `jobs(compare)`, or metrics-only reads use `results(action=get, figures="none")`. Default `get` inlines combined only. For every plot use `results_explorer` or `results(action=download)`, not `figures="all"` in chat unless the user wants all images inline.'});s(T,y,y,{mimeType:n},async()=>{const e=await p("results-explorer");return{contents:[{uri:y,mimeType:n,text:e??"<html><body>Results Explorer view not built yet. Run: npm run build:views</body></html>"}]}}),s(T,A,A,{mimeType:n},async()=>{const e=await p("training-monitor");return{contents:[{uri:A,mimeType:n,text:e??"<html><body>Training Monitor view not built yet.</body></html>"}]}}),x(T),w(T),I(T),f(T),g(T),h(T,_),b(T),j(T),v(T),P(T),k(T),T.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) → `results_explorer(job_id)` to browse figures.","",'**Key tools:** `datasets` (data; reduce_spectral for spectra/long blocks), `train` (map/siom_map/impute/floop_siom; pass `label` for readable compare rows; preset=quick for a fast first map), `jobs` (status/list/compare/cancel/delete — lifecycle only), `results` (get/download/export/recolor; figures="none" for metrics-only), `inference` (predict; batch_predict; impute_column for topology-neighbor pool fill; compare; project_columns; transition_flow; report), `account` (status/credits/queue).',"","**Prep help:** `prepare_training` prompt (checklist) · `training_guidance` (presets/JSON hints + resolved normalization/params) — review, then submit with `train`.","","**Browse results:** `results_explorer(job_id)` is the preferred way to explore figures after a first `results(get)` glance; `training_monitor` shows live progress. Both optional — `results` + `jobs(status)` suffice headless.","","**After training:** `jobs(compare)` across runs, `results(recolor)`, `inference(project_columns)` for variables not in training, `inference(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")}}]})),T.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. Review, then submit with train.",{dataset_id:o.string().describe("Dataset ID to prepare for training")},async({dataset_id:e})=>({messages:[{role:"user",content:{type:"text",text:await M(e)}}]}));const S=new t;(async function(){try{const e=await i(l,c,p);m(e)}catch(e){process.env.BARIVIA_VIZ_PORT&&console.error("Barivia viz server failed to start:",e)}const e=T.server;e.oninitialized=()=>{const t=e.getClientCapabilities(),o=r(t);u(!!o?.mimeTypes?.includes(n))},await T.connect(S)})().catch(console.error);
|
|
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 s,RESOURCE_MIME_TYPE as n}from"@modelcontextprotocol/ext-apps/server";import{startVizServer as i}from"./viz-server.js";import{API_KEY as a,apiCall as l,apiRawCall as c,loadViewHtml as p,setVizPort as m,setClientSupportsMcpApps as u,CLIENT_VERSION as d}from"./shared.js";import{registerDatasetsTool as f}from"./tools/datasets.js";import{registerTrainTool as g}from"./tools/train.js";import{registerJobsTool as h,JOBS_DESCRIPTION_BASE as _}from"./tools/jobs.js";import{registerResultsTool as b}from"./tools/results.js";import{registerExploreMapTool as w,RESULTS_EXPLORER_URI as y}from"./tools/explore_map.js";import{registerAccountTool as j}from"./tools/account.js";import{registerInferenceTool as v}from"./tools/inference.js";import{registerGuideBarsomTool as x}from"./tools/guide_barsom.js";import{registerTrainingGuidanceTool as P}from"./tools/training_guidance.js";import{registerFeedbackTool as k}from"./tools/feedback.js";import{registerTrainingMonitorTool as I,TRAINING_MONITOR_URI as A}from"./tools/training_monitor.js";import{resolvePrepareTrainingPromptText as M}from"./prepare_training_prompt.js";a||(console.error("Error: BARIVIA_API_KEY not set. Set it in your MCP client config."),process.exit(1));const T=new e({name:"analytics-engine",version:d,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 `results_explorer(job_id)` to browse the figures interactively. Then `jobs(compare)`, `results(download/recolor)`, or `inference` (predict / compare / project_columns / impute_column / transition_flow) 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 taxonomy (one tool per stage — do not mix)\n\n| Stage | Tool(s) | Contract |\n|-------|---------|----------|\n| Data | `datasets` | upload, preview, analyze, list, **get** (status/staging), subset, add_expression, reduce_spectral, delete — ingest + feature engineering, no map |\n| Train submit | `train` | map, siom_map, impute (sparse → map + dense imputed.csv), floop_siom (entitled). Async, returns job_id. Submit only |\n| Lifecycle | `jobs` | status, list, compare (**training runs**), cancel, delete. No training/scoring here |\n| Artifacts | `results` | get (default=combined only; figures="none" for metrics-only sweeps; figures="all" or results_explorer/download for every plot), export, download, recolor (async). Read/render completed jobs |\n| Frozen-map ops | `inference` | predict (regime-aware; "compact"|"annotated"), batch_predict, impute_column (neighbor-pool fill for a non-training column), compare (cohort density-diff), project_columns, transition_flow (time-ordered rows only), report. No weight updates |\n| Account | `account` | status, burst/compute actions, history, add_funds |\n| Bootstrap | `guide_barsom_workflow`, `train`, `training_guidance`, `prepare_training` prompt | orientation, submit map/siom/impute/floop, parameter hints, narrative checklist (API/tier-scoped) |\n| Explore (UI) | `results_explorer`, `training_monitor` | richer UX over the same API; `jobs(status)`+`results(get)` still suffice headless |\n| Other | `send_feedback` | only after user agrees |\n\n### Avoid these mis-routes\n- `jobs(compare)` = metrics table across **training runs** (pick best in a sweep). `inference(compare)` = density-diff of one map vs a **second dataset** (drift/cohort). Different tools.\n- Post-training map analysis (predict, compare, project_columns, impute_column, **transition_flow**) is all **inference** — not `results`. `results` is presentation/artifacts only (`recolor` stays here: it re-renders, it doesn\'t touch data).\n- Project a quantity onto the map: existing columns → `inference(project_columns)`; a formula → `datasets(add_expression, project_onto_job=<job>)`.\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 (incl. `transition_flow`) and `results(recolor)` 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 (presets, resolved normalization/transforms, grid/epochs). Review these, then submit with `train`. 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 `results(recolor)`, `inference(transition_flow)`, or `inference(project_columns)`, use the **new** `job_id` returned for follow-up `results` if applicable.\n- **Large results / MCP timeouts:** For sweeps, `jobs(compare)`, or metrics-only reads use `results(action=get, figures="none")`. Default `get` inlines combined only. For every plot use `results_explorer` or `results(action=download)`, not `figures="all"` in chat unless the user wants all images inline.'});s(T,y,y,{mimeType:n},async()=>{const e=await p("results-explorer");return{contents:[{uri:y,mimeType:n,text:e??"<html><body>Results Explorer view not built yet. Run: npm run build:views</body></html>"}]}}),s(T,A,A,{mimeType:n},async()=>{const e=await p("training-monitor");return{contents:[{uri:A,mimeType:n,text:e??"<html><body>Training Monitor view not built yet.</body></html>"}]}}),x(T),w(T),I(T),f(T),g(T),h(T,_),b(T),j(T),v(T),P(T),k(T),T.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) → `results_explorer(job_id)` to browse figures.","",'**Key tools:** `datasets` (data; reduce_spectral for spectra/long blocks), `train` (map/siom_map/impute/floop_siom; pass `label` for readable compare rows; preset=quick for a fast first map), `jobs` (status/list/compare/cancel/delete — lifecycle only), `results` (get/download/export/recolor; figures="none" for metrics-only), `inference` (predict; batch_predict; impute_column for topology-neighbor pool fill; compare; project_columns; transition_flow; report), `account` (status/credits/queue).',"","**Prep help:** `prepare_training` prompt (checklist) · `training_guidance` (presets/JSON hints + resolved normalization/params) — review, then submit with `train`.","","**Browse results:** `results_explorer(job_id)` is the preferred way to explore figures after a first `results(get)` glance; `training_monitor` shows live progress. Both optional — `results` + `jobs(status)` suffice headless.","","**After training:** `jobs(compare)` across runs, `results(recolor)`, `inference(project_columns)` for variables not in training, `inference(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")}}]})),T.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. Review, then submit with train.",{dataset_id:o.string().describe("Dataset ID to prepare for training")},async({dataset_id:e})=>({messages:[{role:"user",content:{type:"text",text:await M(e)}}]}));const S=new t;(async function(){try{const e=await i(l,c,p);m(e)}catch(e){process.env.BARIVIA_VIZ_PORT&&console.error("Barivia viz server failed to start:",e)}const e=T.server;e.oninitialized=()=>{const t=e.getClientCapabilities(),o=r(t);u(!!o?.mimeTypes?.includes(n))},await T.connect(S)})().catch(console.error);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { pollUntilComplete } from "./shared.js";
|
|
1
|
+
import { pollUntilComplete, POLL_FINALIZE_MAX_MS } from "./shared.js";
|
|
2
2
|
/**
|
|
3
3
|
* When the API enqueues prepare_training_matrix for cross-dataset scoring,
|
|
4
4
|
* poll it to completion before polling the main inference job.
|
|
5
5
|
*/
|
|
6
|
-
export async function pollPrepareIfPresent(data, label, timeoutMs =
|
|
6
|
+
export async function pollPrepareIfPresent(data, label, timeoutMs = POLL_FINALIZE_MAX_MS) {
|
|
7
7
|
const prepareJobId = data.prepare_job_id;
|
|
8
8
|
if (!prepareJobId)
|
|
9
9
|
return null;
|
package/dist/job_monitor.js
CHANGED
|
@@ -54,6 +54,14 @@ export function snapshotFromJob(data, elapsedSec, note) {
|
|
|
54
54
|
snap.qe = Math.round(qe * 10_000) / 10_000;
|
|
55
55
|
if (te != null)
|
|
56
56
|
snap.te = Math.round(te * 10_000) / 10_000;
|
|
57
|
+
const panelTe = num(data, "panel_topographic_error");
|
|
58
|
+
if (panelTe != null)
|
|
59
|
+
snap.panel_te = Math.round(panelTe * 10_000) / 10_000;
|
|
60
|
+
if (data.kernel_complete === true)
|
|
61
|
+
snap.kernel_complete = true;
|
|
62
|
+
const failureStage = str(data, "failure_stage");
|
|
63
|
+
if (failureStage)
|
|
64
|
+
snap.failure_stage = failureStage;
|
|
57
65
|
const eta = num(data, "training_eta_sec");
|
|
58
66
|
if (eta != null && eta > 0)
|
|
59
67
|
snap.eta_sec = Math.round(eta);
|
|
@@ -78,7 +86,11 @@ export function shouldRecordSnapshot(prev, next) {
|
|
|
78
86
|
return true;
|
|
79
87
|
if (Math.abs(prev.progress_pct - next.progress_pct) >= 1)
|
|
80
88
|
return true;
|
|
81
|
-
if (prev.qe !== next.qe || prev.te !== next.te)
|
|
89
|
+
if (prev.qe !== next.qe || prev.te !== next.te || prev.panel_te !== next.panel_te)
|
|
90
|
+
return true;
|
|
91
|
+
if (prev.kernel_complete !== next.kernel_complete)
|
|
92
|
+
return true;
|
|
93
|
+
if (prev.failure_stage !== next.failure_stage)
|
|
82
94
|
return true;
|
|
83
95
|
return false;
|
|
84
96
|
}
|
|
@@ -92,6 +104,12 @@ export function formatSnapshotLine(s) {
|
|
|
92
104
|
parts.push(`QE ${s.qe.toFixed(4)}`);
|
|
93
105
|
if (s.te != null)
|
|
94
106
|
parts.push(`Epoch TE ${s.te.toFixed(4)}`);
|
|
107
|
+
if (s.panel_te != null)
|
|
108
|
+
parts.push(`Panel TE ${s.panel_te.toFixed(4)}`);
|
|
109
|
+
if (s.kernel_complete)
|
|
110
|
+
parts.push("kernel complete");
|
|
111
|
+
if (s.failure_stage)
|
|
112
|
+
parts.push(`failure_stage ${s.failure_stage}`);
|
|
95
113
|
if (s.eta_sec != null)
|
|
96
114
|
parts.push(`ETA ~${s.eta_sec}s`);
|
|
97
115
|
if (s.ordering_errors_tail?.length) {
|
|
@@ -118,6 +136,22 @@ export function formatMonitorText(result, opts) {
|
|
|
118
136
|
lines.push(result.suggested_next_step);
|
|
119
137
|
return lines.join("\n");
|
|
120
138
|
}
|
|
139
|
+
function failureStageHint(stage, error) {
|
|
140
|
+
const err = (error ?? "").toLowerCase();
|
|
141
|
+
if (stage === "preprocessing") {
|
|
142
|
+
return "Check datasets(action=preview) and prepare job status; missing columns → subset or train(action=impute).";
|
|
143
|
+
}
|
|
144
|
+
if (stage === "training") {
|
|
145
|
+
if (err.includes("memory") || err.includes("readonlymemory")) {
|
|
146
|
+
return "Reduce grid size or batch_size and retrain.";
|
|
147
|
+
}
|
|
148
|
+
return "Review train hyperparameters (sigma_f, learning_rate, batch_size).";
|
|
149
|
+
}
|
|
150
|
+
if (stage === "visualization" || stage === "upload" || stage === "metrics") {
|
|
151
|
+
return "Re-run jobs(action=monitor, wait_finalize=true) or results(action=get) after finalize completes.";
|
|
152
|
+
}
|
|
153
|
+
return "Read the error above before retrying.";
|
|
154
|
+
}
|
|
121
155
|
function suggestedNextStep(job_id, data) {
|
|
122
156
|
const status = String(data.status ?? "");
|
|
123
157
|
if (status === "completed") {
|
|
@@ -129,7 +163,9 @@ function suggestedNextStep(job_id, data) {
|
|
|
129
163
|
}
|
|
130
164
|
if (status === "failed") {
|
|
131
165
|
const stage = str(data, "failure_stage");
|
|
132
|
-
|
|
166
|
+
const err = str(data, "error");
|
|
167
|
+
const hint = stage ? failureStageHint(stage, err) : "Read the error above before retrying.";
|
|
168
|
+
return `Job failed${stage ? ` at ${stage}` : ""}. ${hint}`;
|
|
133
169
|
}
|
|
134
170
|
if (status === "cancelled")
|
|
135
171
|
return `Job cancelled. Confirm with jobs(action=status, job_id="${job_id}").`;
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import { apiCall } from "./shared.js";
|
|
2
|
-
const
|
|
2
|
+
const FALLBACK_LINES = [
|
|
3
|
+
"Before upload: for train(action=map) ensure training columns have no NaNs; for train(action=impute) missing cells in training columns are OK (plain numeric only).",
|
|
4
|
+
'Please run datasets(action=preview, dataset_id="{id}") to inspect columns, then datasets(action=analyze, dataset_id="{id}") to see which columns and temporal periods are most informative.',
|
|
5
|
+
"Choose the training path:",
|
|
6
|
+
'- train(action=map, dataset_id="{id}", ...) for complete-case data',
|
|
7
|
+
'- train(action=impute, dataset_id="{id}", ...) for sparse matrices',
|
|
8
|
+
'- train(action=siom_map, dataset_id="{id}", ...) for a fixed-grid SIOM',
|
|
9
|
+
'- train(action=floop_siom, dataset_id="{id}", ...) for FLooP-SIOM (default topology=free / CHL; optional topology=chain)',
|
|
10
|
+
];
|
|
3
11
|
/** Used by the `prepare_training` MCP prompt; prefers tier-scoped text from the API when online. */
|
|
4
12
|
export async function resolvePrepareTrainingPromptText(datasetId) {
|
|
5
|
-
let promptText =
|
|
13
|
+
let promptText = FALLBACK_LINES.join("\n").replaceAll("{id}", datasetId);
|
|
6
14
|
try {
|
|
7
15
|
const data = (await apiCall("GET", `/v1/docs/prepare_training?dataset_id=${datasetId}`));
|
|
8
16
|
if (typeof data.prompt === "string" && data.prompt.trim()) {
|
package/dist/shared.js
CHANGED
|
@@ -5,12 +5,13 @@ import fs from "node:fs/promises";
|
|
|
5
5
|
import { createReadStream, createWriteStream } from "node:fs";
|
|
6
6
|
import { createGzip } from "node:zlib";
|
|
7
7
|
import { createHash, randomUUID } from "node:crypto";
|
|
8
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
8
9
|
import { Readable } from "node:stream";
|
|
9
10
|
import { pipeline } from "node:stream/promises";
|
|
10
11
|
import os from "node:os";
|
|
11
12
|
import path from "node:path";
|
|
12
13
|
import { fileURLToPath } from "node:url";
|
|
13
|
-
import { logInfo } from "./logger.js";
|
|
14
|
+
import { logInfo, logWarn } from "./logger.js";
|
|
14
15
|
// ---------------------------------------------------------------------------
|
|
15
16
|
// Config
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
@@ -20,19 +21,50 @@ export const API_URL = process.env.BARIVIA_API_URL ??
|
|
|
20
21
|
export const API_KEY = process.env.BARIVIA_API_KEY ?? process.env.BARSOM_API_KEY ?? "";
|
|
21
22
|
export const FETCH_TIMEOUT_MS = parseInt(process.env.BARIVIA_FETCH_TIMEOUT_MS ?? "60000", 10);
|
|
22
23
|
export const MAX_RETRIES = 2;
|
|
24
|
+
export const RETRY_BASE_MS = parseInt(process.env.BARIVIA_RETRY_BASE_MS ?? "1000", 10);
|
|
23
25
|
export const RETRYABLE_STATUS = new Set([502, 503, 504]);
|
|
26
|
+
/** Exponential backoff for transient API retries (attempt 0 → base, 1 → 2×, …). */
|
|
27
|
+
export function retryDelayMs(attempt) {
|
|
28
|
+
return RETRY_BASE_MS * 2 ** attempt;
|
|
29
|
+
}
|
|
30
|
+
function newRequestId() {
|
|
31
|
+
return randomUUID();
|
|
32
|
+
}
|
|
24
33
|
/**
|
|
25
34
|
* Single source of truth for the proxy version. Sent to the API as
|
|
26
35
|
* X-Barsom-Client-Version so the server can annotate tool guidance with the
|
|
27
36
|
* wrapper version each action requires. Keep in sync with package.json on bump.
|
|
28
37
|
*/
|
|
29
|
-
export const CLIENT_VERSION = "0.
|
|
38
|
+
export const CLIENT_VERSION = "0.22.1";
|
|
30
39
|
/** User-facing links; keep aligned with barivia.se / api.barivia.se. */
|
|
31
40
|
export const PUBLIC_SITE_ORIGIN = "https://barivia.se";
|
|
32
41
|
/** Self-serve account dashboard (manage plan, billing, and API keys). */
|
|
33
42
|
export const PUBLIC_DASHBOARD_URL = `${PUBLIC_SITE_ORIGIN}/dashboard`;
|
|
34
43
|
/** Poll window for datasets(add_expression) / derive jobs (server-side work can exceed 30s). */
|
|
35
44
|
export const POLL_DERIVE_MAX_MS = 120_000;
|
|
45
|
+
/** Poll window for async datasets(analyze) jobs (large tables can exceed derive timeout). */
|
|
46
|
+
export const POLL_ANALYZE_MAX_MS = 300_000; // 5 min
|
|
47
|
+
/** Poll windows for inference/results async jobs (named for grep/tuning). */
|
|
48
|
+
export const POLL_INFERENCE_MAX_MS = 120_000;
|
|
49
|
+
export const POLL_PROJECT_MAX_MS = 90_000;
|
|
50
|
+
export const POLL_RECOLOR_MAX_MS = 60_000;
|
|
51
|
+
export const POLL_FINALIZE_MAX_MS = 600_000;
|
|
52
|
+
export const DPI_MAP = { standard: 1, retina: 2, print: 4 };
|
|
53
|
+
export function resolveOutputDpi(dpi, defaultDpi = "retina") {
|
|
54
|
+
return DPI_MAP[dpi ?? defaultDpi] ?? DPI_MAP[defaultDpi] ?? 2;
|
|
55
|
+
}
|
|
56
|
+
export function fmtDensityDiffNode(n) {
|
|
57
|
+
return ` node ${n.node_index ?? "?"} [${(n.coords ?? [0, 0]).map((v) => Number(v).toFixed(1)).join(",")}] Δ=${Number(n.density_diff ?? 0).toFixed(4)}`;
|
|
58
|
+
}
|
|
59
|
+
/** Map a local upload path to the API Content-Type (csv vs tsv; ignores .gz suffix). */
|
|
60
|
+
export function resolveUploadContentType(filePath) {
|
|
61
|
+
const lower = filePath.toLowerCase();
|
|
62
|
+
const base = lower.endsWith(".gz") ? lower.slice(0, -3) : lower;
|
|
63
|
+
return path.extname(base) === ".tsv" ? "text/tab-separated-values" : "text/csv";
|
|
64
|
+
}
|
|
65
|
+
export function suggestDatasetPreview(datasetId) {
|
|
66
|
+
return `datasets(action=preview, dataset_id=${datasetId})`;
|
|
67
|
+
}
|
|
36
68
|
/** Large CSV uploads may exceed default FETCH_TIMEOUT_MS. */
|
|
37
69
|
export const UPLOAD_DATASET_TIMEOUT_MS = 180_000;
|
|
38
70
|
/** Files at/above this size use the presigned direct-to-R2 streaming upload path. */
|
|
@@ -126,7 +158,11 @@ export async function putPresignedStream(url, srcPath, contentType, timeoutMs =
|
|
|
126
158
|
const resp = await fetch(url, {
|
|
127
159
|
method: "PUT",
|
|
128
160
|
body: webStream,
|
|
129
|
-
headers: {
|
|
161
|
+
headers: {
|
|
162
|
+
"Content-Type": contentType,
|
|
163
|
+
"Content-Length": String(contentLength),
|
|
164
|
+
"Content-Encoding": "gzip",
|
|
165
|
+
},
|
|
130
166
|
// Required by Node's fetch to send a streaming request body.
|
|
131
167
|
duplex: "half",
|
|
132
168
|
signal: controller.signal,
|
|
@@ -342,14 +378,39 @@ function throwApiError(status, bodyText, requestId) {
|
|
|
342
378
|
/**
|
|
343
379
|
* @param requestTimeoutMs Optional per-request timeout (default `BARIVIA_FETCH_TIMEOUT_MS`).
|
|
344
380
|
*/
|
|
381
|
+
// ---- Distributed-trace context (W3C traceparent) ----
|
|
382
|
+
//
|
|
383
|
+
// One logical user action (a tool call that may make several API requests, e.g.
|
|
384
|
+
// upload + analyze + train + poll) shares ONE trace_id so the API and the whole job
|
|
385
|
+
// chain (stage -> prepare -> train -> finalize) can be reconstructed end-to-end. The
|
|
386
|
+
// trace id is scoped per tool invocation via AsyncLocalStorage (see registerAuditedTool);
|
|
387
|
+
// each API call mints a fresh span id. Falls back to a per-call trace if no scope is set.
|
|
388
|
+
const _traceStore = new AsyncLocalStorage();
|
|
389
|
+
function _newTraceId() {
|
|
390
|
+
return randomUUID().replace(/-/g, ""); // 32 hex
|
|
391
|
+
}
|
|
392
|
+
function _newSpanId() {
|
|
393
|
+
return randomUUID().replace(/-/g, "").slice(0, 16); // 16 hex
|
|
394
|
+
}
|
|
395
|
+
/** Run `fn` within a fresh trace scope so all apiCall()s inside share one trace_id. */
|
|
396
|
+
export function runWithTrace(fn) {
|
|
397
|
+
return _traceStore.run(_newTraceId(), fn);
|
|
398
|
+
}
|
|
399
|
+
function _currentTraceId() {
|
|
400
|
+
return _traceStore.getStore() ?? _newTraceId();
|
|
401
|
+
}
|
|
402
|
+
function _traceparentHeader() {
|
|
403
|
+
return `00-${_currentTraceId()}-${_newSpanId()}-01`;
|
|
404
|
+
}
|
|
345
405
|
export async function apiCall(method, path, body, extraHeaders, requestTimeoutMs) {
|
|
346
406
|
const url = `${API_URL}${path}`;
|
|
347
407
|
const contentType = extraHeaders?.["Content-Type"] ?? "application/json";
|
|
348
|
-
const requestId =
|
|
408
|
+
const requestId = newRequestId();
|
|
349
409
|
const headers = {
|
|
350
410
|
Authorization: `Bearer ${API_KEY}`,
|
|
351
411
|
"Content-Type": contentType,
|
|
352
412
|
"X-Request-ID": requestId,
|
|
413
|
+
traceparent: _traceparentHeader(),
|
|
353
414
|
"X-Barsom-Client-Version": CLIENT_VERSION,
|
|
354
415
|
...extraHeaders,
|
|
355
416
|
};
|
|
@@ -377,7 +438,17 @@ export async function apiCall(method, path, body, extraHeaders, requestTimeoutMs
|
|
|
377
438
|
const text = await resp.text();
|
|
378
439
|
if (!resp.ok) {
|
|
379
440
|
if (attempt < MAX_RETRIES && isTransientError(null, resp.status)) {
|
|
380
|
-
|
|
441
|
+
const delayMs = retryDelayMs(attempt);
|
|
442
|
+
logWarn("API retry", {
|
|
443
|
+
rid: requestId,
|
|
444
|
+
method,
|
|
445
|
+
path,
|
|
446
|
+
attempt: attempt + 1,
|
|
447
|
+
max_retries: MAX_RETRIES,
|
|
448
|
+
delay_ms: delayMs,
|
|
449
|
+
error_code: `http_${resp.status}`,
|
|
450
|
+
});
|
|
451
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
381
452
|
continue;
|
|
382
453
|
}
|
|
383
454
|
logInfo("API response", {
|
|
@@ -398,7 +469,17 @@ export async function apiCall(method, path, body, extraHeaders, requestTimeoutMs
|
|
|
398
469
|
catch (err) {
|
|
399
470
|
lastError = err;
|
|
400
471
|
if (attempt < MAX_RETRIES && isTransientError(err)) {
|
|
401
|
-
|
|
472
|
+
const delayMs = retryDelayMs(attempt);
|
|
473
|
+
logWarn("API retry", {
|
|
474
|
+
rid: requestId,
|
|
475
|
+
method,
|
|
476
|
+
path,
|
|
477
|
+
attempt: attempt + 1,
|
|
478
|
+
max_retries: MAX_RETRIES,
|
|
479
|
+
delay_ms: delayMs,
|
|
480
|
+
error_code: err instanceof Error ? err.name : "fetch_error",
|
|
481
|
+
});
|
|
482
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
402
483
|
continue;
|
|
403
484
|
}
|
|
404
485
|
if (err instanceof DOMException &&
|
|
@@ -414,7 +495,7 @@ export async function apiCall(method, path, body, extraHeaders, requestTimeoutMs
|
|
|
414
495
|
/** Fetch raw bytes from the API (for image downloads). */
|
|
415
496
|
export async function apiRawCall(path, requestTimeoutMs) {
|
|
416
497
|
const url = `${API_URL}${path}`;
|
|
417
|
-
const requestId =
|
|
498
|
+
const requestId = newRequestId();
|
|
418
499
|
const effectiveTimeout = requestTimeoutMs ?? FETCH_TIMEOUT_MS;
|
|
419
500
|
let lastError;
|
|
420
501
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
@@ -424,11 +505,21 @@ export async function apiRawCall(path, requestTimeoutMs) {
|
|
|
424
505
|
headers: {
|
|
425
506
|
Authorization: `Bearer ${API_KEY}`,
|
|
426
507
|
"X-Request-ID": requestId,
|
|
508
|
+
traceparent: _traceparentHeader(),
|
|
427
509
|
},
|
|
428
510
|
}, effectiveTimeout);
|
|
429
511
|
if (!resp.ok) {
|
|
430
512
|
if (attempt < MAX_RETRIES && isTransientError(null, resp.status)) {
|
|
431
|
-
|
|
513
|
+
const delayMs = retryDelayMs(attempt);
|
|
514
|
+
logWarn("API retry", {
|
|
515
|
+
rid: requestId,
|
|
516
|
+
path,
|
|
517
|
+
attempt: attempt + 1,
|
|
518
|
+
max_retries: MAX_RETRIES,
|
|
519
|
+
delay_ms: delayMs,
|
|
520
|
+
error_code: `http_${resp.status}`,
|
|
521
|
+
});
|
|
522
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
432
523
|
continue;
|
|
433
524
|
}
|
|
434
525
|
const text = await resp.text();
|
|
@@ -443,7 +534,16 @@ export async function apiRawCall(path, requestTimeoutMs) {
|
|
|
443
534
|
catch (err) {
|
|
444
535
|
lastError = err;
|
|
445
536
|
if (attempt < MAX_RETRIES && isTransientError(err)) {
|
|
446
|
-
|
|
537
|
+
const delayMs = retryDelayMs(attempt);
|
|
538
|
+
logWarn("API retry", {
|
|
539
|
+
rid: requestId,
|
|
540
|
+
path,
|
|
541
|
+
attempt: attempt + 1,
|
|
542
|
+
max_retries: MAX_RETRIES,
|
|
543
|
+
delay_ms: delayMs,
|
|
544
|
+
error_code: err instanceof Error ? err.name : "fetch_error",
|
|
545
|
+
});
|
|
546
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
447
547
|
continue;
|
|
448
548
|
}
|
|
449
549
|
if (err instanceof DOMException &&
|
|
@@ -518,7 +618,13 @@ export async function fetchNormalizationGuideFromApi() {
|
|
|
518
618
|
}
|
|
519
619
|
}
|
|
520
620
|
/** Fetch training guidance from API (used by training_guidance tool). Domain knowledge returned when asked; valid license required. Hints are filtered by plan (allowed_job_types). */
|
|
621
|
+
const GUIDE_CACHE_TTL_MS = 60_000;
|
|
622
|
+
let _trainingGuidanceCache = null;
|
|
623
|
+
let _workflowGuideCache = null;
|
|
521
624
|
export async function fetchTrainingGuidanceFromApi() {
|
|
625
|
+
if (_trainingGuidanceCache && Date.now() - _trainingGuidanceCache.at < GUIDE_CACHE_TTL_MS) {
|
|
626
|
+
return _trainingGuidanceCache.text;
|
|
627
|
+
}
|
|
522
628
|
try {
|
|
523
629
|
const config = (await apiCall("GET", "/v1/training/config"));
|
|
524
630
|
const scopeSuffix = formatGuidanceScopeSuffix(config);
|
|
@@ -531,36 +637,46 @@ export async function fetchTrainingGuidanceFromApi() {
|
|
|
531
637
|
? "\n\n--- Normalization guide (MAD vs SEPD, decision ladder) ---\n\n" + normGuide
|
|
532
638
|
: "\n\n(Normalization guide: GET /v1/docs/normalization — MAD vs SEPD decision ladder.)";
|
|
533
639
|
const hints = config?.training_hints;
|
|
640
|
+
let text;
|
|
534
641
|
if (Array.isArray(hints) && hints.length > 0) {
|
|
535
|
-
|
|
642
|
+
text = presetBlock + "Parameter guidance (from API):\n- " + hints.join("\n- ") + scopeSuffix + normBlock;
|
|
536
643
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
644
|
+
else {
|
|
645
|
+
const pg = config?.parameter_guidance;
|
|
646
|
+
if (pg && typeof pg === "object") {
|
|
647
|
+
const lines = Object.entries(pg).map(([k, v]) => `${k}: ${v}`);
|
|
648
|
+
text = presetBlock + "Parameter guidance (from API):\n- " + lines.join("\n- ") + scopeSuffix + normBlock;
|
|
649
|
+
}
|
|
650
|
+
else if (presetBlock) {
|
|
651
|
+
text = presetBlock + "No additional parameter guidance in config." + scopeSuffix + normBlock;
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
text = "No parameter guidance available.";
|
|
655
|
+
}
|
|
541
656
|
}
|
|
542
|
-
|
|
543
|
-
|
|
657
|
+
_trainingGuidanceCache = { text, at: Date.now() };
|
|
658
|
+
return text;
|
|
544
659
|
}
|
|
545
660
|
catch (e) {
|
|
546
661
|
if (e?.httpStatus === 401 || e?.httpStatus === 403)
|
|
547
662
|
throw e;
|
|
548
663
|
return "Could not fetch parameter guidance. Check API key and connectivity.";
|
|
549
664
|
}
|
|
550
|
-
return "No parameter guidance available.";
|
|
551
665
|
}
|
|
552
666
|
/**
|
|
553
667
|
* Full MCP workflow + bootstrap text from API (tier-scoped). Null on network/parse failure (caller may show offline stub).
|
|
554
668
|
* Re-throws 401/403 so the MCP host surfaces auth errors.
|
|
555
669
|
*/
|
|
556
670
|
export async function fetchWorkflowGuideFromApi() {
|
|
671
|
+
if (_workflowGuideCache && Date.now() - _workflowGuideCache.at < GUIDE_CACHE_TTL_MS) {
|
|
672
|
+
return _workflowGuideCache.text;
|
|
673
|
+
}
|
|
557
674
|
try {
|
|
558
675
|
const data = (await apiCall("GET", "/v1/docs/workflow"));
|
|
559
676
|
const md = data?.workflow_markdown;
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
return null;
|
|
677
|
+
const text = typeof md === "string" && md.trim().length > 0 ? md.trim() : null;
|
|
678
|
+
_workflowGuideCache = { text, at: Date.now() };
|
|
679
|
+
return text;
|
|
564
680
|
}
|
|
565
681
|
catch (e) {
|
|
566
682
|
if (e?.httpStatus === 401 || e?.httpStatus === 403)
|
|
@@ -624,6 +740,16 @@ export function inlineAttachBytesUsed() {
|
|
|
624
740
|
export function shouldFetchAllRemainingFigures(figures) {
|
|
625
741
|
return figures === "all" || figures === "images";
|
|
626
742
|
}
|
|
743
|
+
/** Attach result figures with captions; tracks inlined names for dedup. */
|
|
744
|
+
export async function attachResultsImages(content, jobId, jobType, summary, figures, includeIndividual, inlinedImages) {
|
|
745
|
+
for (const name of getResultsImagesToFetch(jobType, summary, figures, includeIndividual)) {
|
|
746
|
+
const cap = getCaptionForImage(name);
|
|
747
|
+
if (cap)
|
|
748
|
+
content.push({ type: "text", text: cap });
|
|
749
|
+
await tryAttachImage(content, jobId, name);
|
|
750
|
+
inlinedImages.add(name);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
627
753
|
export async function tryAttachImage(content, jobId, filename) {
|
|
628
754
|
// MCP image content only supports raster MIME types (png/jpeg/gif/webp).
|
|
629
755
|
// PDF and SVG must be retrieved out-of-band via get_result_image.
|
package/dist/tools/datasets.js
CHANGED
|
@@ -4,7 +4,7 @@ import { gzipSync } from "node:zlib";
|
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { registerAuditedTool } from "../audit.js";
|
|
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";
|
|
7
|
+
import { apiCall, getWorkspaceRootAsync, resolveFilePathForUpload, textResult, pollUntilComplete, POLL_DERIVE_MAX_MS, POLL_ANALYZE_MAX_MS, UPLOAD_DATASET_TIMEOUT_MS, LARGE_UPLOAD_BYTES, PRESIGNED_PUT_TIMEOUT_MS, POLL_STAGE_MAX_MS, streamFileSha256, putPresignedStream, resolveUploadContentType, suggestDatasetPreview, } from "../shared.js";
|
|
8
8
|
import { GZIP_UPLOAD_HINT } from "../job_status_format.js";
|
|
9
9
|
/**
|
|
10
10
|
* Normalize a nullable string field from the API. Returns "" for absent values,
|
|
@@ -269,9 +269,11 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
269
269
|
if (!name)
|
|
270
270
|
throw new Error("datasets(upload) requires name");
|
|
271
271
|
let body;
|
|
272
|
+
let uploadContentType = "text/csv";
|
|
272
273
|
if (file_path) {
|
|
273
274
|
await apiCall("GET", "/v1/system/info");
|
|
274
275
|
const resolved = await resolveFilePathForUpload(file_path, server);
|
|
276
|
+
uploadContentType = resolveUploadContentType(resolved);
|
|
275
277
|
const lower = resolved.toLowerCase();
|
|
276
278
|
// Accept pre-gzipped CSV/TSV (.csv.gz / .tsv.gz) so large tables transfer
|
|
277
279
|
// ~3x smaller. Already-gzipped files are stored as-is (the platform stores
|
|
@@ -317,9 +319,9 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
317
319
|
// a fresh upload_url and no idempotent_replay flag, so we re-PUT below.
|
|
318
320
|
if (init.idempotent_replay && !init.upload_url) {
|
|
319
321
|
return textResult({ id: datasetId, status: init.status, idempotent_replay: true,
|
|
320
|
-
suggested_next_step:
|
|
322
|
+
suggested_next_step: suggestDatasetPreview(datasetId) });
|
|
321
323
|
}
|
|
322
|
-
await putPresignedStream(init.upload_url, resolved, init.content_type ??
|
|
324
|
+
await putPresignedStream(init.upload_url, resolved, init.content_type ?? uploadContentType, PRESIGNED_PUT_TIMEOUT_MS, isGzipInput);
|
|
323
325
|
const fin = (await apiCall("POST", `/v1/datasets/${datasetId}/finalize`, {}));
|
|
324
326
|
const jobId = (fin.id ?? fin.job_id);
|
|
325
327
|
const poll = await pollUntilComplete(jobId, POLL_STAGE_MAX_MS);
|
|
@@ -332,8 +334,8 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
332
334
|
status: ready ? "ready" : "staging",
|
|
333
335
|
job_id: jobId,
|
|
334
336
|
suggested_next_step: ready
|
|
335
|
-
?
|
|
336
|
-
: `Still staging; poll jobs(action=status, job_id="${jobId}") then
|
|
337
|
+
? `${suggestDatasetPreview(datasetId)} to inspect columns before training.`
|
|
338
|
+
: `Still staging; poll jobs(action=status, job_id="${jobId}") then ${suggestDatasetPreview(datasetId)}.`,
|
|
337
339
|
});
|
|
338
340
|
}
|
|
339
341
|
// Small pre-gzipped files: send the bytes as-is (the API decompresses
|
|
@@ -342,13 +344,13 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
342
344
|
const gzBytes = await fs.readFile(resolved);
|
|
343
345
|
const data = (await apiCall("POST", "/v1/datasets", gzBytes, {
|
|
344
346
|
"X-Dataset-Name": name,
|
|
345
|
-
"Content-Type":
|
|
347
|
+
"Content-Type": uploadContentType,
|
|
346
348
|
"Content-Encoding": "gzip",
|
|
347
349
|
"Idempotency-Key": createHash("sha256").update(`${name}\n`).update(gzBytes).digest("hex"),
|
|
348
350
|
}, UPLOAD_DATASET_TIMEOUT_MS));
|
|
349
351
|
const gid = data.id ?? data.dataset_id;
|
|
350
352
|
if (gid != null)
|
|
351
|
-
data.suggested_next_step =
|
|
353
|
+
data.suggested_next_step = `${suggestDatasetPreview(String(gid))} to inspect columns before training.`;
|
|
352
354
|
return textResult(data);
|
|
353
355
|
}
|
|
354
356
|
body = await fs.readFile(resolved, "utf-8");
|
|
@@ -364,7 +366,7 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
364
366
|
const GZIP_THRESHOLD = 1024 * 1024; // 1 MB
|
|
365
367
|
const uploadHeaders = {
|
|
366
368
|
"X-Dataset-Name": name,
|
|
367
|
-
"Content-Type":
|
|
369
|
+
"Content-Type": uploadContentType,
|
|
368
370
|
// Deterministic key so a timed-out retry of the SAME upload reconciles to
|
|
369
371
|
// the original dataset server-side instead of creating a duplicate.
|
|
370
372
|
"Idempotency-Key": createHash("sha256").update(`${name}\n`).update(body).digest("hex"),
|
|
@@ -377,7 +379,7 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
377
379
|
const data = (await apiCall("POST", "/v1/datasets", uploadBody, uploadHeaders, UPLOAD_DATASET_TIMEOUT_MS));
|
|
378
380
|
const id = data.id ?? data.dataset_id;
|
|
379
381
|
if (id != null)
|
|
380
|
-
data.suggested_next_step =
|
|
382
|
+
data.suggested_next_step = `${suggestDatasetPreview(String(id))} to inspect columns before training.`;
|
|
381
383
|
return textResult(data);
|
|
382
384
|
}
|
|
383
385
|
if (action === "preview") {
|
|
@@ -477,7 +479,7 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
477
479
|
const jobId = (submit.id ?? submit.job_id);
|
|
478
480
|
if (!jobId)
|
|
479
481
|
throw new Error("analyze did not return a job id");
|
|
480
|
-
const poll = await pollUntilComplete(jobId,
|
|
482
|
+
const poll = await pollUntilComplete(jobId, POLL_ANALYZE_MAX_MS);
|
|
481
483
|
if (poll.status === "failed") {
|
|
482
484
|
return { content: [{ type: "text", text: `datasets(analyze) job ${jobId} failed: ${poll.error ?? "unknown error"}` }] };
|
|
483
485
|
}
|
|
@@ -492,7 +494,8 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
492
494
|
data = (await apiCall("GET", `/v1/datasets/${dataset_id}/analyze`));
|
|
493
495
|
}
|
|
494
496
|
else {
|
|
495
|
-
|
|
497
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
498
|
+
throw new Error(`datasets(analyze) failed: ${msg}. If the dataset is very large, try datasets(action=subset, sample_n=10000) first.`);
|
|
496
499
|
}
|
|
497
500
|
}
|
|
498
501
|
return { content: [{ type: "text", text: formatAnalyzeResult(data, dataset_id) }] };
|