@barivia/barsom-mcp 0.7.13 → 0.8.0
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 +59 -11
- package/dist/index.js +1 -1
- package/dist/prepare_training_prompt.js +16 -0
- package/dist/shared.js +58 -5
- package/dist/tools/datasets.js +86 -5
- package/dist/tools/explore_map.js +2 -2
- package/dist/tools/guide_barsom.js +2 -2
- package/dist/tools/inference.js +88 -42
- package/dist/tools/jobs.js +118 -21
- package/dist/tools/results.js +65 -17
- package/dist/tools/training_guidance.js +1 -1
- package/dist/tools/training_prep.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# @barivia/barsom-mcp
|
|
2
2
|
|
|
3
|
-
MCP proxy for the Barivia Analytics Engine — connects any stdio MCP client (Cursor, Claude Desktop, etc.) to the barSOM cloud API. **`guide_barsom_workflow`** is the canonical bootstrap: it
|
|
3
|
+
MCP proxy for the Barivia Analytics Engine — connects any stdio MCP client (Cursor, Claude Desktop, etc.) to the barSOM cloud API. The npm package is **`@barivia/barsom-mcp`**; many configs label the server **`analytics-engine`** (the MCP server name in the client JSON). **`guide_barsom_workflow`** is the canonical bootstrap: it loads plan-scoped workflow text from the Barivia API when online (tool map, async rules, training modes, SOP). If the API is unreachable, it returns a short offline stub. Core tools are `datasets`, `jobs` (`train_map`, `train_siom_map`, `train_floop_siom` where entitled; `train_floop_chain` is deprecated), `results`, and `inference`. **Optional MCP App tools** (`training_prep`, `training_monitor`, `results_explorer`) add embedded or localhost viz; they are **not required** to complete upload → train → poll → results.
|
|
4
|
+
|
|
5
|
+
**Pre-training help (pick one):** **`prepare_training`** prompt = narrative checklist (tier-scoped from the API when online); **`training_guidance`** tool = structured presets and parameter hints; **`training_prep`** tool = interactive UI plus **`submit_prepared_training`**.
|
|
4
6
|
|
|
5
7
|
## Installation
|
|
6
8
|
|
|
@@ -27,6 +29,8 @@ MCP clients typically run it with **`npx`** (downloads on first use):
|
|
|
27
29
|
|
|
28
30
|
**Also available:** hosted HTTP MCP at **`https://mcp.barivia.se/mcp`** (same API key; no npm) for clients that support remote MCP.
|
|
29
31
|
|
|
32
|
+
**Cursor / multi-account note:** Cursor's tool dispatcher resolves tools under a `serverIdentifier` (e.g. `user-barivia-bbb-maps`), which can differ from the key you wrote in `mcp.json` (e.g. `barivia-bbb-maps`). If a tool call fails with **"server does not exist"** or similar, list the resolved identifier with your client's tool inspector (`mcp.list-tools` or equivalent) and use that identifier — not the `mcp.json` key.
|
|
33
|
+
|
|
30
34
|
**Future:** A **private npm org** scoped package is an option later (paid npm teams/orgs; users authenticate with npm, usually simpler than GitHub Packages for pure npm consumers).
|
|
31
35
|
|
|
32
36
|
## Environment Variables
|
|
@@ -38,19 +42,43 @@ MCP clients typically run it with **`npx`** (downloads on first use):
|
|
|
38
42
|
| `BARIVIA_WORKSPACE_ROOT` | No | `process.cwd()` or `PWD` | Directory for relative `file_path` and `save_to_disk`. In Cursor MCP, `process.cwd()` is often the MCP install dir — add `BARIVIA_WORKSPACE_ROOT` to your MCP config `env` with your project path (e.g. `/home/user/myproject`). Absolute paths and `file://` URIs work without it. |
|
|
39
43
|
| `BARIVIA_FETCH_TIMEOUT_MS` | No | `30000` | Per-request HTTP timeout (ms) to the API. Increase (e.g. `120000`) on slow networks or when the API does long-running work. Large dataset uploads use a separate longer timeout internally. |
|
|
40
44
|
| `BARIVIA_VIZ_PORT` | No | OS-assigned | When the client has no MCP Apps support, the proxy may start a local viz server on `127.0.0.1`; set a fixed port if you need stable bookmark URLs. |
|
|
45
|
+
| `BARIVIA_ENFORCE_WORKSPACE_SANDBOX` | No | `1` (enabled) | File uploads are constrained to the MCP workspace root by default. Set to `0` or `false` to allow absolute paths anywhere on the machine (high trust). **Changed in v0.x:** previously defaulted to off; now on for security. If uploads fail with "paths outside the workspace are disabled", either set `BARIVIA_WORKSPACE_ROOT` to cover your data directory, or opt out with `BARIVIA_ENFORCE_WORKSPACE_SANDBOX=0`. |
|
|
41
46
|
|
|
42
47
|
Legacy `BARSOM_API_KEY` / `BARSOM_API_URL` / `BARSOM_WORKSPACE_ROOT` are also accepted as fallbacks.
|
|
43
48
|
|
|
49
|
+
### Trust model (who are we trusting?)
|
|
50
|
+
|
|
51
|
+
- **API key** — Anyone with your key can access your tenant on the Barivia API like any other HTTP client. The proxy does not add a separate authorization layer.
|
|
52
|
+
- **Local machine** — The process runs with **your** OS user. File uploads are now sandboxed to the workspace root by default (`BARIVIA_ENFORCE_WORKSPACE_SANDBOX=1`). Set `BARIVIA_WORKSPACE_ROOT` to your project directory; use `BARIVIA_ENFORCE_WORKSPACE_SANDBOX=0` only if you need unrestricted absolute-path access.
|
|
53
|
+
- **Logs** — stderr may include API paths and tool activity; do not log secrets in MCP client configs.
|
|
54
|
+
|
|
44
55
|
**Supported stack:** Node **18+** (see `engines` in `package.json`). Built with `@modelcontextprotocol/sdk` **^1.x** — if a major MCP client upgrade breaks tools, check SDK release notes alongside this package version.
|
|
45
56
|
|
|
46
57
|
**Local viz fallback:** If the IDE does not advertise MCP Apps, the proxy starts an HTTP server bound to **127.0.0.1** only (not exposed to the LAN). It serves built-in HTML for training prep / monitor / results explorer and proxies read-only API calls with your existing API key. Responses may include `http://127.0.0.1:<port>/viz/...` links. Browser `Access-Control-Allow-Origin: *` applies only to that localhost origin so the embedded pages can load data.
|
|
47
58
|
|
|
48
|
-
## Tools (
|
|
59
|
+
## Tools and prompts (15 tools + 2 prompts)
|
|
60
|
+
|
|
61
|
+
All multi-action tools follow the `datasets` / `jobs` / `results` / `inference` / `account` pattern: a required `action` enum routes to the correct operation.
|
|
62
|
+
|
|
63
|
+
**Prompts (not tools):** `info` (short orientation; prefer `guide_barsom_workflow` for depth), `prepare_training` (requires `dataset_id`; checklist text from API when online).
|
|
64
|
+
|
|
65
|
+
### Flattened catalog (optional grouping in UIs)
|
|
49
66
|
|
|
50
|
-
|
|
67
|
+
| Section | Items |
|
|
68
|
+
|--------|--------|
|
|
69
|
+
| Bootstrap | `guide_barsom_workflow`, prompts `info`, `prepare_training` |
|
|
70
|
+
| Data | `datasets` |
|
|
71
|
+
| Train & poll | `jobs`, `submit_prepared_training` (after `training_prep`), `training_monitor` (optional) |
|
|
72
|
+
| Results | `results`, `results_explorer` (optional) |
|
|
73
|
+
| Inference | `inference` |
|
|
74
|
+
| Account | `account` |
|
|
75
|
+
| Optional UI | `training_prep`, `training_monitor`, `results_explorer` |
|
|
76
|
+
| Advanced / legacy | `training_guidance`, `explore_map` (deprecated alias) |
|
|
77
|
+
|
|
78
|
+
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.
|
|
51
79
|
|
|
52
80
|
### `guide_barsom_workflow`
|
|
53
|
-
Call at the **start of mapping work** (or when the user asks what the MCP can do).
|
|
81
|
+
Call at the **start of mapping work** (or when the user asks what the MCP can do). Returns plan-scoped orientation from the Barivia API when online. No parameters. **`training_guidance`** returns JSON presets and hints with the same entitlement filtering.
|
|
54
82
|
|
|
55
83
|
### `datasets(action)`
|
|
56
84
|
|
|
@@ -62,6 +90,7 @@ Call at the **start of mapping work** (or when the user asks what the MCP can do
|
|
|
62
90
|
| `list` | Finding dataset IDs |
|
|
63
91
|
| `subset` | Creating a filtered/sliced copy (row_range, filter conditions) |
|
|
64
92
|
| `add_expression` | Add a derived column from an expression (formula → new column on the dataset) |
|
|
93
|
+
| `reduce_spectral` | Pre-training reducer for long ordered numeric blocks (spectra, time series, sensor fingerprints, gene panels). Methods: **pca** (top-k principal components), **log_sample** (k columns at log-spaced indices — scattering, audio bands), **uniform_sample** (k columns at evenly-spaced indices — regularly-sampled time series), **stats** (6 fixed per-row summary columns). All produce one feature vector per row; appends derived columns to the dataset. |
|
|
65
94
|
| `delete` | Removing a dataset |
|
|
66
95
|
|
|
67
96
|
### `jobs(action)`
|
|
@@ -97,11 +126,10 @@ All actions use a frozen trained map — no retraining. Derived columns use **`d
|
|
|
97
126
|
|
|
98
127
|
| Action | Output | Timing |
|
|
99
128
|
|--------|--------|--------|
|
|
100
|
-
| `predict` | predictions.csv
|
|
101
|
-
| `enrich` | enriched.csv: training data + bmu_x/y/node_index/cluster_id | 5–60s |
|
|
129
|
+
| `predict` | Score rows against the trained map. **Inputs:** `dataset_id` (defaults to the parent training dataset) **or** inline `rows` (≤500). **Output style** (`output` param): `"compact"` → `predictions.csv` (row_id, bmu_x/y, bmu_node_index, cluster_id [+ QE / qe_p95 / potential_anomaly when scoring **new** data]); `"annotated"` → `enriched.csv` (original CSV + BMU columns appended). **Regime auto-detected:** when the resolved dataset matches the training dataset, QE columns are intentionally omitted in compact output (training-set fit ≠ generalisation; the p95 anomaly flag would be circular). Prefer `dataset_id` for batches and SIOM/irregular maps. | 5–120s |
|
|
102
130
|
| `compare` | density-diff heatmap + top gained/lost nodes — drift, A/B, cohort | 30–120s |
|
|
103
131
|
| `project_columns` | Project one or more dataset columns onto the trained map (component planes) | async |
|
|
104
|
-
| `report` |
|
|
132
|
+
| `report` | Report **manifest** (figure names, download URLs, metrics, cluster summary) — sync; use with `results(download)` on the training `job_id` for `report.pdf` when present; build custom PDFs in Quarto/Jupyter | immediate |
|
|
105
133
|
|
|
106
134
|
### `account(action)`
|
|
107
135
|
|
|
@@ -118,14 +146,31 @@ All actions use a frozen trained map — no retraining. Derived columns use **`d
|
|
|
118
146
|
|
|
119
147
|
| Tool | Role |
|
|
120
148
|
|------|------|
|
|
121
|
-
| `training_prep` | Review variables, transforms, hyperparameters before submit
|
|
149
|
+
| `training_prep` | Review variables, transforms, hyperparameters before submit |
|
|
150
|
+
| `submit_prepared_training` | Submit the reviewed prep (`review_token` + `explicit_confirm=true`) |
|
|
122
151
|
| `training_monitor` | Visual progress for a `job_id`; optional — `jobs(action=status)` is enough |
|
|
123
152
|
| `results_explorer` | Browse metrics and figures after training completes |
|
|
124
|
-
| `explore_map` |
|
|
125
|
-
| `_fetch_figure` |
|
|
153
|
+
| `explore_map` | **Deprecated** — alias of `results_explorer`; migrate configs to `results_explorer` |
|
|
154
|
+
| `_fetch_figure` | **Host / App only** — Results Explorer invokes this for one raster figure; not for agent chat |
|
|
126
155
|
|
|
127
156
|
When the client does not advertise MCP Apps support, the proxy starts **`viz-server`** on **`127.0.0.1`** (localhost-only). Tool responses can include `http://127.0.0.1:PORT/viz/...` links. See **Local viz fallback** under Environment Variables.
|
|
128
157
|
|
|
158
|
+
#### Choosing where to view results
|
|
159
|
+
|
|
160
|
+
The right viewer depends on **(MCP App support)** **and** **(can the human reach the proxy's `127.0.0.1`)** — not on whether the agent is "text" or "GUI".
|
|
161
|
+
|
|
162
|
+
| Path | When to use | Caveats |
|
|
163
|
+
|------|-------------|---------|
|
|
164
|
+
| **Embedded MCP App** (`ui://barsom/results-explorer`) | Client supports MCP Apps (Cursor, Claude Desktop with App support). Preferred default. | None of the topology issues below — transported over the MCP channel. Works for local proxy, remote SSH proxy, or container. |
|
|
165
|
+
| **Localhost `viz-server` link** | MCP Apps unsupported AND the human user is on the same host as the proxy (typical local Cursor + local stdio proxy). | Ephemeral port (lost on proxy restart). **Unreachable** when the proxy runs on a remote SSH host while the user is elsewhere, in a container with no port forwarding, or behind a strict host firewall. Pure text agents on the same host CAN use this path — the user just clicks the link. |
|
|
166
|
+
| **`results(action=get)` text-only path** | Always works. Use when (a) the agent is headless / autonomous (no human to click anything) or (b) proxy and user are on different hosts and MCP Apps is unsupported. With **`figures="none"`** it's also the leanest path for parameter sweeps and LLM clients (no image payloads). | Returns metrics text and inline images via MCP content; never starts a server. |
|
|
167
|
+
|
|
168
|
+
### Migration notes
|
|
169
|
+
|
|
170
|
+
- **`explore_map` → `results_explorer`:** Update Cursor, Claude Desktop, or other MCP configs that still reference `explore_map`. The alias remains for backward compatibility.
|
|
171
|
+
- **`inference(action=enrich)` → `inference(action=predict, output="annotated")`:** the `enrich` action has been removed in favor of regime-aware `predict`. Calling `predict` with `output="annotated"` (and the default training dataset) returns the same `enriched.csv` artifact. Calling `predict` on the training dataset with the default `output="compact"` now correctly omits QE / `qe_p95` / `potential_anomaly` fields — those are fitting errors on training data, not generalisation metrics.
|
|
172
|
+
- **Shorter `info` prompt:** Clients that relied on the old long `info` text should use **`guide_barsom_workflow`** or server **instructions** for the full story.
|
|
173
|
+
|
|
129
174
|
### `send_feedback`
|
|
130
175
|
Submit feedback or feature requests (max 1400 characters, ~190 words).
|
|
131
176
|
|
|
@@ -168,8 +213,11 @@ cd apps/mcp-proxy
|
|
|
168
213
|
npm install
|
|
169
214
|
npm run dev # Run with tsx (hot reload)
|
|
170
215
|
npm run build # Compile to dist/
|
|
216
|
+
npm test # Vitest
|
|
171
217
|
```
|
|
172
218
|
|
|
219
|
+
**Post-change spot-check (real MCP client):** confirm `guide_barsom_workflow` appears near the top of the tool list, `explore_map` still works as an alias of `results_explorer`, and Results Explorer can still load figures (it uses `_fetch_figure` internally — agents should rely on `results` / `results_explorer`).
|
|
220
|
+
|
|
173
221
|
For local development against a local API stack:
|
|
174
222
|
|
|
175
223
|
```bash
|
|
@@ -199,7 +247,7 @@ From the platform root:
|
|
|
199
247
|
|
|
200
248
|
```bash
|
|
201
249
|
cd barivia-platform
|
|
202
|
-
bash scripts/check-mcp-proxy-publish.sh
|
|
250
|
+
bash scripts/checks/check-mcp-proxy-publish.sh
|
|
203
251
|
```
|
|
204
252
|
|
|
205
253
|
Compares local `build:publish` output to the tarball on **public npm**. Exit 0 = match; exit 1 = diff. `VERBOSE=1` for a full `dist/` diff.
|
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 a,registerAppResource as i,RESOURCE_MIME_TYPE as r}from"@modelcontextprotocol/ext-apps/server";import{startVizServer as n}from"./viz-server.js";import{API_KEY as s,apiCall as l,apiRawCall as c,loadViewHtml as p,setVizPort as d,setClientSupportsMcpApps as m}from"./shared.js";import{registerDatasetsTool as u}from"./tools/datasets.js";import{registerJobsTool as f,JOBS_DESCRIPTION_BASE as g}from"./tools/jobs.js";import{registerResultsTool as h}from"./tools/results.js";import{registerExploreMapTool as _,RESULTS_EXPLORER_URI as b}from"./tools/explore_map.js";import{registerAccountTool as y}from"./tools/account.js";import{registerInferenceTool as w}from"./tools/inference.js";import{registerGuideBarsomTool as j}from"./tools/guide_barsom.js";import{registerTrainingGuidanceTool as v}from"./tools/training_guidance.js";import{registerFeedbackTool as x}from"./tools/feedback.js";import{registerTrainingPrepTools as I,TRAINING_PREP_URI as k}from"./tools/training_prep.js";import{registerTrainingMonitorTool as M,TRAINING_MONITOR_URI as P}from"./tools/training_monitor.js";s||(console.error("Error: BARIVIA_API_KEY not set. Set it in your MCP client config."),process.exit(1));const O=new e({name:"analytics-engine",version:"0.7.10",instructions:"# Barivia Mapping Analytics Engine\n\nYou have access to a mapping (Self-Organizing Map) analytics platform that projects high-dimensional data onto a 2D grid, revealing clusters, gradients, and anomalies.\n\n## Typical workflow\n\n1. **Upload** → `datasets(action=upload)` — ingest a CSV\n2. **Preview** → `datasets(action=preview)` — inspect columns, detect cyclics/datetimes; add derived columns with `datasets(action=add_expression)` if needed\n3. **Train** → choose one:\n - `jobs(action=train_map, dataset_id=...)` for a standard fixed-grid SOM\n - `jobs(action=train_siom_map, dataset_id=...)` for a fixed-grid SIOM with coverage regularization\n - `jobs(action=train_floop_siom, dataset_id=...)` for FLooP-SIOM when the plan includes all_algorithms (Premium or Enterprise; default topology=free / CHL; optional topology=chain)\n Returns a job_id; poll `jobs(action=status, job_id=...)` until completed\n4. **Analyze** → `results(action=get)` — view metrics and figures (grid-map or FLooP-specific; no separate analyze tool)\n5. **Compare / Export / Inference** → `jobs(action=compare)`, `results(download)`, `inference` (predict/enrich/compare/report)\n\nFor **proxy model, full tool map, async rules, training modes (tier-scoped), and optional MCP App UIs**, call `guide_barsom_workflow` first—it loads the ground-truth bootstrap from the API (`GET /v1/docs/workflow`; this section stays short on purpose).\n\n## Tool categories\n\n| Category | Tools |\n|----------|-------|\n| Data management | `datasets` (upload/preview/list/subset/delete/add_expression) |\n| Jobs & training | `jobs` (train_map/train_siom_map/train_floop_siom/status/list/compare/cancel/delete/batch_predict; deprecated alias train_floop_chain) |\n| Results | `results` (get/recolor/download/export/transition_flow) |\n| Inference & export | `inference` (predict/enrich/compare/report/project_columns) |\n| Account | `account` (status/request_compute/compute_status/release_compute/history/add_funds) |\n| Utility | `guide_barsom_workflow`, `training_guidance`, `training_prep`, `results_explorer`, `training_monitor`, `send_feedback` |\n\n## Optional rich UI (MCP Apps)\n\n**None of these are required** to train, poll, or read results—`jobs` + `results` + text output are enough. **Recommended** for learning: `training_prep` (preparation), `results_explorer` (postprocessing). **Optional:** `training_monitor(job_id)` for visual progress; `jobs(action=status)` is sufficient without it.\n\n## Async job pattern\n\nMost operations are async. Every tool that submits a job either:\n- **Auto-polls** (results(recolor/transition_flow), inference) — waits up to the action-specific timeout then returns or gives a job_id for manual polling\n- **Returns immediately** (jobs(action=train_map/train_siom_map/train_floop_siom)) — always requires manual `jobs(action=status, job_id=...)` polling (`train_floop_chain` is a deprecated alias for `train_floop_siom`)\n\n**Do not tell the user a job failed because it is still running.** If a tool returns a job_id, poll `jobs(action=status)` every 10–15 seconds. Grid-map training takes 30s–10min depending on grid size and dataset; FLooP-SIOM jobs can also take several minutes for larger `max_nodes` or `thorough` runs.\nFor FLooP-SIOM, `max_nodes` is a total node budget, not a grid side length. If omitted, the backend uses a dataset-size heuristic; reduce `max_nodes` before increasing `effort` when coverage looks collapsed.\n\n## Credit and cost\n\nJobs consume compute credits. Inference jobs are priced the same as projection jobs. Check `account(action=status)` to see the remaining balance and queue depth before starting large jobs.\n\n## Key constraints\n\n- For parameter guidance (grid, epochs, presets, when to use standard SOM vs SIOM vs FLooP-SIOM), use `training_guidance` or `prepare_training` — do not guess.\n- Column names are case-sensitive; always match exactly what `datasets(action=preview)` returns\n- Uploads may include text/categorical columns, but the default training path is still numeric/cyclic/temporal. If a categorical column truly matters, use explicit `categorical_features` for simple baseline encoding.\n- `predict` input must match the model's supported inference contract. Numeric/batch predict expects the training feature set; single-row stateless inference can also accept raw categorical strings for baseline `categorical_features` models."});i(O,b,b,{mimeType:r},async()=>{const e=await p("results-explorer");return{contents:[{uri:b,mimeType:r,text:e??"<html><body>Results Explorer view not built yet. Run: npm run build:views</body></html>"}]}}),i(O,k,k,{mimeType:r},async()=>{const e=await p("training-prep");return{contents:[{uri:k,mimeType:r,text:e??"<html><body>Training Preparation view not built yet.</body></html>"}]}}),i(O,P,P,{mimeType:r},async()=>{const e=await p("training-monitor");return{contents:[{uri:P,mimeType:r,text:e??"<html><body>Training Monitor view not built yet.</body></html>"}]}}),_(O),I(O),M(O),j(O),u(O),f(O,g),h(O),y(O),w(O),v(O),x(O),O.prompt("info","Overview of the Barivia Mapping MCP: capabilities, workflow, tools, analysis types, and tips. Use when the user asks what this MCP can do, how to get started, or what the process is.",{},()=>({messages:[{role:"user",content:{type:"text",text:["Inform the user using this overview:","","**What it is:** Barivia MCP connects you to a mapping analytics engine that learns a 2D map from high-dimensional data for visualization, clustering, pattern discovery, and temporal analysis.","","**Orientation:** Call `guide_barsom_workflow` first for proxy model, full tool list, async rules, training modes, and optional MCP App UIs (defer long-form detail there).","","**Core workflow:**","0. **Prepare** — CSV with header; no NaNs in used columns; numeric and datetime columns are the default training path; raw categoricals can stay in the CSV but should usually be excluded unless you explicitly use simple `categorical_features` encoding","1. **Upload** — `datasets(action=upload)` with a CSV file path or inline data","2. **Preview** — `datasets(action=preview)` to inspect columns, stats, and detect cyclic/datetime fields","3. **Prepare** — use the `prepare_training` prompt for a guided checklist (column selection, transforms, cyclic encoding, feature weights, grid sizing)","4. **Train** — choose one: `jobs(action=train_map, dataset_id=...)` for a standard fixed-grid SOM, `jobs(action=train_siom_map, dataset_id=...)` for a fixed-grid SIOM with coverage regularization, or `jobs(action=train_floop_siom, dataset_id=...)` for FLooP-SIOM only on Premium / Enterprise (`all_algorithms`; default topology=free / CHL; optional topology=chain). Use `preset=quick|standard|refined|high_res` for standard or SIOM map defaults","5. **Monitor** — `jobs(action=status, job_id=...)` every 10–15s until complete (required path). Optional: `training_monitor(job_id=...)` for a visual panel—not required.","6. **View and interpret** — `results(action=get)` returns metrics and figures. Grid-map jobs show U-matrix, component planes, and clusters; FLooP-SIOM jobs show structure/coverage, occupation/profile views, and metrics. Optional: `results_explorer(job_id=...)` for interactive browsing; `results(action=get)` alone is enough.","7. **Feedback** — Ask the user if they'd like to submit feedback via `send_feedback` based on their experience.","8. **Iterate** — `results(action=recolor)` to change colormap, `jobs(action=compare)` to compare hyperparameters, `inference(action=project_columns)` to overlay dataset columns onto the map","","All visualizations and metrics come from `results(action=get)`; use figures=all or export_type=... for more.","","**Data tools:**","- `datasets(action=subset)` — filter by row range, value thresholds (gt/lt/gte/lte), equality, or set membership. Combine row_range + filter","- `datasets(action=add_expression)` — create computed columns from expressions (ratios, differences, etc.)","- `inference(action=project_columns)` — overlay one or more dataset columns onto a trained map (component planes)","","**Output options:** Format (png/pdf/svg) and colormap (coolwarm, viridis, plasma, inferno, hsv, twilight, etc.) can be set at training or changed later via results(action=recolor).","","**Key tools:** datasets, jobs (train_map/train_siom_map/train_floop_siom/status/list/...), results, inference (predict/enrich/compare/project_columns/report), account, guide_barsom_workflow, training_guidance, training_prep, results_explorer, training_monitor.","","**Optional rich UI:** `training_prep` and `results_explorer` are recommended for prep and postprocessing; `training_monitor` is optional. None are required—`jobs` + `results` complete the workflow.","","**Tips:**","- For the full step-by-step SOP and capabilities text (from the API, scoped to your plan), use the `guide_barsom_workflow` tool","- Always `datasets(action=preview)` before training to understand your data","- Use `account(action=status)` to check GPU availability, queue depth, and plan limits before large jobs","- Start with `preset=quick` for fast iteration, then `refined` for publication quality","- For time-series data, consider `transition_flow` after training","","Keep the reply scannable with headers and bullet points."].join("\n")}}]})),O.prompt("prepare_training","Guided pre-training checklist. Use after uploading a dataset and before calling jobs(action=train_map), jobs(action=train_siom_map), or jobs(action=train_floop_siom) when entitled. Walks through data inspection, column selection, transforms, cyclic/temporal features, weighting, subsetting, and grid sizing.",{dataset_id:o.string().describe("Dataset ID to prepare for training")},async({dataset_id:e})=>{let t=`Please run datasets(action=preview, dataset_id="${e}") to inspect columns, then datasets(action=analyze, dataset_id="${e}") to see which columns and temporal periods are most informative. Then choose the training path: jobs(action=train_map, dataset_id="${e}", ...) for a standard fixed-grid SOM, jobs(action=train_siom_map, dataset_id="${e}", ...) for a fixed-grid SIOM, or jobs(action=train_floop_siom, dataset_id="${e}", ...) for FLooP-SIOM (default topology=free / CHL; optional topology=chain).`;try{const o=await l("GET",`/v1/docs/prepare_training?dataset_id=${e}`);o.prompt&&(t=o.prompt)}catch(e){}return{messages:[{role:"user",content:{type:"text",text:t}}]}});const C=new t;(async function(){try{const e=await n(l,c,p);d(e)}catch(e){process.env.BARIVIA_VIZ_PORT&&console.error("Barivia viz server failed to start:",e)}const e=O.server;e.oninitialized=()=>{const t=e.getClientCapabilities(),o=a(t);m(!!o?.mimeTypes?.includes(r))},await O.connect(C)})().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 n,RESOURCE_MIME_TYPE as a}from"@modelcontextprotocol/ext-apps/server";import{startVizServer as s}from"./viz-server.js";import{API_KEY as i,apiCall as l,apiRawCall as p,loadViewHtml as c,setVizPort as m,setClientSupportsMcpApps as d}from"./shared.js";import{registerDatasetsTool as u}from"./tools/datasets.js";import{registerJobsTool as f,JOBS_DESCRIPTION_BASE as g}from"./tools/jobs.js";import{registerResultsTool as _}from"./tools/results.js";import{registerExploreMapTool as h,RESULTS_EXPLORER_URI as b}from"./tools/explore_map.js";import{registerAccountTool as w}from"./tools/account.js";import{registerInferenceTool as y}from"./tools/inference.js";import{registerGuideBarsomTool as j}from"./tools/guide_barsom.js";import{registerTrainingGuidanceTool as v}from"./tools/training_guidance.js";import{registerFeedbackTool as P}from"./tools/feedback.js";import{registerTrainingPrepTools as x,TRAINING_PREP_URI as I}from"./tools/training_prep.js";import{registerTrainingMonitorTool as k,TRAINING_MONITOR_URI as M}from"./tools/training_monitor.js";import{resolvePrepareTrainingPromptText as O}from"./prepare_training_prompt.js";i||(console.error("Error: BARIVIA_API_KEY not set. Set it in your MCP client config."),process.exit(1));const S=new e({name:"analytics-engine",version:"0.8.0",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 `jobs(train_map)`, `jobs(train_siom_map)`, or `jobs(train_floop_siom)` (only if plan allows FLooP) → poll `jobs(status)` every 10–15s until `completed` → `results(get)` for metrics and figures (there is no separate analyze tool). Then `jobs(compare)`, `results(download/recolor/transition_flow)`, or `inference` as needed.\n\n**Full detail:** Call `guide_barsom_workflow` for plan-scoped tool map, training modes, async rules, optional MCP App UIs, and step-by-step SOP (from the Barivia API when online).\n\n## Tool map (compact)\n\n| Area | Tool | Notes |\n|------|------|--------|\n| Data | `datasets` | upload, preview, analyze, list, subset, add_expression, reduce_spectral (pca/log_sample/uniform_sample/stats for long ordered numeric blocks), delete |\n| Jobs | `jobs` | train_map, train_siom_map, train_floop_siom (entitled), status, list, compare, cancel, delete, batch_predict, run_baseline_study; `train_floop_chain` = deprecated alias for train_floop_siom |\n| Results | `results` | get (figures="none" for metrics-only), export, download, recolor (async), transition_flow (async; time-ordered rows only) |\n| Inference | `inference` | predict (regime-aware; output="compact"|"annotated"; "annotated" replaces the removed `enrich`), compare, project_columns, report |\n| Account | `account` | status, burst/compute actions, history, add_funds |\n| Bootstrap | `guide_barsom_workflow` | orientation + SOP |\n| Parameters | `training_guidance` | presets and field hints (API-scoped) |\n| Prep | `prepare_training` prompt, `training_prep` + `submit_prepared_training` | checklist / interactive UI |\n| Explore | `results_explorer`, `training_monitor` | optional MCP Apps; `explore_map` = deprecated alias of `results_explorer`; `jobs(status)` and `results(get)` suffice without them |\n| Other | `send_feedback` | only after user agrees |\n\n## Async pattern\n\n- **Manual poll:** Training submits return `job_id` immediately — poll `jobs(status)` every 10–15s. **Running is not failed**; large grids or FLooP-SIOM can take many minutes. `max_nodes` (FLooP) is a total node budget, not grid side length.\n- **Often auto-polled:** `inference` actions, `results(recolor)`, `results(transition_flow)` may wait in-proxy; if you get a `job_id`, poll `jobs(status)` the same way.\n\nCredits: jobs consume compute credits; check `account(status)` before big runs. Slow networks: users can raise `BARIVIA_FETCH_TIMEOUT_MS`.\n\n## Constraints\n\n- Prep ladder: `prepare_training` prompt = narrative checklist; `training_guidance` = structured hints; `training_prep` = UI + guarded submit. Do not guess tiers or FLooP entitlement.\n- `inference(predict)`: prefer `dataset_id` for batch and for SIOM/irregular maps; single-row `rows` uses a fast path that can fail on some topologies — retry with `dataset_id`. FLooP-SIOM: if predict jobs fail while grid SIOM works, capture errors + `job_id`.\n- Column names are case-sensitive — match `datasets(preview)`.\n- Default training path is numeric/cyclic/temporal; use explicit `categorical_features` for baseline categoricals. `predict` must match the model contract.\n- After `recolor`, `transition_flow`, or `project_columns`, use the **new** `job_id` returned for follow-up `results` if applicable.'});n(S,b,b,{mimeType:a},async()=>{const e=await c("results-explorer");return{contents:[{uri:b,mimeType:a,text:e??"<html><body>Results Explorer view not built yet. Run: npm run build:views</body></html>"}]}}),n(S,I,I,{mimeType:a},async()=>{const e=await c("training-prep");return{contents:[{uri:I,mimeType:a,text:e??"<html><body>Training Preparation view not built yet.</body></html>"}]}}),n(S,M,M,{mimeType:a},async()=>{const e=await c("training-monitor");return{contents:[{uri:M,mimeType:a,text:e??"<html><body>Training Monitor view not built yet.</body></html>"}]}}),j(S),h(S),x(S),k(S),u(S),f(S,g),_(S),w(S),y(S),v(S),P(S),S.prompt("info","Short orientation for the Barivia Mapping MCP. For full plan-scoped workflow, tool map, and SOP, the model should call guide_barsom_workflow. Use when the user asks what this MCP can do or how to get started.",{},()=>({messages:[{role:"user",content:{type:"text",text:["Give a concise, scannable answer (headers + bullets):","","**What it is:** MCP client to the Barivia mapping engine (2D SOM / SIOM / FLooP-SIOM when entitled) over HTTPS.","","**First step:** Call `guide_barsom_workflow` for plan-scoped bootstrap (full tool list, async rules, training modes, optional MCP Apps, SOP).","","**Core path:** `datasets(upload)` → `datasets(preview)` + `datasets(analyze)` → choose training action → poll `jobs(status)` every 10–15s until completed → `results(get)` (all main figures/metrics; no separate analyze tool).","",'**Key tools:** `datasets` (data; reduce_spectral for spectra/long blocks), `jobs` (train/poll/compare/…; train_map accepts an optional `label` for readable compare rows), `results` (get/download/export/recolor/transition_flow; figures="none" for metrics-only), `inference` (predict regime-aware with output="compact"|"annotated"; replaces enrich, compare, project_columns, report), `account` (status/credits/queue).',"","**Prep help:** `prepare_training` prompt (checklist) · `training_guidance` (presets/JSON hints) · `training_prep` + `submit_prepared_training` (interactive UI).","","**Optional UI:** `results_explorer`, `training_monitor` — nice for browsing; not required if you use `results` + `jobs(status)`.","","**After training:** `jobs(compare)` across runs, `results(recolor)`, `inference(project_columns)` for variables not in training, `transition_flow` only if rows are time-ordered.","","**Rules:** Running ≠ failed. Column names must match `datasets(preview)` exactly. Do not call `_fetch_figure` from chat (host/UI only); use `results(get)` or `results_explorer`.","","Offer `send_feedback` only after asking the user."].join("\n")}}]})),S.prompt("prepare_training","Narrative pre-training checklist (prompt). Use after upload and before train. Content is tier-scoped from the API when online. Prep ladder: this prompt = story checklist; training_guidance tool = JSON presets/parameter hints; training_prep tool = interactive UI + submit_prepared_training.",{dataset_id:o.string().describe("Dataset ID to prepare for training")},async({dataset_id:e})=>({messages:[{role:"user",content:{type:"text",text:await O(e)}}]}));const A=new t;(async function(){try{const e=await s(l,p,c);m(e)}catch(e){process.env.BARIVIA_VIZ_PORT&&console.error("Barivia viz server failed to start:",e)}const e=S.server;e.oninitialized=()=>{const t=e.getClientCapabilities(),o=r(t);d(!!o?.mimeTypes?.includes(a))},await S.connect(A)})().catch(console.error);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { apiCall } from "./shared.js";
|
|
2
|
+
const FALLBACK_PREFIX = "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. Then choose the training path: jobs(action=train_map, dataset_id=\"{id}\", ...) for a standard fixed-grid SOM, jobs(action=train_siom_map, dataset_id=\"{id}\", ...) for a fixed-grid SIOM, or jobs(action=train_floop_siom, dataset_id=\"{id}\", ...) for FLooP-SIOM (default topology=free / CHL; optional topology=chain).";
|
|
3
|
+
/** Used by the `prepare_training` MCP prompt; prefers tier-scoped text from the API when online. */
|
|
4
|
+
export async function resolvePrepareTrainingPromptText(datasetId) {
|
|
5
|
+
let promptText = FALLBACK_PREFIX.replaceAll("{id}", datasetId);
|
|
6
|
+
try {
|
|
7
|
+
const data = (await apiCall("GET", `/v1/docs/prepare_training?dataset_id=${datasetId}`));
|
|
8
|
+
if (typeof data.prompt === "string" && data.prompt.trim()) {
|
|
9
|
+
promptText = data.prompt.trim();
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
// fallback
|
|
14
|
+
}
|
|
15
|
+
return promptText;
|
|
16
|
+
}
|
package/dist/shared.js
CHANGED
|
@@ -104,10 +104,17 @@ export function sandboxPath(userPath, root) {
|
|
|
104
104
|
}
|
|
105
105
|
return resolved;
|
|
106
106
|
}
|
|
107
|
+
function enforceWorkspaceSandboxUpload() {
|
|
108
|
+
const v = process.env.BARIVIA_ENFORCE_WORKSPACE_SANDBOX ?? "1";
|
|
109
|
+
if (v === "0" || v.toLowerCase() === "false")
|
|
110
|
+
return false;
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
107
113
|
/**
|
|
108
114
|
* Resolves file_path for dataset upload. Security: auth before read, generic errors, reject "..".
|
|
109
115
|
* Accepts: absolute paths, file:// URIs, relative paths (resolved against workspace root).
|
|
110
116
|
* Pre-security-patch behavior: absolute paths worked without BARIVIA_WORKSPACE_ROOT.
|
|
117
|
+
* Set BARIVIA_ENFORCE_WORKSPACE_SANDBOX=1 to require absolute paths to lie under the resolved workspace root.
|
|
111
118
|
*/
|
|
112
119
|
export async function resolveFilePathForUpload(filePath, mcpServer) {
|
|
113
120
|
const trimmed = filePath.trim();
|
|
@@ -120,15 +127,52 @@ export async function resolveFilePathForUpload(filePath, mcpServer) {
|
|
|
120
127
|
if (url.protocol !== "file:" || (url.hostname && url.hostname !== "localhost")) {
|
|
121
128
|
throw new Error("Only local file:// URIs are allowed (no remote hosts).");
|
|
122
129
|
}
|
|
123
|
-
|
|
130
|
+
const p = fileURLToPath(trimmed);
|
|
131
|
+
if (enforceWorkspaceSandboxUpload()) {
|
|
132
|
+
const root = await getWorkspaceRootAsync(mcpServer);
|
|
133
|
+
const real = await fs.realpath(p);
|
|
134
|
+
const realRoot = await fs.realpath(root);
|
|
135
|
+
if (real !== realRoot && !real.startsWith(realRoot + path.sep)) {
|
|
136
|
+
throw new Error(`file:// paths outside the workspace are disabled (BARIVIA_ENFORCE_WORKSPACE_SANDBOX). Workspace: ${realRoot}`);
|
|
137
|
+
}
|
|
138
|
+
const stat = await fs.stat(real);
|
|
139
|
+
if (!stat.isFile()) {
|
|
140
|
+
throw new Error("Path must be a regular file, not a directory.");
|
|
141
|
+
}
|
|
142
|
+
return real;
|
|
143
|
+
}
|
|
144
|
+
return p;
|
|
124
145
|
}
|
|
125
146
|
catch (err) {
|
|
126
147
|
if (err instanceof Error && err.message.includes("remote hosts"))
|
|
127
148
|
throw err;
|
|
149
|
+
if (err instanceof Error && err.message.includes("BARIVIA_ENFORCE_WORKSPACE_SANDBOX"))
|
|
150
|
+
throw err;
|
|
151
|
+
if (err instanceof Error && err.message.includes("regular file"))
|
|
152
|
+
throw err;
|
|
128
153
|
throw new Error("Invalid file:// URI. Use a path like file:///path/to/your/file.csv");
|
|
129
154
|
}
|
|
130
155
|
}
|
|
131
156
|
if (path.isAbsolute(trimmed)) {
|
|
157
|
+
if (enforceWorkspaceSandboxUpload()) {
|
|
158
|
+
const root = await getWorkspaceRootAsync(mcpServer);
|
|
159
|
+
let real;
|
|
160
|
+
try {
|
|
161
|
+
real = await fs.realpath(trimmed);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
throw new Error(`File not accessible at absolute path. With BARIVIA_ENFORCE_WORKSPACE_SANDBOX, use a path under the workspace root (${root}).`);
|
|
165
|
+
}
|
|
166
|
+
const realRoot = await fs.realpath(root);
|
|
167
|
+
if (real !== realRoot && !real.startsWith(realRoot + path.sep)) {
|
|
168
|
+
throw new Error(`Absolute paths outside the workspace are disabled (BARIVIA_ENFORCE_WORKSPACE_SANDBOX). Workspace root: ${realRoot}`);
|
|
169
|
+
}
|
|
170
|
+
const stat = await fs.stat(real);
|
|
171
|
+
if (!stat.isFile()) {
|
|
172
|
+
throw new Error("Path must be a regular file, not a directory.");
|
|
173
|
+
}
|
|
174
|
+
return real;
|
|
175
|
+
}
|
|
132
176
|
return trimmed;
|
|
133
177
|
}
|
|
134
178
|
const root = await getWorkspaceRootAsync(mcpServer);
|
|
@@ -183,10 +227,16 @@ export function formatApiErrorMessage(status, bodyText, requestId) {
|
|
|
183
227
|
: status === 409
|
|
184
228
|
? " The job may not be in the expected state."
|
|
185
229
|
: status === 429
|
|
186
|
-
? "
|
|
187
|
-
: status
|
|
188
|
-
? "
|
|
189
|
-
:
|
|
230
|
+
? " Plan limit (e.g. dataset cap) or rate limit — read the error above; delete unused datasets or wait and retry."
|
|
231
|
+
: status === 502
|
|
232
|
+
? " Object storage (R2/S3) error from API — retry later."
|
|
233
|
+
: status === 503
|
|
234
|
+
? parsed?.error_code === "system_info_unavailable"
|
|
235
|
+
? " System info (plan/queue) temporarily unavailable — retry later."
|
|
236
|
+
: " API or database temporarily unavailable — retry later."
|
|
237
|
+
: status >= 500
|
|
238
|
+
? " Server error — retry later."
|
|
239
|
+
: "";
|
|
190
240
|
const rid = ` (request id: ${requestId} — include if contacting support)`;
|
|
191
241
|
return `${detail}${code}${hint}${rid}`;
|
|
192
242
|
}
|
|
@@ -460,6 +510,9 @@ export async function tryAttachImage(content, jobId, filename) {
|
|
|
460
510
|
}
|
|
461
511
|
/** Resolve get_results figures param to list of image filenames to fetch. */
|
|
462
512
|
export function getResultsImagesToFetch(jobType, summary, figures, includeIndividual) {
|
|
513
|
+
// Metrics-only mode: skip every image fetch (sweeps and LLM clients).
|
|
514
|
+
if (figures === "none")
|
|
515
|
+
return [];
|
|
463
516
|
const ext = summary.output_format ?? "pdf";
|
|
464
517
|
if (jobType === "transition_flow") {
|
|
465
518
|
const lag = summary.lag ?? 1;
|
package/dist/tools/datasets.js
CHANGED
|
@@ -13,6 +13,7 @@ export function registerDatasetsTool(server) {
|
|
|
13
13
|
| list | Finding dataset IDs for train_map, preview, or subset — see all available datasets |
|
|
14
14
|
| subset | Creating a filtered/sliced view without re-uploading the full CSV |
|
|
15
15
|
| add_expression | Add a computed column from an expression (same as project(expression) without project_onto_job) — dataset_id + name + expression |
|
|
16
|
+
| 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. |
|
|
16
17
|
| delete | Cleaning up after experiments or freeing the dataset slot |
|
|
17
18
|
|
|
18
19
|
action=upload: PREFER file_path — server reads from workspace root (token-efficient; no file content in context). Use csv_data only for small inline pastes (e.g. <10KB). Returns dataset ID. Then use datasets(action=preview) before jobs(action=train_map).
|
|
@@ -30,14 +31,20 @@ action=subset: Create a new dataset from a subset of an existing one. Requires n
|
|
|
30
31
|
Examples: { column: "region", op: "eq", value: "Europe" } | { column: "age", op: "between", value: [18, 65] }
|
|
31
32
|
- Combine row_range + filters to slice both rows and values.
|
|
32
33
|
- Single filter object is also accepted (auto-wrapped).
|
|
34
|
+
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:
|
|
35
|
+
- pca: top-k principal components — general first try when many columns are correlated (spectroscopy, gene panels, sensor arrays). Returns explained_variance_ratio.
|
|
36
|
+
- log_sample: keep k columns at log-spaced indices — SAXS/scattering, audio frequency bands, attenuation curves (anywhere the index axis is logarithmically informative).
|
|
37
|
+
- uniform_sample: keep k columns at evenly-spaced indices — regularly-sampled time series, frame-by-frame features, evenly-binned histograms.
|
|
38
|
+
- stats: 6 fixed per-row statistics (mean, std, min, max, skew, integral) — cheap baseline for any sequenced numeric block; k is ignored.
|
|
39
|
+
Required params: name (prefix for derived columns), method, columns_block (ordered source column names ≥ 2), k (≥ 1, < length(columns_block); ignored for stats).
|
|
33
40
|
action=delete: Remove a dataset and all S3 data permanently.
|
|
34
41
|
|
|
35
42
|
BEST FOR: Tabular numeric data. CSV with header required.
|
|
36
43
|
NOT FOR: Real-time data streams or binary files — upload a snapshot CSV instead.
|
|
37
44
|
ESCALATION: If upload fails with column errors, open the file locally and verify the header row. If preview shows unexpected nulls, the user must clean the CSV before training.`, {
|
|
38
45
|
action: z
|
|
39
|
-
.enum(["upload", "preview", "analyze", "list", "subset", "delete", "add_expression"])
|
|
40
|
-
.describe("upload: add CSV; preview: inspect columns/stats; analyze: pre-training correlation and periodicity; list: see all datasets; subset: create filtered subset; delete: remove dataset; add_expression: add derived column from expression"),
|
|
46
|
+
.enum(["upload", "preview", "analyze", "list", "subset", "delete", "add_expression", "reduce_spectral"])
|
|
47
|
+
.describe("upload: add CSV; preview: inspect columns/stats; analyze: pre-training correlation and periodicity; list: see all datasets; subset: create filtered subset; delete: remove dataset; add_expression: add derived column from expression; reduce_spectral: collapse a long ordered numeric block (e.g. spectrum, time series) into a small per-row feature set via PCA / log_sample / uniform_sample / stats"),
|
|
41
48
|
name: z.string().optional().describe("Dataset name (required for action=upload and subset)"),
|
|
42
49
|
file_path: z.string().optional().describe("Path to local CSV (PREFERRED): absolute path, file:// URI, or relative to workspace root. Token-efficient; server reads file."),
|
|
43
50
|
csv_data: z.string().optional().describe("Inline CSV string for small pastes only (<10KB). Avoid for large files — use file_path instead to avoid token explosion."),
|
|
@@ -81,7 +88,21 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
81
88
|
})
|
|
82
89
|
.optional()
|
|
83
90
|
.describe("action=add_expression: evaluation options (missing, window for rolling)"),
|
|
84
|
-
|
|
91
|
+
method: z
|
|
92
|
+
.enum(["pca", "log_sample", "uniform_sample", "stats"])
|
|
93
|
+
.optional()
|
|
94
|
+
.describe("action=reduce_spectral: pca = top-k principal components (general first try); log_sample = k columns at log-spaced indices (scattering, audio bands, attenuation); uniform_sample = k columns at evenly-spaced indices (regularly-sampled time series); stats = 6 fixed per-row summary columns (mean, std, min, max, skew, integral; k ignored)"),
|
|
95
|
+
columns_block: z
|
|
96
|
+
.array(z.string())
|
|
97
|
+
.optional()
|
|
98
|
+
.describe("action=reduce_spectral: ordered list of source column names representing the block (≥ 2; ≥ k+1 for non-stats methods)"),
|
|
99
|
+
k: z
|
|
100
|
+
.number()
|
|
101
|
+
.int()
|
|
102
|
+
.min(1)
|
|
103
|
+
.optional()
|
|
104
|
+
.describe("action=reduce_spectral: output dimensionality. Required for pca/log_sample/uniform_sample; ignored for stats."),
|
|
105
|
+
}, async ({ action, name, file_path, csv_data, dataset_id, n_rows, row_range, filters, filter, expression, options, method, columns_block, k }) => {
|
|
85
106
|
if (action === "upload") {
|
|
86
107
|
if (!name)
|
|
87
108
|
throw new Error("datasets(upload) requires name");
|
|
@@ -93,10 +114,17 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
93
114
|
if (ext !== ".csv" && ext !== ".tsv") {
|
|
94
115
|
throw new Error("Only .csv and .tsv files can be uploaded as datasets.");
|
|
95
116
|
}
|
|
117
|
+
const MAX_UPLOAD_BYTES = 100 * 1024 * 1024; // 100 MB
|
|
96
118
|
try {
|
|
119
|
+
const stat = await fs.stat(resolved);
|
|
120
|
+
if (stat.size > MAX_UPLOAD_BYTES) {
|
|
121
|
+
throw new Error(`File too large (${(stat.size / 1024 / 1024).toFixed(1)} MB). Maximum upload size is ${MAX_UPLOAD_BYTES / 1024 / 1024} MB.`);
|
|
122
|
+
}
|
|
97
123
|
body = await fs.readFile(resolved, "utf-8");
|
|
98
124
|
}
|
|
99
|
-
catch {
|
|
125
|
+
catch (err) {
|
|
126
|
+
if (err instanceof Error && err.message.includes("too large"))
|
|
127
|
+
throw err;
|
|
100
128
|
throw new Error(`File not accessible at resolved path. file_path is relative to workspace root. ` +
|
|
101
129
|
`Set BARIVIA_WORKSPACE_ROOT in your MCP config env if needed (current: ${await getWorkspaceRootAsync(server)}).`);
|
|
102
130
|
}
|
|
@@ -265,7 +293,8 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
265
293
|
const periodicStrong = periodicity.filter((p) => p.dominant_score > 0.4);
|
|
266
294
|
if (periodicStrong.length > 0) {
|
|
267
295
|
const lags = [...new Set(periodicStrong.map((p) => p.dominant_lag))].sort((a, b) => a - b);
|
|
268
|
-
nextSteps.push(`
|
|
296
|
+
nextSteps.push(`Note: periodicity is computed on row order. If your CSV is not sorted by time, ignore lag-based suggestions; only use cyclic_features when the column is genuinely cyclic (e.g. hour, month, angle).`);
|
|
297
|
+
nextSteps.push(`Columns show periodic behavior at lags ${lags.join(", ")} (assuming row order is meaningful). Consider cyclic_features or temporal_features with matching periods.`);
|
|
269
298
|
nextSteps.push(`For datetime columns: run datasets(action=preview) for temporal_suggestions; match lags (e.g. 365→day_of_year, 12→month, 7→day_of_week, 24→hour_of_day) to choose which temporal components to extract.`);
|
|
270
299
|
}
|
|
271
300
|
nextSteps.push(`Then call jobs(action=train_map, dataset_id=${dataset_id}, columns=[...], ...).`);
|
|
@@ -311,6 +340,58 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
311
340
|
content: [{ type: "text", text: `datasets(add_expression) job ${deriveJobId} submitted. Poll with jobs(action=status, job_id="${deriveJobId}").` }],
|
|
312
341
|
};
|
|
313
342
|
}
|
|
343
|
+
if (action === "reduce_spectral") {
|
|
344
|
+
if (!dataset_id)
|
|
345
|
+
throw new Error("datasets(reduce_spectral) requires dataset_id");
|
|
346
|
+
if (!name)
|
|
347
|
+
throw new Error("datasets(reduce_spectral) requires name (prefix for derived columns)");
|
|
348
|
+
if (!method)
|
|
349
|
+
throw new Error("datasets(reduce_spectral) requires method: pca | log_sample | uniform_sample | stats");
|
|
350
|
+
if (!columns_block || columns_block.length < 2) {
|
|
351
|
+
throw new Error("datasets(reduce_spectral) requires columns_block: ordered array of ≥ 2 source column names");
|
|
352
|
+
}
|
|
353
|
+
if (method !== "stats" && (k === undefined || k < 1)) {
|
|
354
|
+
throw new Error(`datasets(reduce_spectral) requires k ≥ 1 for method '${method}'`);
|
|
355
|
+
}
|
|
356
|
+
if (method !== "stats" && k !== undefined && k >= columns_block.length) {
|
|
357
|
+
throw new Error(`datasets(reduce_spectral) requires k < length(columns_block); got k=${k}, columns_block=${columns_block.length}`);
|
|
358
|
+
}
|
|
359
|
+
const body = { name, method, columns: columns_block };
|
|
360
|
+
if (method !== "stats")
|
|
361
|
+
body.k = k;
|
|
362
|
+
const data = (await apiCall("POST", `/v1/datasets/${dataset_id}/reduce_spectral`, body));
|
|
363
|
+
const reduceJobId = data.id;
|
|
364
|
+
const poll = await pollUntilComplete(reduceJobId, 120_000);
|
|
365
|
+
if (poll.status === "completed") {
|
|
366
|
+
const results = (await apiCall("GET", `/v1/results/${reduceJobId}`));
|
|
367
|
+
const summary = (results.summary ?? {});
|
|
368
|
+
const outCols = summary.output_columns ?? [];
|
|
369
|
+
const sourceCols = summary.source_columns ?? [];
|
|
370
|
+
const lines = [
|
|
371
|
+
`Spectral reduction complete (${method}) — job: ${reduceJobId}`,
|
|
372
|
+
`Source: ${sourceCols.length} columns → Output: ${outCols.length} columns appended to dataset ${dataset_id}`,
|
|
373
|
+
`New columns: ${outCols.join(", ")}`,
|
|
374
|
+
];
|
|
375
|
+
if (method === "pca") {
|
|
376
|
+
const ev = summary.explained_variance_ratio;
|
|
377
|
+
const evTotal = summary.explained_variance_total;
|
|
378
|
+
if (ev)
|
|
379
|
+
lines.push(`Explained variance per component: [${ev.map((v) => v.toFixed(3)).join(", ")}] | total: ${evTotal !== undefined ? evTotal.toFixed(3) : "N/A"}`);
|
|
380
|
+
}
|
|
381
|
+
else if (method === "log_sample" || method === "uniform_sample") {
|
|
382
|
+
const idxs = summary.selected_indices;
|
|
383
|
+
if (idxs) {
|
|
384
|
+
lines.push(`Selected source-column indices (1-based): [${idxs.join(", ")}]`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
lines.push(`Use datasets(action=preview, dataset_id=${dataset_id}) to inspect the new columns, then jobs(action=train_map, dataset_id=${dataset_id}, columns=[..., "${outCols[0] ?? ""}", …]) to train on the reduced features.`);
|
|
388
|
+
return { content: [{ type: "text", text: lines.filter(Boolean).join("\n") }] };
|
|
389
|
+
}
|
|
390
|
+
if (poll.status === "failed") {
|
|
391
|
+
return { content: [{ type: "text", text: `datasets(reduce_spectral) job ${reduceJobId} failed: ${poll.error ?? "unknown error"}` }] };
|
|
392
|
+
}
|
|
393
|
+
return { content: [{ type: "text", text: `datasets(reduce_spectral) job ${reduceJobId} submitted. Poll with jobs(action=status, job_id="${reduceJobId}").` }] };
|
|
394
|
+
}
|
|
314
395
|
if (action === "subset") {
|
|
315
396
|
if (!dataset_id)
|
|
316
397
|
throw new Error("datasets(subset) requires dataset_id");
|
|
@@ -168,9 +168,9 @@ export function registerExploreMapTool(server) {
|
|
|
168
168
|
registerAppTool(server, "explore_map", {
|
|
169
169
|
...toolConfig,
|
|
170
170
|
title: "Results Explorer",
|
|
171
|
-
description: `${toolConfig.description}
|
|
171
|
+
description: `${toolConfig.description} Deprecated alias for results_explorer — migrate configs to results_explorer.`,
|
|
172
172
|
}, async ({ job_id }) => handleResultsExplorer(job_id));
|
|
173
|
-
server.tool("_fetch_figure", "
|
|
173
|
+
server.tool("_fetch_figure", "Host / MCP App use only — do NOT invoke from agent chat. Results Explorer calls this to load one raster figure as base64; agents should use results(action=get) or results_explorer instead.", {
|
|
174
174
|
job_id: z.string(),
|
|
175
175
|
filename: z.string(),
|
|
176
176
|
}, async ({ job_id, filename }) => {
|
|
@@ -8,10 +8,10 @@ Configure \`BARIVIA_API_KEY\` and optional \`BARIVIA_API_URL\`, then call **guid
|
|
|
8
8
|
|
|
9
9
|
**Parameter hints:** call \`training_guidance\` (also API-scoped). **Async:** poll \`jobs(action=status)\` every 10–15s after submit.`;
|
|
10
10
|
export function registerGuideBarsomTool(server) {
|
|
11
|
-
server.tool("guide_barsom_workflow", "
|
|
11
|
+
server.tool("guide_barsom_workflow", "Plan-scoped orientation: proxy model, tool categories, async rules, training modes, and step-by-step SOP — loaded from the Barivia API when online. Call at the start of mapping work. Offline: short stub. For field-level parameters, use training_guidance; for a narrative pre-train checklist, use the prepare_training prompt or training_prep.", {}, async () => {
|
|
12
12
|
const md = await fetchWorkflowGuideFromApi();
|
|
13
13
|
if (md) {
|
|
14
|
-
const text = "
|
|
14
|
+
const text = "Plan-scoped workflow (from Barivia API). Text below reflects your API key / plan.\n\n" + md;
|
|
15
15
|
return { content: [{ type: "text", text }] };
|
|
16
16
|
}
|
|
17
17
|
return { content: [{ type: "text", text: OFFLINE_STUB }] };
|
package/dist/tools/inference.js
CHANGED
|
@@ -1,46 +1,89 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { apiCall, pollUntilComplete, tryAttachImage } from "../shared.js";
|
|
3
|
+
const PREDICT_PREVIEW_ROW_CAP = 10;
|
|
4
|
+
/** One line per scored row when worker embedded `predictions_preview` (n_rows ≤ cap). Exported for tests. */
|
|
5
|
+
export function formatPredictPreviewLines(summary) {
|
|
6
|
+
const nRows = Number(summary.n_rows ?? 0);
|
|
7
|
+
if (nRows < 1 || nRows > PREDICT_PREVIEW_ROW_CAP)
|
|
8
|
+
return [];
|
|
9
|
+
const preview = summary.predictions_preview;
|
|
10
|
+
if (!Array.isArray(preview) || preview.length === 0)
|
|
11
|
+
return [];
|
|
12
|
+
const lines = ["Per-row summary (same columns as predictions.csv):"];
|
|
13
|
+
for (const raw of preview) {
|
|
14
|
+
const p = raw;
|
|
15
|
+
const rid = String(p.row_id ?? "?");
|
|
16
|
+
const bx = p.bmu_x !== undefined ? Number(p.bmu_x).toFixed(2) : "?";
|
|
17
|
+
const by = p.bmu_y !== undefined ? Number(p.bmu_y).toFixed(2) : "?";
|
|
18
|
+
const node = p.bmu_node_index ?? "?";
|
|
19
|
+
const cl = p.cluster_id ?? "?";
|
|
20
|
+
if (p.quantization_error !== undefined) {
|
|
21
|
+
const qe = Number(p.quantization_error).toFixed(4);
|
|
22
|
+
const anom = p.potential_anomaly === true ? "yes" : "no";
|
|
23
|
+
lines.push(` row ${rid}: BMU (${bx},${by}) node=${node} cluster=${cl} QE=${qe} anomaly=${anom}`);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
// regime=training: QE columns are intentionally omitted (training-set fit, not generalisation)
|
|
27
|
+
lines.push(` row ${rid}: BMU (${bx},${by}) node=${node} cluster=${cl}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return lines;
|
|
31
|
+
}
|
|
3
32
|
export function registerInferenceTool(server) {
|
|
4
|
-
server.tool("inference", `Use a trained map as a persistent inference artifact — score
|
|
33
|
+
server.tool("inference", `Use a trained map as a persistent inference artifact — score data, annotate the source CSV, compare datasets, project columns, or generate a report manifest.
|
|
5
34
|
|
|
6
35
|
| Action | Use when | Timing |
|
|
7
36
|
|--------|----------|--------|
|
|
8
|
-
| predict | Scoring
|
|
9
|
-
| enrich | Appending BMU coordinates + cluster_id to the original training CSV | 5–60s |
|
|
37
|
+
| predict | Scoring rows against the trained map (new data OR the training set itself) | 5–120s |
|
|
10
38
|
| compare | Comparing hit distributions of a second dataset against training (drift, A/B) | 30–120s |
|
|
11
39
|
| project_columns | Project one or more dataset columns onto the map (component planes); dataset can be training set or partial-feature set | 10–90s |
|
|
12
40
|
| report | Get a report manifest (artifact keys + URLs) to build your own report in Quarto/Notebook/script | Immediate (sync) |
|
|
13
41
|
|
|
14
|
-
Sync/async: predict
|
|
15
|
-
Artifacts: When complete, use results(action=download, job_id=<returned_job_id>) to get: predict → predictions.csv;
|
|
42
|
+
Sync/async: predict and compare are async jobs. The proxy auto-polls and usually returns when the job completes. If it returns a job_id instead (e.g. timeout), poll jobs(action=status, job_id=...) then results(action=download, job_id=...) to retrieve the artifact.
|
|
43
|
+
Artifacts: When complete, use results(action=download, job_id=<returned_job_id>) to get: predict (output="compact") → predictions.csv; predict (output="annotated") → enriched.csv; compare → density-diff figure (e.g. density_diff.png).
|
|
16
44
|
report is the only synchronous inference action — returns manifest immediately; no job to poll.
|
|
17
45
|
NOT FOR: Retraining or changing the map — all actions treat the trained map as frozen.
|
|
18
46
|
ESCALATION: If any action returns "missing column", verify column names with datasets(action=preview). Column names are case-sensitive and must match the training feature set exactly.
|
|
19
47
|
|
|
20
|
-
action=predict:
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
48
|
+
action=predict: Score rows against the trained map.
|
|
49
|
+
Inputs (one of):
|
|
50
|
+
- dataset_id (default = parent training job's dataset). Use a different dataset for new/unseen data; omit it (or pass the training dataset_id) to score the training set itself.
|
|
51
|
+
- rows (≤500 inline). Always treated as new data.
|
|
52
|
+
Output style (output param, default "compact"):
|
|
53
|
+
- "compact" → predictions.csv (row_id, bmu_x, bmu_y, bmu_node_index, cluster_id [, quantization_error, potential_anomaly]).
|
|
54
|
+
- "annotated" → enriched.csv (the full source CSV with bmu_x, bmu_y, bmu_node_index, cluster_id appended). Requires a dataset (no inline rows). Replaces the previous inference(action=enrich) — migrate by passing output="annotated".
|
|
55
|
+
Regime auto-detected:
|
|
56
|
+
- If the resolved dataset matches the parent training dataset, regime="training" and QE / qe_p95 / potential_anomaly fields are omitted from the compact output. QE on training data is fitting error, not a generalisation metric, and the p95 anomaly flag would be circular. Use a held-out dataset for quality assessment.
|
|
57
|
+
- Otherwise regime="new" and the full QE columns are returned.
|
|
58
|
+
Schema rules: dataset / batch rows must match the trained feature set including cyclic-expanded columns (e.g. key_cos, key_sin, not raw key). For a single inline row, the proxy uses the stateless model endpoint — raw categorical strings are allowed for baseline categorical_features models, and raw cyclic inputs are expanded from training config.
|
|
59
|
+
Routing: prefer dataset_id for many rows or whenever the map uses irregular SIOM / GeneralTopology layouts — the async worker path is the supported batch scorer. Single-row rows take a fast stateless path that may return invalid_inference_input on some topologies; if so, retry with dataset_id (a one-row dataset is fine). FLooP-SIOM: use dataset_id predict first.
|
|
60
|
+
When the scored set has at most ${PREDICT_PREVIEW_ROW_CAP} rows, completed responses include a short per-line preview in the tool text for chat agents.
|
|
61
|
+
|
|
62
|
+
action=compare: dataset_id must refer to a dataset with the same feature set as training (same column names and preprocessing, including cyclic expansion). A = training dataset; B = cohort to compare. Density-diff: positive = B gained vs A; negative = A had more. Returns density-diff heatmap (e.g. density_diff.png).
|
|
24
63
|
action=project_columns: Project one or more columns from a dataset onto the trained map. Pass dataset_id (the dataset containing the columns) and columns (array of column names). Uses cached BMUs when dataset is the training set; supports partial-feature mapping when dataset has only a subset of training features. Returns one component plane image per column. Get files via results(action=download, job_id=<returned_job_id>).
|
|
25
|
-
action=report: Returns a report manifest for the given job_id (job must be completed). Includes figure_manifest (logical names → filenames), download_urls for all artifacts, cluster_summary when available, and summary metrics. Stakeholder report PDF (if generated) is available via results(action=download, job_id=<training_job_id>), filename e.g. report.pdf
|
|
64
|
+
action=report: Returns a report manifest for the given job_id (job must be completed). Includes figure_manifest (logical names → filenames), download_urls for all artifacts, cluster_summary when available, and summary metrics. Stakeholder report PDF (if generated) is available via results(action=download, job_id=<training_job_id>), filename e.g. report.pdf.`, {
|
|
26
65
|
action: z
|
|
27
|
-
.enum(["predict", "
|
|
28
|
-
.describe("predict: score
|
|
66
|
+
.enum(["predict", "compare", "project_columns", "report"])
|
|
67
|
+
.describe("predict: score rows; compare: drift/cohort diff heatmap; project_columns: project dataset columns onto map; report: manifest of primitives for custom report. (Note: the previous 'enrich' action is now predict with output=\"annotated\".)"),
|
|
29
68
|
job_id: z.string().describe("Job ID of a completed map training job"),
|
|
30
|
-
dataset_id: z.string().optional().describe("action=predict/compare/project_columns: Dataset ID. predict=
|
|
69
|
+
dataset_id: z.string().optional().describe("action=predict/compare/project_columns: Dataset ID. predict=data to score (defaults to the training dataset when omitted); compare=dataset B; project_columns=dataset with columns to project."),
|
|
31
70
|
columns: z.array(z.string()).optional().describe("action=project_columns: column names to project onto the map (must exist in the dataset)."),
|
|
32
71
|
rows: z.array(z.record(z.string(), z.union([z.number(), z.string()]))).optional().describe("action=predict: inline rows to score (max 500). For a single inline row, raw categorical strings are allowed for baseline categorical_features models. Batch rows should remain numeric and match the training schema."),
|
|
72
|
+
output: z.enum(["compact", "annotated"]).optional().default("compact").describe("action=predict: output style. compact = predictions.csv (default); annotated = enriched.csv (original CSV + BMU columns). Use annotated to get the training set with BMU labels appended (the former inference(action=enrich) workflow)."),
|
|
33
73
|
colormap: z.string().optional().describe("action=compare: colormap for diff heatmap (default: balance). action=report: n/a."),
|
|
34
74
|
output_format: z.enum(["png", "pdf", "svg"]).optional().default("png").describe("action=compare: output format for heatmap (default: png)"),
|
|
35
75
|
output_dpi: z.enum(["standard", "retina", "print"]).optional().default("retina").describe("Resolution: standard (1x), retina (2x, default), print (4x)"),
|
|
36
76
|
top_n: z.number().int().min(1).max(50).optional().default(10).describe("action=compare: number of top gained/lost nodes to report (default: 10)"),
|
|
37
|
-
}, async ({ action, job_id, dataset_id, columns, rows, colormap, output_format, output_dpi, top_n }) => {
|
|
77
|
+
}, async ({ action, job_id, dataset_id, columns, rows, output, colormap, output_format, output_dpi, top_n }) => {
|
|
38
78
|
const dpiMap = { standard: 1, retina: 2, print: 4 };
|
|
39
79
|
const numericDpi = dpiMap[output_dpi ?? "retina"] ?? 2;
|
|
40
80
|
if (action === "predict") {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
81
|
+
const outputStyle = output ?? "compact";
|
|
82
|
+
if (outputStyle === "annotated" && rows) {
|
|
83
|
+
throw new Error("inference(predict, output=\"annotated\") requires a dataset (not inline rows). Either omit rows and pass dataset_id, or use output=\"compact\".");
|
|
84
|
+
}
|
|
85
|
+
// Stateless single-row fast path: only valid for compact output and inline rows.
|
|
86
|
+
if (!dataset_id && rows && rows.length === 1 && outputStyle === "compact") {
|
|
44
87
|
const row = rows[0];
|
|
45
88
|
const data = (await apiCall("POST", `/v1/models/${job_id}/project`, { features: row }));
|
|
46
89
|
return {
|
|
@@ -62,50 +105,53 @@ action=report: Returns a report manifest for the given job_id (job must be compl
|
|
|
62
105
|
throw new Error("Batch inline predict rows must remain numeric. For baseline categorical inference, submit a single inline row so the proxy can use the stateless model endpoint.");
|
|
63
106
|
}
|
|
64
107
|
}
|
|
65
|
-
const body = {};
|
|
108
|
+
const body = { output_style: outputStyle };
|
|
66
109
|
if (dataset_id)
|
|
67
110
|
body.dataset_id = dataset_id;
|
|
68
111
|
if (rows)
|
|
69
112
|
body.rows = rows;
|
|
70
113
|
const data = (await apiCall("POST", `/v1/results/${job_id}/predict`, body));
|
|
71
114
|
const predictJobId = data.id;
|
|
115
|
+
// annotated runs over the full dataset; allow up to 120s like compact
|
|
72
116
|
const poll = await pollUntilComplete(predictJobId, 120_000);
|
|
73
117
|
if (poll.status === "completed") {
|
|
74
118
|
const results = (await apiCall("GET", `/v1/results/${predictJobId}`));
|
|
75
119
|
const summary = (results.summary ?? {});
|
|
76
120
|
const urls = (results.download_urls ?? {});
|
|
121
|
+
const previewLines = formatPredictPreviewLines(summary);
|
|
122
|
+
const regime = String(summary.regime ?? "new");
|
|
123
|
+
const effectiveStyle = String(summary.output_style ?? outputStyle);
|
|
124
|
+
const isAnnotated = effectiveStyle === "annotated";
|
|
125
|
+
const artifactName = isAnnotated ? "enriched.csv" : "predictions.csv";
|
|
126
|
+
const headerLine = isAnnotated
|
|
127
|
+
? `Annotated dataset ready — job: ${predictJobId}`
|
|
128
|
+
: `Predictions complete — job: ${predictJobId}`;
|
|
129
|
+
const trainingCaveat = regime === "training"
|
|
130
|
+
? `Training-set BMU lookup. QE on training data is not a generalisation metric; the QE / qe_p95 / potential_anomaly columns are intentionally omitted. Use a held-out dataset (different dataset_id) for quality assessment.`
|
|
131
|
+
: "";
|
|
132
|
+
const metricsLine = (regime !== "training" && !isAnnotated)
|
|
133
|
+
? `Mean QE: ${summary.mean_qe !== undefined ? Number(summary.mean_qe).toFixed(4) : "N/A"} | Max QE: ${summary.max_qe !== undefined ? Number(summary.max_qe).toFixed(4) : "N/A"} | qe_p95: ${summary.qe_p95 !== undefined ? Number(summary.qe_p95).toFixed(4) : "N/A"}`
|
|
134
|
+
: "";
|
|
135
|
+
const outputLine = isAnnotated
|
|
136
|
+
? `Output: enriched.csv (original CSV + bmu_x, bmu_y, bmu_node_index, cluster_id appended). Clusters: ${summary.n_clusters ?? Object.keys(summary.cluster_counts ?? {}).length}.`
|
|
137
|
+
: (regime === "training"
|
|
138
|
+
? `Output: predictions.csv (row_id, bmu_x, bmu_y, bmu_node_index, cluster_id). Clusters: ${Object.keys(summary.cluster_counts ?? {}).length}.`
|
|
139
|
+
: `Output: predictions.csv (row_id, bmu_x, bmu_y, bmu_node_index, cluster_id, quantization_error, potential_anomaly). Summary includes mean_qe, max_qe, qe_p95. Clusters: ${Object.keys(summary.cluster_counts ?? {}).length}.`);
|
|
77
140
|
return { content: [{ type: "text", text: [
|
|
78
|
-
|
|
141
|
+
headerLine,
|
|
142
|
+
`Regime: ${regime}${regime === "training" ? " (scored against the training set)" : ""} | Style: ${effectiveStyle}`,
|
|
79
143
|
`Rows scored: ${summary.n_rows ?? "?"}`,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
`
|
|
83
|
-
|
|
144
|
+
metricsLine,
|
|
145
|
+
outputLine,
|
|
146
|
+
urls[artifactName] ? `Download: ${urls[artifactName]}` : "",
|
|
147
|
+
trainingCaveat,
|
|
148
|
+
...previewLines,
|
|
84
149
|
].filter(Boolean).join("\n") }] };
|
|
85
150
|
}
|
|
86
151
|
if (poll.status === "failed")
|
|
87
152
|
return { content: [{ type: "text", text: `inference(predict) job ${predictJobId} failed: ${poll.error ?? "unknown error"}` }] };
|
|
88
153
|
return { content: [{ type: "text", text: `inference(predict) job ${predictJobId} submitted. Poll with jobs(action=status, job_id="${predictJobId}").` }] };
|
|
89
154
|
}
|
|
90
|
-
if (action === "enrich") {
|
|
91
|
-
const data = (await apiCall("POST", `/v1/results/${job_id}/enrich_dataset`, {}));
|
|
92
|
-
const enrichJobId = data.id;
|
|
93
|
-
const poll = await pollUntilComplete(enrichJobId, 60_000);
|
|
94
|
-
if (poll.status === "completed") {
|
|
95
|
-
const results = (await apiCall("GET", `/v1/results/${enrichJobId}`));
|
|
96
|
-
const summary = (results.summary ?? {});
|
|
97
|
-
const urls = (results.download_urls ?? {});
|
|
98
|
-
return { content: [{ type: "text", text: [
|
|
99
|
-
`Enriched dataset ready — job: ${enrichJobId}`,
|
|
100
|
-
`Rows: ${summary.n_rows ?? "?"} | Clusters: ${summary.n_clusters ?? "?"}`,
|
|
101
|
-
`Appended: bmu_x, bmu_y, bmu_node_index, cluster_id`,
|
|
102
|
-
urls["enriched.csv"] ? `Download: ${urls["enriched.csv"]}` : "",
|
|
103
|
-
].filter(Boolean).join("\n") }] };
|
|
104
|
-
}
|
|
105
|
-
if (poll.status === "failed")
|
|
106
|
-
return { content: [{ type: "text", text: `inference(enrich) job ${enrichJobId} failed: ${poll.error ?? "unknown error"}` }] };
|
|
107
|
-
return { content: [{ type: "text", text: `inference(enrich) job ${enrichJobId} submitted. Poll with jobs(action=status, job_id="${enrichJobId}").` }] };
|
|
108
|
-
}
|
|
109
155
|
if (action === "compare") {
|
|
110
156
|
if (!dataset_id)
|
|
111
157
|
throw new Error("inference(compare) requires dataset_id (dataset B)");
|
package/dist/tools/jobs.js
CHANGED
|
@@ -5,7 +5,7 @@ export const JOBS_DESCRIPTION_BASE = `Manage and inspect jobs.
|
|
|
5
5
|
| Action | Use when |
|
|
6
6
|
|--------|----------|
|
|
7
7
|
| status | Polling after any async job submission — call every 10–15s |
|
|
8
|
-
| list | Finding job IDs, checking what is pending/completed, reviewing hyperparameters. Response includes job_type (train_map,
|
|
8
|
+
| list | Finding job IDs, checking what is pending/completed, reviewing hyperparameters. Response includes job_type (train_map, report, recolor, project, transition_flow, compare, predict, reduce_spectral) to filter or display. |
|
|
9
9
|
| compare | Picking the best training run from a set of completed jobs |
|
|
10
10
|
| train_map | Submitting a new map training job — returns job_id for polling |
|
|
11
11
|
| train_siom_map | Submitting a self-interacting map training job — same map flow with SIOM coverage control |
|
|
@@ -35,18 +35,118 @@ action=train_map / train_siom_map: Submits a grid-map training job. Returns job_
|
|
|
35
35
|
Presets refined/high_res may use GPU. On CPU-only hosts pass backend=cpu. API expects strings "cpu" | "gpu" | "gpu_graphs" (no colon). Future backends (e.g. non-CUDA) may be added under the same contract.
|
|
36
36
|
normalize: "auto" (default) = scale only non-cyclic features; "all" = scale every feature. Use "auto" when using cyclic_features.
|
|
37
37
|
categorical_features: optional baseline categorical support using explicit weighted one-hot encoding. Provide the raw column name, allowed categories, and a weight. Advanced categorical embeddings are intentionally outside this default tool surface.
|
|
38
|
+
label (optional, ≤120 chars): user-supplied run name; appears in jobs(list) and jobs(compare) output. Strongly recommended for sweeps so compare rows stay readable.
|
|
38
39
|
train_siom_map only: siom_feature_geometry l2 (default) | mixed | auto — torus distance on cyclic (cos,sin) pairs when mixed or auto with cyclic encodings. siom_qe_backend cpu|cuda|auto and siom_qe_batch_size align siom_qe with training geometry when mixed.
|
|
39
40
|
action=train_floop_siom (preferred) or train_floop_chain (deprecated alias): Submits FLooP-SIOM — a growing node-budget manifold instead of a fixed hex grid. **Only if the API key’s plan includes all_algorithms** (Premium or Enterprise); otherwise the API returns a clear upgrade message. Default topology is free (CHL dynamic graph); topology=chain is optional for a strict 1D linked-list backbone.
|
|
40
41
|
Key params: topology (default free), max_nodes, effort, gamma, siom_decay.
|
|
41
42
|
max_nodes is a total node budget, not a grid width or area. If omitted, the backend uses a dataset-size heuristic (roughly 2*sqrt(n_samples), capped for stability).
|
|
42
43
|
effort controls passes only: quick≈15, standard≈30, thorough≈60. If coverage collapses, reduce max_nodes before increasing effort.
|
|
43
|
-
action=compare: Returns a metrics table (QE, TE, explained variance, silhouette) for 2+ jobs.
|
|
44
|
+
action=compare: Returns a metrics table (QE, TE, explained variance, silhouette) for 2+ jobs. The table only shows hyperparameter columns that actually differ across the compared runs (preset, backend, batch_size, normalize, periodic, n_features, etc.) so multi-run sweeps stay readable. Tip: pass label="sweep_a" / "sweep_b" on train_map to make rows self-explanatory.
|
|
44
45
|
action=cancel: Not instant — worker checks between phases. Expect up to 30s delay.
|
|
45
46
|
action=delete: WARNING — job ID will no longer work with results or any other tool.`;
|
|
46
47
|
export async function fetchTrainingPresets() {
|
|
47
48
|
const configData = (await apiCall("GET", "/v1/training/config"));
|
|
48
49
|
return configData?.presets || {};
|
|
49
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Render the jobs(compare) markdown table. Strategy: always show job_id (or label
|
|
53
|
+
* if present) + the four core metrics (QE, TE, Expl.Var, Silhouette); add any
|
|
54
|
+
* hyperparameter column iff at least two compared jobs differ on it. Keeps
|
|
55
|
+
* multi-run sweeps readable by hiding columns that are identical across runs.
|
|
56
|
+
*
|
|
57
|
+
* Error rows (job not completed / not found) are preserved as a single error cell.
|
|
58
|
+
*/
|
|
59
|
+
export function renderCompareTable(comparisons) {
|
|
60
|
+
if (comparisons.length === 0)
|
|
61
|
+
return "(no comparisons)";
|
|
62
|
+
const ok = comparisons.filter((c) => !c.error);
|
|
63
|
+
const fmt = (v) => (v !== null && v !== undefined ? Number(v).toFixed(4) : "—");
|
|
64
|
+
const fmtAny = (v) => {
|
|
65
|
+
if (v === null || v === undefined)
|
|
66
|
+
return "—";
|
|
67
|
+
if (Array.isArray(v))
|
|
68
|
+
return v.map((x) => String(x)).join(",");
|
|
69
|
+
if (typeof v === "boolean")
|
|
70
|
+
return v ? "true" : "false";
|
|
71
|
+
return String(v);
|
|
72
|
+
};
|
|
73
|
+
const renderGrid = (g) => {
|
|
74
|
+
if (Array.isArray(g) && g.length >= 2)
|
|
75
|
+
return `${g[0]}×${g[1]}`;
|
|
76
|
+
return "—";
|
|
77
|
+
};
|
|
78
|
+
const renderEpochs = (e) => {
|
|
79
|
+
if (Array.isArray(e) && e.length >= 2)
|
|
80
|
+
return `${e[0]}+${e[1]}`;
|
|
81
|
+
if (typeof e === "number")
|
|
82
|
+
return String(e);
|
|
83
|
+
return "—";
|
|
84
|
+
};
|
|
85
|
+
// Candidate hyperparameter columns considered for inclusion when they vary.
|
|
86
|
+
const candidates = [
|
|
87
|
+
{ key: "grid", label: "Grid", render: renderGrid },
|
|
88
|
+
{ key: "epochs", label: "Epochs", render: renderEpochs },
|
|
89
|
+
{ key: "model", label: "Model", render: fmtAny },
|
|
90
|
+
{ key: "periodic", label: "Periodic", render: fmtAny },
|
|
91
|
+
{ key: "backend", label: "Backend", render: fmtAny },
|
|
92
|
+
{ key: "batch_size", label: "Batch", render: fmtAny },
|
|
93
|
+
{ key: "normalize", label: "Normalize", render: fmtAny },
|
|
94
|
+
{ key: "quality_metrics", label: "QualMetrics", render: fmtAny },
|
|
95
|
+
{ key: "n_features", label: "Features", render: fmtAny },
|
|
96
|
+
{ key: "n_samples", label: "Samples", render: fmtAny },
|
|
97
|
+
];
|
|
98
|
+
// Include a candidate iff completed jobs disagree on it.
|
|
99
|
+
const varying = candidates.filter((col) => {
|
|
100
|
+
const seen = new Set();
|
|
101
|
+
for (const c of ok) {
|
|
102
|
+
seen.add(JSON.stringify(c[col.key] ?? null));
|
|
103
|
+
if (seen.size > 1)
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
});
|
|
108
|
+
const anyLabel = ok.some((c) => c.label != null && String(c.label) !== "");
|
|
109
|
+
const idHeader = anyLabel ? "Run" : "Job ID";
|
|
110
|
+
const idCell = (c) => {
|
|
111
|
+
const lbl = c.label != null && String(c.label) !== "" ? String(c.label) : "";
|
|
112
|
+
const idShort = `${c.job_id.slice(0, 8)}...`;
|
|
113
|
+
return lbl ? `${lbl} (${idShort})` : idShort;
|
|
114
|
+
};
|
|
115
|
+
// Always include job_id/label + four core metrics. Add varying columns in between.
|
|
116
|
+
const metrics = [
|
|
117
|
+
{ key: "quantization_error", label: "QE", render: fmt },
|
|
118
|
+
{ key: "topographic_error", label: "TE", render: fmt },
|
|
119
|
+
{ key: "explained_variance", label: "Expl.Var", render: fmt },
|
|
120
|
+
{ key: "silhouette", label: "Silhouette", render: fmt },
|
|
121
|
+
];
|
|
122
|
+
const headerCols = [idHeader, ...varying.map((c) => c.label), ...metrics.map((m) => m.label)];
|
|
123
|
+
const lines = [
|
|
124
|
+
`| ${headerCols.join(" | ")} |`,
|
|
125
|
+
`|${headerCols.map(() => "---").join("|")}|`,
|
|
126
|
+
];
|
|
127
|
+
for (const c of comparisons) {
|
|
128
|
+
if (c.error) {
|
|
129
|
+
const filler = Array(varying.length + metrics.length - 1).fill("—").join(" | ");
|
|
130
|
+
lines.push(`| ${idCell(c)} | ${c.error} | ${filler} |`);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const cells = [
|
|
134
|
+
idCell(c),
|
|
135
|
+
...varying.map((col) => col.render(c[col.key])),
|
|
136
|
+
...metrics.map((m) => m.render(c[m.key])),
|
|
137
|
+
];
|
|
138
|
+
lines.push(`| ${cells.join(" | ")} |`);
|
|
139
|
+
}
|
|
140
|
+
if (varying.length === 0 && ok.length >= 2) {
|
|
141
|
+
lines.push("");
|
|
142
|
+
lines.push("(All hyperparameters identical across compared jobs — only metrics shown. Pass label=\"…\" on train_map for readable run names.)");
|
|
143
|
+
}
|
|
144
|
+
else if (!anyLabel && ok.length >= 2) {
|
|
145
|
+
lines.push("");
|
|
146
|
+
lines.push("Tip: pass label=\"sweep_xyz\" on train_map / train_siom_map / train_floop_siom to make compare rows readable.");
|
|
147
|
+
}
|
|
148
|
+
return lines.join("\n");
|
|
149
|
+
}
|
|
50
150
|
export function buildTrainMapParams(args, presets) {
|
|
51
151
|
const { preset, grid_x, grid_y, epochs, model, periodic, columns, cyclic_features, temporal_features, feature_weights, transforms, auto_log_transforms, time_delay_embeddings, categorical_features, normalize, 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;
|
|
52
152
|
const p = preset ? presets[preset] : undefined;
|
|
@@ -145,6 +245,11 @@ export function registerJobsTool(server, description) {
|
|
|
145
245
|
.string()
|
|
146
246
|
.optional()
|
|
147
247
|
.describe("Required for action=train_map. For action=list, filter jobs by this dataset ID."),
|
|
248
|
+
label: z
|
|
249
|
+
.string()
|
|
250
|
+
.max(120)
|
|
251
|
+
.optional()
|
|
252
|
+
.describe("Optional run label (≤120 chars) for train_map / train_siom_map / train_floop_siom — appears in jobs(list) and the jobs(compare) table; sanitized server-side. Useful for sweeps (e.g. label=\"sweep_periodic_true\")."),
|
|
148
253
|
preset: z.enum(["quick", "standard", "refined", "high_res"]).optional(),
|
|
149
254
|
grid_x: z.number().int().optional(),
|
|
150
255
|
grid_y: z.number().int().optional(),
|
|
@@ -256,7 +361,7 @@ export function registerJobsTool(server, description) {
|
|
|
256
361
|
return { content: [{ type: "text", text: `Baseline study submitted. Job ID: ${jid}\nGrid size: ${side}x${side}\nNormalization: MAD\n\nPoll with jobs(action=status, job_id="${jid}") until complete, then retrieve with results(action=get, job_id="${jid}"). Optional: training_monitor(job_id="${jid}") for a visual panel—not required.` }] };
|
|
257
362
|
}
|
|
258
363
|
if (action === "train_map" || action === "train_siom_map") {
|
|
259
|
-
const { preset, grid_x, grid_y, epochs, model, periodic, columns, cyclic_features, temporal_features, feature_weights, transforms, auto_log_transforms, time_delay_embeddings, categorical_features, normalize, 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, } = args;
|
|
364
|
+
const { preset, grid_x, grid_y, epochs, model, periodic, columns, cyclic_features, temporal_features, feature_weights, transforms, auto_log_transforms, time_delay_embeddings, categorical_features, normalize, 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, } = args;
|
|
260
365
|
let PRESETS = {};
|
|
261
366
|
try {
|
|
262
367
|
PRESETS = await fetchTrainingPresets();
|
|
@@ -304,7 +409,10 @@ export function registerJobsTool(server, description) {
|
|
|
304
409
|
totalRows = Number(preview?.total_rows ?? 0);
|
|
305
410
|
}
|
|
306
411
|
catch { /* ignore */ }
|
|
307
|
-
const
|
|
412
|
+
const submitBody = { dataset_id, params };
|
|
413
|
+
if (label && label.trim() !== "")
|
|
414
|
+
submitBody.label = label;
|
|
415
|
+
const data = (await apiCall("POST", "/v1/jobs", submitBody));
|
|
308
416
|
const newJobId = data.id;
|
|
309
417
|
const variantPrefix = action === "train_siom_map" ? "variant=siom" : "variant=som";
|
|
310
418
|
data.effective_params = `${variantPrefix}, ${paramSummary}`;
|
|
@@ -335,7 +443,7 @@ export function registerJobsTool(server, description) {
|
|
|
335
443
|
return textResult(data);
|
|
336
444
|
}
|
|
337
445
|
if (action === "train_floop_siom" || action === "train_floop_chain") {
|
|
338
|
-
const { columns, cyclic_features, temporal_features, feature_weights, transforms, auto_log_transforms, normalize, row_range, output_format, output_dpi, colormap, topology, max_nodes, effort, n_initial, n_passes, growth_interval, neighborhood_size, edge_max_age, max_degree, gamma, siom_decay, siom_penalty, penalty_alpha, error_threshold, error_decay, ring_close_threshold, sigma_0, sigma_f, eta_0, eta_f, elastic_lambda, elastic_mu, elastic_anchor, anchor_percentile, } = args;
|
|
446
|
+
const { columns, cyclic_features, temporal_features, feature_weights, transforms, auto_log_transforms, normalize, row_range, output_format, output_dpi, colormap, topology, max_nodes, effort, n_initial, n_passes, growth_interval, neighborhood_size, edge_max_age, max_degree, gamma, siom_decay, siom_penalty, penalty_alpha, error_threshold, error_decay, ring_close_threshold, sigma_0, sigma_f, eta_0, eta_f, elastic_lambda, elastic_mu, elastic_anchor, anchor_percentile, label, } = args;
|
|
339
447
|
if (!dataset_id)
|
|
340
448
|
throw new Error("jobs(train_floop_siom) requires dataset_id");
|
|
341
449
|
const params = {
|
|
@@ -408,7 +516,10 @@ export function registerJobsTool(server, description) {
|
|
|
408
516
|
params.elastic_anchor = elastic_anchor;
|
|
409
517
|
if (anchor_percentile !== undefined)
|
|
410
518
|
params.anchor_percentile = anchor_percentile;
|
|
411
|
-
const
|
|
519
|
+
const submitBody = { dataset_id, params };
|
|
520
|
+
if (label && label.trim() !== "")
|
|
521
|
+
submitBody.label = label;
|
|
522
|
+
const data = (await apiCall("POST", "/v1/jobs", submitBody));
|
|
412
523
|
const newJobId = data.id;
|
|
413
524
|
const maxNodeSummary = max_nodes === undefined ? "max_nodes=auto(~2*sqrt(n_samples))" : `max_nodes=${max_nodes}`;
|
|
414
525
|
const paramSummary = [
|
|
@@ -465,21 +576,7 @@ export function registerJobsTool(server, description) {
|
|
|
465
576
|
const ids = job_ids.join(",");
|
|
466
577
|
const data = (await apiCall("GET", `/v1/jobs/compare?ids=${ids}`));
|
|
467
578
|
const comparisons = (data.comparisons ?? []);
|
|
468
|
-
|
|
469
|
-
"| Job ID | Grid | Epochs | Model | QE | TE | Expl.Var | Silhouette |",
|
|
470
|
-
"|--------|------|--------|-------|----|----|----------|------------|",
|
|
471
|
-
];
|
|
472
|
-
for (const c of comparisons) {
|
|
473
|
-
if (c.error) {
|
|
474
|
-
lines.push(`| ${c.job_id.slice(0, 8)}... | — | — | — | ${c.error} | — | — | — |`);
|
|
475
|
-
continue;
|
|
476
|
-
}
|
|
477
|
-
const g = c.grid;
|
|
478
|
-
const ep = c.epochs;
|
|
479
|
-
const fmt = (v) => v !== null && v !== undefined ? Number(v).toFixed(4) : "—";
|
|
480
|
-
lines.push(`| ${c.job_id.slice(0, 8)}... | ${g ? `${g[0]}×${g[1]}` : "—"} | ${ep ? `${ep[0]}+${ep[1]}` : "—"} | ${c.model ?? "—"} | ${fmt(c.quantization_error)} | ${fmt(c.topographic_error)} | ${fmt(c.explained_variance)} | ${fmt(c.silhouette)} |`);
|
|
481
|
-
}
|
|
482
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
579
|
+
return { content: [{ type: "text", text: renderCompareTable(comparisons) }] };
|
|
483
580
|
}
|
|
484
581
|
if (action === "cancel") {
|
|
485
582
|
if (!job_id)
|
package/dist/tools/results.js
CHANGED
|
@@ -17,7 +17,7 @@ ONLY call this after jobs(action=status) returns "completed".
|
|
|
17
17
|
ESCALATION: If job not found, verify job_id. If "job not complete", poll with jobs(action=status).
|
|
18
18
|
|
|
19
19
|
action=get: Returns text summary with quality metrics and inline images.
|
|
20
|
-
- figures: omit = combined only. "all" = all plots. Array = specific logical names (combined, umatrix, hit_histogram, learning_curve, correlation, component_1..N).
|
|
20
|
+
- 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).
|
|
21
21
|
- include_individual: if true (and figures omitted), inlines every component plane.
|
|
22
22
|
- After recolor or project, use the job_id returned by that operation to get the new artifacts, not the original training job_id.
|
|
23
23
|
- After showing results, guide the user: QE interpretation, whether to retrain, which features to explore.
|
|
@@ -48,9 +48,9 @@ NOT FOR: Jobs that haven't completed. Use jobs(action=status) to check first.`,
|
|
|
48
48
|
.describe("get: inline results + metrics; recolor: new colormap/format (async); download: save to disk; export: structured data (training_log/weights/nodes); transition_flow: temporal dynamics (async)"),
|
|
49
49
|
job_id: z.string().describe("Job ID of a completed job"),
|
|
50
50
|
figures: z
|
|
51
|
-
.union([z.enum(["default", "combined_only", "all", "images"]), z.array(z.string())])
|
|
51
|
+
.union([z.enum(["default", "combined_only", "all", "images", "none"]), z.array(z.string())])
|
|
52
52
|
.optional()
|
|
53
|
-
.describe("action=get: omit=combined only; 'all'=all plots; array=specific (combined,umatrix,hit_histogram,learning_curve,correlation,component_1..N). action=download: 'all'=all image files; array=specific filenames."),
|
|
53
|
+
.describe("action=get: omit=combined only; 'all'=all plots; 'none'=metrics text only (no images, ideal for sweeps and LLM clients); array=specific (combined,umatrix,hit_histogram,learning_curve,correlation,component_1..N). action=download: 'all'=all image files; array=specific filenames."),
|
|
54
54
|
include_individual: z
|
|
55
55
|
.boolean()
|
|
56
56
|
.optional()
|
|
@@ -202,22 +202,65 @@ NOT FOR: Jobs that haven't completed. Use jobs(action=status) to check first.`,
|
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
204
|
else if (jobType === "enrich_dataset") {
|
|
205
|
+
// Historical job kind: pre-merge `enrich_dataset` worker output. New jobs
|
|
206
|
+
// never produce this — they use predict with output_style="annotated"
|
|
207
|
+
// (job_type="predict"). Kept as a read-only display path so old completed
|
|
208
|
+
// jobs remain reviewable.
|
|
205
209
|
const files = summary.files ?? [];
|
|
206
210
|
content.push({ type: "text", text: [
|
|
207
|
-
`
|
|
208
|
-
`Parent map job: ${summary.parent_job_id ?? "N/A"} |
|
|
211
|
+
`Annotated Dataset (legacy enrich_dataset job) — ${resultsHeader}`,
|
|
212
|
+
`Parent map job: ${summary.parent_job_id ?? "N/A"} | Rows: ${summary.n_rows ?? summary.n_samples ?? 0}`,
|
|
209
213
|
`Output: ${files.filter((f) => f !== "summary.json").join(", ")}`,
|
|
210
214
|
`Use results(action=download, job_id="${job_id}") to save enriched.csv.`,
|
|
215
|
+
`(For new jobs, use inference(action=predict, output="annotated").)`,
|
|
211
216
|
].join("\n") });
|
|
212
217
|
}
|
|
213
218
|
else if (jobType === "predict") {
|
|
214
219
|
const files = summary.files ?? [];
|
|
220
|
+
const regime = String(summary.regime ?? "new");
|
|
221
|
+
const outputStyle = String(summary.output_style ?? "compact");
|
|
222
|
+
const isAnnotated = outputStyle === "annotated";
|
|
223
|
+
const headerLabel = isAnnotated ? "Predict (annotated)" : "Predict";
|
|
224
|
+
const trainingCaveat = (regime === "training" && !isAnnotated)
|
|
225
|
+
? "QE columns intentionally omitted: regime=training (fitting error, not generalisation). Score a held-out dataset for quality assessment."
|
|
226
|
+
: "";
|
|
227
|
+
const metricsLine = (regime !== "training" && !isAnnotated)
|
|
228
|
+
? `Mean QE: ${summary.mean_qe !== undefined ? Number(summary.mean_qe).toFixed(4) : "N/A"} | Max QE: ${summary.max_qe !== undefined ? Number(summary.max_qe).toFixed(4) : "N/A"} | qe_p95: ${summary.qe_p95 !== undefined ? Number(summary.qe_p95).toFixed(4) : "N/A"}`
|
|
229
|
+
: "";
|
|
230
|
+
const downloadName = isAnnotated ? "enriched.csv" : "predictions.csv";
|
|
215
231
|
content.push({ type: "text", text: [
|
|
216
|
-
|
|
217
|
-
`Parent map job: ${summary.parent_job_id ?? "N/A"} |
|
|
232
|
+
`${headerLabel} — ${resultsHeader}`,
|
|
233
|
+
`Parent map job: ${summary.parent_job_id ?? "N/A"} | Regime: ${regime} | Style: ${outputStyle}`,
|
|
234
|
+
`Rows: ${summary.n_rows ?? summary.n_samples ?? 0}`,
|
|
235
|
+
metricsLine,
|
|
218
236
|
`Output: ${files.filter((f) => f !== "summary.json").join(", ")}`,
|
|
219
|
-
`Use results(action=download, job_id="${job_id}") to save
|
|
220
|
-
|
|
237
|
+
`Use results(action=download, job_id="${job_id}") to save ${downloadName}.`,
|
|
238
|
+
trainingCaveat,
|
|
239
|
+
].filter(Boolean).join("\n") });
|
|
240
|
+
}
|
|
241
|
+
else if (jobType === "reduce_spectral") {
|
|
242
|
+
const method = String(summary.method ?? "?");
|
|
243
|
+
const sourceCols = summary.source_columns ?? [];
|
|
244
|
+
const outCols = summary.output_columns ?? [];
|
|
245
|
+
const lines = [
|
|
246
|
+
`Reduce Spectral — ${resultsHeader}`,
|
|
247
|
+
`Method: ${method} | Source columns: ${sourceCols.length} | Output columns: ${outCols.length}`,
|
|
248
|
+
`Source: ${sourceCols.length <= 10 ? sourceCols.join(", ") : sourceCols.slice(0, 5).join(", ") + ", … " + sourceCols.slice(-3).join(", ")}`,
|
|
249
|
+
`Output: ${outCols.join(", ")}`,
|
|
250
|
+
];
|
|
251
|
+
if (method === "pca") {
|
|
252
|
+
const ev = summary.explained_variance_ratio;
|
|
253
|
+
const evTotal = summary.explained_variance_total;
|
|
254
|
+
if (ev)
|
|
255
|
+
lines.push(`Explained variance per component: [${ev.map((v) => v.toFixed(3)).join(", ")}] | total: ${evTotal !== undefined ? evTotal.toFixed(3) : "N/A"}`);
|
|
256
|
+
}
|
|
257
|
+
else if (method === "log_sample" || method === "uniform_sample") {
|
|
258
|
+
const idxs = summary.selected_indices;
|
|
259
|
+
if (idxs)
|
|
260
|
+
lines.push(`Selected indices: [${idxs.join(", ")}]`);
|
|
261
|
+
}
|
|
262
|
+
lines.push(`The new columns are appended to the source dataset and ready to use in jobs(action=train_map, columns=[..., "${outCols[0] ?? "feat_1"}", …]).`);
|
|
263
|
+
content.push({ type: "text", text: lines.join("\n") });
|
|
221
264
|
}
|
|
222
265
|
else if (jobType === "train_floop_siom") {
|
|
223
266
|
const siom = summary.siom ?? {};
|
|
@@ -357,24 +400,29 @@ NOT FOR: Jobs that haven't completed. Use jobs(action=status) to check first.`,
|
|
|
357
400
|
const jobType2 = summary.job_type ?? "train_som";
|
|
358
401
|
const isImage = (f) => f.endsWith(".png") || f.endsWith(".svg") || f.endsWith(".pdf");
|
|
359
402
|
const hasExportableJson = files.some((f) => f.endsWith(".json") && f !== "summary.json");
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
403
|
+
// figures="none": metrics-only mode. Skip any further image attaches and the
|
|
404
|
+
// "Available figures…" hint; keep the structured-data export hint since it
|
|
405
|
+
// points at non-image artifacts.
|
|
406
|
+
if (figures !== "none") {
|
|
407
|
+
for (const fname of files) {
|
|
408
|
+
if (isImage(fname) && !inlinedImages.has(fname)) {
|
|
409
|
+
const cap = getCaptionForImage(fname);
|
|
410
|
+
if (cap)
|
|
411
|
+
content.push({ type: "text", text: cap });
|
|
412
|
+
await tryAttachImage(content, job_id, fname);
|
|
413
|
+
}
|
|
366
414
|
}
|
|
367
415
|
}
|
|
368
416
|
if (hasExportableJson && (jobType2 === "train_som" || jobType2 === "train_siom" || jobType2 === "render_variant")) {
|
|
369
417
|
content.push({ type: "text", text: `Structured data: results(action=export, export_type=weights|nodes|training_log)` });
|
|
370
418
|
}
|
|
371
419
|
const featuresForLog = summary.features ?? [];
|
|
372
|
-
const showAvailable = files.length > 0 && !(figures === "all" || figures === "images");
|
|
420
|
+
const showAvailable = files.length > 0 && !(figures === "all" || figures === "images" || figures === "none");
|
|
373
421
|
if (showAvailable) {
|
|
374
422
|
const logicalNames = jobType2 === "train_som" || jobType2 === "train_siom" || jobType2 === "render_variant"
|
|
375
423
|
? `Logical names: combined, umatrix, hit_histogram, correlation, ${featuresForLog.map((_, i) => `component_${i + 1}`).join(", ")}. `
|
|
376
424
|
: "";
|
|
377
|
-
content.push({ type: "text", text: `Available: ${files.join(", ")}. ${logicalNames}Use results(action=get, figures=[...]) for specific plots
|
|
425
|
+
content.push({ type: "text", text: `Available: ${files.join(", ")}. ${logicalNames}Use results(action=get, figures=[...]) for specific plots, figures=all for everything, or figures="none" for metrics-only.` });
|
|
378
426
|
}
|
|
379
427
|
return { content };
|
|
380
428
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { fetchTrainingGuidanceFromApi } from "../shared.js";
|
|
2
2
|
export function registerTrainingGuidanceTool(server) {
|
|
3
|
-
server.tool("training_guidance", "
|
|
3
|
+
server.tool("training_guidance", "Structured parameter guidance for training (presets, grid/epochs/batch, model, periodic, SIOM/FLooP fields where your plan allows, categorical baselines). Served from the API and filtered to allowed_job_types. Prep ladder: prepare_training prompt = narrative checklist; this tool = JSON/hints; training_prep = interactive UI + submit_prepared_training. For full orientation and SOP, call guide_barsom_workflow first. Optional UIs: training_prep, results_explorer—not required; jobs + results suffice.", {}, async () => {
|
|
4
4
|
const text = await fetchTrainingGuidanceFromApi();
|
|
5
5
|
return { content: [{ type: "text", text }] };
|
|
6
6
|
});
|
|
@@ -321,7 +321,7 @@ export function registerTrainingPrepTools(server) {
|
|
|
321
321
|
};
|
|
322
322
|
registerAppTool(server, "training_prep", {
|
|
323
323
|
title: "Training Preparation",
|
|
324
|
-
description: "
|
|
324
|
+
description: "Interactive training prep UI + guarded submit (submit_prepared_training). Prep ladder: prepare_training prompt = narrative checklist; training_guidance tool = JSON/presets; this tool = visual review. Opens inline review of variables, transforms, encodings, and hyperparameters.",
|
|
325
325
|
inputSchema: trainPrepSchema,
|
|
326
326
|
_meta: { ui: { resourceUri: TRAINING_PREP_URI } },
|
|
327
327
|
}, async ({ dataset_id, ...args }) => {
|