@barivia/barsom-mcp 0.21.0 → 0.22.2
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 +19 -5
- package/dist/index.js +1 -1
- package/dist/inference_prepare.js +2 -2
- package/dist/job_monitor.js +38 -2
- package/dist/job_status_format.js +23 -0
- package/dist/prepare_training_prompt.js +23 -6
- package/dist/shared.js +120 -21
- package/dist/tools/account.js +9 -76
- package/dist/tools/datasets.js +26 -13
- 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
|
|
|
@@ -152,12 +163,11 @@ All actions use a frozen trained map — no retraining. Derived columns use **`d
|
|
|
152
163
|
| Action | Use when |
|
|
153
164
|
|--------|----------|
|
|
154
165
|
| `status` | Before large jobs — plan tier, GPU availability, queue depth, credit balance, training time estimates |
|
|
155
|
-
| `request_compute` | Upgrading to cloud burst. Leave tier blank to list options. |
|
|
156
|
-
| `compute_status` | Checking active lease time remaining |
|
|
157
|
-
| `release_compute` | Manually stopping a lease to stop billing |
|
|
158
166
|
| `history` | Viewing recent compute usage and spend |
|
|
159
167
|
| `add_funds` | Getting instructions to add credits |
|
|
160
168
|
|
|
169
|
+
Capacity is shared LOCAL/GKE worker pools (no per-key cloud burst lease).
|
|
170
|
+
|
|
161
171
|
### MCP App tools (optional UIs)
|
|
162
172
|
|
|
163
173
|
| Tool | Role |
|
|
@@ -180,6 +190,10 @@ The right viewer depends on **(MCP App support)** **and** **(can the human reach
|
|
|
180
190
|
|
|
181
191
|
### Migration notes
|
|
182
192
|
|
|
193
|
+
- **Account surface (0.22.0, breaking):** `account(request_compute|compute_status|release_compute)` removed — AWS cloud burst leases are gone. Use `account(status|history|add_funds)`. Capacity is shared LOCAL/GKE pools.
|
|
194
|
+
- **Structured results + train hints (0.21.x, non-breaking):**
|
|
195
|
+
- **`results(action=get)`** returns **`structuredContent`** (job id/type, label, summary metrics, file list) alongside the text summary — same pattern as `training_monitor` / `results_explorer`. Agent hosts can read fields without parsing prose.
|
|
196
|
+
- **`train`** submit responses may include a **`results_hint`** string pointing agents to the next post-train step.
|
|
183
197
|
- **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.
|
|
184
198
|
- **Features (0.20.0, non-breaking):**
|
|
185
199
|
- **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.
|
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{resolvePrepareTrainingPrompt 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, 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})=>{const t=await M(e);return{messages:[{role:"user",content:{type:"text",text:(t.used_fallback?`[offline fallback ${t.fallback_version}]\n\n`:"")+t.text}}]}});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}").`;
|
|
@@ -70,6 +70,17 @@ export function formatJobStatusText(job_id, data) {
|
|
|
70
70
|
if (epoch != null && totalEpochs != null && totalEpochs > 0) {
|
|
71
71
|
parts.push(`epoch ${epoch}/${totalEpochs}`);
|
|
72
72
|
}
|
|
73
|
+
const panelTe = data.panel_te != null && !Number.isNaN(Number(data.panel_te))
|
|
74
|
+
? Number(data.panel_te)
|
|
75
|
+
: data.panel_topographic_error != null && !Number.isNaN(Number(data.panel_topographic_error))
|
|
76
|
+
? Number(data.panel_topographic_error)
|
|
77
|
+
: null;
|
|
78
|
+
if (panelTe != null) {
|
|
79
|
+
parts.push(`panel_te: ${panelTe.toFixed(4)}`);
|
|
80
|
+
}
|
|
81
|
+
if (data.kernel_complete === true) {
|
|
82
|
+
parts.push("kernel_complete");
|
|
83
|
+
}
|
|
73
84
|
}
|
|
74
85
|
const datasetRows = data.dataset_rows != null ? Number(data.dataset_rows) : null;
|
|
75
86
|
const datasetName = data.dataset_name != null ? String(data.dataset_name) : null;
|
|
@@ -120,6 +131,18 @@ export function formatJobStatusText(job_id, data) {
|
|
|
120
131
|
else if (status === "cancelled") {
|
|
121
132
|
parts.push(`Cancelled. Poll again to confirm; dataset and partial worker state are unchanged.`);
|
|
122
133
|
}
|
|
134
|
+
const suggested = data.suggested_next_step != null && String(data.suggested_next_step) !== ""
|
|
135
|
+
? String(data.suggested_next_step)
|
|
136
|
+
: status === "completed"
|
|
137
|
+
? `results(action=get, job_id="${job_id}") then results_explorer(job_id="${job_id}")`
|
|
138
|
+
: status === "failed"
|
|
139
|
+
? `Inspect failure_stage/error; fix data or params; resubmit train or jobs.`
|
|
140
|
+
: status === "running" || status === "pending"
|
|
141
|
+
? `jobs(action=status, job_id="${job_id}") again in 10–15s (or training_monitor).`
|
|
142
|
+
: null;
|
|
143
|
+
if (suggested) {
|
|
144
|
+
parts.push(`suggested_next_step: ${suggested}`);
|
|
145
|
+
}
|
|
123
146
|
return parts.join(" | ");
|
|
124
147
|
}
|
|
125
148
|
export function formatJobCancelText(job_id, data) {
|
|
@@ -1,16 +1,33 @@
|
|
|
1
1
|
import { apiCall } from "./shared.js";
|
|
2
|
-
const
|
|
2
|
+
export const PREPARE_TRAINING_FALLBACK_VERSION = "2026.07-offline";
|
|
3
|
+
const FALLBACK_LINES = [
|
|
4
|
+
"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).",
|
|
5
|
+
'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.',
|
|
6
|
+
"Choose the training path:",
|
|
7
|
+
'- train(action=map, dataset_id="{id}", ...) for complete-case data',
|
|
8
|
+
'- train(action=impute, dataset_id="{id}", ...) for sparse matrices',
|
|
9
|
+
'- train(action=siom_map, dataset_id="{id}", ...) for a fixed-grid SIOM',
|
|
10
|
+
'- train(action=floop_siom, dataset_id="{id}", ...) for FLooP-SIOM (default topology=free / CHL; optional topology=chain)',
|
|
11
|
+
];
|
|
3
12
|
/** Used by the `prepare_training` MCP prompt; prefers tier-scoped text from the API when online. */
|
|
4
|
-
export async function
|
|
5
|
-
|
|
13
|
+
export async function resolvePrepareTrainingPrompt(datasetId) {
|
|
14
|
+
const fallbackText = FALLBACK_LINES.join("\n").replaceAll("{id}", datasetId);
|
|
6
15
|
try {
|
|
7
16
|
const data = (await apiCall("GET", `/v1/docs/prepare_training?dataset_id=${datasetId}`));
|
|
8
17
|
if (typeof data.prompt === "string" && data.prompt.trim()) {
|
|
9
|
-
|
|
18
|
+
return { text: data.prompt.trim(), used_fallback: false, fallback_version: null };
|
|
10
19
|
}
|
|
11
20
|
}
|
|
12
21
|
catch {
|
|
13
|
-
//
|
|
22
|
+
// fall through
|
|
14
23
|
}
|
|
15
|
-
return
|
|
24
|
+
return {
|
|
25
|
+
text: fallbackText,
|
|
26
|
+
used_fallback: true,
|
|
27
|
+
fallback_version: PREPARE_TRAINING_FALLBACK_VERSION,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** @deprecated Prefer resolvePrepareTrainingPrompt for structured fallback metadata. */
|
|
31
|
+
export async function resolvePrepareTrainingPromptText(datasetId) {
|
|
32
|
+
return (await resolvePrepareTrainingPrompt(datasetId)).text;
|
|
16
33
|
}
|
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.2";
|
|
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.
|