@barivia/barsom-mcp 0.22.2 → 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/prepare_training_prompt.js +1 -0
- package/dist/shared.js +301 -49
- package/dist/tools/account.js +118 -8
- package/dist/tools/datasets.js +61 -25
- package/dist/tools/explore_map.js +37 -18
- 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/results.js +7 -1
- package/dist/tools/train.js +28 -5
- package/dist/tools/training_core.js +6 -1
- package/dist/tools/training_monitor.js +80 -37
- package/dist/train_submit_message.js +5 -7
- package/dist/training_monitor_curve.js +32 -0
- package/dist/ui-delivery.js +185 -0
- package/dist/views/src/views/results-explorer/index.html +16 -13
- package/dist/views/src/views/training-monitor/index.html +30 -18
- package/dist/viz_links.js +25 -0
- package/package.json +1 -1
package/dist/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,9 +138,10 @@ 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) |
|
|
142
|
-
|
|
|
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) |
|
|
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. |
|
|
145
147
|
| delete | Cleaning up after experiments or freeing the dataset slot |
|
|
@@ -151,15 +153,20 @@ 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).
|
|
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.
|
|
162
|
+
|
|
156
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.
|
|
157
164
|
- row_range: [start, end] 1-based inclusive (e.g. [1, 2000] for first 2000 rows)
|
|
158
165
|
- filters: array of conditions, ALL must match (AND logic). Each: { column, op, value }.
|
|
159
166
|
Operators: eq, ne, in, gt, lt, gte, lte, between
|
|
160
167
|
Examples: { column: "region", op: "eq", value: "Europe" } | { column: "age", op: "between", value: [18, 65] }
|
|
161
|
-
- sample_n: keep a random N-row sample (seeded via sample_seed, default 42; row order preserved).
|
|
162
|
-
- Combine row_range + filters + sample_n to slice rows, values, then downsample.
|
|
168
|
+
- sample_n: keep a random N-row sample (seeded via sample_seed, default 42; row order preserved). User/ops tool only — not an agent default. Applied after row_range/filters; can also be used alone.
|
|
169
|
+
- Combine row_range + filters + sample_n to slice rows, values, then downsample when the user asked for it.
|
|
163
170
|
- Single filter object is also accepted (auto-wrapped).
|
|
164
171
|
action=reduce_spectral: Run a pre-training reducer over an ordered block of numeric columns. All four methods produce one feature vector per row (rows in = rows out; only the column dimension is collapsed) and append derived columns to the dataset. Choose by data shape:
|
|
165
172
|
- pca: top-k principal components — general first try when many columns are correlated (spectroscopy, gene panels, sensor arrays). Returns explained_variance_ratio.
|
|
@@ -173,9 +180,11 @@ BEST FOR: Tabular numeric data. CSV with header required.
|
|
|
173
180
|
NOT FOR: Real-time data streams or binary files — upload a snapshot CSV instead.
|
|
174
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).`, {
|
|
175
182
|
action: z
|
|
176
|
-
.enum(["upload", "preview", "analyze", "list", "get", "subset", "delete", "add_expression", "reduce_spectral"])
|
|
177
|
-
.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"),
|
|
178
|
-
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."),
|
|
179
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."),
|
|
180
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."),
|
|
181
190
|
dataset_id: z.string().optional().describe("Dataset ID (required for preview, get, subset, and delete)"),
|
|
@@ -258,13 +267,13 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
258
267
|
.int()
|
|
259
268
|
.min(1)
|
|
260
269
|
.optional()
|
|
261
|
-
.describe("action=subset: keep a random N-row sample (seeded, row order preserved).
|
|
270
|
+
.describe("action=subset: keep a random N-row sample (seeded, row order preserved). Only when the user explicitly requests downsampling — not an agent default. Combine with row_range/filters, or use alone."),
|
|
262
271
|
sample_seed: z
|
|
263
272
|
.number()
|
|
264
273
|
.int()
|
|
265
274
|
.optional()
|
|
266
275
|
.describe("action=subset: RNG seed for sample_n (default 42; same seed + dataset reproduces the same sample)."),
|
|
267
|
-
}, 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 }) => {
|
|
268
277
|
if (action === "upload") {
|
|
269
278
|
if (!name)
|
|
270
279
|
throw new Error("datasets(upload) requires name");
|
|
@@ -304,7 +313,12 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
304
313
|
const idem = await streamFileSha256(resolved);
|
|
305
314
|
let init;
|
|
306
315
|
try {
|
|
307
|
-
|
|
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 }));
|
|
308
322
|
}
|
|
309
323
|
catch (e) {
|
|
310
324
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -342,12 +356,17 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
342
356
|
// Content-Encoding: gzip), never decoding them as UTF-8 text.
|
|
343
357
|
if (isGzipInput) {
|
|
344
358
|
const gzBytes = await fs.readFile(resolved);
|
|
345
|
-
const
|
|
359
|
+
const gzHeaders = {
|
|
346
360
|
"X-Dataset-Name": name,
|
|
347
361
|
"Content-Type": uploadContentType,
|
|
348
362
|
"Content-Encoding": "gzip",
|
|
349
363
|
"Idempotency-Key": createHash("sha256").update(`${name}\n`).update(gzBytes).digest("hex"),
|
|
350
|
-
}
|
|
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));
|
|
351
370
|
const gid = data.id ?? data.dataset_id;
|
|
352
371
|
if (gid != null)
|
|
353
372
|
data.suggested_next_step = `${suggestDatasetPreview(String(gid))} to inspect columns before training.`;
|
|
@@ -371,6 +390,10 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
371
390
|
// the original dataset server-side instead of creating a duplicate.
|
|
372
391
|
"Idempotency-Key": createHash("sha256").update(`${name}\n`).update(body).digest("hex"),
|
|
373
392
|
};
|
|
393
|
+
if (description !== undefined)
|
|
394
|
+
uploadHeaders["X-Dataset-Description"] = description;
|
|
395
|
+
if (tags !== undefined)
|
|
396
|
+
uploadHeaders["X-Dataset-Tags"] = JSON.stringify(tags);
|
|
374
397
|
let uploadBody = body;
|
|
375
398
|
if (Buffer.byteLength(body, "utf-8") > GZIP_THRESHOLD) {
|
|
376
399
|
uploadBody = gzipSync(Buffer.from(body, "utf-8"));
|
|
@@ -489,7 +512,7 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
489
512
|
job_id: jobId,
|
|
490
513
|
request_id: rid ?? null,
|
|
491
514
|
message: errText,
|
|
492
|
-
suggested_next_step: "
|
|
515
|
+
suggested_next_step: "Do not auto-subset. Report the error to the user; full-dataset analyze is supported at scale. Use datasets(subset) only if the user explicitly asks for downsampling.",
|
|
493
516
|
}, errText + (rid ? ` (request_id=${rid})` : ""));
|
|
494
517
|
}
|
|
495
518
|
if (poll.status !== "completed") {
|
|
@@ -505,7 +528,7 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
505
528
|
}
|
|
506
529
|
else {
|
|
507
530
|
const msg = err instanceof Error ? err.message : String(err);
|
|
508
|
-
throw new Error(`datasets(analyze) failed: ${msg}.
|
|
531
|
+
throw new Error(`datasets(analyze) failed: ${msg}. Do not auto-subset — use datasets(subset) only if the user explicitly asked.`);
|
|
509
532
|
}
|
|
510
533
|
}
|
|
511
534
|
return { content: [{ type: "text", text: formatAnalyzeResult(data, dataset_id) }] };
|
|
@@ -679,15 +702,9 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
679
702
|
const data = (await apiCall("GET", "/v1/datasets"));
|
|
680
703
|
if (Array.isArray(data)) {
|
|
681
704
|
const lines = data.map((ds) => {
|
|
682
|
-
const
|
|
683
|
-
const name = String(ds.name ?? "");
|
|
684
|
-
const rows = ds.rows != null ? Number(ds.rows) : "?";
|
|
685
|
-
const cols = ds.cols != null ? Number(ds.cols) : "?";
|
|
686
|
-
const st = ds.status != null ? String(ds.status) : "ready";
|
|
687
|
-
const statusBit = st !== "ready" ? ` | status=${st}` : "";
|
|
705
|
+
const base = formatDatasetInventoryLine(ds);
|
|
688
706
|
const ingestErr = cleanNullable(ds.ingest_error);
|
|
689
|
-
|
|
690
|
-
return `${name} (${id}) — ${rows}×${cols}${statusBit}${err}`;
|
|
707
|
+
return ingestErr ? `${base} | ingest_error=${ingestErr}` : base;
|
|
691
708
|
});
|
|
692
709
|
return { content: [{ type: "text", text: lines.length > 0 ? lines.join("\n") : "No datasets." }] };
|
|
693
710
|
}
|
|
@@ -697,11 +714,14 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
697
714
|
if (!dataset_id)
|
|
698
715
|
throw new Error("datasets(get) requires dataset_id");
|
|
699
716
|
const ds = (await apiCall("GET", `/v1/datasets/${dataset_id}`));
|
|
717
|
+
const tagStr = formatTags(ds.tags);
|
|
700
718
|
const lines = [
|
|
701
719
|
`Dataset: ${ds.name ?? "?"} (${ds.id ?? dataset_id})`,
|
|
702
720
|
`Status: ${ds.status ?? "ready"}`,
|
|
703
721
|
`Rows × cols: ${ds.rows ?? "?"} × ${ds.cols ?? "?"}`,
|
|
704
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}` : "",
|
|
705
725
|
ds.staged_prefix != null ? `Staged prefix: ${String(ds.staged_prefix)}` : "",
|
|
706
726
|
ds.staged_version != null ? `Staged version: ${String(ds.staged_version)}` : "",
|
|
707
727
|
ds.stage_job_id != null ? `Stage job: ${String(ds.stage_job_id)} (poll jobs(action=status))` : "",
|
|
@@ -710,6 +730,22 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
710
730
|
].filter(Boolean);
|
|
711
731
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
712
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
|
+
}
|
|
713
749
|
if (action === "delete") {
|
|
714
750
|
if (!dataset_id)
|
|
715
751
|
throw new Error("datasets(delete) requires dataset_id");
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
|
|
3
3
|
import { registerAuditedTool, runMcpToolAudit } from "../audit.js";
|
|
4
|
-
import { apiCall, apiRawCall,
|
|
4
|
+
import { apiCall, apiRawCall, BARSOM_VIZ_VIEWS, buildStandaloneVizPageUrl, buildVizRelatedUrls, getVizPort, getCaptionForImage, getResultsImagesToFetch, mimeForFilename, structuredTextResult, tryAttachImage, } from "../shared.js";
|
|
5
|
+
import { buildVizStandaloneUrl } from "../viz_links.js";
|
|
6
|
+
import { appendUiDeliveryContent, beginInlineFallback, buildStandaloneVizUrl, resolveUiDelivery, } from "../ui-delivery.js";
|
|
5
7
|
export const RESULTS_EXPLORER_URI = "ui://barsom/results-explorer";
|
|
6
8
|
function labelForFigure(filename) {
|
|
7
9
|
const base = filename.replace(/\.(png|pdf|svg)$/i, "");
|
|
@@ -111,6 +113,10 @@ export function buildPayload(jobId, data) {
|
|
|
111
113
|
summary.selected_columns ? `Variables: ${summary.selected_columns.join(", ")}` : "",
|
|
112
114
|
].filter(Boolean);
|
|
113
115
|
const port = getVizPort();
|
|
116
|
+
const standaloneUrl = buildStandaloneVizPageUrl(port, BARSOM_VIZ_VIEWS.resultsExplorer, jobId) ??
|
|
117
|
+
buildVizStandaloneUrl("results-explorer", jobId);
|
|
118
|
+
const trainingMonitorUrl = buildStandaloneVizPageUrl(port, BARSOM_VIZ_VIEWS.trainingMonitor, jobId) ??
|
|
119
|
+
buildVizStandaloneUrl("training-monitor", jobId);
|
|
114
120
|
return {
|
|
115
121
|
type: "results-explorer",
|
|
116
122
|
jobId,
|
|
@@ -123,42 +129,55 @@ export function buildPayload(jobId, data) {
|
|
|
123
129
|
availableFigures: figures,
|
|
124
130
|
defaultFigureKey: figures.find((figure) => figure.key === "combined")?.key
|
|
125
131
|
?? figures[0]?.key,
|
|
126
|
-
standaloneUrl
|
|
132
|
+
standaloneUrl,
|
|
133
|
+
trainingMonitorUrl,
|
|
134
|
+
relatedUrls: buildVizRelatedUrls(port, jobId, BARSOM_VIZ_VIEWS),
|
|
127
135
|
};
|
|
128
136
|
}
|
|
129
137
|
async function handleResultsExplorer(job_id) {
|
|
138
|
+
beginInlineFallback();
|
|
130
139
|
const data = (await apiCall("GET", `/v1/results/${job_id}`));
|
|
131
140
|
const payload = buildPayload(job_id, data);
|
|
132
141
|
const summary = (data.summary ?? {});
|
|
133
|
-
const
|
|
134
|
-
{
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
payload.highlights[0] ?? "",
|
|
141
|
-
].filter(Boolean).join("\n"),
|
|
142
|
-
},
|
|
143
|
-
];
|
|
142
|
+
const summaryText = [
|
|
143
|
+
`Results Explorer ready for job ${job_id}.`,
|
|
144
|
+
payload.gridLabel ? `Grid: ${payload.gridLabel}` : "",
|
|
145
|
+
payload.subtitle ? `Label: ${payload.subtitle}` : "",
|
|
146
|
+
payload.highlights[0] ?? "",
|
|
147
|
+
].filter(Boolean).join("\n");
|
|
148
|
+
const content = [{ type: "text", text: summaryText }];
|
|
144
149
|
const defaultFigure = payload.availableFigures.find((figure) => figure.key === payload.defaultFigureKey);
|
|
150
|
+
let inlineImageAttached = false;
|
|
145
151
|
if (defaultFigure) {
|
|
152
|
+
const before = content.length;
|
|
146
153
|
await tryAttachImage(content, job_id, defaultFigure.filename);
|
|
154
|
+
inlineImageAttached = content.length > before && content.some((c) => c.type === "image");
|
|
147
155
|
}
|
|
148
156
|
else {
|
|
149
157
|
const ext = summary.output_format ?? "png";
|
|
158
|
+
const before = content.length;
|
|
150
159
|
await tryAttachImage(content, job_id, `combined.${ext}`);
|
|
160
|
+
inlineImageAttached = content.length > before && content.some((c) => c.type === "image");
|
|
151
161
|
}
|
|
152
|
-
|
|
162
|
+
const uiDelivery = resolveUiDelivery({
|
|
163
|
+
tool: "results_explorer",
|
|
164
|
+
jobId: job_id,
|
|
165
|
+
jobStatus: "completed",
|
|
166
|
+
inlineImageAttached,
|
|
167
|
+
});
|
|
168
|
+
appendUiDeliveryContent(content, uiDelivery, {
|
|
169
|
+
linkLabel: "Open results explorer",
|
|
170
|
+
primaryUrl: payload.standaloneUrl ?? buildStandaloneVizUrl("results_explorer", job_id),
|
|
171
|
+
});
|
|
172
|
+
const monitorUrl = payload.trainingMonitorUrl ?? buildStandaloneVizUrl("training_monitor", job_id);
|
|
173
|
+
if (monitorUrl) {
|
|
153
174
|
content.push({
|
|
154
175
|
type: "text",
|
|
155
|
-
text: `
|
|
156
|
-
`AGENT: surface this to the user as a clickable markdown link (e.g. "[Open results explorer](${payload.standaloneUrl})") in your reply — do not leave it buried in tool output. ` +
|
|
157
|
-
`This localhost port is assigned per MCP session and changes if the proxy restarts — if the page won't load, re-run results_explorer for a fresh URL, or set BARIVIA_VIZ_PORT in your MCP env for a persistent port.`,
|
|
176
|
+
text: `Review training curves: [Open training monitor](${monitorUrl})`,
|
|
158
177
|
});
|
|
159
178
|
}
|
|
160
179
|
return {
|
|
161
|
-
...structuredTextResult(payload, undefined, content),
|
|
180
|
+
...structuredTextResult({ ...payload, ui_delivery: uiDelivery }, undefined, content),
|
|
162
181
|
_meta: { ui: { resourceUri: RESULTS_EXPLORER_URI } },
|
|
163
182
|
};
|
|
164
183
|
}
|
package/dist/tools/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/results.js
CHANGED
|
@@ -19,7 +19,7 @@ ONLY call this after jobs(action=status) returns "completed".
|
|
|
19
19
|
ESCALATION: If job not found, verify job_id. If "job not complete", poll with jobs(action=status).
|
|
20
20
|
|
|
21
21
|
action=get: Returns text summary with quality metrics and inline images.
|
|
22
|
-
- figures: omit = combined only. "all" = all plots. "none" = metrics text only, no images (recommended for sweeps and LLM clients to keep tool payloads small). Array = specific logical names (combined, umatrix, hit_histogram, learning_curve, correlation, component_1..N).
|
|
22
|
+
- figures: omit = combined only. "all" = all published plots (excludes unpublished cyclic cos/sin expansions unless train used viz_upload_cyclic_components=true). "none" = metrics text only, no images (recommended for sweeps and LLM clients to keep tool payloads small). Array = specific logical names (combined, umatrix, hit_histogram, learning_curve, correlation, component_1..N). Request unpublished cyclic planes via results(recolor, figures=[component_N, ...]).
|
|
23
23
|
- include_individual: if true (and figures omitted), inlines every component plane.
|
|
24
24
|
- After recolor or project, use the job_id returned by that operation to get the new artifacts, not the original training job_id.
|
|
25
25
|
- After showing results, guide the user: QE interpretation, whether to retrain, which features to explore.
|
|
@@ -574,14 +574,20 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
|
|
|
574
574
|
throw new Error("results(download) requires folder");
|
|
575
575
|
const data = (await apiCall("GET", `/v1/results/${job_id}`));
|
|
576
576
|
const summary = (data.summary ?? {});
|
|
577
|
+
const downloadUrls = (data.download_urls ?? {});
|
|
577
578
|
const jobLabel = data.label != null && data.label !== "" ? String(data.label) : null;
|
|
578
579
|
const files = summary.files ?? [];
|
|
579
580
|
const jobType = summary.job_type ?? "train_som";
|
|
580
581
|
const needsAllFiles = ["enrich_dataset", "predict", "impute_column", "compare_datasets"].includes(jobType);
|
|
581
582
|
const isImage = (f) => f.endsWith(".png") || f.endsWith(".svg") || f.endsWith(".pdf");
|
|
583
|
+
const hasUrlIndex = Object.keys(downloadUrls).length > 0;
|
|
584
|
+
const isPublished = (f) => !hasUrlIndex || f in downloadUrls;
|
|
582
585
|
let toDownload;
|
|
583
586
|
if (figures === "all" || figures === "images" || figures === undefined) {
|
|
584
587
|
toDownload = needsAllFiles || include_json ? files : files.filter(isImage);
|
|
588
|
+
if (hasUrlIndex) {
|
|
589
|
+
toDownload = toDownload.filter((f) => !isImage(f) || isPublished(f));
|
|
590
|
+
}
|
|
585
591
|
}
|
|
586
592
|
else if (Array.isArray(figures)) {
|
|
587
593
|
toDownload = figures;
|