@barivia/barsom-mcp 0.21.0 → 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 +13 -2
- 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 +120 -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
|
|
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
|
@@ -11,7 +11,7 @@ import { pipeline } from "node:stream/promises";
|
|
|
11
11
|
import os from "node:os";
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
|
-
import { logInfo } from "./logger.js";
|
|
14
|
+
import { logInfo, logWarn } from "./logger.js";
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
16
16
|
// Config
|
|
17
17
|
// ---------------------------------------------------------------------------
|
|
@@ -21,19 +21,50 @@ export const API_URL = process.env.BARIVIA_API_URL ??
|
|
|
21
21
|
export const API_KEY = process.env.BARIVIA_API_KEY ?? process.env.BARSOM_API_KEY ?? "";
|
|
22
22
|
export const FETCH_TIMEOUT_MS = parseInt(process.env.BARIVIA_FETCH_TIMEOUT_MS ?? "60000", 10);
|
|
23
23
|
export const MAX_RETRIES = 2;
|
|
24
|
+
export const RETRY_BASE_MS = parseInt(process.env.BARIVIA_RETRY_BASE_MS ?? "1000", 10);
|
|
24
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
|
+
}
|
|
25
33
|
/**
|
|
26
34
|
* Single source of truth for the proxy version. Sent to the API as
|
|
27
35
|
* X-Barsom-Client-Version so the server can annotate tool guidance with the
|
|
28
36
|
* wrapper version each action requires. Keep in sync with package.json on bump.
|
|
29
37
|
*/
|
|
30
|
-
export const CLIENT_VERSION = "0.
|
|
38
|
+
export const CLIENT_VERSION = "0.22.1";
|
|
31
39
|
/** User-facing links; keep aligned with barivia.se / api.barivia.se. */
|
|
32
40
|
export const PUBLIC_SITE_ORIGIN = "https://barivia.se";
|
|
33
41
|
/** Self-serve account dashboard (manage plan, billing, and API keys). */
|
|
34
42
|
export const PUBLIC_DASHBOARD_URL = `${PUBLIC_SITE_ORIGIN}/dashboard`;
|
|
35
43
|
/** Poll window for datasets(add_expression) / derive jobs (server-side work can exceed 30s). */
|
|
36
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
|
+
}
|
|
37
68
|
/** Large CSV uploads may exceed default FETCH_TIMEOUT_MS. */
|
|
38
69
|
export const UPLOAD_DATASET_TIMEOUT_MS = 180_000;
|
|
39
70
|
/** Files at/above this size use the presigned direct-to-R2 streaming upload path. */
|
|
@@ -127,7 +158,11 @@ export async function putPresignedStream(url, srcPath, contentType, timeoutMs =
|
|
|
127
158
|
const resp = await fetch(url, {
|
|
128
159
|
method: "PUT",
|
|
129
160
|
body: webStream,
|
|
130
|
-
headers: {
|
|
161
|
+
headers: {
|
|
162
|
+
"Content-Type": contentType,
|
|
163
|
+
"Content-Length": String(contentLength),
|
|
164
|
+
"Content-Encoding": "gzip",
|
|
165
|
+
},
|
|
131
166
|
// Required by Node's fetch to send a streaming request body.
|
|
132
167
|
duplex: "half",
|
|
133
168
|
signal: controller.signal,
|
|
@@ -370,7 +405,7 @@ function _traceparentHeader() {
|
|
|
370
405
|
export async function apiCall(method, path, body, extraHeaders, requestTimeoutMs) {
|
|
371
406
|
const url = `${API_URL}${path}`;
|
|
372
407
|
const contentType = extraHeaders?.["Content-Type"] ?? "application/json";
|
|
373
|
-
const requestId =
|
|
408
|
+
const requestId = newRequestId();
|
|
374
409
|
const headers = {
|
|
375
410
|
Authorization: `Bearer ${API_KEY}`,
|
|
376
411
|
"Content-Type": contentType,
|
|
@@ -403,7 +438,17 @@ export async function apiCall(method, path, body, extraHeaders, requestTimeoutMs
|
|
|
403
438
|
const text = await resp.text();
|
|
404
439
|
if (!resp.ok) {
|
|
405
440
|
if (attempt < MAX_RETRIES && isTransientError(null, resp.status)) {
|
|
406
|
-
|
|
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));
|
|
407
452
|
continue;
|
|
408
453
|
}
|
|
409
454
|
logInfo("API response", {
|
|
@@ -424,7 +469,17 @@ export async function apiCall(method, path, body, extraHeaders, requestTimeoutMs
|
|
|
424
469
|
catch (err) {
|
|
425
470
|
lastError = err;
|
|
426
471
|
if (attempt < MAX_RETRIES && isTransientError(err)) {
|
|
427
|
-
|
|
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));
|
|
428
483
|
continue;
|
|
429
484
|
}
|
|
430
485
|
if (err instanceof DOMException &&
|
|
@@ -440,7 +495,7 @@ export async function apiCall(method, path, body, extraHeaders, requestTimeoutMs
|
|
|
440
495
|
/** Fetch raw bytes from the API (for image downloads). */
|
|
441
496
|
export async function apiRawCall(path, requestTimeoutMs) {
|
|
442
497
|
const url = `${API_URL}${path}`;
|
|
443
|
-
const requestId =
|
|
498
|
+
const requestId = newRequestId();
|
|
444
499
|
const effectiveTimeout = requestTimeoutMs ?? FETCH_TIMEOUT_MS;
|
|
445
500
|
let lastError;
|
|
446
501
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
@@ -455,7 +510,16 @@ export async function apiRawCall(path, requestTimeoutMs) {
|
|
|
455
510
|
}, effectiveTimeout);
|
|
456
511
|
if (!resp.ok) {
|
|
457
512
|
if (attempt < MAX_RETRIES && isTransientError(null, resp.status)) {
|
|
458
|
-
|
|
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));
|
|
459
523
|
continue;
|
|
460
524
|
}
|
|
461
525
|
const text = await resp.text();
|
|
@@ -470,7 +534,16 @@ export async function apiRawCall(path, requestTimeoutMs) {
|
|
|
470
534
|
catch (err) {
|
|
471
535
|
lastError = err;
|
|
472
536
|
if (attempt < MAX_RETRIES && isTransientError(err)) {
|
|
473
|
-
|
|
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));
|
|
474
547
|
continue;
|
|
475
548
|
}
|
|
476
549
|
if (err instanceof DOMException &&
|
|
@@ -545,7 +618,13 @@ export async function fetchNormalizationGuideFromApi() {
|
|
|
545
618
|
}
|
|
546
619
|
}
|
|
547
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;
|
|
548
624
|
export async function fetchTrainingGuidanceFromApi() {
|
|
625
|
+
if (_trainingGuidanceCache && Date.now() - _trainingGuidanceCache.at < GUIDE_CACHE_TTL_MS) {
|
|
626
|
+
return _trainingGuidanceCache.text;
|
|
627
|
+
}
|
|
549
628
|
try {
|
|
550
629
|
const config = (await apiCall("GET", "/v1/training/config"));
|
|
551
630
|
const scopeSuffix = formatGuidanceScopeSuffix(config);
|
|
@@ -558,36 +637,46 @@ export async function fetchTrainingGuidanceFromApi() {
|
|
|
558
637
|
? "\n\n--- Normalization guide (MAD vs SEPD, decision ladder) ---\n\n" + normGuide
|
|
559
638
|
: "\n\n(Normalization guide: GET /v1/docs/normalization — MAD vs SEPD decision ladder.)";
|
|
560
639
|
const hints = config?.training_hints;
|
|
640
|
+
let text;
|
|
561
641
|
if (Array.isArray(hints) && hints.length > 0) {
|
|
562
|
-
|
|
642
|
+
text = presetBlock + "Parameter guidance (from API):\n- " + hints.join("\n- ") + scopeSuffix + normBlock;
|
|
563
643
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
+
}
|
|
568
656
|
}
|
|
569
|
-
|
|
570
|
-
|
|
657
|
+
_trainingGuidanceCache = { text, at: Date.now() };
|
|
658
|
+
return text;
|
|
571
659
|
}
|
|
572
660
|
catch (e) {
|
|
573
661
|
if (e?.httpStatus === 401 || e?.httpStatus === 403)
|
|
574
662
|
throw e;
|
|
575
663
|
return "Could not fetch parameter guidance. Check API key and connectivity.";
|
|
576
664
|
}
|
|
577
|
-
return "No parameter guidance available.";
|
|
578
665
|
}
|
|
579
666
|
/**
|
|
580
667
|
* Full MCP workflow + bootstrap text from API (tier-scoped). Null on network/parse failure (caller may show offline stub).
|
|
581
668
|
* Re-throws 401/403 so the MCP host surfaces auth errors.
|
|
582
669
|
*/
|
|
583
670
|
export async function fetchWorkflowGuideFromApi() {
|
|
671
|
+
if (_workflowGuideCache && Date.now() - _workflowGuideCache.at < GUIDE_CACHE_TTL_MS) {
|
|
672
|
+
return _workflowGuideCache.text;
|
|
673
|
+
}
|
|
584
674
|
try {
|
|
585
675
|
const data = (await apiCall("GET", "/v1/docs/workflow"));
|
|
586
676
|
const md = data?.workflow_markdown;
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
return null;
|
|
677
|
+
const text = typeof md === "string" && md.trim().length > 0 ? md.trim() : null;
|
|
678
|
+
_workflowGuideCache = { text, at: Date.now() };
|
|
679
|
+
return text;
|
|
591
680
|
}
|
|
592
681
|
catch (e) {
|
|
593
682
|
if (e?.httpStatus === 401 || e?.httpStatus === 403)
|
|
@@ -651,6 +740,16 @@ export function inlineAttachBytesUsed() {
|
|
|
651
740
|
export function shouldFetchAllRemainingFigures(figures) {
|
|
652
741
|
return figures === "all" || figures === "images";
|
|
653
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
|
+
}
|
|
654
753
|
export async function tryAttachImage(content, jobId, filename) {
|
|
655
754
|
// MCP image content only supports raster MIME types (png/jpeg/gif/webp).
|
|
656
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) }] };
|
|
@@ -5,7 +5,7 @@ const OFFLINE_STUB = `## Offline / API unavailable
|
|
|
5
5
|
|
|
6
6
|
Configure \`BARIVIA_API_KEY\` and optional \`BARIVIA_API_URL\`, then call **guide_barsom_workflow** again. Full tool map, async rules, training modes, and step-by-step SOP are loaded from the Barivia API (authenticated) and scoped to your plan.
|
|
7
7
|
|
|
8
|
-
**Core tools:** \`datasets\`, \`
|
|
8
|
+
**Core tools:** \`datasets\`, \`train\` (map, siom_map, impute, floop_siom where entitled), \`jobs\` (status/list/compare — lifecycle only), \`results\`, \`inference\`, \`account\`, \`training_guidance\`, \`guide_barsom_workflow\`.
|
|
9
9
|
|
|
10
10
|
**Parameter hints:** call \`training_guidance\` (also API-scoped). **Async:** poll \`jobs(action=status)\` every 10–15s after submit.`;
|
|
11
11
|
export function registerGuideBarsomTool(server) {
|