@barivia/barsom-mcp 0.22.4 → 0.23.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/dist/audit.js +3 -1
- package/dist/deferred_feedback.js +88 -0
- package/dist/index.js +1 -1
- package/dist/inventory_format.js +69 -0
- package/dist/shared.js +240 -43
- package/dist/tools/account.js +112 -8
- package/dist/tools/datasets.js +53 -19
- package/dist/tools/feedback.js +72 -6
- package/dist/tools/guide_barsom.js +9 -4
- package/dist/tools/jobs.js +73 -16
- package/dist/tools/train.js +18 -2
- package/package.json +1 -1
package/dist/audit.js
CHANGED
|
@@ -110,10 +110,12 @@ function stripAuditMeta(result) {
|
|
|
110
110
|
function errorCodeFrom(err) {
|
|
111
111
|
if (err && typeof err === "object") {
|
|
112
112
|
const e = err;
|
|
113
|
+
if (e.errorCode)
|
|
114
|
+
return e.errorCode;
|
|
113
115
|
if (e.httpStatus !== undefined)
|
|
114
116
|
return `http_${e.httpStatus}`;
|
|
115
117
|
const m = e.message ?? "";
|
|
116
|
-
const codeMatch = m.match(/error_code
|
|
118
|
+
const codeMatch = m.match(/error_code[=:]\s*(\w+)/);
|
|
117
119
|
if (codeMatch)
|
|
118
120
|
return codeMatch[1];
|
|
119
121
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local offline queue for send_feedback when the API or object storage is down.
|
|
3
|
+
* Persists under ~/.barivia/deferred-feedback (or BARIVIA_FEEDBACK_QUEUE_DIR).
|
|
4
|
+
* Proxies flush the queue on the next successful feedback submission.
|
|
5
|
+
*/
|
|
6
|
+
import fs from "node:fs/promises";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
10
|
+
export function deferredFeedbackQueueDir() {
|
|
11
|
+
const override = process.env.BARIVIA_FEEDBACK_QUEUE_DIR?.trim();
|
|
12
|
+
if (override)
|
|
13
|
+
return path.resolve(override);
|
|
14
|
+
return path.join(os.homedir(), ".barivia", "deferred-feedback");
|
|
15
|
+
}
|
|
16
|
+
async function ensureQueueDir() {
|
|
17
|
+
const dir = deferredFeedbackQueueDir();
|
|
18
|
+
await fs.mkdir(dir, { recursive: true });
|
|
19
|
+
return dir;
|
|
20
|
+
}
|
|
21
|
+
function entryPath(dir, id) {
|
|
22
|
+
return path.join(dir, `${id}.json`);
|
|
23
|
+
}
|
|
24
|
+
export async function enqueueDeferredFeedback(product, body, lastError) {
|
|
25
|
+
const dir = await ensureQueueDir();
|
|
26
|
+
const id = randomUUID();
|
|
27
|
+
const entry = {
|
|
28
|
+
id,
|
|
29
|
+
product,
|
|
30
|
+
created_at: new Date().toISOString(),
|
|
31
|
+
body,
|
|
32
|
+
...(lastError ? { last_error: lastError } : {}),
|
|
33
|
+
};
|
|
34
|
+
const filePath = entryPath(dir, id);
|
|
35
|
+
await fs.writeFile(filePath, JSON.stringify(entry, null, 2), "utf8");
|
|
36
|
+
return { id, path: filePath };
|
|
37
|
+
}
|
|
38
|
+
export async function listDeferredFeedback(product) {
|
|
39
|
+
const dir = deferredFeedbackQueueDir();
|
|
40
|
+
let names;
|
|
41
|
+
try {
|
|
42
|
+
names = await fs.readdir(dir);
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
if (err.code === "ENOENT")
|
|
46
|
+
return [];
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
const out = [];
|
|
50
|
+
for (const name of names) {
|
|
51
|
+
if (!name.endsWith(".json"))
|
|
52
|
+
continue;
|
|
53
|
+
try {
|
|
54
|
+
const raw = await fs.readFile(path.join(dir, name), "utf8");
|
|
55
|
+
const entry = JSON.parse(raw);
|
|
56
|
+
if (product && entry.product !== product)
|
|
57
|
+
continue;
|
|
58
|
+
out.push(entry);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
/* skip corrupt */
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
out.sort((a, b) => a.created_at.localeCompare(b.created_at));
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Attempt to submit each queued entry via `submit`. Successful entries are deleted.
|
|
69
|
+
* Failures leave the file in place for a later retry.
|
|
70
|
+
*/
|
|
71
|
+
export async function flushDeferredFeedback(product, submit) {
|
|
72
|
+
const entries = await listDeferredFeedback(product);
|
|
73
|
+
let flushed = 0;
|
|
74
|
+
const errors = [];
|
|
75
|
+
const dir = deferredFeedbackQueueDir();
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
try {
|
|
78
|
+
await submit(entry.body);
|
|
79
|
+
await fs.unlink(entryPath(dir, entry.id)).catch(() => undefined);
|
|
80
|
+
flushed += 1;
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
errors.push(err instanceof Error ? err.message : String(err));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const remaining = (await listDeferredFeedback(product)).length;
|
|
87
|
+
return { flushed, remaining, errors };
|
|
88
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{McpServer as e}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as t}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as o}from"zod";import{getUiCapability as r,registerAppResource as s,RESOURCE_MIME_TYPE as n}from"@modelcontextprotocol/ext-apps/server";import{startVizServer as a}from"./viz-server.js";import{API_KEY as i,apiCall as l,apiRawCall as p,loadViewHtml as c,setVizPort as u,setClientSupportsMcpApps as d,CLIENT_VERSION as m}from"./shared.js";import{registerDatasetsTool as f}from"./tools/datasets.js";import{registerTrainTool as g}from"./tools/train.js";import{registerJobsTool as h,JOBS_DESCRIPTION_BASE as _}from"./tools/jobs.js";import{registerResultsTool as b}from"./tools/results.js";import{registerExploreMapTool as y,RESULTS_EXPLORER_URI as w}from"./tools/explore_map.js";import{registerAccountTool as v}from"./tools/account.js";import{registerInferenceTool as j}from"./tools/inference.js";import{registerGuideBarsomTool as x}from"./tools/guide_barsom.js";import{registerTrainingGuidanceTool as k}from"./tools/training_guidance.js";import{registerFeedbackTool as P}from"./tools/feedback.js";import{registerTrainingMonitorTool as A,TRAINING_MONITOR_URI as I}from"./tools/training_monitor.js";import{resolvePrepareTrainingPrompt as C}from"./prepare_training_prompt.js";i||(console.error("Error: BARIVIA_API_KEY not set. Set it in your MCP client config."),process.exit(1));const M=new e({name:"analytics-engine",version:m,instructions:'# Barivia Mapping Analytics Engine\n\nSelf-organizing map (SOM) analytics: project high-dimensional data to a 2D grid for clusters, gradients, and anomalies.\n\n## Workflow (short)\n\nUpload (`datasets(upload)`) → `datasets(preview)` and `datasets(analyze)` before train → submit one of `train(action=map)`, `train(action=siom_map)`, or `train(action=floop_siom)` (only if plan allows FLooP) → poll `jobs(status)` every 10–15s until `completed` → `results(get)` for metrics and figures (there is no separate analyze tool), then `results_explorer(job_id)` to browse the figures interactively. Then `jobs(compare)`, `results(download/recolor)`, or `inference` (predict / compare / project_columns / impute_column / transition_flow) as needed.\n\n**Full detail:** Call `guide_barsom_workflow` for plan-scoped tool map, training modes, async rules, optional MCP App UIs, and step-by-step SOP (from the Barivia API when online).\n\n## Tool taxonomy (one tool per stage — do not mix)\n\n| Stage | Tool(s) | Contract |\n|-------|---------|----------|\n| Data | `datasets` | upload, preview, analyze, list, **get** (status/staging), subset, add_expression, reduce_spectral, delete — ingest + feature engineering, no map |\n| Train submit | `train` | map, siom_map, impute (sparse → map + dense imputed.csv), floop_siom (entitled). Async, returns job_id. Submit only |\n| Lifecycle | `jobs` | status, list, compare (**training runs**), cancel, delete. No training/scoring here |\n| Artifacts | `results` | get (default=combined only; figures="none" for metrics-only sweeps; figures="all" or results_explorer/download for every plot), export, download, recolor (async). Read/render completed jobs |\n| Frozen-map ops | `inference` | predict (regime-aware; "compact"|"annotated"), batch_predict, impute_column (neighbor-pool fill for a non-training column), compare (cohort density-diff), project_columns, transition_flow (time-ordered rows only), report. No weight updates |\n| Account | `account` | status, history, add_funds |\n| Bootstrap | `guide_barsom_workflow`, `train`, `training_guidance`, `prepare_training` prompt | orientation, submit map/siom/impute/floop, parameter hints, narrative checklist (API/tier-scoped) |\n| Explore (UI) | `results_explorer`, `training_monitor` | richer UX over the same API; `jobs(status)`+`results(get)` still suffice headless |\n| Other | `send_feedback` | only after user agrees |\n\n### Avoid these mis-routes\n- `jobs(compare)` = metrics table across **training runs** (pick best in a sweep). `inference(compare)` = density-diff of one map vs a **second dataset** (drift/cohort). Different tools.\n- Post-training map analysis (predict, compare, project_columns, impute_column, **transition_flow**) is all **inference** — not `results`. `results` is presentation/artifacts only (`recolor` stays here: it re-renders, it doesn\'t touch data).\n- Project a quantity onto the map: existing columns → `inference(project_columns)`; a formula → `datasets(add_expression, project_onto_job=<job>)`.\n\n## Async pattern\n\n- **Manual poll:** Training submits return `job_id` immediately — poll `jobs(status)` every 10–15s. **Running is not failed**; large grids or FLooP-SIOM can take many minutes. `max_nodes` (FLooP) is a total node budget, not grid side length.\n- **Often auto-polled:** `inference` actions (incl. `transition_flow`) and `results(recolor)` may wait in-proxy; if you get a `job_id`, poll `jobs(status)` the same way.\n\nCredits: jobs consume compute credits; check `account(status)` before big runs. Per-request HTTP timeout defaults to 60s (`datasets(analyze)` is async + auto-polled, so it is not bound by it); on slow networks raise `BARIVIA_FETCH_TIMEOUT_MS`. Timeouts are NOT auto-retried (avoids duplicating slow/expensive work).\n\n## Constraints\n\n- **Do not auto-subset:** Never call `datasets(subset)` or `sample_n` unless the user explicitly asks for a filter, slice, or random sample. Train and analyze on the uploaded `dataset_id` at full row count — GPU staging/prepare handles hundreds of thousands of rows quickly; silent downsampling hides real prepare/train behavior and wastes scale demos. Use small named fixtures (e.g. sample.csv, periodic_load) only when the user or runbook names them.\n- Prep ladder: `prepare_training` prompt = narrative checklist; `training_guidance` = structured hints (presets, resolved normalization/transforms, grid/epochs). Review these, then submit with `train`. Do not guess tiers or FLooP entitlement.\n- `inference(predict)`: prefer `dataset_id` for batch and for SIOM/irregular maps; single-row `rows` uses a fast path that can fail on some topologies — retry with `dataset_id`. FLooP-SIOM: if predict jobs fail while grid SIOM works, capture errors + `job_id`.\n- Column names are case-sensitive — match `datasets(preview)`.\n- Default training path is numeric/cyclic/temporal; use explicit `categorical_features` for baseline categoricals. `predict` must match the model contract.\n- After `results(recolor)`, `inference(transition_flow)`, or `inference(project_columns)`, use the **new** `job_id` returned for follow-up `results` if applicable.\n- **Large results / MCP timeouts:** For sweeps, `jobs(compare)`, or metrics-only reads use `results(action=get, figures="none")`. Default `get` inlines combined only. For every plot use `results_explorer` or `results(action=download)`, not `figures="all"` in chat unless the user wants all images inline.\n\n## UI delivery (heterogeneous MCP clients)\n\nHosts differ (Cursor, Claude Desktop, CLI agents): some embed MCP App panels; others list `ui://` but never mount them (expected on some Cursor builds); others are text-only. Tool results **always lead with a standalone localhost URL** — that is the intended fallback, not a broken App.\n\n| Tier | When | Agent should |\n|------|------|--------------|\n| **embedded** | Client advertises MCP Apps | Let the panel render **and** surface the standalone localhost URL from tool output / `ui_delivery.urls` (hosts often list Apps but do not mount). |\n| **localhost** | No MCP Apps; viz server running | Post the markdown link from tool output to the user (never broken workspace paths). Re-post after job completes if needed. |\n| **inline_image** | No Apps / override `inline` | Summarize metrics; inline raster (combined.png, learning curve) is attached automatically — show it in the reply. |\n| **text_only** | Viz server down | Use `jobs(status)` + `results(get)`; no panel or localhost link. |\n\nEvery `training_monitor` / `results_explorer` response includes a structured `ui_delivery` block (`delivery`, `urls`, `hint_for_agent`). Follow `hint_for_agent` without user prompting. Standalone viz pages cross-link monitor ↔ results for the same `job_id`.\n\n**Env overrides:** `BARIVIA_UI_DELIVERY=auto|apps|localhost|inline` (default `auto`). `BARIVIA_VIZ_PORT` pins the localhost viz port across proxy restarts. **Diagnostics:** `account(action=status)` reports detected MCP Apps support, viz port, and active tier.'});s(M,w,w,{mimeType:n},async()=>{const e=await c("results-explorer");return{contents:[{uri:w,mimeType:n,text:e??"<html><body>Results Explorer view not built yet. Run: npm run build:views</body></html>"}]}}),s(M,I,I,{mimeType:n},async()=>{const e=await c("training-monitor");return{contents:[{uri:I,mimeType:n,text:e??"<html><body>Training Monitor view not built yet.</body></html>"}]}}),x(M),y(M),A(M),f(M),g(M),h(M,_),b(M),v(M),j(M),k(M),P(M),M.prompt("info","Short orientation for the Barivia Mapping MCP. For full plan-scoped workflow, tool map, and SOP, the model should call guide_barsom_workflow. Use when the user asks what this MCP can do or how to get started.",{},()=>({messages:[{role:"user",content:{type:"text",text:["Give a concise, scannable answer (headers + bullets):","","**What it is:** MCP client to the Barivia mapping engine (2D SOM / SIOM / FLooP-SIOM when entitled) over HTTPS.","","**First step:** Call `guide_barsom_workflow` for plan-scoped bootstrap (full tool list, async rules, training modes, optional MCP Apps, SOP).","","**Core path:** `datasets(upload)` → `datasets(preview)` + `datasets(analyze)` → choose training action → poll `jobs(status)` every 10–15s until completed → `results(get)` (all main figures/metrics; no separate analyze tool) → `results_explorer(job_id)` to browse figures.","",'**Key tools:** `datasets` (data; reduce_spectral for spectra/long blocks), `train` (map/siom_map/impute/floop_siom; pass `label` for readable compare rows; preset=quick for a fast first map), `jobs` (status/list/compare/cancel/delete — lifecycle only), `results` (get/download/export/recolor; figures="none" for metrics-only), `inference` (predict; batch_predict; impute_column for topology-neighbor pool fill; compare; project_columns; transition_flow; report), `account` (status/credits/queue).',"","**Prep help:** `prepare_training` prompt (checklist) · `training_guidance` (presets/JSON hints + resolved normalization/params) — review, then submit with `train`.","","**Browse results:** `results_explorer(job_id)` is the preferred way to explore figures after a first `results(get)` glance; `training_monitor` shows live progress. Both optional — `results` + `jobs(status)` suffice headless.","","**After training:** `jobs(compare)` across runs, `results(recolor)`, `inference(project_columns)` for variables not in training, `inference(transition_flow)` only if rows are time-ordered.","","**Rules:** Running ≠ failed. Column names must match `datasets(preview)` exactly. Do not auto-subset — never call `datasets(subset)` / `sample_n` unless the user explicitly asks; train on the full uploaded `dataset_id`. Do not call `_fetch_figure` from chat (host/UI only); use `results(get)` or `results_explorer`.","","Offer `send_feedback` only after asking the user."].join("\n")}}]})),M.prompt("prepare_training","Narrative pre-training checklist (prompt). Use after upload and before train. Content is tier-scoped from the API when online. Prep ladder: this prompt = story checklist; training_guidance tool = JSON presets/parameter hints. Review, then submit with train.",{dataset_id:o.string().describe("Dataset ID to prepare for training")},async({dataset_id:e})=>{const t=await C(e);return{messages:[{role:"user",content:{type:"text",text:(t.used_fallback?`[offline fallback ${t.fallback_version}]\n\n`:"")+t.text}}]}});const T=new t;(async function(){try{const e=await a(l,p,c);u(e)}catch(e){process.env.BARIVIA_VIZ_PORT&&console.error("Barivia viz server failed to start:",e)}const e=M.server;e.oninitialized=()=>{const t=e.getClientCapabilities(),o=r(t);d(!!o?.mimeTypes?.includes(n))},await M.connect(T)})().catch(console.error);
|
|
2
|
+
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 s,registerAppResource as r,RESOURCE_MIME_TYPE as n}from"@modelcontextprotocol/ext-apps/server";import{startVizServer as a}from"./viz-server.js";import{API_KEY as i,apiCall as l,apiRawCall as p,loadViewHtml as c,setVizPort as u,setClientSupportsMcpApps as d,CLIENT_VERSION as m}from"./shared.js";import{registerDatasetsTool as f}from"./tools/datasets.js";import{registerTrainTool as h}from"./tools/train.js";import{registerJobsTool as g,JOBS_DESCRIPTION_BASE as b}from"./tools/jobs.js";import{registerResultsTool as _}from"./tools/results.js";import{registerExploreMapTool as y,RESULTS_EXPLORER_URI as w}from"./tools/explore_map.js";import{registerAccountTool as v}from"./tools/account.js";import{registerInferenceTool as j}from"./tools/inference.js";import{registerGuideBarsomTool as k}from"./tools/guide_barsom.js";import{registerTrainingGuidanceTool as x}from"./tools/training_guidance.js";import{registerFeedbackTool as P}from"./tools/feedback.js";import{registerTrainingMonitorTool as A,TRAINING_MONITOR_URI as I}from"./tools/training_monitor.js";import{resolvePrepareTrainingPrompt as C}from"./prepare_training_prompt.js";i||(console.error("Error: BARIVIA_API_KEY not set. Set it in your MCP client config."),process.exit(1));const T=new e({name:"analytics-engine",version:m,instructions:'# Barivia Mapping Analytics Engine\n\nSelf-organizing map (SOM) analytics: project high-dimensional data to a 2D grid for clusters, gradients, and anomalies.\n\n## Workflow (short)\n\nUpload (`datasets(upload)`) → `datasets(preview)` and `datasets(analyze)` before train → submit one of `train(action=map)`, `train(action=siom_map)`, or `train(action=floop_siom)` (only if plan allows FLooP) → poll `jobs(status)` every 10–15s until `completed` → `results(get)` for metrics and figures (there is no separate analyze tool), then `results_explorer(job_id)` to browse the figures interactively. Then `jobs(compare)`, `results(download/recolor)`, or `inference` (predict / compare / project_columns / impute_column / transition_flow) as needed.\n\n**Full detail:** Call `guide_barsom_workflow` for plan-scoped tool map, training modes, async rules, optional MCP App UIs, and step-by-step SOP (from the Barivia API when online).\n\n## Tool taxonomy (one tool per stage — do not mix)\n\n| Stage | Tool(s) | Contract |\n|-------|---------|----------|\n| Data | `datasets` | upload, preview, analyze, list, **get** (status/staging), subset, add_expression, reduce_spectral, delete — ingest + feature engineering, no map |\n| Train submit | `train` | map, siom_map, impute (sparse → map + dense imputed.csv), floop_siom (entitled). Async, returns job_id. Submit only |\n| Lifecycle | `jobs` | status, list (slim + limit/cursor/status/job_type/has_results), update (label/description/tags), compare (**training runs**), cancel, delete. No training/scoring here |\n| Artifacts | `results` | get (default=combined only; figures="none" for metrics-only sweeps; figures="all" or results_explorer/download for every plot), export, download, recolor (async). Read/render completed jobs. Results catalog = `jobs(list, has_results=true)` |\n| Frozen-map ops | `inference` | predict (regime-aware; "compact"|"annotated"), batch_predict, impute_column (neighbor-pool fill for a non-training column), compare (cohort density-diff), project_columns, transition_flow (time-ordered rows only), report. No weight updates |\n| Account | `account` | status, history, **inventory** (markdown datasets+jobs overview), add_funds, **health** (API liveness via GET /health — no R2) |\n| Bootstrap | `guide_barsom_workflow`, `train`, `training_guidance`, `prepare_training` prompt | orientation, submit map/siom/impute/floop, parameter hints, narrative checklist (API/tier-scoped) |\n| Explore (UI) | `results_explorer`, `training_monitor` | richer UX over the same API; `jobs(status)`+`results(get)` still suffice headless |\n| Other | `send_feedback` | only after user agrees; queues locally if API/storage is down |\n\n### Avoid these mis-routes\n- `jobs(compare)` = metrics table across **training runs** (pick best in a sweep). `inference(compare)` = density-diff of one map vs a **second dataset** (drift/cohort). Different tools.\n- Post-training map analysis (predict, compare, project_columns, impute_column, **transition_flow**) is all **inference** — not `results`. `results` is presentation/artifacts only (`recolor` stays here: it re-renders, it doesn\'t touch data).\n- Project a quantity onto the map: existing columns → `inference(project_columns)`; a formula → `datasets(add_expression, project_onto_job=<job>)`.\n\n## Async pattern\n\n- **Manual poll:** Training submits return `job_id` immediately — poll `jobs(status)` every 10–15s. **Running is not failed**; large grids or FLooP-SIOM can take many minutes. `max_nodes` (FLooP) is a total node budget, not grid side length.\n- **Often auto-polled:** `inference` actions (incl. `transition_flow`) and `results(recolor)` may wait in-proxy; if you get a `job_id`, poll `jobs(status)` the same way.\n\nCredits: jobs consume compute credits; check `account(status)` before big runs. Per-request HTTP timeout defaults to 60s (`datasets(analyze)` is async + auto-polled, so it is not bound by it); on slow networks raise `BARIVIA_FETCH_TIMEOUT_MS`. Timeouts are NOT auto-retried (avoids duplicating slow/expensive work).\n\n## Org inventory recipe\n\nTo rebuild a durable markdown overview of an org (datasets + jobs):\n1. `account(action=inventory)` — preferred one-shot (datasets name/id/rows/cols/description/tags + slim recent jobs).\n2. Or compose: `datasets(list)` + `jobs(action=list, limit=50)` (page with `cursor` / filter with `status` / `job_type` / `has_results=true` for a results catalog).\n3. Set `description` + `tags` on `datasets(upload|update)` and `train(...)` / `jobs(update)` so duplicates stay navigable.\n\n## API briefly unavailable (pause, do not crash)\nIf tools return `api_unreachable` / gateway HTML / slim `error_code` lines: summarize in **one calm sentence** — never paste Cloudflare HTML. The proxy already burst-retried (**3** tries, ~**2s** apart); honor `retry_after_sec` (~**30**) before another round. Use `account(action=health)` (no R2). Local downloads and prior results stay usable; `send_feedback` queues under `~/.barivia/deferred-feedback` and flushes when the API returns. Do not start large uploads/trains until `account(status)` or health looks healthy again.\n\n## Constraints\n\n- **Do not auto-subset:** Never call `datasets(subset)` or `sample_n` unless the user explicitly asks for a filter, slice, or random sample. Train and analyze on the uploaded `dataset_id` at full row count — GPU staging/prepare handles hundreds of thousands of rows quickly; silent downsampling hides real prepare/train behavior and wastes scale demos. Use small named fixtures (e.g. sample.csv, periodic_load) only when the user or runbook names them.\n- Prep ladder: `prepare_training` prompt = narrative checklist; `training_guidance` = structured hints (presets, resolved normalization/transforms, grid/epochs). Review these, then submit with `train`. Do not guess tiers or FLooP entitlement.\n- `inference(predict)`: prefer `dataset_id` for batch and for SIOM/irregular maps; single-row `rows` uses a fast path that can fail on some topologies — retry with `dataset_id`. FLooP-SIOM: if predict jobs fail while grid SIOM works, capture errors + `job_id`.\n- Column names are case-sensitive — match `datasets(preview)`.\n- Default training path is numeric/cyclic/temporal; use explicit `categorical_features` for baseline categoricals. `predict` must match the model contract.\n- After `results(recolor)`, `inference(transition_flow)`, or `inference(project_columns)`, use the **new** `job_id` returned for follow-up `results` if applicable.\n- **Large results / MCP timeouts:** For sweeps, `jobs(compare)`, or metrics-only reads use `results(action=get, figures="none")`. Default `get` inlines combined only. For every plot use `results_explorer` or `results(action=download)`, not `figures="all"` in chat unless the user wants all images inline.\n\n## UI delivery (heterogeneous MCP clients)\n\nHosts differ (Cursor, Claude Desktop, CLI agents): some embed MCP App panels; others list `ui://` but never mount them (expected on some Cursor builds); others are text-only. Tool results **always lead with a standalone localhost URL** — that is the intended fallback, not a broken App.\n\n| Tier | When | Agent should |\n|------|------|--------------|\n| **embedded** | Client advertises MCP Apps | Let the panel render **and** surface the standalone localhost URL from tool output / `ui_delivery.urls` (hosts often list Apps but do not mount). |\n| **localhost** | No MCP Apps; viz server running | Post the markdown link from tool output to the user (never broken workspace paths). Re-post after job completes if needed. |\n| **inline_image** | No Apps / override `inline` | Summarize metrics; inline raster (combined.png, learning curve) is attached automatically — show it in the reply. |\n| **text_only** | Viz server down | Use `jobs(status)` + `results(get)`; no panel or localhost link. |\n\nEvery `training_monitor` / `results_explorer` response includes a structured `ui_delivery` block (`delivery`, `urls`, `hint_for_agent`). Follow `hint_for_agent` without user prompting. Standalone viz pages cross-link monitor ↔ results for the same `job_id`.\n\n**Env overrides:** `BARIVIA_UI_DELIVERY=auto|apps|localhost|inline` (default `auto`). `BARIVIA_VIZ_PORT` pins the localhost viz port across proxy restarts. **Diagnostics:** `account(action=status)` reports detected MCP Apps support, viz port, and active tier.'});r(T,w,w,{mimeType:n},async()=>{const e=await c("results-explorer");return{contents:[{uri:w,mimeType:n,text:e??"<html><body>Results Explorer view not built yet. Run: npm run build:views</body></html>"}]}}),r(T,I,I,{mimeType:n},async()=>{const e=await c("training-monitor");return{contents:[{uri:I,mimeType:n,text:e??"<html><body>Training Monitor view not built yet.</body></html>"}]}}),k(T),y(T),A(T),f(T),h(T),g(T,b),_(T),v(T),j(T),x(T),P(T),T.prompt("info","Short orientation for the Barivia Mapping MCP. For full plan-scoped workflow, tool map, and SOP, the model should call guide_barsom_workflow. Use when the user asks what this MCP can do or how to get started.",{},()=>({messages:[{role:"user",content:{type:"text",text:["Give a concise, scannable answer (headers + bullets):","","**What it is:** MCP client to the Barivia mapping engine (2D SOM / SIOM / FLooP-SIOM when entitled) over HTTPS.","","**First step:** Call `guide_barsom_workflow` for plan-scoped bootstrap (full tool list, async rules, training modes, optional MCP Apps, SOP).","","**Core path:** `datasets(upload)` → `datasets(preview)` + `datasets(analyze)` → choose training action → poll `jobs(status)` every 10–15s until completed → `results(get)` (all main figures/metrics; no separate analyze tool) → `results_explorer(job_id)` to browse figures.","",'**Key tools:** `datasets` (data; reduce_spectral for spectra/long blocks), `train` (map/siom_map/impute/floop_siom; pass `label` for readable compare rows; preset=quick for a fast first map), `jobs` (status/list/compare/cancel/delete — lifecycle only), `results` (get/download/export/recolor; figures="none" for metrics-only), `inference` (predict; batch_predict; impute_column for topology-neighbor pool fill; compare; project_columns; transition_flow; report), `account` (status/credits/queue).',"","**Prep help:** `prepare_training` prompt (checklist) · `training_guidance` (presets/JSON hints + resolved normalization/params) — review, then submit with `train`.","","**Browse results:** `results_explorer(job_id)` is the preferred way to explore figures after a first `results(get)` glance; `training_monitor` shows live progress. Both optional — `results` + `jobs(status)` suffice headless.","","**After training:** `jobs(compare)` across runs, `results(recolor)`, `inference(project_columns)` for variables not in training, `inference(transition_flow)` only if rows are time-ordered.","","**Rules:** Running ≠ failed. Column names must match `datasets(preview)` exactly. Do not auto-subset — never call `datasets(subset)` / `sample_n` unless the user explicitly asks; train on the full uploaded `dataset_id`. Do not call `_fetch_figure` from chat (host/UI only); use `results(get)` or `results_explorer`.","","Offer `send_feedback` only after asking the user."].join("\n")}}]})),T.prompt("prepare_training","Narrative pre-training checklist (prompt). Use after upload and before train. Content is tier-scoped from the API when online. Prep ladder: this prompt = story checklist; training_guidance tool = JSON presets/parameter hints. Review, then submit with train.",{dataset_id:o.string().describe("Dataset ID to prepare for training")},async({dataset_id:e})=>{const t=await C(e);return{messages:[{role:"user",content:{type:"text",text:(t.used_fallback?`[offline fallback ${t.fallback_version}]\n\n`:"")+t.text}}]}});const M=new t;(async function(){try{const e=await a(l,p,c);u(e)}catch(e){process.env.BARIVIA_VIZ_PORT&&console.error("Barivia viz server failed to start:",e)}const e=T.server;e.oninitialized=()=>{const t=e.getClientCapabilities(),o=s(t);d(!!o?.mimeTypes?.includes(n))},await T.connect(M)})().catch(console.error);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** Shared markdown inventory formatting for account(inventory) / jobs list. */
|
|
2
|
+
export function formatTags(tags) {
|
|
3
|
+
if (!Array.isArray(tags) || tags.length === 0)
|
|
4
|
+
return "";
|
|
5
|
+
return tags.map((t) => String(t)).filter(Boolean).join(", ");
|
|
6
|
+
}
|
|
7
|
+
export function formatDatasetInventoryLine(ds) {
|
|
8
|
+
const id = String(ds.id ?? "");
|
|
9
|
+
const name = String(ds.name ?? "");
|
|
10
|
+
const rows = ds.rows != null ? Number(ds.rows) : "?";
|
|
11
|
+
const cols = ds.cols != null ? Number(ds.cols) : "?";
|
|
12
|
+
const st = ds.status != null ? String(ds.status) : "ready";
|
|
13
|
+
const statusBit = st !== "ready" ? ` | status=${st}` : "";
|
|
14
|
+
const desc = ds.description != null && String(ds.description).trim() !== ""
|
|
15
|
+
? ` — ${String(ds.description).trim()}`
|
|
16
|
+
: "";
|
|
17
|
+
const tags = formatTags(ds.tags);
|
|
18
|
+
const tagsBit = tags ? ` [${tags}]` : "";
|
|
19
|
+
return `${name} (${id}) — ${rows}×${cols}${statusBit}${tagsBit}${desc}`;
|
|
20
|
+
}
|
|
21
|
+
export function formatJobInventoryLine(job) {
|
|
22
|
+
const id = String(job.id ?? "");
|
|
23
|
+
const st = String(job.status ?? "");
|
|
24
|
+
const label = job.label != null && job.label !== "" ? String(job.label) : null;
|
|
25
|
+
const jt = job.job_type != null ? String(job.job_type) : "?";
|
|
26
|
+
const ds = job.dataset_id != null ? String(job.dataset_id) : "";
|
|
27
|
+
const created = job.created_at != null ? String(job.created_at) : "";
|
|
28
|
+
const result = job.result_ref != null && String(job.result_ref) !== "" ? "yes" : "no";
|
|
29
|
+
const tags = formatTags(job.tags);
|
|
30
|
+
const tagsBit = tags ? ` [${tags}]` : "";
|
|
31
|
+
const desc = job.description != null && String(job.description).trim() !== ""
|
|
32
|
+
? ` — ${String(job.description).trim()}`
|
|
33
|
+
: "";
|
|
34
|
+
const head = label ? `${label} (id: ${id})` : `id: ${id}`;
|
|
35
|
+
return `${head} — ${st} | type=${jt} | dataset=${ds} | created=${created} | result=${result}${tagsBit}${desc}`;
|
|
36
|
+
}
|
|
37
|
+
export function extractJobsList(data) {
|
|
38
|
+
if (Array.isArray(data)) {
|
|
39
|
+
return { jobs: data, next_cursor: null };
|
|
40
|
+
}
|
|
41
|
+
if (data && typeof data === "object") {
|
|
42
|
+
const obj = data;
|
|
43
|
+
const jobs = Array.isArray(obj.jobs) ? obj.jobs : [];
|
|
44
|
+
const next = obj.next_cursor != null && String(obj.next_cursor) !== ""
|
|
45
|
+
? String(obj.next_cursor)
|
|
46
|
+
: null;
|
|
47
|
+
return { jobs, next_cursor: next };
|
|
48
|
+
}
|
|
49
|
+
return { jobs: [], next_cursor: null };
|
|
50
|
+
}
|
|
51
|
+
export function buildJobsListQuery(opts) {
|
|
52
|
+
const qs = new URLSearchParams();
|
|
53
|
+
if (opts.dataset_id)
|
|
54
|
+
qs.set("dataset_id", opts.dataset_id);
|
|
55
|
+
if (opts.status)
|
|
56
|
+
qs.set("status", opts.status);
|
|
57
|
+
if (opts.job_type)
|
|
58
|
+
qs.set("job_type", opts.job_type);
|
|
59
|
+
if (opts.has_results)
|
|
60
|
+
qs.set("has_results", "true");
|
|
61
|
+
if (opts.limit != null)
|
|
62
|
+
qs.set("limit", String(opts.limit));
|
|
63
|
+
if (opts.cursor)
|
|
64
|
+
qs.set("cursor", opts.cursor);
|
|
65
|
+
if (opts.fields)
|
|
66
|
+
qs.set("fields", opts.fields);
|
|
67
|
+
const s = qs.toString();
|
|
68
|
+
return s ? `/v1/jobs?${s}` : "/v1/jobs";
|
|
69
|
+
}
|
package/dist/shared.js
CHANGED
|
@@ -20,12 +20,16 @@ export const API_URL = process.env.BARIVIA_API_URL ??
|
|
|
20
20
|
"https://api.barivia.se";
|
|
21
21
|
export const API_KEY = process.env.BARIVIA_API_KEY ?? process.env.BARSOM_API_KEY ?? "";
|
|
22
22
|
export const FETCH_TIMEOUT_MS = parseInt(process.env.BARIVIA_FETCH_TIMEOUT_MS ?? "60000", 10);
|
|
23
|
-
|
|
24
|
-
export const
|
|
23
|
+
/** Retries after the first attempt; default 2 → 3 total tries. Override with BARIVIA_MAX_RETRIES. */
|
|
24
|
+
export const MAX_RETRIES = parseInt(process.env.BARIVIA_MAX_RETRIES ?? "2", 10);
|
|
25
|
+
/** Delay between burst retries (ms). Default 2000 → ~2s between each of the 3 attempts. */
|
|
26
|
+
export const RETRY_BASE_MS = parseInt(process.env.BARIVIA_RETRY_BASE_MS ?? "2000", 10);
|
|
25
27
|
export const RETRYABLE_STATUS = new Set([502, 503, 504]);
|
|
26
|
-
/**
|
|
27
|
-
export
|
|
28
|
-
|
|
28
|
+
/** Cool-down hint after the burst is exhausted — agents should wait before another round. */
|
|
29
|
+
export const UNAVAILABLE_RETRY_AFTER_SEC = 30;
|
|
30
|
+
/** Fixed delay between burst retries (not exponential). */
|
|
31
|
+
export function retryDelayMs(_attempt) {
|
|
32
|
+
return RETRY_BASE_MS;
|
|
29
33
|
}
|
|
30
34
|
function newRequestId() {
|
|
31
35
|
return randomUUID();
|
|
@@ -35,7 +39,7 @@ function newRequestId() {
|
|
|
35
39
|
* X-Barsom-Client-Version so the server can annotate tool guidance with the
|
|
36
40
|
* wrapper version each action requires. Keep in sync with package.json on bump.
|
|
37
41
|
*/
|
|
38
|
-
export const CLIENT_VERSION = "0.
|
|
42
|
+
export const CLIENT_VERSION = "0.23.0";
|
|
39
43
|
/** User-facing links; keep aligned with barivia.se / api.barivia.se. */
|
|
40
44
|
export const PUBLIC_SITE_ORIGIN = "https://barivia.se";
|
|
41
45
|
/** Self-serve account dashboard (manage plan, billing, and API keys). */
|
|
@@ -374,52 +378,237 @@ export async function resolveFilePathForUpload(filePath, mcpServer) {
|
|
|
374
378
|
`"C:\\\\Users\\\\you\\\\data.csv") or a file:// URI. For relative paths, set BARIVIA_WORKSPACE_ROOT in your MCP config.`);
|
|
375
379
|
}
|
|
376
380
|
}
|
|
377
|
-
|
|
378
|
-
|
|
381
|
+
function looksLikeHtml(bodyText) {
|
|
382
|
+
const t = bodyText.trim().slice(0, 200).toLowerCase();
|
|
383
|
+
return t.startsWith("<!doctype") || t.startsWith("<html") || /<\s*html[\s>]/.test(t);
|
|
384
|
+
}
|
|
385
|
+
function isStorageErrorCode(code) {
|
|
386
|
+
if (!code)
|
|
387
|
+
return false;
|
|
388
|
+
const c = code.toLowerCase();
|
|
389
|
+
return c === "storage_upload_failed" || (c.includes("storage") && !c.includes("unavailable"));
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Classify an HTTP API failure into a compact structured error.
|
|
393
|
+
* Gateway HTML 502/503 bodies are never echoed — they become api_unreachable.
|
|
394
|
+
*/
|
|
395
|
+
export function classifyApiError(status, bodyText, requestId) {
|
|
379
396
|
let parsed = null;
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
397
|
+
const html = looksLikeHtml(bodyText);
|
|
398
|
+
if (!html && bodyText.trim()) {
|
|
399
|
+
try {
|
|
400
|
+
const j = JSON.parse(bodyText);
|
|
401
|
+
if (j && typeof j === "object")
|
|
402
|
+
parsed = j;
|
|
403
|
+
}
|
|
404
|
+
catch {
|
|
405
|
+
/* non-JSON */
|
|
406
|
+
}
|
|
387
407
|
}
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
408
|
+
const apiCode = parsed?.error_code != null ? String(parsed.error_code) : "";
|
|
409
|
+
const apiMsg = parsed?.error != null && String(parsed.error).trim()
|
|
410
|
+
? String(parsed.error).trim()
|
|
411
|
+
: "";
|
|
412
|
+
let cause = "server";
|
|
413
|
+
let error_code = apiCode || `http_${status}`;
|
|
414
|
+
let message = apiMsg || `HTTP ${status}`;
|
|
415
|
+
let hint = "";
|
|
416
|
+
let retryable = status === 502 || status === 503 || status === 504;
|
|
391
417
|
const accountHint = ` Check your email or manage your key at ${PUBLIC_DASHBOARD_URL} if needed.`;
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
418
|
+
if (status === 400) {
|
|
419
|
+
cause = "client";
|
|
420
|
+
hint = "Check parameter types and required fields.";
|
|
421
|
+
retryable = false;
|
|
422
|
+
}
|
|
423
|
+
else if (status === 401) {
|
|
424
|
+
cause = "auth";
|
|
425
|
+
error_code = apiCode || "unauthorized";
|
|
426
|
+
hint = `Check BARIVIA_API_KEY in your MCP config.${accountHint}`;
|
|
427
|
+
retryable = false;
|
|
428
|
+
}
|
|
429
|
+
else if (status === 403) {
|
|
430
|
+
cause = "auth";
|
|
431
|
+
error_code = apiCode || "forbidden";
|
|
432
|
+
hint = `Access denied for this operation (plan or permissions).${accountHint}`;
|
|
433
|
+
retryable = false;
|
|
434
|
+
}
|
|
435
|
+
else if (status === 402) {
|
|
436
|
+
cause = "client";
|
|
437
|
+
hint = `Credits or subscription may be insufficient — upgrade at ${PUBLIC_DASHBOARD_URL}.`;
|
|
438
|
+
retryable = false;
|
|
439
|
+
}
|
|
440
|
+
else if (status === 404) {
|
|
441
|
+
cause = "client";
|
|
442
|
+
hint = "The resource may not exist or may have been deleted.";
|
|
443
|
+
retryable = false;
|
|
444
|
+
}
|
|
445
|
+
else if (status === 409) {
|
|
446
|
+
cause = "client";
|
|
447
|
+
hint = "The job may not be in the expected state.";
|
|
448
|
+
retryable = false;
|
|
449
|
+
}
|
|
450
|
+
else if (status === 429) {
|
|
451
|
+
cause = "rate_limit";
|
|
452
|
+
hint = "Plan limit (e.g. dataset cap) or rate limit — delete unused datasets or wait and retry.";
|
|
453
|
+
retryable = true;
|
|
454
|
+
}
|
|
455
|
+
else if (status === 502 || status === 503 || status === 504) {
|
|
456
|
+
if (isStorageErrorCode(apiCode) || /object storage|r2\/s3|storage upload/i.test(apiMsg)) {
|
|
457
|
+
cause = "storage";
|
|
458
|
+
error_code = apiCode || "storage_upload_failed";
|
|
459
|
+
message = apiMsg || "Object storage (R2/S3) error from API.";
|
|
460
|
+
hint = "Retry later; feedback may still succeed via DB when storage is degraded.";
|
|
461
|
+
}
|
|
462
|
+
else if (html || !parsed) {
|
|
463
|
+
cause = "api_unreachable";
|
|
464
|
+
error_code = apiCode || "api_unreachable";
|
|
465
|
+
message = "API temporarily unavailable (gateway/tunnel returned a non-JSON error).";
|
|
466
|
+
hint =
|
|
467
|
+
`Proxy already tried 3 times (~2s apart). Wait ~${UNAVAILABLE_RETRY_AFTER_SEC}s, then retry. ` +
|
|
468
|
+
"Use account(action=health) — /health does not depend on R2. Pause — do not paste HTML.";
|
|
469
|
+
}
|
|
470
|
+
else if (apiCode === "system_info_unavailable") {
|
|
471
|
+
cause = "server";
|
|
472
|
+
message = apiMsg || "System info (plan/queue) temporarily unavailable.";
|
|
473
|
+
hint = `Wait ~${UNAVAILABLE_RETRY_AFTER_SEC}s, then retry.`;
|
|
474
|
+
}
|
|
475
|
+
else if (apiCode === "feedback_unavailable" || apiCode === "database_error") {
|
|
476
|
+
cause = "server";
|
|
477
|
+
error_code = apiCode;
|
|
478
|
+
message = apiMsg || "API or database temporarily unavailable.";
|
|
479
|
+
hint =
|
|
480
|
+
`Wait ~${UNAVAILABLE_RETRY_AFTER_SEC}s; send_feedback queues locally if the API stays down.`;
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
cause = "server";
|
|
484
|
+
message = apiMsg || `HTTP ${status}`;
|
|
485
|
+
hint = `API or database temporarily unavailable — wait ~${UNAVAILABLE_RETRY_AFTER_SEC}s, then retry.`;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
else if (status >= 500) {
|
|
489
|
+
cause = "server";
|
|
490
|
+
hint = `Server error — wait ~${UNAVAILABLE_RETRY_AFTER_SEC}s, then retry.`;
|
|
491
|
+
retryable = true;
|
|
492
|
+
}
|
|
493
|
+
else if (!apiMsg && html) {
|
|
494
|
+
cause = "api_unreachable";
|
|
495
|
+
error_code = "api_unreachable";
|
|
496
|
+
message = `HTTP ${status} (non-JSON gateway body)`;
|
|
497
|
+
hint = `Wait ~${UNAVAILABLE_RETRY_AFTER_SEC}s, then retry. Do not paste HTML.`;
|
|
498
|
+
retryable = status >= 500;
|
|
499
|
+
}
|
|
500
|
+
const retry_after_sec = retryable ? UNAVAILABLE_RETRY_AFTER_SEC : undefined;
|
|
501
|
+
return {
|
|
502
|
+
status,
|
|
503
|
+
error_code,
|
|
504
|
+
message,
|
|
505
|
+
hint,
|
|
506
|
+
request_id: requestId,
|
|
507
|
+
retryable,
|
|
508
|
+
cause,
|
|
509
|
+
...(retry_after_sec !== undefined ? { retry_after_sec } : {}),
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
/** User-visible API error line (slim; includes request id). Exported for tests. */
|
|
513
|
+
export function formatApiErrorMessage(status, bodyText, requestId) {
|
|
514
|
+
const c = classifyApiError(status, bodyText, requestId);
|
|
515
|
+
const parts = [
|
|
516
|
+
c.message,
|
|
517
|
+
`error_code=${c.error_code}`,
|
|
518
|
+
`cause=${c.cause}`,
|
|
519
|
+
c.retry_after_sec != null ? `retry_after_sec=${c.retry_after_sec}` : "",
|
|
520
|
+
c.hint,
|
|
521
|
+
`request_id=${c.request_id}`,
|
|
522
|
+
].filter((p) => p && String(p).trim());
|
|
523
|
+
return parts.join(" | ");
|
|
417
524
|
}
|
|
418
525
|
function throwApiError(status, bodyText, requestId) {
|
|
526
|
+
const classified = classifyApiError(status, bodyText, requestId);
|
|
419
527
|
const err = new Error(formatApiErrorMessage(status, bodyText, requestId));
|
|
420
528
|
err.httpStatus = status;
|
|
529
|
+
err.errorCode = classified.error_code;
|
|
530
|
+
err.requestId = requestId;
|
|
531
|
+
err.retryable = classified.retryable;
|
|
532
|
+
err.cause = classified.cause;
|
|
533
|
+
err.retryAfterSec = classified.retry_after_sec;
|
|
534
|
+
err.classified = classified;
|
|
421
535
|
throw err;
|
|
422
536
|
}
|
|
537
|
+
/** Final network failure after the burst — same calm shape as gateway HTML 502. */
|
|
538
|
+
function throwNetworkUnreachable(requestId, err) {
|
|
539
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
540
|
+
const classified = {
|
|
541
|
+
status: 0,
|
|
542
|
+
error_code: "api_unreachable",
|
|
543
|
+
message: "API temporarily unavailable (network error).",
|
|
544
|
+
hint: `Proxy already tried 3 times (~2s apart). Wait ~${UNAVAILABLE_RETRY_AFTER_SEC}s, then retry. ` +
|
|
545
|
+
`Detail: ${detail.slice(0, 120)}`,
|
|
546
|
+
request_id: requestId,
|
|
547
|
+
retryable: true,
|
|
548
|
+
cause: "api_unreachable",
|
|
549
|
+
retry_after_sec: UNAVAILABLE_RETRY_AFTER_SEC,
|
|
550
|
+
};
|
|
551
|
+
const msg = [
|
|
552
|
+
classified.message,
|
|
553
|
+
`error_code=${classified.error_code}`,
|
|
554
|
+
`cause=${classified.cause}`,
|
|
555
|
+
`retry_after_sec=${classified.retry_after_sec}`,
|
|
556
|
+
classified.hint,
|
|
557
|
+
`request_id=${classified.request_id}`,
|
|
558
|
+
].join(" | ");
|
|
559
|
+
const out = new Error(msg);
|
|
560
|
+
out.httpStatus = 0;
|
|
561
|
+
out.errorCode = classified.error_code;
|
|
562
|
+
out.requestId = requestId;
|
|
563
|
+
out.retryable = true;
|
|
564
|
+
out.cause = "api_unreachable";
|
|
565
|
+
out.retryAfterSec = UNAVAILABLE_RETRY_AFTER_SEC;
|
|
566
|
+
out.classified = classified;
|
|
567
|
+
throw out;
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Liveness (+ optional readiness) probe against the API origin.
|
|
571
|
+
* Uses GET /health which never touches R2 — safe during storage outages.
|
|
572
|
+
*/
|
|
573
|
+
export async function probeApiHealth() {
|
|
574
|
+
const base = { storage: "not_probed" };
|
|
575
|
+
try {
|
|
576
|
+
const live = await fetchWithTimeout(`${API_URL}/health`, { method: "GET" }, 8_000);
|
|
577
|
+
const liveText = await live.text();
|
|
578
|
+
if (!live.ok) {
|
|
579
|
+
return { ...base, liveness: "down", readiness: "unknown", detail: `health HTTP ${live.status}` };
|
|
580
|
+
}
|
|
581
|
+
let readiness = "unknown";
|
|
582
|
+
let db;
|
|
583
|
+
let redis;
|
|
584
|
+
try {
|
|
585
|
+
const ready = await fetchWithTimeout(`${API_URL}/ready`, { method: "GET" }, 8_000);
|
|
586
|
+
const readyText = await ready.text();
|
|
587
|
+
try {
|
|
588
|
+
const j = JSON.parse(readyText);
|
|
589
|
+
readiness = ready.ok && j.status === "ready" ? "ready" : "not_ready";
|
|
590
|
+
db = j.db;
|
|
591
|
+
redis = j.redis;
|
|
592
|
+
}
|
|
593
|
+
catch {
|
|
594
|
+
readiness = ready.ok ? "ready" : "not_ready";
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
catch {
|
|
598
|
+
readiness = "unknown";
|
|
599
|
+
}
|
|
600
|
+
void liveText;
|
|
601
|
+
return { ...base, liveness: "ok", readiness, db, redis };
|
|
602
|
+
}
|
|
603
|
+
catch (err) {
|
|
604
|
+
return {
|
|
605
|
+
...base,
|
|
606
|
+
liveness: "down",
|
|
607
|
+
readiness: "unknown",
|
|
608
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
}
|
|
423
612
|
/**
|
|
424
613
|
* @param requestTimeoutMs Optional per-request timeout (default `BARIVIA_FETCH_TIMEOUT_MS`).
|
|
425
614
|
*/
|
|
@@ -532,9 +721,13 @@ export async function apiCall(method, path, body, extraHeaders, requestTimeoutMs
|
|
|
532
721
|
!err.httpStatus) {
|
|
533
722
|
throw new Error(`Request timed out after ${effectiveTimeout}ms. Increase BARIVIA_FETCH_TIMEOUT_MS in your MCP env (e.g. 120000) for slow or large requests. (request id: ${requestId})`);
|
|
534
723
|
}
|
|
724
|
+
if (err instanceof TypeError)
|
|
725
|
+
throwNetworkUnreachable(requestId, err);
|
|
535
726
|
throw err;
|
|
536
727
|
}
|
|
537
728
|
}
|
|
729
|
+
if (lastError instanceof TypeError)
|
|
730
|
+
throwNetworkUnreachable(requestId, lastError);
|
|
538
731
|
throw lastError;
|
|
539
732
|
}
|
|
540
733
|
/** Fetch raw bytes from the API (for image downloads). */
|
|
@@ -596,9 +789,13 @@ export async function apiRawCall(path, requestTimeoutMs) {
|
|
|
596
789
|
!err.httpStatus) {
|
|
597
790
|
throw new Error(`Request timed out after ${effectiveTimeout}ms. Increase BARIVIA_FETCH_TIMEOUT_MS (e.g. 120000) for large images. (request id: ${requestId})`);
|
|
598
791
|
}
|
|
792
|
+
if (err instanceof TypeError)
|
|
793
|
+
throwNetworkUnreachable(requestId, err);
|
|
599
794
|
throw err;
|
|
600
795
|
}
|
|
601
796
|
}
|
|
797
|
+
if (lastError instanceof TypeError)
|
|
798
|
+
throwNetworkUnreachable(requestId, lastError);
|
|
602
799
|
throw lastError;
|
|
603
800
|
}
|
|
604
801
|
// ---------------------------------------------------------------------------
|
package/dist/tools/account.js
CHANGED
|
@@ -1,27 +1,95 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { registerAuditedTool } from "../audit.js";
|
|
3
|
-
import { apiCall, CLIENT_VERSION } from "../shared.js";
|
|
3
|
+
import { apiCall, CLIENT_VERSION, probeApiHealth, structuredTextResult, UNAVAILABLE_RETRY_AFTER_SEC, } from "../shared.js";
|
|
4
4
|
import { getMcpClientDiagnostics } from "../ui-delivery.js";
|
|
5
|
+
import { listDeferredFeedback } from "../deferred_feedback.js";
|
|
6
|
+
import { buildJobsListQuery, extractJobsList, formatDatasetInventoryLine, formatJobInventoryLine, } from "../inventory_format.js";
|
|
7
|
+
function degradedStatusResult(opts) {
|
|
8
|
+
const retry_after_sec = UNAVAILABLE_RETRY_AFTER_SEC;
|
|
9
|
+
const payload = {
|
|
10
|
+
ok: false,
|
|
11
|
+
retry_after_sec,
|
|
12
|
+
api_reachable: false,
|
|
13
|
+
message: "Barivia is temporarily unavailable (we could not reach the API). " +
|
|
14
|
+
"Your local files and prior downloads are still fine. Try again in about 30 seconds.",
|
|
15
|
+
offline_ok: [
|
|
16
|
+
"already-downloaded results",
|
|
17
|
+
"local CSVs",
|
|
18
|
+
"draft notes / deferred feedback queue",
|
|
19
|
+
],
|
|
20
|
+
needs_api: ["upload", "train", "feedback flush", "list/status", "other cloud tools"],
|
|
21
|
+
request_id: opts.requestId ?? null,
|
|
22
|
+
hint_for_host: "Barivia: reconnecting…",
|
|
23
|
+
detail: opts.detail ?? null,
|
|
24
|
+
};
|
|
25
|
+
const rid = opts.requestId
|
|
26
|
+
? ` (request id: ${opts.requestId} — include if contacting support)`
|
|
27
|
+
: "";
|
|
28
|
+
const text = [
|
|
29
|
+
payload.message + rid,
|
|
30
|
+
`retry_after_sec=${retry_after_sec} | api_reachable=false`,
|
|
31
|
+
`AGENT: Calm banner ("${payload.hint_for_host}"); do not dump HTML. Proxy already burst-retried; wait ~${retry_after_sec}s before another round. Do not start large uploads/trains until ok.`,
|
|
32
|
+
].join("\n");
|
|
33
|
+
return structuredTextResult(payload, text);
|
|
34
|
+
}
|
|
5
35
|
export function registerAccountTool(server) {
|
|
6
|
-
registerAuditedTool(server, "account", `Manage your Barivia account — check plan/license info, view usage history, add funds.
|
|
36
|
+
registerAuditedTool(server, "account", `Manage your Barivia account — check plan/license info, view usage history, rebuild an org inventory, add funds, or probe API health.
|
|
7
37
|
|
|
8
38
|
| Action | Use when |
|
|
9
39
|
|--------|----------|
|
|
10
|
-
| status | Before large jobs — see plan tier, GPU availability, queue depth, training time estimates, credit balance |
|
|
40
|
+
| status | Before large jobs — see plan tier, GPU availability, queue depth, training time estimates, credit balance. When the API is down, returns calm \`{ok:false, retry_after_sec}\` instead of HTML. |
|
|
11
41
|
| history | Viewing recent compute usage and credit spend |
|
|
42
|
+
| inventory | Rebuild a durable markdown overview of datasets + recent jobs (agent-friendly org catalog) |
|
|
12
43
|
| add_funds | Getting instructions to add credits |
|
|
44
|
+
| health | Diagnose API reachability during outages — GET /health + /ready; does **not** probe R2/object storage |
|
|
13
45
|
|
|
14
46
|
action=status: Returns plan tier, compute class (CPU/GPU), usage limits, live queue state, training time estimates, and credit balance.
|
|
15
47
|
Use BEFORE large jobs to check GPU availability and estimate wait time.
|
|
48
|
+
When the API/gateway is down, returns structured \`{ok:false, retry_after_sec, …}\` — treat as a pause, not a crash.
|
|
49
|
+
action=inventory: Lists datasets (name/id/rows/cols/description/tags) and slim recent jobs (label/id/status/job_type/dataset_id/created_at/result). Prefer this over hand-assembling datasets(list)+jobs(list). Page jobs with jobs(action=list, cursor=…).
|
|
50
|
+
action=health: Lightweight liveness/readiness (no R2). Use when tools return api_unreachable or before retrying deferred send_feedback.
|
|
16
51
|
Capacity is shared LOCAL/GKE worker pools — there is no per-key cloud burst lease.
|
|
17
52
|
NOT FOR: Training itself — use train(action=map). This tool only manages the account.`, {
|
|
18
53
|
action: z
|
|
19
|
-
.enum(["status", "history", "add_funds"])
|
|
20
|
-
.describe("status: plan/license/queue info; history: recent compute usage; add_funds: instructions"),
|
|
21
|
-
limit: z.number().optional().describe("action=history: number of records to return (default: 10)"),
|
|
54
|
+
.enum(["status", "history", "inventory", "add_funds", "health"])
|
|
55
|
+
.describe("status: plan/license/queue info; history: recent compute usage; inventory: datasets+jobs overview; add_funds: instructions; health: API liveness without R2"),
|
|
56
|
+
limit: z.number().optional().describe("action=history or inventory: number of job records to return (default: 10 history / 50 inventory)"),
|
|
22
57
|
}, async ({ action, limit }) => {
|
|
58
|
+
if (action === "health") {
|
|
59
|
+
const health = await probeApiHealth();
|
|
60
|
+
const deferred = await listDeferredFeedback("barsom").catch(() => []);
|
|
61
|
+
const lines = [
|
|
62
|
+
`API liveness: ${health.liveness}`,
|
|
63
|
+
`API readiness: ${health.readiness}`,
|
|
64
|
+
health.db ? `DB: ${health.db}` : null,
|
|
65
|
+
health.redis ? `Redis: ${health.redis}` : null,
|
|
66
|
+
`Object storage (R2): ${health.storage} (intentionally not checked)`,
|
|
67
|
+
health.detail ? `Detail: ${health.detail}` : null,
|
|
68
|
+
`Deferred feedback drafts (barsom): ${deferred.length}`,
|
|
69
|
+
health.liveness === "down"
|
|
70
|
+
? `retry_after_sec=${UNAVAILABLE_RETRY_AFTER_SEC} (wait before another tool round)`
|
|
71
|
+
: null,
|
|
72
|
+
].filter(Boolean);
|
|
73
|
+
return structuredTextResult({
|
|
74
|
+
...health,
|
|
75
|
+
deferred_feedback_pending: deferred.length,
|
|
76
|
+
...(health.liveness === "down"
|
|
77
|
+
? { ok: false, retry_after_sec: UNAVAILABLE_RETRY_AFTER_SEC }
|
|
78
|
+
: { ok: true }),
|
|
79
|
+
}, lines.join("\n"));
|
|
80
|
+
}
|
|
23
81
|
if (action === "status") {
|
|
24
|
-
|
|
82
|
+
let data;
|
|
83
|
+
try {
|
|
84
|
+
data = (await apiCall("GET", "/v1/system/info"));
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
const e = err;
|
|
88
|
+
return degradedStatusResult({
|
|
89
|
+
requestId: e.requestId,
|
|
90
|
+
detail: e.message,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
25
93
|
const plan = data.plan ?? {};
|
|
26
94
|
const backend = data.backend ?? {};
|
|
27
95
|
const status = data.status ?? {};
|
|
@@ -95,6 +163,42 @@ NOT FOR: Training itself — use train(action=map). This tool only manages the a
|
|
|
95
163
|
content: [{ type: "text", text: `Credit Balance: $${(data.credit_balance_cents / 100).toFixed(2)}\n\nRecent Usage:\n${history}` }]
|
|
96
164
|
};
|
|
97
165
|
}
|
|
166
|
+
if (action === "inventory") {
|
|
167
|
+
const jobLimit = limit && limit > 0 ? Math.min(Math.floor(limit), 100) : 50;
|
|
168
|
+
const [datasetsRaw, jobsRaw] = await Promise.all([
|
|
169
|
+
apiCall("GET", "/v1/datasets"),
|
|
170
|
+
apiCall("GET", buildJobsListQuery({ limit: jobLimit })),
|
|
171
|
+
]);
|
|
172
|
+
const datasets = Array.isArray(datasetsRaw) ? datasetsRaw : [];
|
|
173
|
+
const { jobs, next_cursor } = extractJobsList(jobsRaw);
|
|
174
|
+
const lines = [
|
|
175
|
+
`# Org inventory`,
|
|
176
|
+
``,
|
|
177
|
+
`## Datasets (${datasets.length})`,
|
|
178
|
+
];
|
|
179
|
+
if (datasets.length === 0) {
|
|
180
|
+
lines.push("(none)");
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
for (const ds of datasets)
|
|
184
|
+
lines.push(`- ${formatDatasetInventoryLine(ds)}`);
|
|
185
|
+
}
|
|
186
|
+
lines.push(``, `## Jobs (latest ${jobs.length}${next_cursor ? ", more available" : ""})`);
|
|
187
|
+
if (jobs.length === 0) {
|
|
188
|
+
lines.push("(none)");
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
for (const job of jobs)
|
|
192
|
+
lines.push(`- ${formatJobInventoryLine(job)}`);
|
|
193
|
+
}
|
|
194
|
+
if (next_cursor) {
|
|
195
|
+
lines.push(``);
|
|
196
|
+
lines.push(`More jobs: jobs(action=list, cursor="${next_cursor}", limit=${jobLimit})`);
|
|
197
|
+
}
|
|
198
|
+
lines.push(``);
|
|
199
|
+
lines.push(`Tips: set description/tags on datasets(upload|update) and train(...)/jobs(update). Filter jobs with status/job_type/has_results.`);
|
|
200
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
201
|
+
}
|
|
98
202
|
if (action === "add_funds") {
|
|
99
203
|
return {
|
|
100
204
|
content: [{ type: "text", text: `To add funds to your account, please visit the Barivia Billing Portal (integration pending) or ask your administrator to use the CLI tool:
|
|
@@ -102,7 +206,7 @@ bash scripts/billing/manage-credits.sh add <org_id> <amount_usd>` }]
|
|
|
102
206
|
};
|
|
103
207
|
}
|
|
104
208
|
return {
|
|
105
|
-
content: [{ type: "text", text: `Unknown action: ${action}. Valid: status, history, add_funds.` }]
|
|
209
|
+
content: [{ type: "text", text: `Unknown action: ${action}. Valid: status, history, inventory, add_funds, health.` }]
|
|
106
210
|
};
|
|
107
211
|
});
|
|
108
212
|
}
|
package/dist/tools/datasets.js
CHANGED
|
@@ -6,6 +6,7 @@ import { z } from "zod";
|
|
|
6
6
|
import { registerAuditedTool } from "../audit.js";
|
|
7
7
|
import { apiCall, getWorkspaceRootAsync, resolveFilePathForUpload, textResult, structuredTextResult, pollUntilComplete, POLL_DERIVE_MAX_MS, POLL_ANALYZE_MAX_MS, UPLOAD_DATASET_TIMEOUT_MS, LARGE_UPLOAD_BYTES, PRESIGNED_PUT_TIMEOUT_MS, POLL_STAGE_MAX_MS, streamFileSha256, putPresignedStream, resolveUploadContentType, suggestDatasetPreview, } from "../shared.js";
|
|
8
8
|
import { GZIP_UPLOAD_HINT } from "../job_status_format.js";
|
|
9
|
+
import { formatDatasetInventoryLine, formatTags } from "../inventory_format.js";
|
|
9
10
|
/**
|
|
10
11
|
* Normalize a nullable string field from the API. Returns "" for absent values,
|
|
11
12
|
* empty strings, and the literal SQL/serialization sentinels "missing"/"null"
|
|
@@ -137,8 +138,9 @@ Formats: plain CSV/TSV or gzip (.csv.gz / .tsv.gz). For files above ~100 MB, pre
|
|
|
137
138
|
| upload | You have a CSV or .csv.gz file to add — do this first |
|
|
138
139
|
| preview | Before train(action=map) — always preview an unfamiliar dataset to spot cyclics, nulls, column types |
|
|
139
140
|
| analyze | Pre-training column analysis: correlation matrix, periodicity ranking, column recommendations (train / drop / project later). Run after preview, before train_map. |
|
|
140
|
-
| list | Finding dataset IDs for train_map, preview, or subset — see all available datasets |
|
|
141
|
-
| get | Fetch one dataset by id — status, staging fields, ingest_error (use after upload or when staging is slow) |
|
|
141
|
+
| list | Finding dataset IDs for train_map, preview, or subset — see all available datasets (includes description/tags) |
|
|
142
|
+
| get | Fetch one dataset by id — status, staging fields, ingest_error, description/tags (use after upload or when staging is slow) |
|
|
143
|
+
| update | Edit name / description / tags on an existing dataset (helps navigate duplicates) |
|
|
142
144
|
| subset | ONLY when the user asks for row_range, filters, or sample_n — never auto-shrink before train/analyze |
|
|
143
145
|
| add_expression | Add a computed column from an expression (dataset_id + name + expression). With project_onto_job=<training_job_id>, instead projects the computed expression onto that map as a component plane in one step (no column added). |
|
|
144
146
|
| 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. |
|
|
@@ -151,9 +153,12 @@ Step 1 (after upload): always datasets(action=preview) to verify column types an
|
|
|
151
153
|
|
|
152
154
|
action=preview: Show columns, stats, sample rows, cyclic/datetime detections. ALWAYS preview before train(action=map) on an unfamiliar dataset.
|
|
153
155
|
action=analyze: Pre-training analysis on numeric columns — Pearson correlation, autocorrelation periodicity scores, and column recommendations (train, consider_dropping, project_later, low_variance). Use to choose which columns to include in training and which to project onto the map after training.
|
|
154
|
-
action=list: List all datasets belonging to the organisation with id, name, rows, cols, created_at (use
|
|
155
|
-
action=get: Fetch one dataset by dataset_id — returns status, staged_prefix, staged_version, ingest_error, and stage_job_id when staging is pending.
|
|
156
|
+
action=list: List all datasets belonging to the organisation with id, name, rows, cols, created_at, description, tags (use tags/description or id to distinguish datasets with the same name).
|
|
157
|
+
action=get: Fetch one dataset by dataset_id — returns status, staged_prefix, staged_version, ingest_error, description/tags, and stage_job_id when staging is pending.
|
|
158
|
+
action=update: PATCH name/description/tags on an existing dataset.
|
|
159
|
+
action=upload: Optional description and tags help later inventory (also settable via update).
|
|
156
160
|
DO NOT AUTO-SUBSET: Call action=subset only when the user explicitly requests a filter, slice, or random sample. Default path is upload → preview/analyze → train on the full dataset_id (GPU handles scale). Never downsample to "save time" or "be helpful".
|
|
161
|
+
Org overview: account(action=inventory) for a markdown datasets+jobs catalog.
|
|
157
162
|
|
|
158
163
|
action=subset: Create a new dataset from a subset of an existing one. Requires name and at least one of row_range, filters, or sample_n.
|
|
159
164
|
- row_range: [start, end] 1-based inclusive (e.g. [1, 2000] for first 2000 rows)
|
|
@@ -175,9 +180,11 @@ BEST FOR: Tabular numeric data. CSV with header required.
|
|
|
175
180
|
NOT FOR: Real-time data streams or binary files — upload a snapshot CSV instead.
|
|
176
181
|
ESCALATION: If upload fails with column errors, open the file locally and verify the header row. If preview shows nulls in training columns: use train(action=impute) for sparse data, or clean/subset for train(action=map).`, {
|
|
177
182
|
action: z
|
|
178
|
-
.enum(["upload", "preview", "analyze", "list", "get", "subset", "delete", "add_expression", "reduce_spectral"])
|
|
179
|
-
.describe("upload: add CSV or .csv.gz; preview: inspect columns/stats; analyze: pre-training correlation and periodicity; list: see all datasets; get: fetch one dataset metadata (status/staging); subset: create filtered subset; delete: remove dataset; add_expression: add derived column from expression (or, with project_onto_job, project the expression onto a trained map as a component plane); 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"),
|
|
180
|
-
name: z.string().optional().describe("Dataset name (required for action=upload and subset)"),
|
|
183
|
+
.enum(["upload", "preview", "analyze", "list", "get", "update", "subset", "delete", "add_expression", "reduce_spectral"])
|
|
184
|
+
.describe("upload: add CSV or .csv.gz; preview: inspect columns/stats; analyze: pre-training correlation and periodicity; list: see all datasets; get: fetch one dataset metadata (status/staging); update: edit name/description/tags; subset: create filtered subset; delete: remove dataset; add_expression: add derived column from expression (or, with project_onto_job, project the expression onto a trained map as a component plane); 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"),
|
|
185
|
+
name: z.string().optional().describe("Dataset name (required for action=upload and subset; optional for update)"),
|
|
186
|
+
description: z.string().optional().describe("Optional free-text description (upload/update). Pass \"\" on update to clear."),
|
|
187
|
+
tags: z.array(z.string()).optional().describe("Optional tags for inventory/navigation (upload/update). Pass [] on update to clear."),
|
|
181
188
|
file_path: z.string().optional().describe("Path to local CSV or .csv.gz (PREFERRED): absolute path, file:// URI, or path relative to the workspace root. Token-efficient; server reads file. NOTE: relative paths resolve against the MCP workspace root — in Cursor/IDE clients that root is often the MCP install dir, not your project, so set BARIVIA_WORKSPACE_ROOT in the MCP config env (or just pass an absolute path) if a relative path is 'not accessible'. Use .csv.gz for large scientific tables."),
|
|
182
189
|
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."),
|
|
183
190
|
dataset_id: z.string().optional().describe("Dataset ID (required for preview, get, subset, and delete)"),
|
|
@@ -266,7 +273,7 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
266
273
|
.int()
|
|
267
274
|
.optional()
|
|
268
275
|
.describe("action=subset: RNG seed for sample_n (default 42; same seed + dataset reproduces the same sample)."),
|
|
269
|
-
}, async ({ action, name, file_path, csv_data, dataset_id, n_rows, row_range, filters, filter, expression, options, project_onto_job, aggregation, output_format, method, columns_block, columns_range, columns_except, k, sample_n, sample_seed }) => {
|
|
276
|
+
}, async ({ action, name, description, tags, file_path, csv_data, dataset_id, n_rows, row_range, filters, filter, expression, options, project_onto_job, aggregation, output_format, method, columns_block, columns_range, columns_except, k, sample_n, sample_seed }) => {
|
|
270
277
|
if (action === "upload") {
|
|
271
278
|
if (!name)
|
|
272
279
|
throw new Error("datasets(upload) requires name");
|
|
@@ -306,7 +313,12 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
306
313
|
const idem = await streamFileSha256(resolved);
|
|
307
314
|
let init;
|
|
308
315
|
try {
|
|
309
|
-
|
|
316
|
+
const uploadUrlBody = { name, size_bytes: stat.size };
|
|
317
|
+
if (description !== undefined)
|
|
318
|
+
uploadUrlBody.description = description;
|
|
319
|
+
if (tags !== undefined)
|
|
320
|
+
uploadUrlBody.tags = tags;
|
|
321
|
+
init = (await apiCall("POST", "/v1/datasets/upload-url", uploadUrlBody, { "Idempotency-Key": idem }));
|
|
310
322
|
}
|
|
311
323
|
catch (e) {
|
|
312
324
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -344,12 +356,17 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
344
356
|
// Content-Encoding: gzip), never decoding them as UTF-8 text.
|
|
345
357
|
if (isGzipInput) {
|
|
346
358
|
const gzBytes = await fs.readFile(resolved);
|
|
347
|
-
const
|
|
359
|
+
const gzHeaders = {
|
|
348
360
|
"X-Dataset-Name": name,
|
|
349
361
|
"Content-Type": uploadContentType,
|
|
350
362
|
"Content-Encoding": "gzip",
|
|
351
363
|
"Idempotency-Key": createHash("sha256").update(`${name}\n`).update(gzBytes).digest("hex"),
|
|
352
|
-
}
|
|
364
|
+
};
|
|
365
|
+
if (description !== undefined)
|
|
366
|
+
gzHeaders["X-Dataset-Description"] = description;
|
|
367
|
+
if (tags !== undefined)
|
|
368
|
+
gzHeaders["X-Dataset-Tags"] = JSON.stringify(tags);
|
|
369
|
+
const data = (await apiCall("POST", "/v1/datasets", gzBytes, gzHeaders, UPLOAD_DATASET_TIMEOUT_MS));
|
|
353
370
|
const gid = data.id ?? data.dataset_id;
|
|
354
371
|
if (gid != null)
|
|
355
372
|
data.suggested_next_step = `${suggestDatasetPreview(String(gid))} to inspect columns before training.`;
|
|
@@ -373,6 +390,10 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
373
390
|
// the original dataset server-side instead of creating a duplicate.
|
|
374
391
|
"Idempotency-Key": createHash("sha256").update(`${name}\n`).update(body).digest("hex"),
|
|
375
392
|
};
|
|
393
|
+
if (description !== undefined)
|
|
394
|
+
uploadHeaders["X-Dataset-Description"] = description;
|
|
395
|
+
if (tags !== undefined)
|
|
396
|
+
uploadHeaders["X-Dataset-Tags"] = JSON.stringify(tags);
|
|
376
397
|
let uploadBody = body;
|
|
377
398
|
if (Buffer.byteLength(body, "utf-8") > GZIP_THRESHOLD) {
|
|
378
399
|
uploadBody = gzipSync(Buffer.from(body, "utf-8"));
|
|
@@ -681,15 +702,9 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
681
702
|
const data = (await apiCall("GET", "/v1/datasets"));
|
|
682
703
|
if (Array.isArray(data)) {
|
|
683
704
|
const lines = data.map((ds) => {
|
|
684
|
-
const
|
|
685
|
-
const name = String(ds.name ?? "");
|
|
686
|
-
const rows = ds.rows != null ? Number(ds.rows) : "?";
|
|
687
|
-
const cols = ds.cols != null ? Number(ds.cols) : "?";
|
|
688
|
-
const st = ds.status != null ? String(ds.status) : "ready";
|
|
689
|
-
const statusBit = st !== "ready" ? ` | status=${st}` : "";
|
|
705
|
+
const base = formatDatasetInventoryLine(ds);
|
|
690
706
|
const ingestErr = cleanNullable(ds.ingest_error);
|
|
691
|
-
|
|
692
|
-
return `${name} (${id}) — ${rows}×${cols}${statusBit}${err}`;
|
|
707
|
+
return ingestErr ? `${base} | ingest_error=${ingestErr}` : base;
|
|
693
708
|
});
|
|
694
709
|
return { content: [{ type: "text", text: lines.length > 0 ? lines.join("\n") : "No datasets." }] };
|
|
695
710
|
}
|
|
@@ -699,11 +714,14 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
699
714
|
if (!dataset_id)
|
|
700
715
|
throw new Error("datasets(get) requires dataset_id");
|
|
701
716
|
const ds = (await apiCall("GET", `/v1/datasets/${dataset_id}`));
|
|
717
|
+
const tagStr = formatTags(ds.tags);
|
|
702
718
|
const lines = [
|
|
703
719
|
`Dataset: ${ds.name ?? "?"} (${ds.id ?? dataset_id})`,
|
|
704
720
|
`Status: ${ds.status ?? "ready"}`,
|
|
705
721
|
`Rows × cols: ${ds.rows ?? "?"} × ${ds.cols ?? "?"}`,
|
|
706
722
|
ds.size_bytes != null ? `Size: ${Number(ds.size_bytes).toLocaleString()} bytes` : "",
|
|
723
|
+
ds.description != null && String(ds.description).trim() !== "" ? `Description: ${String(ds.description)}` : "",
|
|
724
|
+
tagStr ? `Tags: ${tagStr}` : "",
|
|
707
725
|
ds.staged_prefix != null ? `Staged prefix: ${String(ds.staged_prefix)}` : "",
|
|
708
726
|
ds.staged_version != null ? `Staged version: ${String(ds.staged_version)}` : "",
|
|
709
727
|
ds.stage_job_id != null ? `Stage job: ${String(ds.stage_job_id)} (poll jobs(action=status))` : "",
|
|
@@ -712,6 +730,22 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
712
730
|
].filter(Boolean);
|
|
713
731
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
714
732
|
}
|
|
733
|
+
if (action === "update") {
|
|
734
|
+
if (!dataset_id)
|
|
735
|
+
throw new Error("datasets(update) requires dataset_id");
|
|
736
|
+
const body = {};
|
|
737
|
+
if (name !== undefined)
|
|
738
|
+
body.name = name;
|
|
739
|
+
if (description !== undefined)
|
|
740
|
+
body.description = description;
|
|
741
|
+
if (tags !== undefined)
|
|
742
|
+
body.tags = tags;
|
|
743
|
+
if (Object.keys(body).length === 0) {
|
|
744
|
+
throw new Error("datasets(update) requires at least one of name, description, tags");
|
|
745
|
+
}
|
|
746
|
+
const data = await apiCall("PATCH", `/v1/datasets/${dataset_id}`, body);
|
|
747
|
+
return textResult(data);
|
|
748
|
+
}
|
|
715
749
|
if (action === "delete") {
|
|
716
750
|
if (!dataset_id)
|
|
717
751
|
throw new Error("datasets(delete) requires dataset_id");
|
package/dist/tools/feedback.js
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { registerAuditedTool } from "../audit.js";
|
|
3
|
-
import { apiCall, API_URL, fetchWithTimeout,
|
|
3
|
+
import { apiCall, API_URL, fetchWithTimeout, probeApiHealth, structuredTextResult, } from "../shared.js";
|
|
4
|
+
import { enqueueDeferredFeedback, flushDeferredFeedback, listDeferredFeedback, } from "../deferred_feedback.js";
|
|
5
|
+
function isDeferworthy(err) {
|
|
6
|
+
const e = err;
|
|
7
|
+
if (e?.retryable)
|
|
8
|
+
return true;
|
|
9
|
+
if (e?.cause === "api_unreachable" || e?.cause === "storage" || e?.cause === "network")
|
|
10
|
+
return true;
|
|
11
|
+
const status = e?.httpStatus;
|
|
12
|
+
if (status === 502 || status === 503 || status === 504)
|
|
13
|
+
return true;
|
|
14
|
+
if (err instanceof TypeError)
|
|
15
|
+
return true;
|
|
16
|
+
if (err instanceof Error && /fetch failed|ECONNREFUSED|ENOTFOUND|ECONNRESET|network|timed out/i.test(err.message)) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
4
21
|
export function registerFeedbackTool(server) {
|
|
5
22
|
registerAuditedTool(server, "send_feedback", `Send feedback or feature requests to Barivia developers. Use when the user has suggestions, ran into issues, or wants something improved. Do NOT call without asking the user first — but after any group of actions or downloading of results, you SHOULD prepare feedback based on the user's workflow or errors encountered, show it to them, and ask for permission to send it. Once they accept, call this tool.
|
|
6
23
|
|
|
7
|
-
For a substantial issue, prefer feedback_items: submit several focused instances (each max 1400 chars) covering, e.g., symptoms, exact reproduction steps, environment, and concrete asks — they are stored together as one batch so developers see the full picture. Use the single feedback field for a short one-off note
|
|
24
|
+
For a substantial issue, prefer feedback_items: submit several focused instances (each max 1400 chars) covering, e.g., symptoms, exact reproduction steps, environment, and concrete asks — they are stored together as one batch so developers see the full picture. Use the single feedback field for a short one-off note.
|
|
25
|
+
|
|
26
|
+
If the API or object storage is unavailable, this tool queues feedback locally (~/.barivia/deferred-feedback) and returns deferred=true with request_id / api_health — call again later to flush. Probe API liveness with account(action=health) (GET /health; does not touch R2).`, {
|
|
8
27
|
feedback: z.string().max(1400).optional().describe("Single feedback note (max 1400 characters). Use feedback_items instead for a multi-part report."),
|
|
9
28
|
feedback_items: z.array(z.string().max(1400)).max(10).optional().describe("Several feedback instances (each max 1400 characters, up to 10), stored together as one batch. Prefer this for a detailed issue: split it into focused parts (symptoms, reproduction, environment, asks)."),
|
|
10
29
|
}, async ({ feedback, feedback_items }) => {
|
|
@@ -15,9 +34,20 @@ For a substantial issue, prefer feedback_items: submit several focused instances
|
|
|
15
34
|
throw new Error("send_feedback requires feedback or feedback_items (at least one non-empty entry).");
|
|
16
35
|
}
|
|
17
36
|
const body = items.length === 1 ? { feedback: items[0] } : { feedback_items: items };
|
|
37
|
+
const submit = async (b) => apiCall("POST", "/v1/feedback", b);
|
|
38
|
+
// Best-effort flush of earlier offline drafts before sending the new one.
|
|
39
|
+
const flushed = await flushDeferredFeedback("barsom", submit).catch(() => null);
|
|
18
40
|
try {
|
|
19
|
-
const data = await
|
|
20
|
-
return
|
|
41
|
+
const data = (await submit(body));
|
|
42
|
+
return structuredTextResult({
|
|
43
|
+
...data,
|
|
44
|
+
deferred: false,
|
|
45
|
+
...(flushed && flushed.flushed > 0
|
|
46
|
+
? { flushed_deferred: flushed.flushed, deferred_remaining: flushed.remaining }
|
|
47
|
+
: {}),
|
|
48
|
+
}, typeof data.message === "string"
|
|
49
|
+
? data.message
|
|
50
|
+
: "Feedback received. Thank you.");
|
|
21
51
|
}
|
|
22
52
|
catch (err) {
|
|
23
53
|
if (err?.httpStatus === 401) {
|
|
@@ -31,10 +61,46 @@ For a substantial issue, prefer feedback_items: submit several focused instances
|
|
|
31
61
|
const text = await resp.text();
|
|
32
62
|
if (resp.ok) {
|
|
33
63
|
const parsed = JSON.parse(text);
|
|
34
|
-
return
|
|
64
|
+
return structuredTextResult({ ...parsed, deferred: false, note: "Feedback sent anonymously (API key invalid or expired)." }, "Feedback sent anonymously (API key invalid or expired).");
|
|
35
65
|
}
|
|
36
66
|
}
|
|
37
|
-
catch { /* fall through
|
|
67
|
+
catch { /* fall through */ }
|
|
68
|
+
}
|
|
69
|
+
if (isDeferworthy(err)) {
|
|
70
|
+
const e = err;
|
|
71
|
+
const queued = await enqueueDeferredFeedback("barsom", body, {
|
|
72
|
+
status: e.httpStatus,
|
|
73
|
+
error_code: e.errorCode,
|
|
74
|
+
request_id: e.requestId,
|
|
75
|
+
message: e.message,
|
|
76
|
+
});
|
|
77
|
+
const api_health = await probeApiHealth().catch(() => ({
|
|
78
|
+
liveness: "down",
|
|
79
|
+
readiness: "unknown",
|
|
80
|
+
storage: "not_probed",
|
|
81
|
+
}));
|
|
82
|
+
const pending = await listDeferredFeedback("barsom");
|
|
83
|
+
return structuredTextResult({
|
|
84
|
+
ok: true,
|
|
85
|
+
deferred: true,
|
|
86
|
+
deferred_id: queued.id,
|
|
87
|
+
deferred_path: queued.path,
|
|
88
|
+
deferred_pending: pending.length,
|
|
89
|
+
error_code: e.errorCode ?? "api_unreachable",
|
|
90
|
+
cause: e.cause ?? "api_unreachable",
|
|
91
|
+
request_id: e.requestId ?? null,
|
|
92
|
+
api_health,
|
|
93
|
+
message: "API/storage unavailable — feedback saved locally and will be sent on the next successful send_feedback call.",
|
|
94
|
+
}, [
|
|
95
|
+
"Feedback deferred (API/storage unavailable).",
|
|
96
|
+
`Saved to ${queued.path}`,
|
|
97
|
+
e.requestId ? `request_id=${e.requestId}` : null,
|
|
98
|
+
e.errorCode ? `error_code=${e.errorCode}` : null,
|
|
99
|
+
`api_health.liveness=${api_health.liveness} (storage not_probed)`,
|
|
100
|
+
"Retry send_feedback later to flush the local queue.",
|
|
101
|
+
]
|
|
102
|
+
.filter(Boolean)
|
|
103
|
+
.join(" | "));
|
|
38
104
|
}
|
|
39
105
|
throw err;
|
|
40
106
|
}
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import { registerAuditedTool } from "../audit.js";
|
|
2
|
-
import { fetchWorkflowGuideFromApi } from "../shared.js";
|
|
2
|
+
import { fetchWorkflowGuideFromApi, UNAVAILABLE_RETRY_AFTER_SEC } from "../shared.js";
|
|
3
3
|
/** Minimal orientation when the API is unreachable; full SOP lives on GET /v1/docs/workflow. */
|
|
4
4
|
const OFFLINE_STUB = `## Offline / API unavailable
|
|
5
5
|
|
|
6
|
-
Configure \`BARIVIA_API_KEY\` and optional \`BARIVIA_API_URL\`, then call **guide_barsom_workflow** again. Full tool map, async rules, training modes, and step-by-step SOP are loaded from the Barivia API (authenticated) and scoped to your plan.
|
|
6
|
+
Configure \`BARIVIA_API_KEY\` and optional \`BARIVIA_API_URL\`, then call **guide_barsom_workflow** again when online. Full tool map, async rules, training modes, and step-by-step SOP are loaded from the Barivia API (authenticated) and scoped to your plan.
|
|
7
7
|
|
|
8
|
-
**
|
|
8
|
+
**Still usable offline:** already-downloaded results, local CSVs, draft notes / feedback drafts (queued under \`~/.barivia/deferred-feedback\`).
|
|
9
|
+
**Needs API:** upload, train, list/status, feedback flush, other cloud tools.
|
|
10
|
+
|
|
11
|
+
**Core tools (when healthy):** \`datasets\`, \`train\` (map, siom_map, impute, floop_siom where entitled), \`jobs\` (status/list/compare — lifecycle only), \`results\`, \`inference\`, \`account\`, \`training_guidance\`, \`guide_barsom_workflow\`.
|
|
12
|
+
|
|
13
|
+
**Degraded-session rules:** Do not paste Cloudflare/HTML into chat — one-sentence summary. The proxy already burst-retried (3×~2s); wait ~${UNAVAILABLE_RETRY_AFTER_SEC}s (see \`retry_after_sec\`) before another round, then ask the user. Check \`account(health)\` or \`account(status)\` before large uploads/trains (\`ok:false\` + \`retry_after_sec\` = pause, not a crash). Save feedback drafts locally; resend via \`send_feedback\` when healthy.
|
|
9
14
|
|
|
10
15
|
**Parameter hints:** call \`training_guidance\` (also API-scoped). **Async:** poll \`jobs(action=status)\` every 10–15s after submit.`;
|
|
11
16
|
export function registerGuideBarsomTool(server) {
|
|
12
|
-
registerAuditedTool(server, "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.", {}, async () => {
|
|
17
|
+
registerAuditedTool(server, "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 with offline-vs-API capabilities and degraded-session rules. For field-level parameters, use training_guidance; for a narrative pre-train checklist, use the prepare_training prompt.", {}, async () => {
|
|
13
18
|
const md = await fetchWorkflowGuideFromApi();
|
|
14
19
|
if (md) {
|
|
15
20
|
const text = "Plan-scoped workflow (from Barivia API). Text below reflects your API key / plan.\n\n" + md;
|
package/dist/tools/jobs.js
CHANGED
|
@@ -3,13 +3,15 @@ import { registerAuditedTool } from "../audit.js";
|
|
|
3
3
|
import { apiCall, textResult } from "../shared.js";
|
|
4
4
|
import { formatJobCancelText, formatJobStatusText } from "../job_status_format.js";
|
|
5
5
|
import { DEFAULT_BLOCK_UNTIL_SEC, DEFAULT_POLL_INTERVAL_SEC, runJobMonitor } from "../job_monitor.js";
|
|
6
|
+
import { buildJobsListQuery, extractJobsList, formatJobInventoryLine, } from "../inventory_format.js";
|
|
6
7
|
export const JOBS_DESCRIPTION_BASE = `Manage and inspect jobs (lifecycle only).
|
|
7
8
|
|
|
8
9
|
| Action | Use when |
|
|
9
10
|
|--------|----------|
|
|
10
11
|
| monitor | After async submit — blocks server-side with 5s snapshots until terminal (preferred for agents) |
|
|
11
12
|
| status | One-shot progress check (manual poll every 10–15s if not using monitor) |
|
|
12
|
-
| list | Finding job IDs / reviewing pipeline state (
|
|
13
|
+
| list | Finding job IDs / reviewing pipeline state (slim by default; filter by dataset_id/status/job_type; paginate with limit+cursor) |
|
|
14
|
+
| update | Edit label / description / tags on an existing job |
|
|
13
15
|
| compare | Picking the best TRAINING run from a set (metrics table). NOT inference(compare), which diffs one dataset's distribution against the training set. |
|
|
14
16
|
| cancel | Stopping a running or pending job to free the worker |
|
|
15
17
|
| delete | Permanently removing a job and all its S3 result files |
|
|
@@ -28,9 +30,13 @@ ASYNC POLLING PROTOCOL (action=status):
|
|
|
28
30
|
- Map training typical times: 10×10 ~30s | 20×20 ~3–5 min | 40×40 ~15–30 min.
|
|
29
31
|
- completed → results(action=get); failed → read error + optional failure_stage (preprocessing/training/metrics/visualization/upload): memory error → smaller grid/batch and retrain; column missing → datasets(action=preview); NaN on a standard map → train(action=impute) for sparse columns or clean the CSV.
|
|
30
32
|
|
|
33
|
+
action=list: Slim rows by default (label,id,status,job_type,dataset_id,created_at,result_ref,description,tags — no fat params). Use status/job_type/has_results/limit/cursor to page. has_results=true is the results catalog. Pass fields="full" only when you need params/error.
|
|
34
|
+
action=update: PATCH label/description/tags (pass at train submit when possible).
|
|
31
35
|
action=compare: metrics table (QE, TE, explained variance, silhouette) for 2+ COMPLETED TRAINING jobs — pick the best run in a sweep. NOT inference(action=compare) (single trained job + second dataset → density-diff heatmap). Only hyperparameter columns that differ across runs are shown; pass label="…" at train time for readable rows.
|
|
32
36
|
action=cancel: Not instant — worker checks between phases. Expect up to 30s delay.
|
|
33
|
-
action=delete: WARNING — job ID will no longer work with results or any other tool
|
|
37
|
+
action=delete: WARNING — job ID will no longer work with results or any other tool.
|
|
38
|
+
|
|
39
|
+
Org overview: prefer account(action=inventory) for a markdown datasets+jobs inventory.`;
|
|
34
40
|
/**
|
|
35
41
|
* Render the jobs(compare) markdown table. Strategy: always show job_id (or label
|
|
36
42
|
* if present) + the four core metrics (QE, TE, Expl.Var, Silhouette); add any
|
|
@@ -143,12 +149,12 @@ export function renderCompareTable(comparisons) {
|
|
|
143
149
|
export function registerJobsTool(server, description) {
|
|
144
150
|
registerAuditedTool(server, "jobs", description, {
|
|
145
151
|
action: z
|
|
146
|
-
.enum(["status", "monitor", "list", "compare", "cancel", "delete"])
|
|
147
|
-
.describe("status: one-shot check; monitor: block until terminal with snapshots; list/compare/cancel/delete as labeled. (Training via train; scoring via inference.)"),
|
|
152
|
+
.enum(["status", "monitor", "list", "update", "compare", "cancel", "delete"])
|
|
153
|
+
.describe("status: one-shot check; monitor: block until terminal with snapshots; list/update/compare/cancel/delete as labeled. (Training via train; scoring via inference.)"),
|
|
148
154
|
job_id: z
|
|
149
155
|
.string()
|
|
150
156
|
.optional()
|
|
151
|
-
.describe("Job ID — required for action=status, monitor, cancel, delete."),
|
|
157
|
+
.describe("Job ID — required for action=status, monitor, update, cancel, delete."),
|
|
152
158
|
block_until_sec: z
|
|
153
159
|
.number()
|
|
154
160
|
.int()
|
|
@@ -173,8 +179,38 @@ export function registerJobsTool(server, description) {
|
|
|
173
179
|
.string()
|
|
174
180
|
.optional()
|
|
175
181
|
.describe("action=list: filter jobs by this dataset ID."),
|
|
182
|
+
status: z
|
|
183
|
+
.enum(["pending", "running", "completed", "failed", "cancelled"])
|
|
184
|
+
.optional()
|
|
185
|
+
.describe("action=list: filter by job status."),
|
|
186
|
+
job_type: z
|
|
187
|
+
.string()
|
|
188
|
+
.optional()
|
|
189
|
+
.describe("action=list: filter by MCP job_type label (e.g. train_map, train_impute, predict, cfd_mesh_convergence)."),
|
|
190
|
+
has_results: z
|
|
191
|
+
.boolean()
|
|
192
|
+
.optional()
|
|
193
|
+
.describe("action=list: true = completed jobs with result_ref (results catalog)."),
|
|
194
|
+
limit: z
|
|
195
|
+
.number()
|
|
196
|
+
.int()
|
|
197
|
+
.min(1)
|
|
198
|
+
.max(100)
|
|
199
|
+
.optional()
|
|
200
|
+
.describe("action=list: page size (default 50, max 100)."),
|
|
201
|
+
cursor: z
|
|
202
|
+
.string()
|
|
203
|
+
.optional()
|
|
204
|
+
.describe("action=list: next_cursor from a prior list response for keyset pagination."),
|
|
205
|
+
fields: z
|
|
206
|
+
.enum(["slim", "full"])
|
|
207
|
+
.optional()
|
|
208
|
+
.describe("action=list: slim (default, no params) or full (includes params/error)."),
|
|
209
|
+
label: z.string().optional().describe("action=update: new job label (≤120 chars)."),
|
|
210
|
+
description: z.string().optional().describe("action=update: free-text description (pass \"\" to clear)."),
|
|
211
|
+
tags: z.array(z.string()).optional().describe("action=update: replace tags (pass [] to clear)."),
|
|
176
212
|
}, async (args) => {
|
|
177
|
-
const { action, job_id, job_ids, dataset_id, block_until_sec, poll_interval_sec, wait_finalize } = args;
|
|
213
|
+
const { action, job_id, job_ids, dataset_id, status, job_type, has_results, limit, cursor, fields, block_until_sec, poll_interval_sec, wait_finalize, label, description, tags, } = args;
|
|
178
214
|
// ---- Lifecycle ----
|
|
179
215
|
if (action === "monitor") {
|
|
180
216
|
if (!job_id)
|
|
@@ -188,17 +224,38 @@ export function registerJobsTool(server, description) {
|
|
|
188
224
|
return { content: [{ type: "text", text: formatJobStatusText(job_id, data) }] };
|
|
189
225
|
}
|
|
190
226
|
if (action === "list") {
|
|
191
|
-
const listPath =
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
});
|
|
200
|
-
|
|
227
|
+
const listPath = buildJobsListQuery({
|
|
228
|
+
dataset_id, status, job_type, has_results, limit, cursor, fields,
|
|
229
|
+
});
|
|
230
|
+
const data = await apiCall("GET", listPath);
|
|
231
|
+
const { jobs, next_cursor } = extractJobsList(data);
|
|
232
|
+
const lines = jobs.map((job) => formatJobInventoryLine(job));
|
|
233
|
+
if (next_cursor) {
|
|
234
|
+
lines.push("");
|
|
235
|
+
lines.push(`next_cursor: ${next_cursor}`);
|
|
236
|
+
lines.push(`Tip: jobs(action=list, cursor="${next_cursor}", limit=${limit ?? 50}) for the next page.`);
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
content: [{
|
|
240
|
+
type: "text",
|
|
241
|
+
text: lines.length > 0 ? lines.join("\n") : "No jobs found.",
|
|
242
|
+
}],
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
if (action === "update") {
|
|
246
|
+
if (!job_id)
|
|
247
|
+
throw new Error("jobs(update) requires job_id");
|
|
248
|
+
const body = {};
|
|
249
|
+
if (label !== undefined)
|
|
250
|
+
body.label = label;
|
|
251
|
+
if (description !== undefined)
|
|
252
|
+
body.description = description;
|
|
253
|
+
if (tags !== undefined)
|
|
254
|
+
body.tags = tags;
|
|
255
|
+
if (Object.keys(body).length === 0) {
|
|
256
|
+
throw new Error("jobs(update) requires at least one of label, description, tags");
|
|
201
257
|
}
|
|
258
|
+
const data = await apiCall("PATCH", `/v1/jobs/${job_id}`, body);
|
|
202
259
|
return textResult(data);
|
|
203
260
|
}
|
|
204
261
|
if (action === "compare") {
|
package/dist/tools/train.js
CHANGED
|
@@ -55,6 +55,10 @@ async function submitTrainJob(opts) {
|
|
|
55
55
|
const submitBody = { dataset_id: opts.dataset_id, params: opts.params };
|
|
56
56
|
if (opts.label && opts.label.trim() !== "")
|
|
57
57
|
submitBody.label = opts.label;
|
|
58
|
+
if (opts.description !== undefined)
|
|
59
|
+
submitBody.description = opts.description;
|
|
60
|
+
if (opts.tags !== undefined)
|
|
61
|
+
submitBody.tags = opts.tags;
|
|
58
62
|
const data = (await apiCall("POST", "/v1/jobs", submitBody));
|
|
59
63
|
const newJobId = data.id;
|
|
60
64
|
data.effective_params = `${opts.variantPrefix}, ${opts.paramSummary}`;
|
|
@@ -104,7 +108,7 @@ async function submitTrainJob(opts) {
|
|
|
104
108
|
export async function runTrain(action, args) {
|
|
105
109
|
const dataset_id = args.dataset_id;
|
|
106
110
|
if (action === "map" || action === "siom_map" || action === "impute") {
|
|
107
|
-
const { preset, grid_x, grid_y, epochs, model, periodic, columns, cyclic_features, cyclic_pairs, temporal_features, feature_weights, transforms, auto_log_transforms, time_delay_embeddings, categorical_features, normalize, normalization_methods, sigma_f, learning_rate, batch_size, quality_metrics, backend, output_format, output_dpi, colormap, row_range, gamma, gamma_f, siom_decay, siom_penalty, penalty_alpha, reset_per_epoch, siom_feature_geometry, siom_qe_backend, siom_qe_batch_size, label, cv_folds, viz_mode, viz_top_components, viz_upload_cyclic_components, emit_cell_uncertainty, } = args;
|
|
111
|
+
const { preset, grid_x, grid_y, epochs, model, periodic, columns, cyclic_features, cyclic_pairs, temporal_features, feature_weights, transforms, auto_log_transforms, time_delay_embeddings, categorical_features, normalize, normalization_methods, sigma_f, learning_rate, batch_size, quality_metrics, backend, output_format, output_dpi, colormap, row_range, gamma, gamma_f, siom_decay, siom_penalty, penalty_alpha, reset_per_epoch, siom_feature_geometry, siom_qe_backend, siom_qe_batch_size, label, description, tags, cv_folds, viz_mode, viz_top_components, viz_upload_cyclic_components, emit_cell_uncertainty, } = args;
|
|
108
112
|
let PRESETS = {};
|
|
109
113
|
try {
|
|
110
114
|
PRESETS = await fetchTrainingPresets();
|
|
@@ -200,6 +204,8 @@ export async function runTrain(action, args) {
|
|
|
200
204
|
dataset_id,
|
|
201
205
|
params,
|
|
202
206
|
label,
|
|
207
|
+
description,
|
|
208
|
+
tags,
|
|
203
209
|
totalRows,
|
|
204
210
|
hasTransforms,
|
|
205
211
|
variantPrefix,
|
|
@@ -213,7 +219,7 @@ export async function runTrain(action, args) {
|
|
|
213
219
|
if (!dataset_id)
|
|
214
220
|
throw new Error("train(floop_siom) requires dataset_id");
|
|
215
221
|
assertValidNormalizationArgs(args);
|
|
216
|
-
const { transforms, label } = args;
|
|
222
|
+
const { transforms, label, description, tags } = args;
|
|
217
223
|
const { params, paramSummary } = buildFloopParams(args);
|
|
218
224
|
let totalRows = 0;
|
|
219
225
|
try {
|
|
@@ -226,6 +232,8 @@ export async function runTrain(action, args) {
|
|
|
226
232
|
dataset_id,
|
|
227
233
|
params,
|
|
228
234
|
label,
|
|
235
|
+
description,
|
|
236
|
+
tags,
|
|
229
237
|
totalRows,
|
|
230
238
|
hasTransforms,
|
|
231
239
|
variantPrefix: "variant=floop",
|
|
@@ -246,6 +254,14 @@ export function registerTrainTool(server) {
|
|
|
246
254
|
.max(120)
|
|
247
255
|
.optional()
|
|
248
256
|
.describe("Optional run label (≤120 chars); appears in jobs(list) and the jobs(compare) table; sanitized server-side. Useful for sweeps (e.g. label=\"sweep_periodic_true\")."),
|
|
257
|
+
description: z
|
|
258
|
+
.string()
|
|
259
|
+
.optional()
|
|
260
|
+
.describe("Optional free-text description for inventory/navigation (shown in jobs(list) and account(inventory))."),
|
|
261
|
+
tags: z
|
|
262
|
+
.array(z.string())
|
|
263
|
+
.optional()
|
|
264
|
+
.describe("Optional tags for inventory/navigation (e.g. [\"sweep\",\"baseline\"])."),
|
|
249
265
|
...TRAINING_INPUT_SCHEMA,
|
|
250
266
|
}, async (args) => runTrain(args.action, args));
|
|
251
267
|
}
|