@barivia/barsom-mcp 0.22.2 → 0.22.4
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/dist/index.js +1 -1
- package/dist/prepare_training_prompt.js +1 -0
- package/dist/shared.js +62 -7
- package/dist/tools/account.js +7 -1
- package/dist/tools/datasets.js +8 -6
- package/dist/tools/explore_map.js +37 -18
- package/dist/tools/results.js +7 -1
- package/dist/tools/train.js +11 -4
- package/dist/tools/training_core.js +6 -1
- package/dist/tools/training_monitor.js +80 -37
- package/dist/train_submit_message.js +5 -7
- package/dist/training_monitor_curve.js +32 -0
- package/dist/ui-delivery.js +185 -0
- package/dist/views/src/views/results-explorer/index.html +16 -13
- package/dist/views/src/views/training-monitor/index.html +30 -18
- package/dist/viz_links.js +25 -0
- package/package.json +1 -1
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{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);
|
|
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 a}from"./viz-server.js";import{API_KEY as i,apiCall as l,apiRawCall as p,loadViewHtml as c,setVizPort as u,setClientSupportsMcpApps as d,CLIENT_VERSION as m}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 y,RESULTS_EXPLORER_URI as w}from"./tools/explore_map.js";import{registerAccountTool as v}from"./tools/account.js";import{registerInferenceTool as j}from"./tools/inference.js";import{registerGuideBarsomTool as x}from"./tools/guide_barsom.js";import{registerTrainingGuidanceTool as k}from"./tools/training_guidance.js";import{registerFeedbackTool as P}from"./tools/feedback.js";import{registerTrainingMonitorTool as A,TRAINING_MONITOR_URI as I}from"./tools/training_monitor.js";import{resolvePrepareTrainingPrompt as C}from"./prepare_training_prompt.js";i||(console.error("Error: BARIVIA_API_KEY not set. Set it in your MCP client config."),process.exit(1));const M=new e({name:"analytics-engine",version:m,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- **Do not auto-subset:** Never call `datasets(subset)` or `sample_n` unless the user explicitly asks for a filter, slice, or random sample. Train and analyze on the uploaded `dataset_id` at full row count — GPU staging/prepare handles hundreds of thousands of rows quickly; silent downsampling hides real prepare/train behavior and wastes scale demos. Use small named fixtures (e.g. sample.csv, periodic_load) only when the user or runbook names them.\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.\n\n## UI delivery (heterogeneous MCP clients)\n\nHosts differ (Cursor, Claude Desktop, CLI agents): some embed MCP App panels; others list `ui://` but never mount them (expected on some Cursor builds); others are text-only. Tool results **always lead with a standalone localhost URL** — that is the intended fallback, not a broken App.\n\n| Tier | When | Agent should |\n|------|------|--------------|\n| **embedded** | Client advertises MCP Apps | Let the panel render **and** surface the standalone localhost URL from tool output / `ui_delivery.urls` (hosts often list Apps but do not mount). |\n| **localhost** | No MCP Apps; viz server running | Post the markdown link from tool output to the user (never broken workspace paths). Re-post after job completes if needed. |\n| **inline_image** | No Apps / override `inline` | Summarize metrics; inline raster (combined.png, learning curve) is attached automatically — show it in the reply. |\n| **text_only** | Viz server down | Use `jobs(status)` + `results(get)`; no panel or localhost link. |\n\nEvery `training_monitor` / `results_explorer` response includes a structured `ui_delivery` block (`delivery`, `urls`, `hint_for_agent`). Follow `hint_for_agent` without user prompting. Standalone viz pages cross-link monitor ↔ results for the same `job_id`.\n\n**Env overrides:** `BARIVIA_UI_DELIVERY=auto|apps|localhost|inline` (default `auto`). `BARIVIA_VIZ_PORT` pins the localhost viz port across proxy restarts. **Diagnostics:** `account(action=status)` reports detected MCP Apps support, viz port, and active tier.'});s(M,w,w,{mimeType:n},async()=>{const e=await c("results-explorer");return{contents:[{uri:w,mimeType:n,text:e??"<html><body>Results Explorer view not built yet. Run: npm run build:views</body></html>"}]}}),s(M,I,I,{mimeType:n},async()=>{const e=await c("training-monitor");return{contents:[{uri:I,mimeType:n,text:e??"<html><body>Training Monitor view not built yet.</body></html>"}]}}),x(M),y(M),A(M),f(M),g(M),h(M,_),b(M),v(M),j(M),k(M),P(M),M.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 auto-subset — never call `datasets(subset)` / `sample_n` unless the user explicitly asks; train on the full uploaded `dataset_id`. 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")}}]})),M.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 C(e);return{messages:[{role:"user",content:{type:"text",text:(t.used_fallback?`[offline fallback ${t.fallback_version}]\n\n`:"")+t.text}}]}});const T=new t;(async function(){try{const e=await a(l,p,c);u(e)}catch(e){process.env.BARIVIA_VIZ_PORT&&console.error("Barivia viz server failed to start:",e)}const e=M.server;e.oninitialized=()=>{const t=e.getClientCapabilities(),o=r(t);d(!!o?.mimeTypes?.includes(n))},await M.connect(T)})().catch(console.error);
|
|
@@ -2,6 +2,7 @@ import { apiCall } from "./shared.js";
|
|
|
2
2
|
export const PREPARE_TRAINING_FALLBACK_VERSION = "2026.07-offline";
|
|
3
3
|
const FALLBACK_LINES = [
|
|
4
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
|
+
"Do not auto-subset: never call datasets(subset) or sample_n unless the user explicitly asks — train on the full uploaded dataset_id.",
|
|
5
6
|
'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
7
|
"Choose the training path:",
|
|
7
8
|
'- train(action=map, dataset_id="{id}", ...) for complete-case data',
|
package/dist/shared.js
CHANGED
|
@@ -35,7 +35,7 @@ function newRequestId() {
|
|
|
35
35
|
* X-Barsom-Client-Version so the server can annotate tool guidance with the
|
|
36
36
|
* wrapper version each action requires. Keep in sync with package.json on bump.
|
|
37
37
|
*/
|
|
38
|
-
export const CLIENT_VERSION = "0.22.
|
|
38
|
+
export const CLIENT_VERSION = "0.22.4";
|
|
39
39
|
/** User-facing links; keep aligned with barivia.se / api.barivia.se. */
|
|
40
40
|
export const PUBLIC_SITE_ORIGIN = "https://barivia.se";
|
|
41
41
|
/** Self-serve account dashboard (manage plan, billing, and API keys). */
|
|
@@ -73,6 +73,51 @@ export const LARGE_UPLOAD_BYTES = 64 * 1024 * 1024; // 64 MB
|
|
|
73
73
|
export const PRESIGNED_PUT_TIMEOUT_MS = 30 * 60_000; // 30 min
|
|
74
74
|
/** Poll window for the async stage_dataset job (large files take minutes). */
|
|
75
75
|
export const POLL_STAGE_MAX_MS = 30 * 60_000; // 30 min
|
|
76
|
+
export const BARSOM_VIZ_VIEWS = {
|
|
77
|
+
trainingMonitor: "training-monitor",
|
|
78
|
+
resultsExplorer: "results-explorer",
|
|
79
|
+
};
|
|
80
|
+
export function buildStandaloneVizPageUrl(port, viewPath, jobId) {
|
|
81
|
+
if (!port)
|
|
82
|
+
return undefined;
|
|
83
|
+
return `http://localhost:${port}/viz/${viewPath}?mode=standalone&job_id=${encodeURIComponent(jobId)}`;
|
|
84
|
+
}
|
|
85
|
+
export function buildVizRelatedUrls(port, jobId, views) {
|
|
86
|
+
if (!port)
|
|
87
|
+
return undefined;
|
|
88
|
+
return {
|
|
89
|
+
trainingMonitor: buildStandaloneVizPageUrl(port, views.trainingMonitor, jobId),
|
|
90
|
+
resultsExplorer: buildStandaloneVizPageUrl(port, views.resultsExplorer, jobId),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/** Prominent standalone URL blocks for tool results — always emitted when a viz port exists. */
|
|
94
|
+
export function buildVizUrlContentBlocks(options) {
|
|
95
|
+
const { label, toolName, standaloneUrl, port, jobId, clientSupportsMcpApps } = options;
|
|
96
|
+
if (!standaloneUrl)
|
|
97
|
+
return [];
|
|
98
|
+
const embedNote = clientSupportsMcpApps
|
|
99
|
+
? "Embedded panel if your host supports MCP Apps — otherwise open the standalone URL below (expected on some Cursor builds)."
|
|
100
|
+
: "Open the standalone URL below when no inline panel appears.";
|
|
101
|
+
const health = port > 0
|
|
102
|
+
? ` Health check: http://localhost:${port}/api/health?job_id=${encodeURIComponent(jobId)}`
|
|
103
|
+
: "";
|
|
104
|
+
const blocks = [
|
|
105
|
+
{
|
|
106
|
+
type: "text",
|
|
107
|
+
text: `${embedNote}\n` +
|
|
108
|
+
`Standalone ${label} URL (copy if Open is blocked):\n${standaloneUrl}\n` +
|
|
109
|
+
`AGENT: surface this URL prominently in your reply — use a markdown link "[Open ${label}](${standaloneUrl})" or tell the user to copy/paste into a browser. Do not bury it after metrics.\n` +
|
|
110
|
+
`This localhost port is per MCP session and goes stale if the proxy restarts — re-run ${toolName} for a fresh URL, or set BARIVIA_VIZ_PORT for a persistent port.${health}`,
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
if (!clientSupportsMcpApps) {
|
|
114
|
+
blocks.push({
|
|
115
|
+
type: "text",
|
|
116
|
+
text: `[Open ${label}](${standaloneUrl})`,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return blocks;
|
|
120
|
+
}
|
|
76
121
|
// ---------------------------------------------------------------------------
|
|
77
122
|
// Shared state (mutable)
|
|
78
123
|
// ---------------------------------------------------------------------------
|
|
@@ -860,12 +905,17 @@ export function getResultsImagesToFetch(jobType, summary, figures, includeIndivi
|
|
|
860
905
|
}
|
|
861
906
|
// train_som / train_siom
|
|
862
907
|
const features = summary.features ?? [];
|
|
863
|
-
const
|
|
864
|
-
const
|
|
865
|
-
const
|
|
866
|
-
const
|
|
908
|
+
const summaryFiles = summary.files ?? [];
|
|
909
|
+
const imageFilesFromSummary = summaryFiles.filter((f) => /\.(png|pdf|svg)$/i.test(f));
|
|
910
|
+
const combinedName = imageFilesFromSummary.find((f) => f.startsWith("combined.")) ?? `combined.${ext}`;
|
|
911
|
+
const umatrixName = imageFilesFromSummary.find((f) => f.startsWith("umatrix.")) ?? `umatrix.${ext}`;
|
|
912
|
+
const hitHistName = imageFilesFromSummary.find((f) => f.startsWith("hit_histogram.")) ?? `hit_histogram.${ext}`;
|
|
913
|
+
const correlationName = imageFilesFromSummary.find((f) => f.startsWith("correlation.")) ?? `correlation.${ext}`;
|
|
867
914
|
const componentNames = features.map((f, i) => `component_${i + 1}_${f.replace(/[^a-zA-Z0-9_]/g, "_")}.${ext}`);
|
|
868
|
-
const
|
|
915
|
+
const publishedComponents = imageFilesFromSummary.filter((f) => /^component_\d+_/.test(f));
|
|
916
|
+
const allList = imageFilesFromSummary.length > 0
|
|
917
|
+
? imageFilesFromSummary
|
|
918
|
+
: [combinedName, umatrixName, hitHistName, correlationName, ...componentNames];
|
|
869
919
|
if (figures === undefined || figures === "default") {
|
|
870
920
|
return includeIndividual ? allList : [combinedName];
|
|
871
921
|
}
|
|
@@ -880,8 +930,13 @@ export function getResultsImagesToFetch(jobType, summary, figures, includeIndivi
|
|
|
880
930
|
hit_histogram: hitHistName,
|
|
881
931
|
correlation: correlationName,
|
|
882
932
|
};
|
|
933
|
+
for (const file of imageFilesFromSummary) {
|
|
934
|
+
const key = file.replace(/\.(png|pdf|svg)$/i, "");
|
|
935
|
+
nameToFile[key] = file;
|
|
936
|
+
}
|
|
883
937
|
features.forEach((_, i) => {
|
|
884
|
-
|
|
938
|
+
const published = publishedComponents.find((f) => f.startsWith(`component_${i + 1}_`));
|
|
939
|
+
nameToFile[`component_${i + 1}`] = published ?? componentNames[i];
|
|
885
940
|
});
|
|
886
941
|
return figures
|
|
887
942
|
.map((key) => {
|
package/dist/tools/account.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { registerAuditedTool } from "../audit.js";
|
|
3
|
-
import { apiCall } from "../shared.js";
|
|
3
|
+
import { apiCall, CLIENT_VERSION } from "../shared.js";
|
|
4
|
+
import { getMcpClientDiagnostics } from "../ui-delivery.js";
|
|
4
5
|
export function registerAccountTool(server) {
|
|
5
6
|
registerAuditedTool(server, "account", `Manage your Barivia account — check plan/license info, view usage history, add funds.
|
|
6
7
|
|
|
@@ -80,6 +81,11 @@ NOT FOR: Training itself — use train(action=map). This tool only manages the a
|
|
|
80
81
|
lines.push(` ${k}: ~${v}s`);
|
|
81
82
|
}
|
|
82
83
|
}
|
|
84
|
+
const mcpDiag = getMcpClientDiagnostics();
|
|
85
|
+
lines.push(``, `MCP client (local proxy @ ${CLIENT_VERSION}):`, ` MCP Apps detected: ${mcpDiag.mcp_apps_supported ? "yes" : "no"}`, ` Viz port: ${mcpDiag.viz_port ?? "none"}${mcpDiag.viz_port_pinned ? " (BARIVIA_VIZ_PORT pinned)" : " (ephemeral — set BARIVIA_VIZ_PORT for stable URLs)"}`, ` UI delivery override: ${mcpDiag.ui_delivery_override}`, ` Active delivery tier: ${mcpDiag.active_delivery_tier}`);
|
|
86
|
+
if (mcpDiag.health_url) {
|
|
87
|
+
lines.push(` Viz health: ${mcpDiag.health_url}`);
|
|
88
|
+
}
|
|
83
89
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
84
90
|
}
|
|
85
91
|
if (action === "history") {
|
package/dist/tools/datasets.js
CHANGED
|
@@ -139,7 +139,7 @@ Formats: plain CSV/TSV or gzip (.csv.gz / .tsv.gz). For files above ~100 MB, pre
|
|
|
139
139
|
| analyze | Pre-training column analysis: correlation matrix, periodicity ranking, column recommendations (train / drop / project later). Run after preview, before train_map. |
|
|
140
140
|
| list | Finding dataset IDs for train_map, preview, or subset — see all available datasets |
|
|
141
141
|
| get | Fetch one dataset by id — status, staging fields, ingest_error (use after upload or when staging is slow) |
|
|
142
|
-
| subset |
|
|
142
|
+
| subset | ONLY when the user asks for row_range, filters, or sample_n — never auto-shrink before train/analyze |
|
|
143
143
|
| add_expression | Add a computed column from an expression (dataset_id + name + expression). With project_onto_job=<training_job_id>, instead projects the computed expression onto that map as a component plane in one step (no column added). |
|
|
144
144
|
| reduce_spectral | Collapse a long ordered numeric block (e.g. 1000-point spectrum, time series, sensor fingerprint) into a small per-row feature set so the SOM can train on signal-dense features. Methods: pca, log_sample, uniform_sample, stats. |
|
|
145
145
|
| delete | Cleaning up after experiments or freeing the dataset slot |
|
|
@@ -153,13 +153,15 @@ action=preview: Show columns, stats, sample rows, cyclic/datetime detections. AL
|
|
|
153
153
|
action=analyze: Pre-training analysis on numeric columns — Pearson correlation, autocorrelation periodicity scores, and column recommendations (train, consider_dropping, project_later, low_variance). Use to choose which columns to include in training and which to project onto the map after training.
|
|
154
154
|
action=list: List all datasets belonging to the organisation with id, name, rows, cols, created_at (use created_at or id to distinguish datasets with the same name).
|
|
155
155
|
action=get: Fetch one dataset by dataset_id — returns status, staged_prefix, staged_version, ingest_error, and stage_job_id when staging is pending.
|
|
156
|
+
DO NOT AUTO-SUBSET: Call action=subset only when the user explicitly requests a filter, slice, or random sample. Default path is upload → preview/analyze → train on the full dataset_id (GPU handles scale). Never downsample to "save time" or "be helpful".
|
|
157
|
+
|
|
156
158
|
action=subset: Create a new dataset from a subset of an existing one. Requires name and at least one of row_range, filters, or sample_n.
|
|
157
159
|
- row_range: [start, end] 1-based inclusive (e.g. [1, 2000] for first 2000 rows)
|
|
158
160
|
- filters: array of conditions, ALL must match (AND logic). Each: { column, op, value }.
|
|
159
161
|
Operators: eq, ne, in, gt, lt, gte, lte, between
|
|
160
162
|
Examples: { column: "region", op: "eq", value: "Europe" } | { column: "age", op: "between", value: [18, 65] }
|
|
161
|
-
- sample_n: keep a random N-row sample (seeded via sample_seed, default 42; row order preserved).
|
|
162
|
-
- Combine row_range + filters + sample_n to slice rows, values, then downsample.
|
|
163
|
+
- sample_n: keep a random N-row sample (seeded via sample_seed, default 42; row order preserved). User/ops tool only — not an agent default. Applied after row_range/filters; can also be used alone.
|
|
164
|
+
- Combine row_range + filters + sample_n to slice rows, values, then downsample when the user asked for it.
|
|
163
165
|
- Single filter object is also accepted (auto-wrapped).
|
|
164
166
|
action=reduce_spectral: Run a pre-training reducer over an ordered block of numeric columns. All four methods produce one feature vector per row (rows in = rows out; only the column dimension is collapsed) and append derived columns to the dataset. Choose by data shape:
|
|
165
167
|
- pca: top-k principal components — general first try when many columns are correlated (spectroscopy, gene panels, sensor arrays). Returns explained_variance_ratio.
|
|
@@ -258,7 +260,7 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
258
260
|
.int()
|
|
259
261
|
.min(1)
|
|
260
262
|
.optional()
|
|
261
|
-
.describe("action=subset: keep a random N-row sample (seeded, row order preserved).
|
|
263
|
+
.describe("action=subset: keep a random N-row sample (seeded, row order preserved). Only when the user explicitly requests downsampling — not an agent default. Combine with row_range/filters, or use alone."),
|
|
262
264
|
sample_seed: z
|
|
263
265
|
.number()
|
|
264
266
|
.int()
|
|
@@ -489,7 +491,7 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
489
491
|
job_id: jobId,
|
|
490
492
|
request_id: rid ?? null,
|
|
491
493
|
message: errText,
|
|
492
|
-
suggested_next_step: "
|
|
494
|
+
suggested_next_step: "Do not auto-subset. Report the error to the user; full-dataset analyze is supported at scale. Use datasets(subset) only if the user explicitly asks for downsampling.",
|
|
493
495
|
}, errText + (rid ? ` (request_id=${rid})` : ""));
|
|
494
496
|
}
|
|
495
497
|
if (poll.status !== "completed") {
|
|
@@ -505,7 +507,7 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
505
507
|
}
|
|
506
508
|
else {
|
|
507
509
|
const msg = err instanceof Error ? err.message : String(err);
|
|
508
|
-
throw new Error(`datasets(analyze) failed: ${msg}.
|
|
510
|
+
throw new Error(`datasets(analyze) failed: ${msg}. Do not auto-subset — use datasets(subset) only if the user explicitly asked.`);
|
|
509
511
|
}
|
|
510
512
|
}
|
|
511
513
|
return { content: [{ type: "text", text: formatAnalyzeResult(data, dataset_id) }] };
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
|
|
3
3
|
import { registerAuditedTool, runMcpToolAudit } from "../audit.js";
|
|
4
|
-
import { apiCall, apiRawCall,
|
|
4
|
+
import { apiCall, apiRawCall, BARSOM_VIZ_VIEWS, buildStandaloneVizPageUrl, buildVizRelatedUrls, getVizPort, getCaptionForImage, getResultsImagesToFetch, mimeForFilename, structuredTextResult, tryAttachImage, } from "../shared.js";
|
|
5
|
+
import { buildVizStandaloneUrl } from "../viz_links.js";
|
|
6
|
+
import { appendUiDeliveryContent, beginInlineFallback, buildStandaloneVizUrl, resolveUiDelivery, } from "../ui-delivery.js";
|
|
5
7
|
export const RESULTS_EXPLORER_URI = "ui://barsom/results-explorer";
|
|
6
8
|
function labelForFigure(filename) {
|
|
7
9
|
const base = filename.replace(/\.(png|pdf|svg)$/i, "");
|
|
@@ -111,6 +113,10 @@ export function buildPayload(jobId, data) {
|
|
|
111
113
|
summary.selected_columns ? `Variables: ${summary.selected_columns.join(", ")}` : "",
|
|
112
114
|
].filter(Boolean);
|
|
113
115
|
const port = getVizPort();
|
|
116
|
+
const standaloneUrl = buildStandaloneVizPageUrl(port, BARSOM_VIZ_VIEWS.resultsExplorer, jobId) ??
|
|
117
|
+
buildVizStandaloneUrl("results-explorer", jobId);
|
|
118
|
+
const trainingMonitorUrl = buildStandaloneVizPageUrl(port, BARSOM_VIZ_VIEWS.trainingMonitor, jobId) ??
|
|
119
|
+
buildVizStandaloneUrl("training-monitor", jobId);
|
|
114
120
|
return {
|
|
115
121
|
type: "results-explorer",
|
|
116
122
|
jobId,
|
|
@@ -123,42 +129,55 @@ export function buildPayload(jobId, data) {
|
|
|
123
129
|
availableFigures: figures,
|
|
124
130
|
defaultFigureKey: figures.find((figure) => figure.key === "combined")?.key
|
|
125
131
|
?? figures[0]?.key,
|
|
126
|
-
standaloneUrl
|
|
132
|
+
standaloneUrl,
|
|
133
|
+
trainingMonitorUrl,
|
|
134
|
+
relatedUrls: buildVizRelatedUrls(port, jobId, BARSOM_VIZ_VIEWS),
|
|
127
135
|
};
|
|
128
136
|
}
|
|
129
137
|
async function handleResultsExplorer(job_id) {
|
|
138
|
+
beginInlineFallback();
|
|
130
139
|
const data = (await apiCall("GET", `/v1/results/${job_id}`));
|
|
131
140
|
const payload = buildPayload(job_id, data);
|
|
132
141
|
const summary = (data.summary ?? {});
|
|
133
|
-
const
|
|
134
|
-
{
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
payload.highlights[0] ?? "",
|
|
141
|
-
].filter(Boolean).join("\n"),
|
|
142
|
-
},
|
|
143
|
-
];
|
|
142
|
+
const summaryText = [
|
|
143
|
+
`Results Explorer ready for job ${job_id}.`,
|
|
144
|
+
payload.gridLabel ? `Grid: ${payload.gridLabel}` : "",
|
|
145
|
+
payload.subtitle ? `Label: ${payload.subtitle}` : "",
|
|
146
|
+
payload.highlights[0] ?? "",
|
|
147
|
+
].filter(Boolean).join("\n");
|
|
148
|
+
const content = [{ type: "text", text: summaryText }];
|
|
144
149
|
const defaultFigure = payload.availableFigures.find((figure) => figure.key === payload.defaultFigureKey);
|
|
150
|
+
let inlineImageAttached = false;
|
|
145
151
|
if (defaultFigure) {
|
|
152
|
+
const before = content.length;
|
|
146
153
|
await tryAttachImage(content, job_id, defaultFigure.filename);
|
|
154
|
+
inlineImageAttached = content.length > before && content.some((c) => c.type === "image");
|
|
147
155
|
}
|
|
148
156
|
else {
|
|
149
157
|
const ext = summary.output_format ?? "png";
|
|
158
|
+
const before = content.length;
|
|
150
159
|
await tryAttachImage(content, job_id, `combined.${ext}`);
|
|
160
|
+
inlineImageAttached = content.length > before && content.some((c) => c.type === "image");
|
|
151
161
|
}
|
|
152
|
-
|
|
162
|
+
const uiDelivery = resolveUiDelivery({
|
|
163
|
+
tool: "results_explorer",
|
|
164
|
+
jobId: job_id,
|
|
165
|
+
jobStatus: "completed",
|
|
166
|
+
inlineImageAttached,
|
|
167
|
+
});
|
|
168
|
+
appendUiDeliveryContent(content, uiDelivery, {
|
|
169
|
+
linkLabel: "Open results explorer",
|
|
170
|
+
primaryUrl: payload.standaloneUrl ?? buildStandaloneVizUrl("results_explorer", job_id),
|
|
171
|
+
});
|
|
172
|
+
const monitorUrl = payload.trainingMonitorUrl ?? buildStandaloneVizUrl("training_monitor", job_id);
|
|
173
|
+
if (monitorUrl) {
|
|
153
174
|
content.push({
|
|
154
175
|
type: "text",
|
|
155
|
-
text: `
|
|
156
|
-
`AGENT: surface this to the user as a clickable markdown link (e.g. "[Open results explorer](${payload.standaloneUrl})") in your reply — do not leave it buried in tool output. ` +
|
|
157
|
-
`This localhost port is assigned per MCP session and changes if the proxy restarts — if the page won't load, re-run results_explorer for a fresh URL, or set BARIVIA_VIZ_PORT in your MCP env for a persistent port.`,
|
|
176
|
+
text: `Review training curves: [Open training monitor](${monitorUrl})`,
|
|
158
177
|
});
|
|
159
178
|
}
|
|
160
179
|
return {
|
|
161
|
-
...structuredTextResult(payload, undefined, content),
|
|
180
|
+
...structuredTextResult({ ...payload, ui_delivery: uiDelivery }, undefined, content),
|
|
162
181
|
_meta: { ui: { resourceUri: RESULTS_EXPLORER_URI } },
|
|
163
182
|
};
|
|
164
183
|
}
|
package/dist/tools/results.js
CHANGED
|
@@ -19,7 +19,7 @@ ONLY call this after jobs(action=status) returns "completed".
|
|
|
19
19
|
ESCALATION: If job not found, verify job_id. If "job not complete", poll with jobs(action=status).
|
|
20
20
|
|
|
21
21
|
action=get: Returns text summary with quality metrics and inline images.
|
|
22
|
-
- figures: omit = combined only. "all" = all plots. "none" = metrics text only, no images (recommended for sweeps and LLM clients to keep tool payloads small). Array = specific logical names (combined, umatrix, hit_histogram, learning_curve, correlation, component_1..N).
|
|
22
|
+
- figures: omit = combined only. "all" = all published plots (excludes unpublished cyclic cos/sin expansions unless train used viz_upload_cyclic_components=true). "none" = metrics text only, no images (recommended for sweeps and LLM clients to keep tool payloads small). Array = specific logical names (combined, umatrix, hit_histogram, learning_curve, correlation, component_1..N). Request unpublished cyclic planes via results(recolor, figures=[component_N, ...]).
|
|
23
23
|
- include_individual: if true (and figures omitted), inlines every component plane.
|
|
24
24
|
- After recolor or project, use the job_id returned by that operation to get the new artifacts, not the original training job_id.
|
|
25
25
|
- After showing results, guide the user: QE interpretation, whether to retrain, which features to explore.
|
|
@@ -574,14 +574,20 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
|
|
|
574
574
|
throw new Error("results(download) requires folder");
|
|
575
575
|
const data = (await apiCall("GET", `/v1/results/${job_id}`));
|
|
576
576
|
const summary = (data.summary ?? {});
|
|
577
|
+
const downloadUrls = (data.download_urls ?? {});
|
|
577
578
|
const jobLabel = data.label != null && data.label !== "" ? String(data.label) : null;
|
|
578
579
|
const files = summary.files ?? [];
|
|
579
580
|
const jobType = summary.job_type ?? "train_som";
|
|
580
581
|
const needsAllFiles = ["enrich_dataset", "predict", "impute_column", "compare_datasets"].includes(jobType);
|
|
581
582
|
const isImage = (f) => f.endsWith(".png") || f.endsWith(".svg") || f.endsWith(".pdf");
|
|
583
|
+
const hasUrlIndex = Object.keys(downloadUrls).length > 0;
|
|
584
|
+
const isPublished = (f) => !hasUrlIndex || f in downloadUrls;
|
|
582
585
|
let toDownload;
|
|
583
586
|
if (figures === "all" || figures === "images" || figures === undefined) {
|
|
584
587
|
toDownload = needsAllFiles || include_json ? files : files.filter(isImage);
|
|
588
|
+
if (hasUrlIndex) {
|
|
589
|
+
toDownload = toDownload.filter((f) => !isImage(f) || isPublished(f));
|
|
590
|
+
}
|
|
585
591
|
}
|
|
586
592
|
else if (Array.isArray(figures)) {
|
|
587
593
|
toDownload = figures;
|
package/dist/tools/train.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { registerAuditedTool } from "../audit.js";
|
|
3
|
-
import { apiCall
|
|
3
|
+
import { apiCall } from "../shared.js";
|
|
4
4
|
import { buildTrainSubmitExtras } from "../train_submit_message.js";
|
|
5
|
+
import { TRAINING_MONITOR_URI, buildTrainingMonitorResult, } from "./training_monitor.js";
|
|
5
6
|
import { TRAINING_INPUT_SCHEMA, buildTrainMapParams, buildFloopParams, fetchTrainingPresets, assertValidNormalizationArgs, } from "./training_core.js";
|
|
6
7
|
export const TRAIN_DESCRIPTION = `Submit a self-organizing map training job. Returns a job_id; poll with jobs(action=status) and then results(action=get). All actions are async.
|
|
7
8
|
|
|
@@ -89,7 +90,12 @@ async function submitTrainJob(opts) {
|
|
|
89
90
|
}
|
|
90
91
|
data.message = msg;
|
|
91
92
|
data.suggested_next_step = extras.suggested_next_step;
|
|
92
|
-
|
|
93
|
+
const monitor = await buildTrainingMonitorResult(newJobId, { preamble: msg });
|
|
94
|
+
return {
|
|
95
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }, ...monitor.content],
|
|
96
|
+
structuredContent: monitor.structuredContent,
|
|
97
|
+
_meta: { ui: { resourceUri: TRAINING_MONITOR_URI } },
|
|
98
|
+
};
|
|
93
99
|
}
|
|
94
100
|
/**
|
|
95
101
|
* Core training dispatcher. Shared by the `train` tool and the deprecated
|
|
@@ -98,7 +104,7 @@ async function submitTrainJob(opts) {
|
|
|
98
104
|
export async function runTrain(action, args) {
|
|
99
105
|
const dataset_id = args.dataset_id;
|
|
100
106
|
if (action === "map" || action === "siom_map" || action === "impute") {
|
|
101
|
-
const { preset, grid_x, grid_y, epochs, model, periodic, columns, cyclic_features, cyclic_pairs, temporal_features, feature_weights, transforms, auto_log_transforms, time_delay_embeddings, categorical_features, normalize, normalization_methods, sigma_f, learning_rate, batch_size, quality_metrics, backend, output_format, output_dpi, colormap, row_range, gamma, gamma_f, siom_decay, siom_penalty, penalty_alpha, reset_per_epoch, siom_feature_geometry, siom_qe_backend, siom_qe_batch_size, label, cv_folds, viz_mode, viz_top_components, emit_cell_uncertainty, } = args;
|
|
107
|
+
const { preset, grid_x, grid_y, epochs, model, periodic, columns, cyclic_features, cyclic_pairs, temporal_features, feature_weights, transforms, auto_log_transforms, time_delay_embeddings, categorical_features, normalize, normalization_methods, sigma_f, learning_rate, batch_size, quality_metrics, backend, output_format, output_dpi, colormap, row_range, gamma, gamma_f, siom_decay, siom_penalty, penalty_alpha, reset_per_epoch, siom_feature_geometry, siom_qe_backend, siom_qe_batch_size, label, cv_folds, viz_mode, viz_top_components, viz_upload_cyclic_components, emit_cell_uncertainty, } = args;
|
|
102
108
|
let PRESETS = {};
|
|
103
109
|
try {
|
|
104
110
|
PRESETS = await fetchTrainingPresets();
|
|
@@ -123,7 +129,8 @@ export async function runTrain(action, args) {
|
|
|
123
129
|
temporal_features, feature_weights, transforms, auto_log_transforms,
|
|
124
130
|
time_delay_embeddings, categorical_features,
|
|
125
131
|
normalize, normalization_methods, sigma_f, learning_rate, batch_size, quality_metrics, backend,
|
|
126
|
-
output_format, output_dpi, colormap, row_range,
|
|
132
|
+
output_format, output_dpi, colormap, row_range, siom_feature_geometry, siom_qe_backend, siom_qe_batch_size,
|
|
133
|
+
viz_upload_cyclic_components,
|
|
127
134
|
}, PRESETS);
|
|
128
135
|
if (action === "impute") {
|
|
129
136
|
params._job_type = "train_impute";
|
|
@@ -113,6 +113,8 @@ export const TRAINING_INPUT_SCHEMA = {
|
|
|
113
113
|
.describe("Visualization density; auto-capped when feature count > 40"),
|
|
114
114
|
viz_top_components: z.number().int().min(0).max(64).optional()
|
|
115
115
|
.describe("With viz_mode=summary_plus_top: upload top-N component maps by variance (default 8)"),
|
|
116
|
+
viz_upload_cyclic_components: z.boolean().optional()
|
|
117
|
+
.describe("When true with individual component PNGs, also publish cos/sin cyclic expansion planes (default false; recovered hour/month panels stay in combined.png)"),
|
|
116
118
|
emit_cell_uncertainty: z.boolean().optional()
|
|
117
119
|
.describe("impute: write imputation_uncertainty.csv (pool_std = prototype-neighbourhood dispersion, not calibrated SD; pool_n = contributing nodes)"),
|
|
118
120
|
gamma: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional())
|
|
@@ -245,7 +247,7 @@ export function buildFloopParams(args) {
|
|
|
245
247
|
return { params, paramSummary };
|
|
246
248
|
}
|
|
247
249
|
export function buildTrainMapParams(args, presets) {
|
|
248
|
-
const { preset, grid_x, grid_y, epochs, model, periodic, columns, cyclic_features, cyclic_pairs, temporal_features, feature_weights, transforms, auto_log_transforms, time_delay_embeddings, categorical_features, normalize, normalization_methods, sigma_f, learning_rate, batch_size, quality_metrics, backend, output_format, output_dpi, colormap, row_range, siom_feature_geometry, siom_qe_backend, siom_qe_batch_size, } = args;
|
|
250
|
+
const { preset, grid_x, grid_y, epochs, model, periodic, columns, cyclic_features, cyclic_pairs, temporal_features, feature_weights, transforms, auto_log_transforms, time_delay_embeddings, categorical_features, normalize, normalization_methods, sigma_f, learning_rate, batch_size, quality_metrics, backend, output_format, output_dpi, colormap, row_range, siom_feature_geometry, siom_qe_backend, siom_qe_batch_size, viz_upload_cyclic_components, } = args;
|
|
249
251
|
const p = preset ? presets[preset] : undefined;
|
|
250
252
|
const params = {
|
|
251
253
|
model: model ?? "SOM",
|
|
@@ -308,6 +310,9 @@ export function buildTrainMapParams(args, presets) {
|
|
|
308
310
|
params.siom_qe_backend = siom_qe_backend;
|
|
309
311
|
if (siom_qe_batch_size !== undefined)
|
|
310
312
|
params.siom_qe_batch_size = siom_qe_batch_size;
|
|
313
|
+
if (viz_upload_cyclic_components !== undefined) {
|
|
314
|
+
params.viz_upload_cyclic_components = viz_upload_cyclic_components;
|
|
315
|
+
}
|
|
311
316
|
const effectiveGrid = params.grid;
|
|
312
317
|
const effectiveEpochs = params.epochs;
|
|
313
318
|
const effectiveBatch = params.batch_size;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
|
|
3
3
|
import { runMcpToolAudit } from "../audit.js";
|
|
4
|
-
import { apiCall,
|
|
4
|
+
import { apiCall, BARSOM_VIZ_VIEWS, buildStandaloneVizPageUrl, buildVizRelatedUrls, getVizPort, structuredTextResult, } from "../shared.js";
|
|
5
|
+
import { appendUiDeliveryContent, beginInlineFallback, resolveUiDelivery, tryAttachLearningCurve, } from "../ui-delivery.js";
|
|
5
6
|
import { lastEpochTeFromCurves } from "../training_monitor_curve.js";
|
|
7
|
+
import { buildVizStandaloneUrl, resultsExplorerStandaloneLinkText, } from "../viz_links.js";
|
|
6
8
|
export const TRAINING_MONITOR_URI = "ui://barsom/training-monitor";
|
|
7
9
|
export const TRAINING_MONITOR_REFRESH_MS = 5000;
|
|
8
10
|
function hasCurveArrays(data) {
|
|
@@ -14,8 +16,12 @@ function hasCurveArrays(data) {
|
|
|
14
16
|
];
|
|
15
17
|
return keys.some((k) => Array.isArray(data[k]) && data[k].length > 0);
|
|
16
18
|
}
|
|
17
|
-
function buildStructuredContent(job_id, data) {
|
|
19
|
+
function buildStructuredContent(job_id, data, port = getVizPort()) {
|
|
18
20
|
const id = String(data.id ?? job_id);
|
|
21
|
+
const standaloneUrl = buildStandaloneVizPageUrl(port, BARSOM_VIZ_VIEWS.trainingMonitor, id) ??
|
|
22
|
+
buildVizStandaloneUrl("training-monitor", id);
|
|
23
|
+
const resultsExplorerUrl = buildStandaloneVizPageUrl(port, BARSOM_VIZ_VIEWS.resultsExplorer, id) ??
|
|
24
|
+
buildVizStandaloneUrl("results-explorer", id);
|
|
19
25
|
return {
|
|
20
26
|
...data,
|
|
21
27
|
type: "training-monitor",
|
|
@@ -23,6 +29,9 @@ function buildStructuredContent(job_id, data) {
|
|
|
23
29
|
id,
|
|
24
30
|
job_id: id,
|
|
25
31
|
refresh_interval_ms: TRAINING_MONITOR_REFRESH_MS,
|
|
32
|
+
standaloneUrl,
|
|
33
|
+
resultsExplorerUrl,
|
|
34
|
+
relatedUrls: buildVizRelatedUrls(port, id, BARSOM_VIZ_VIEWS),
|
|
26
35
|
};
|
|
27
36
|
}
|
|
28
37
|
function hasTeCurveArrays(data) {
|
|
@@ -81,6 +90,71 @@ async function enrichWithTrainingLog(job_id, data) {
|
|
|
81
90
|
return data;
|
|
82
91
|
}
|
|
83
92
|
}
|
|
93
|
+
export async function buildTrainingMonitorResult(job_id, opts) {
|
|
94
|
+
const { fetch_training_log, preamble } = opts ?? {};
|
|
95
|
+
let data = (await apiCall("GET", `/v1/jobs/${job_id}`));
|
|
96
|
+
const jobStatus = String(data.status ?? "");
|
|
97
|
+
if (fetch_training_log || needsTrainingLogEnrichment(data)) {
|
|
98
|
+
data = await enrichWithTrainingLog(job_id, data);
|
|
99
|
+
}
|
|
100
|
+
const port = getVizPort();
|
|
101
|
+
const structuredContent = buildStructuredContent(job_id, data, port);
|
|
102
|
+
const status = jobStatus;
|
|
103
|
+
const progress = (data.progress ?? 0) * 100;
|
|
104
|
+
const etaSec = data.training_eta_sec != null ? Number(data.training_eta_sec) : null;
|
|
105
|
+
const elapsedSec = data.training_elapsed_sec != null ? Number(data.training_elapsed_sec) : null;
|
|
106
|
+
const epoch = data.epoch != null ? Number(data.epoch) : null;
|
|
107
|
+
const totalEpochs = data.total_epochs != null ? Number(data.total_epochs) : null;
|
|
108
|
+
const timingParts = [];
|
|
109
|
+
if (elapsedSec != null && elapsedSec >= 0)
|
|
110
|
+
timingParts.push(`elapsed ${Math.round(elapsedSec)}s`);
|
|
111
|
+
if (etaSec != null && etaSec > 0)
|
|
112
|
+
timingParts.push(`ETA ~${Math.round(etaSec)}s`);
|
|
113
|
+
if (epoch != null && totalEpochs != null)
|
|
114
|
+
timingParts.push(`epoch ${epoch}/${totalEpochs}`);
|
|
115
|
+
const timingNote = timingParts.length > 0 ? ` ${timingParts.join(", ")}.` : "";
|
|
116
|
+
const text = (preamble ? `${preamble}\n\n` : "") +
|
|
117
|
+
`Training monitor (live when embedded, refreshes every ${TRAINING_MONITOR_REFRESH_MS / 1000}s): job ${job_id} — ${status} (${progress.toFixed(1)}%).${timingNote} ` +
|
|
118
|
+
`Optional UI; jobs(action=status) is enough to finish the workflow headless.`;
|
|
119
|
+
const content = [{ type: "text", text }];
|
|
120
|
+
const terminal = status === "completed" || status === "failed" || status === "cancelled";
|
|
121
|
+
let inlineImageAttached = false;
|
|
122
|
+
if (terminal) {
|
|
123
|
+
inlineImageAttached = await tryAttachLearningCurve(content, job_id);
|
|
124
|
+
}
|
|
125
|
+
const uiDelivery = resolveUiDelivery({
|
|
126
|
+
tool: "training_monitor",
|
|
127
|
+
jobId: job_id,
|
|
128
|
+
jobStatus: status,
|
|
129
|
+
inlineImageAttached,
|
|
130
|
+
});
|
|
131
|
+
appendUiDeliveryContent(content, uiDelivery, {
|
|
132
|
+
linkLabel: "Open training monitor",
|
|
133
|
+
primaryUrl: structuredContent.standaloneUrl,
|
|
134
|
+
});
|
|
135
|
+
if (status === "completed" && uiDelivery.urls?.length) {
|
|
136
|
+
const resultsUrl = uiDelivery.urls.find((u) => u.includes("results-explorer"));
|
|
137
|
+
if (resultsUrl) {
|
|
138
|
+
content.push({
|
|
139
|
+
type: "text",
|
|
140
|
+
text: `Job completed — browse map figures: [Open results explorer](${resultsUrl})`,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
structuredContent.ui_delivery = uiDelivery;
|
|
145
|
+
return { structuredContent, content, text };
|
|
146
|
+
}
|
|
147
|
+
/** After terminal + finalize, append results explorer open hint (monitor completion path). */
|
|
148
|
+
export function appendResultsExplorerCompletionHint(content, job_id) {
|
|
149
|
+
const link = resultsExplorerStandaloneLinkText(job_id);
|
|
150
|
+
if (link) {
|
|
151
|
+
content.push({
|
|
152
|
+
type: "text",
|
|
153
|
+
text: `${link}\n` +
|
|
154
|
+
`AGENT: when the job is fully complete (finalize done if any), call results_explorer(job_id="${job_id}") in the same turn and surface this link — do not wait for the user to ask.`,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
84
158
|
export function registerTrainingMonitorTool(server) {
|
|
85
159
|
registerAppTool(server, "training_monitor", {
|
|
86
160
|
title: "Training Monitor",
|
|
@@ -94,42 +168,11 @@ export function registerTrainingMonitorTool(server) {
|
|
|
94
168
|
},
|
|
95
169
|
_meta: { ui: { resourceUri: TRAINING_MONITOR_URI } },
|
|
96
170
|
}, async (args) => runMcpToolAudit("training_monitor", "default", args, async () => {
|
|
171
|
+
beginInlineFallback();
|
|
97
172
|
const { job_id, fetch_training_log } = args;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
data = await enrichWithTrainingLog(job_id, data);
|
|
102
|
-
}
|
|
103
|
-
const structuredContent = buildStructuredContent(job_id, data);
|
|
104
|
-
const status = jobStatus;
|
|
105
|
-
const progress = (data.progress ?? 0) * 100;
|
|
106
|
-
const etaSec = data.training_eta_sec != null ? Number(data.training_eta_sec) : null;
|
|
107
|
-
const elapsedSec = data.training_elapsed_sec != null ? Number(data.training_elapsed_sec) : null;
|
|
108
|
-
const epoch = data.epoch != null ? Number(data.epoch) : null;
|
|
109
|
-
const totalEpochs = data.total_epochs != null ? Number(data.total_epochs) : null;
|
|
110
|
-
const timingParts = [];
|
|
111
|
-
if (elapsedSec != null && elapsedSec >= 0)
|
|
112
|
-
timingParts.push(`elapsed ${Math.round(elapsedSec)}s`);
|
|
113
|
-
if (etaSec != null && etaSec > 0)
|
|
114
|
-
timingParts.push(`ETA ~${Math.round(etaSec)}s`);
|
|
115
|
-
if (epoch != null && totalEpochs != null)
|
|
116
|
-
timingParts.push(`epoch ${epoch}/${totalEpochs}`);
|
|
117
|
-
const timingNote = timingParts.length > 0 ? ` ${timingParts.join(", ")}.` : "";
|
|
118
|
-
const text = `Training monitor (live when embedded, refreshes every ${TRAINING_MONITOR_REFRESH_MS / 1000}s): job ${job_id} — ${status} (${progress.toFixed(1)}%).${timingNote} ` +
|
|
119
|
-
`Optional UI; jobs(action=status) is enough to finish the workflow.`;
|
|
120
|
-
const content = [{ type: "text", text }];
|
|
121
|
-
const port = getVizPort();
|
|
122
|
-
const standaloneUrl = port
|
|
123
|
-
? `http://localhost:${port}/viz/training-monitor?mode=standalone&job_id=${encodeURIComponent(job_id)}`
|
|
124
|
-
: undefined;
|
|
125
|
-
if (!getClientSupportsMcpApps() && standaloneUrl) {
|
|
126
|
-
content.push({
|
|
127
|
-
type: "text",
|
|
128
|
-
text: `Live training monitor: [Open training monitor](${standaloneUrl})\n` +
|
|
129
|
-
`AGENT: surface this to the user as a clickable markdown link (e.g. "[Open training monitor](${standaloneUrl})") in your reply — do not leave it buried in tool output. Re-post the link after the job completes so the user can return to the curves.\n` +
|
|
130
|
-
`This localhost port is assigned per MCP session and changes if the proxy restarts — if the page stays at "running 0%" or stops updating, the link is stale: re-run training_monitor for a fresh URL, or set BARIVIA_VIZ_PORT in your MCP env for a persistent port. Health check: http://localhost:${port}/api/health?job_id=${encodeURIComponent(job_id)}`,
|
|
131
|
-
});
|
|
132
|
-
}
|
|
173
|
+
const { structuredContent, content, text } = await buildTrainingMonitorResult(job_id, {
|
|
174
|
+
fetch_training_log,
|
|
175
|
+
});
|
|
133
176
|
return {
|
|
134
177
|
...structuredTextResult(structuredContent, text, content),
|
|
135
178
|
_meta: { ui: { resourceUri: TRAINING_MONITOR_URI } },
|
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { buildVizStandaloneUrl } from "./viz_links.js";
|
|
2
2
|
export function buildTrainSubmitExtras(opts) {
|
|
3
3
|
const { newJobId, totalRows, hasTransforms, prepareJobId, variantPrefix, paramSummary, impute, resultsHint } = opts;
|
|
4
|
-
const monitorUrl =
|
|
5
|
-
? `http://localhost:${getVizPort()}/viz/training-monitor?mode=standalone&job_id=${encodeURIComponent(newJobId)}`
|
|
6
|
-
: null;
|
|
4
|
+
const monitorUrl = buildVizStandaloneUrl("training-monitor", newJobId);
|
|
7
5
|
let msg = `Job submitted (${variantPrefix}, ${paramSummary}). `;
|
|
8
6
|
if (prepareJobId) {
|
|
9
7
|
msg += `Preprocessing job prepare_training_matrix (${prepareJobId}) runs on worker-io first; train job ${newJobId} starts after it completes. Poll jobs(action=status, job_id="${prepareJobId}") until completed, then poll the train job. `;
|
|
10
8
|
}
|
|
11
|
-
msg += `
|
|
9
|
+
msg += `Training monitor opened for job ${newJobId}`;
|
|
12
10
|
if (monitorUrl)
|
|
13
|
-
msg += `
|
|
14
|
-
msg +=
|
|
11
|
+
msg += ` — standalone: ${monitorUrl}`;
|
|
12
|
+
msg += `. Poll jobs(action=status, job_id="${newJobId}") every 60–120s for large jobs if needed. `;
|
|
15
13
|
msg += `When status is completed, check jobs(action=status) for finalize_job_id — if present, poll that finalize_training job before results(action=get). `;
|
|
16
14
|
msg += `Then use results(action=get, job_id="${newJobId}")`;
|
|
17
15
|
if (resultsHint) {
|