@barivia/barsom-mcp 0.21.0 → 0.22.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -5
- package/dist/index.js +1 -1
- package/dist/inference_prepare.js +2 -2
- package/dist/job_monitor.js +38 -2
- package/dist/job_status_format.js +23 -0
- package/dist/prepare_training_prompt.js +23 -6
- package/dist/shared.js +120 -21
- package/dist/tools/account.js +9 -76
- package/dist/tools/datasets.js +26 -13
- package/dist/tools/guide_barsom.js +1 -1
- package/dist/tools/inference.js +10 -12
- package/dist/tools/results.js +52 -61
- package/dist/tools/train.js +68 -53
- package/dist/tools/training_core.js +29 -8
- package/dist/tools/training_guidance.js +1 -1
- package/dist/train_finalize.js +2 -2
- package/dist/training_monitor_curve.js +1 -1
- package/dist/views/src/views/training-monitor/index.html +28 -28
- package/package.json +1 -1
package/dist/tools/account.js
CHANGED
|
@@ -2,28 +2,23 @@ import { z } from "zod";
|
|
|
2
2
|
import { registerAuditedTool } from "../audit.js";
|
|
3
3
|
import { apiCall } from "../shared.js";
|
|
4
4
|
export function registerAccountTool(server) {
|
|
5
|
-
registerAuditedTool(server, "account", `Manage your Barivia account — check plan/license info,
|
|
5
|
+
registerAuditedTool(server, "account", `Manage your Barivia account — check plan/license info, view usage history, add funds.
|
|
6
6
|
|
|
7
7
|
| Action | Use when |
|
|
8
8
|
|--------|----------|
|
|
9
9
|
| status | Before large jobs — see plan tier, GPU availability, queue depth, training time estimates, credit balance |
|
|
10
|
-
|
|
|
11
|
-
| compute_status | Checking if a burst lease is active and how much time remains |
|
|
12
|
-
| release_compute | Manually terminating an active lease to stop billing |
|
|
13
|
-
| history | Viewing recent compute leases and credit spend |
|
|
10
|
+
| history | Viewing recent compute usage and credit spend |
|
|
14
11
|
| add_funds | Getting instructions to add credits |
|
|
15
12
|
|
|
16
13
|
action=status: Returns plan tier, compute class (CPU/GPU), usage limits, live queue state, training time estimates, and credit balance.
|
|
17
14
|
Use BEFORE large jobs to check GPU availability and estimate wait time.
|
|
18
|
-
|
|
19
|
-
NOT FOR: Training itself — use train(action=map). This tool only manages the account
|
|
15
|
+
Capacity is shared LOCAL/GKE worker pools — there is no per-key cloud burst lease.
|
|
16
|
+
NOT FOR: Training itself — use train(action=map). This tool only manages the account.`, {
|
|
20
17
|
action: z
|
|
21
|
-
.enum(["status", "
|
|
22
|
-
.describe("status: plan/license/queue info;
|
|
23
|
-
tier: z.string().optional().describe("action=request_compute: tier ID — CPU: cpu-mini, cpu-2, cpu-8..cpu-48 (C7i); GPU: gpu-t4-small, gpu-t4..gpu-t4xxx, gpu-t4-12x (G4dn), gpu-p4d (P4d), gpu-l4, gpu-a10. Omit to list options."),
|
|
24
|
-
duration_minutes: z.number().optional().describe("action=request_compute: lease duration in minutes (default: 60)"),
|
|
18
|
+
.enum(["status", "history", "add_funds"])
|
|
19
|
+
.describe("status: plan/license/queue info; history: recent compute usage; add_funds: instructions"),
|
|
25
20
|
limit: z.number().optional().describe("action=history: number of records to return (default: 10)"),
|
|
26
|
-
}, async ({ action,
|
|
21
|
+
}, async ({ action, limit }) => {
|
|
27
22
|
if (action === "status") {
|
|
28
23
|
const data = (await apiCall("GET", "/v1/system/info"));
|
|
29
24
|
const plan = data.plan ?? {};
|
|
@@ -35,7 +30,6 @@ NOT FOR: Training itself — use train(action=map). This tool only manages the a
|
|
|
35
30
|
const computeDesc = gpuEnabled ? (backend.gpu_model ? `GPU (${backend.gpu_model}${backend.gpu_vram_gb ? `, ${backend.gpu_vram_gb}GB` : ""})` : "GPU") : "CPU only";
|
|
36
31
|
const fmtLimit = (v) => v === -1 || v === "-1" ? "unlimited" : String(v ?? "?");
|
|
37
32
|
const historyData = await apiCall("GET", "/v1/compute/history?limit=5").catch(() => null);
|
|
38
|
-
const leaseData = await apiCall("GET", "/v1/compute/lease").catch(() => null);
|
|
39
33
|
const algoNames = {
|
|
40
34
|
train_som: "SOM",
|
|
41
35
|
train_impute: "Imputation",
|
|
@@ -62,7 +56,6 @@ NOT FOR: Training itself — use train(action=map). This tool only manages the a
|
|
|
62
56
|
lines.push(` Credits: $${(historyData.credit_balance_cents / 100).toFixed(2)} remaining`);
|
|
63
57
|
}
|
|
64
58
|
else {
|
|
65
|
-
// Unknown billing mode: avoid misleading $0.00 for postpaid accounts
|
|
66
59
|
const cents = historyData.credit_balance_cents ?? 0;
|
|
67
60
|
if (cents === 0) {
|
|
68
61
|
lines.push(` Credits: N/A (postpaid or zero balance — check plan)`);
|
|
@@ -74,9 +67,6 @@ NOT FOR: Training itself — use train(action=map). This tool only manages the a
|
|
|
74
67
|
}
|
|
75
68
|
if (backend.memory_gb)
|
|
76
69
|
lines.push(` Backend Memory: ${backend.memory_gb} GB`);
|
|
77
|
-
if (leaseData && leaseData.lease_id) {
|
|
78
|
-
lines.push(``, `Active Burst Lease: ${leaseData.tier} | ${Math.round(leaseData.time_remaining_ms / 60000)} min left`);
|
|
79
|
-
}
|
|
80
70
|
const running = Number(status.running_jobs ?? data.running_jobs ?? 0);
|
|
81
71
|
const pending = Number(status.pending_jobs ?? data.pending_jobs ?? 0);
|
|
82
72
|
const compute_eta = Number(estimates?.total || 0);
|
|
@@ -92,63 +82,6 @@ NOT FOR: Training itself — use train(action=map). This tool only manages the a
|
|
|
92
82
|
}
|
|
93
83
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
94
84
|
}
|
|
95
|
-
if (action === "request_compute") {
|
|
96
|
-
if (!tier) {
|
|
97
|
-
try {
|
|
98
|
-
const tiersData = await apiCall("GET", "/v1/compute/tiers");
|
|
99
|
-
const lines = ["Available Compute Tiers:"];
|
|
100
|
-
for (const [tId, tData] of Object.entries(tiersData.tiers)) {
|
|
101
|
-
lines.push(` ${tId}: ${tData.desc}`);
|
|
102
|
-
}
|
|
103
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
104
|
-
}
|
|
105
|
-
catch (err) {
|
|
106
|
-
const msg = err?.message ?? "Failed to fetch compute tiers. Ensure you have burst access enabled.";
|
|
107
|
-
return { content: [{ type: "text", text: msg }] };
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
const data = await apiCall("POST", "/v1/compute/lease", { tier, duration_minutes });
|
|
111
|
-
const billingLine = data.billing_mode === "postpaid"
|
|
112
|
-
? `Billing: Postpaid (usage logged, billed retrospectively)\nAccrued Balance: $${(data.credit_balance_cents / 100).toFixed(2)}`
|
|
113
|
-
: `Credits Remaining After Reserve: $${(data.credit_balance_cents / 100).toFixed(2)}`;
|
|
114
|
-
return {
|
|
115
|
-
content: [{ type: "text", text: `Compute Lease Requested:
|
|
116
|
-
Lease ID: ${data.lease_id}
|
|
117
|
-
Status: ${data.status}
|
|
118
|
-
Estimated Wait: ${data.estimated_wait_minutes} minutes
|
|
119
|
-
Estimated Cost: $${(data.estimated_cost_cents / 100).toFixed(2)}
|
|
120
|
-
${billingLine}
|
|
121
|
-
|
|
122
|
-
IMPORTANT: Cloud burst active. Data is pulled from shared Cloudflare R2, so you do NOT need to re-upload datasets. Just wait ~3 minutes and check status.` }]
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
if (action === "compute_status") {
|
|
126
|
-
const data = await apiCall("GET", "/v1/compute/lease");
|
|
127
|
-
if (data.status === "none" || !data.lease_id) {
|
|
128
|
-
return { content: [{ type: "text", text: "No active lease -- running on default Primary Server." }] };
|
|
129
|
-
}
|
|
130
|
-
return {
|
|
131
|
-
content: [{ type: "text", text: `Active Compute Lease:
|
|
132
|
-
Lease ID: ${data.lease_id}
|
|
133
|
-
Status: ${data.status}
|
|
134
|
-
Tier: ${data.tier} (${data.instance_type})
|
|
135
|
-
Time Remaining: ${Math.round(data.time_remaining_ms / 60000)} minutes` }]
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
if (action === "release_compute") {
|
|
139
|
-
const data = await apiCall("DELETE", "/v1/compute/lease");
|
|
140
|
-
const costLine = data.billing_mode === "postpaid"
|
|
141
|
-
? `Cost Logged: $${(data.cost_cents / 100).toFixed(2)} (postpaid — billed retrospectively)`
|
|
142
|
-
: `Credits Deducted: $${((data.credits_deducted || data.cost_cents) / 100).toFixed(2)}`;
|
|
143
|
-
return {
|
|
144
|
-
content: [{ type: "text", text: `Compute Released:
|
|
145
|
-
Duration Billed: ${data.duration_minutes} minutes
|
|
146
|
-
${costLine}
|
|
147
|
-
Balance: $${(data.final_balance_cents / 100).toFixed(2)}
|
|
148
|
-
|
|
149
|
-
Routing reverted to default Primary Server.` }]
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
85
|
if (action === "history") {
|
|
153
86
|
const data = await apiCall("GET", `/v1/compute/history?limit=${limit || 10}`);
|
|
154
87
|
const history = data.history.map((h) => `- ${h.started_at} | ${h.tier} | ${h.duration_minutes} min | $${(h.credits_charged / 100).toFixed(2)}`).join("\n");
|
|
@@ -159,11 +92,11 @@ Routing reverted to default Primary Server.` }]
|
|
|
159
92
|
if (action === "add_funds") {
|
|
160
93
|
return {
|
|
161
94
|
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:
|
|
162
|
-
bash scripts/manage-credits.sh add <org_id> <amount_usd>` }]
|
|
95
|
+
bash scripts/billing/manage-credits.sh add <org_id> <amount_usd>` }]
|
|
163
96
|
};
|
|
164
97
|
}
|
|
165
98
|
return {
|
|
166
|
-
content: [{ type: "text", text: `Unknown action: ${action}. Valid: status,
|
|
99
|
+
content: [{ type: "text", text: `Unknown action: ${action}. Valid: status, history, add_funds.` }]
|
|
167
100
|
};
|
|
168
101
|
});
|
|
169
102
|
}
|
package/dist/tools/datasets.js
CHANGED
|
@@ -4,7 +4,7 @@ import { gzipSync } from "node:zlib";
|
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { registerAuditedTool } from "../audit.js";
|
|
7
|
-
import { apiCall, getWorkspaceRootAsync, resolveFilePathForUpload, textResult, pollUntilComplete, POLL_DERIVE_MAX_MS, UPLOAD_DATASET_TIMEOUT_MS, LARGE_UPLOAD_BYTES, PRESIGNED_PUT_TIMEOUT_MS, POLL_STAGE_MAX_MS, streamFileSha256, putPresignedStream, } from "../shared.js";
|
|
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
9
|
/**
|
|
10
10
|
* Normalize a nullable string field from the API. Returns "" for absent values,
|
|
@@ -269,9 +269,11 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
269
269
|
if (!name)
|
|
270
270
|
throw new Error("datasets(upload) requires name");
|
|
271
271
|
let body;
|
|
272
|
+
let uploadContentType = "text/csv";
|
|
272
273
|
if (file_path) {
|
|
273
274
|
await apiCall("GET", "/v1/system/info");
|
|
274
275
|
const resolved = await resolveFilePathForUpload(file_path, server);
|
|
276
|
+
uploadContentType = resolveUploadContentType(resolved);
|
|
275
277
|
const lower = resolved.toLowerCase();
|
|
276
278
|
// Accept pre-gzipped CSV/TSV (.csv.gz / .tsv.gz) so large tables transfer
|
|
277
279
|
// ~3x smaller. Already-gzipped files are stored as-is (the platform stores
|
|
@@ -317,9 +319,9 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
317
319
|
// a fresh upload_url and no idempotent_replay flag, so we re-PUT below.
|
|
318
320
|
if (init.idempotent_replay && !init.upload_url) {
|
|
319
321
|
return textResult({ id: datasetId, status: init.status, idempotent_replay: true,
|
|
320
|
-
suggested_next_step:
|
|
322
|
+
suggested_next_step: suggestDatasetPreview(datasetId) });
|
|
321
323
|
}
|
|
322
|
-
await putPresignedStream(init.upload_url, resolved, init.content_type ??
|
|
324
|
+
await putPresignedStream(init.upload_url, resolved, init.content_type ?? uploadContentType, PRESIGNED_PUT_TIMEOUT_MS, isGzipInput);
|
|
323
325
|
const fin = (await apiCall("POST", `/v1/datasets/${datasetId}/finalize`, {}));
|
|
324
326
|
const jobId = (fin.id ?? fin.job_id);
|
|
325
327
|
const poll = await pollUntilComplete(jobId, POLL_STAGE_MAX_MS);
|
|
@@ -332,8 +334,8 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
332
334
|
status: ready ? "ready" : "staging",
|
|
333
335
|
job_id: jobId,
|
|
334
336
|
suggested_next_step: ready
|
|
335
|
-
?
|
|
336
|
-
: `Still staging; poll jobs(action=status, job_id="${jobId}") then
|
|
337
|
+
? `${suggestDatasetPreview(datasetId)} to inspect columns before training.`
|
|
338
|
+
: `Still staging; poll jobs(action=status, job_id="${jobId}") then ${suggestDatasetPreview(datasetId)}.`,
|
|
337
339
|
});
|
|
338
340
|
}
|
|
339
341
|
// Small pre-gzipped files: send the bytes as-is (the API decompresses
|
|
@@ -342,13 +344,13 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
342
344
|
const gzBytes = await fs.readFile(resolved);
|
|
343
345
|
const data = (await apiCall("POST", "/v1/datasets", gzBytes, {
|
|
344
346
|
"X-Dataset-Name": name,
|
|
345
|
-
"Content-Type":
|
|
347
|
+
"Content-Type": uploadContentType,
|
|
346
348
|
"Content-Encoding": "gzip",
|
|
347
349
|
"Idempotency-Key": createHash("sha256").update(`${name}\n`).update(gzBytes).digest("hex"),
|
|
348
350
|
}, UPLOAD_DATASET_TIMEOUT_MS));
|
|
349
351
|
const gid = data.id ?? data.dataset_id;
|
|
350
352
|
if (gid != null)
|
|
351
|
-
data.suggested_next_step =
|
|
353
|
+
data.suggested_next_step = `${suggestDatasetPreview(String(gid))} to inspect columns before training.`;
|
|
352
354
|
return textResult(data);
|
|
353
355
|
}
|
|
354
356
|
body = await fs.readFile(resolved, "utf-8");
|
|
@@ -364,7 +366,7 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
364
366
|
const GZIP_THRESHOLD = 1024 * 1024; // 1 MB
|
|
365
367
|
const uploadHeaders = {
|
|
366
368
|
"X-Dataset-Name": name,
|
|
367
|
-
"Content-Type":
|
|
369
|
+
"Content-Type": uploadContentType,
|
|
368
370
|
// Deterministic key so a timed-out retry of the SAME upload reconciles to
|
|
369
371
|
// the original dataset server-side instead of creating a duplicate.
|
|
370
372
|
"Idempotency-Key": createHash("sha256").update(`${name}\n`).update(body).digest("hex"),
|
|
@@ -377,7 +379,7 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
377
379
|
const data = (await apiCall("POST", "/v1/datasets", uploadBody, uploadHeaders, UPLOAD_DATASET_TIMEOUT_MS));
|
|
378
380
|
const id = data.id ?? data.dataset_id;
|
|
379
381
|
if (id != null)
|
|
380
|
-
data.suggested_next_step =
|
|
382
|
+
data.suggested_next_step = `${suggestDatasetPreview(String(id))} to inspect columns before training.`;
|
|
381
383
|
return textResult(data);
|
|
382
384
|
}
|
|
383
385
|
if (action === "preview") {
|
|
@@ -477,12 +479,22 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
477
479
|
const jobId = (submit.id ?? submit.job_id);
|
|
478
480
|
if (!jobId)
|
|
479
481
|
throw new Error("analyze did not return a job id");
|
|
480
|
-
const poll = await pollUntilComplete(jobId,
|
|
482
|
+
const poll = await pollUntilComplete(jobId, POLL_ANALYZE_MAX_MS);
|
|
481
483
|
if (poll.status === "failed") {
|
|
482
|
-
|
|
484
|
+
const rid = poll.request_id;
|
|
485
|
+
const errText = `datasets(analyze) job ${jobId} failed: ${poll.error ?? "unknown error"}`;
|
|
486
|
+
return structuredTextResult({
|
|
487
|
+
error: true,
|
|
488
|
+
action: "analyze",
|
|
489
|
+
job_id: jobId,
|
|
490
|
+
request_id: rid ?? null,
|
|
491
|
+
message: errText,
|
|
492
|
+
suggested_next_step: "If the dataset is very large, try datasets(action=subset, sample_n=10000) then analyze again.",
|
|
493
|
+
}, errText + (rid ? ` (request_id=${rid})` : ""));
|
|
483
494
|
}
|
|
484
495
|
if (poll.status !== "completed") {
|
|
485
|
-
|
|
496
|
+
const text = `datasets(analyze) job ${jobId} is still running. Poll with jobs(action=status, job_id="${jobId}") then results(action=get, job_id="${jobId}").`;
|
|
497
|
+
return structuredTextResult({ error: false, action: "analyze", job_id: jobId, status: poll.status, message: text }, text);
|
|
486
498
|
}
|
|
487
499
|
const results = (await apiCall("GET", `/v1/results/${jobId}`));
|
|
488
500
|
data = (results.summary ?? results);
|
|
@@ -492,7 +504,8 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
|
|
|
492
504
|
data = (await apiCall("GET", `/v1/datasets/${dataset_id}/analyze`));
|
|
493
505
|
}
|
|
494
506
|
else {
|
|
495
|
-
|
|
507
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
508
|
+
throw new Error(`datasets(analyze) failed: ${msg}. If the dataset is very large, try datasets(action=subset, sample_n=10000) first.`);
|
|
496
509
|
}
|
|
497
510
|
}
|
|
498
511
|
return { content: [{ type: "text", text: formatAnalyzeResult(data, dataset_id) }] };
|
|
@@ -5,7 +5,7 @@ const OFFLINE_STUB = `## Offline / API unavailable
|
|
|
5
5
|
|
|
6
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.
|
|
7
7
|
|
|
8
|
-
**Core tools:** \`datasets\`, \`
|
|
8
|
+
**Core tools:** \`datasets\`, \`train\` (map, siom_map, impute, floop_siom where entitled), \`jobs\` (status/list/compare — lifecycle only), \`results\`, \`inference\`, \`account\`, \`training_guidance\`, \`guide_barsom_workflow\`.
|
|
9
9
|
|
|
10
10
|
**Parameter hints:** call \`training_guidance\` (also API-scoped). **Async:** poll \`jobs(action=status)\` every 10–15s after submit.`;
|
|
11
11
|
export function registerGuideBarsomTool(server) {
|
package/dist/tools/inference.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { registerAuditedTool } from "../audit.js";
|
|
3
|
-
import { apiCall, pollUntilComplete, tryAttachImage } from "../shared.js";
|
|
3
|
+
import { apiCall, pollUntilComplete, tryAttachImage, POLL_INFERENCE_MAX_MS, POLL_PROJECT_MAX_MS, resolveOutputDpi, fmtDensityDiffNode } from "../shared.js";
|
|
4
4
|
import { pollPrepareIfPresent } from "../inference_prepare.js";
|
|
5
5
|
const PREDICT_PREVIEW_ROW_CAP = 10;
|
|
6
6
|
/** One line per scored row when worker embedded `predictions_preview` (n_rows ≤ cap). Exported for tests. */
|
|
@@ -123,8 +123,7 @@ action=report: Returns a report manifest for the given job_id (job must be compl
|
|
|
123
123
|
top_k: z.number().int().min(1).optional().default(10)
|
|
124
124
|
.describe("action=transition_flow: number of top-flow nodes in the statistics summary (default 10)."),
|
|
125
125
|
}, async ({ action, job_id, dataset_id, columns, rows, output, colormap, output_format, output_dpi, top_n, target_column, only_missing, impute_aggregation, cv_folds, target_column_kind, weighting, inputs, lag, min_transitions, top_k }) => {
|
|
126
|
-
const
|
|
127
|
-
const numericDpi = dpiMap[output_dpi ?? "retina"] ?? 2;
|
|
126
|
+
const numericDpi = resolveOutputDpi(output_dpi);
|
|
128
127
|
if (action === "batch_predict")
|
|
129
128
|
return runBatchPredict({ job_id, inputs });
|
|
130
129
|
if (action === "transition_flow") {
|
|
@@ -136,10 +135,10 @@ action=report: Returns a report manifest for the given job_id (job must be compl
|
|
|
136
135
|
if (colormap !== undefined)
|
|
137
136
|
body.colormap = colormap;
|
|
138
137
|
if (output_dpi && output_dpi !== "retina")
|
|
139
|
-
body.output_dpi =
|
|
138
|
+
body.output_dpi = resolveOutputDpi(output_dpi);
|
|
140
139
|
const data = (await apiCall("POST", `/v1/results/${job_id}/transition-flow`, body));
|
|
141
140
|
const flowJobId = data.id;
|
|
142
|
-
const poll = await pollUntilComplete(flowJobId,
|
|
141
|
+
const poll = await pollUntilComplete(flowJobId, POLL_INFERENCE_MAX_MS);
|
|
143
142
|
if (poll.status === "completed") {
|
|
144
143
|
const res = (await apiCall("GET", `/v1/results/${flowJobId}`));
|
|
145
144
|
const summary = (res.summary ?? {});
|
|
@@ -195,7 +194,7 @@ action=report: Returns a report manifest for the given job_id (job must be compl
|
|
|
195
194
|
const predictJobId = data.id;
|
|
196
195
|
const prepareId = await pollPrepareIfPresent(data, "inference(predict)");
|
|
197
196
|
// annotated runs over the full dataset; allow up to 120s like compact
|
|
198
|
-
const poll = await pollUntilComplete(predictJobId,
|
|
197
|
+
const poll = await pollUntilComplete(predictJobId, POLL_INFERENCE_MAX_MS);
|
|
199
198
|
if (poll.status === "completed") {
|
|
200
199
|
const results = (await apiCall("GET", `/v1/results/${predictJobId}`));
|
|
201
200
|
const summary = (results.summary ?? {});
|
|
@@ -254,7 +253,7 @@ action=report: Returns a report manifest for the given job_id (job must be compl
|
|
|
254
253
|
body.weighting = weighting;
|
|
255
254
|
const data = (await apiCall("POST", `/v1/results/${job_id}/impute_column`, body));
|
|
256
255
|
const imputeJobId = data.id;
|
|
257
|
-
const poll = await pollUntilComplete(imputeJobId,
|
|
256
|
+
const poll = await pollUntilComplete(imputeJobId, POLL_INFERENCE_MAX_MS);
|
|
258
257
|
if (poll.status === "completed") {
|
|
259
258
|
const results = (await apiCall("GET", `/v1/results/${imputeJobId}`));
|
|
260
259
|
const summary = (results.summary ?? {});
|
|
@@ -281,19 +280,18 @@ action=report: Returns a report manifest for the given job_id (job must be compl
|
|
|
281
280
|
body.colormap = colormap;
|
|
282
281
|
const data = (await apiCall("POST", `/v1/results/${job_id}/compare_datasets`, body));
|
|
283
282
|
const compareJobId = data.id;
|
|
284
|
-
const poll = await pollUntilComplete(compareJobId,
|
|
283
|
+
const poll = await pollUntilComplete(compareJobId, POLL_INFERENCE_MAX_MS);
|
|
285
284
|
if (poll.status === "completed") {
|
|
286
285
|
const results = (await apiCall("GET", `/v1/results/${compareJobId}`));
|
|
287
286
|
const summary = (results.summary ?? {});
|
|
288
287
|
const gained = summary.top_gained_nodes ?? [];
|
|
289
288
|
const lost = summary.top_lost_nodes ?? [];
|
|
290
|
-
const fmtNode = (n) => ` node ${n.node_index ?? "?"} [${(n.coords ?? [0, 0]).map(v => Number(v).toFixed(1)).join(",")}] Δ=${Number(n.density_diff ?? 0).toFixed(4)}`;
|
|
291
289
|
const n = top_n ?? 10;
|
|
292
290
|
const content = [{ type: "text", text: [
|
|
293
291
|
`Dataset comparison — job: ${compareJobId}`,
|
|
294
292
|
`Dataset A rows: ${summary.n_rows_a ?? "?"} | Dataset B rows: ${summary.n_rows_b ?? "?"}`,
|
|
295
|
-
`Top ${Math.min(n, gained.length)} gained (B > A):`, ...gained.slice(0, n).map(
|
|
296
|
-
`Top ${Math.min(n, lost.length)} lost (A > B):`, ...lost.slice(0, n).map(
|
|
293
|
+
`Top ${Math.min(n, gained.length)} gained (B > A):`, ...gained.slice(0, n).map(fmtDensityDiffNode),
|
|
294
|
+
`Top ${Math.min(n, lost.length)} lost (A > B):`, ...lost.slice(0, n).map(fmtDensityDiffNode),
|
|
297
295
|
].join("\n") }];
|
|
298
296
|
content.push({ type: "text", text: "Density difference — Positive (red) = Dataset B gained density; Negative (blue) = Dataset A had more density." });
|
|
299
297
|
await tryAttachImage(content, compareJobId, `density_diff.${summary.output_format ?? output_format ?? "png"}`);
|
|
@@ -314,7 +312,7 @@ action=report: Returns a report manifest for the given job_id (job must be compl
|
|
|
314
312
|
const data = (await apiCall("POST", `/v1/results/${job_id}/project_columns`, body));
|
|
315
313
|
const projJobId = data.id;
|
|
316
314
|
const prepareId = await pollPrepareIfPresent(data, "inference(project_columns)");
|
|
317
|
-
const poll = await pollUntilComplete(projJobId,
|
|
315
|
+
const poll = await pollUntilComplete(projJobId, POLL_PROJECT_MAX_MS);
|
|
318
316
|
if (poll.status === "completed") {
|
|
319
317
|
const results = (await apiCall("GET", `/v1/results/${projJobId}`));
|
|
320
318
|
const summary = (results.summary ?? {});
|
package/dist/tools/results.js
CHANGED
|
@@ -2,7 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { registerAuditedTool } from "../audit.js";
|
|
5
|
-
import { apiCall, apiRawCall, getWorkspaceRootAsync, sandboxPath, pollUntilComplete, tryAttachImage, resetInlineAttachBudget,
|
|
5
|
+
import { apiCall, apiRawCall, getWorkspaceRootAsync, sandboxPath, pollUntilComplete, tryAttachImage, resetInlineAttachBudget, attachResultsImages, resolveOutputDpi, POLL_RECOLOR_MAX_MS, fmtDensityDiffNode, getCaptionForImage, shouldFetchAllRemainingFigures, mimeForFilename, structuredTextResult, } from "../shared.js";
|
|
6
6
|
export function registerResultsTool(server) {
|
|
7
7
|
registerAuditedTool(server, "results", `Retrieve, recolor, download, or export figures and metrics for a completed map job. Presentation and artifact access only — operations on the frozen map (predict, compare, project, impute_column, transition_flow) live in **inference**.
|
|
8
8
|
|
|
@@ -85,7 +85,7 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
|
|
|
85
85
|
.optional()
|
|
86
86
|
.describe("action=export: training_log=learning curve; weights=weight matrix; nodes=per-node stats; clusters=regime boundaries; transition_matrix=Markov transition matrix"),
|
|
87
87
|
}, async ({ action, job_id, figures, include_individual, include_json, folder, colormap, output_format, output_dpi, recolor_figures, export_type: exportType }) => {
|
|
88
|
-
const
|
|
88
|
+
const figuresArg = figures;
|
|
89
89
|
if (action === "get") {
|
|
90
90
|
resetInlineAttachBudget();
|
|
91
91
|
const data = (await apiCall("GET", `/v1/results/${job_id}`));
|
|
@@ -119,13 +119,7 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
|
|
|
119
119
|
``, `Arrows show net directional drift. Long/bright = frequent transitions. Short = stable states.`,
|
|
120
120
|
`Background = U-matrix. Use results(action=transition_flow, lag=N) with larger N for longer-term structure.`,
|
|
121
121
|
].join("\n") });
|
|
122
|
-
|
|
123
|
-
const cap = getCaptionForImage(name);
|
|
124
|
-
if (cap)
|
|
125
|
-
content.push({ type: "text", text: cap });
|
|
126
|
-
await tryAttachImage(content, job_id, name);
|
|
127
|
-
inlinedImages.add(name);
|
|
128
|
-
}
|
|
122
|
+
await attachResultsImages(content, job_id, jobType, summary, figuresArg, include_individual, inlinedImages);
|
|
129
123
|
}
|
|
130
124
|
else if (jobType === "project_variable") {
|
|
131
125
|
const varName = summary.variable_name ?? "variable";
|
|
@@ -140,13 +134,7 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
|
|
|
140
134
|
` Mean: ${stats.mean !== undefined ? Number(stats.mean).toFixed(3) : "N/A"}`,
|
|
141
135
|
` Nodes with data: ${stats.n_nodes_with_data ?? "N/A"}`,
|
|
142
136
|
].join("\n") });
|
|
143
|
-
|
|
144
|
-
const cap = getCaptionForImage(name);
|
|
145
|
-
if (cap)
|
|
146
|
-
content.push({ type: "text", text: cap });
|
|
147
|
-
await tryAttachImage(content, job_id, name);
|
|
148
|
-
inlinedImages.add(name);
|
|
149
|
-
}
|
|
137
|
+
await attachResultsImages(content, job_id, jobType, summary, figuresArg, include_individual, inlinedImages);
|
|
150
138
|
}
|
|
151
139
|
else if (jobType === "project_columns") {
|
|
152
140
|
const cols = summary.columns ?? [];
|
|
@@ -158,18 +146,12 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
|
|
|
158
146
|
...Object.entries(colStats).map(([col, s]) => ` ${col}: nodes=${s.n_nodes_with_data ?? "?"} | min=${s.min !== undefined ? Number(s.min).toFixed(3) : "—"} | max=${s.max !== undefined ? Number(s.max).toFixed(3) : "—"}`),
|
|
159
147
|
];
|
|
160
148
|
content.push({ type: "text", text: lines.join("\n") });
|
|
161
|
-
|
|
162
|
-
const cap = getCaptionForImage(name);
|
|
163
|
-
if (cap)
|
|
164
|
-
content.push({ type: "text", text: cap });
|
|
165
|
-
await tryAttachImage(content, job_id, name);
|
|
166
|
-
inlinedImages.add(name);
|
|
167
|
-
}
|
|
149
|
+
await attachResultsImages(content, job_id, jobType, summary, figuresArg, include_individual, inlinedImages);
|
|
168
150
|
}
|
|
169
151
|
else if (jobType === "compare_datasets") {
|
|
170
152
|
const gained = summary.top_gained_nodes ?? [];
|
|
171
153
|
const lost = summary.top_lost_nodes ?? [];
|
|
172
|
-
const fmtNode =
|
|
154
|
+
const fmtNode = fmtDensityDiffNode;
|
|
173
155
|
const lines = [
|
|
174
156
|
`Dataset Comparison — ${resultsHeader}`,
|
|
175
157
|
`Parent map job: ${summary.parent_job_id ?? "N/A"} | Dataset A: ${summary.n_rows_a ?? "?"} rows | Dataset B: ${summary.n_rows_b ?? "?"} rows`,
|
|
@@ -179,13 +161,7 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
|
|
|
179
161
|
...lost.slice(0, 5).map(fmtNode),
|
|
180
162
|
];
|
|
181
163
|
content.push({ type: "text", text: lines.join("\n") });
|
|
182
|
-
|
|
183
|
-
const cap = getCaptionForImage(name);
|
|
184
|
-
if (cap)
|
|
185
|
-
content.push({ type: "text", text: cap });
|
|
186
|
-
await tryAttachImage(content, job_id, name);
|
|
187
|
-
inlinedImages.add(name);
|
|
188
|
-
}
|
|
164
|
+
await attachResultsImages(content, job_id, jobType, summary, figuresArg, include_individual, inlinedImages);
|
|
189
165
|
}
|
|
190
166
|
else if (jobType === "enrich_dataset") {
|
|
191
167
|
// Older exports: summary.job_type from stored results (read-only display).
|
|
@@ -256,23 +232,22 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
|
|
|
256
232
|
: []),
|
|
257
233
|
cv ? [
|
|
258
234
|
"",
|
|
259
|
-
`Held-out imputation accuracy (cv_folds=${cv.cv_folds ?? "?"}, method=${cv.cv_method ?? "
|
|
235
|
+
`Held-out imputation accuracy (cv_folds=${cv.cv_folds ?? "?"}, method=${cv.cv_method ?? "kfold_holdout_pool"}):`,
|
|
260
236
|
cv.columns_sampled ? ` (sampled ${cv.columns_evaluated ?? "?"} columns for speed)` : "",
|
|
261
|
-
cv.aggregate ? ` Aggregate MAE: ${fmt(cv.aggregate.mae)} |
|
|
262
|
-
|
|
237
|
+
cv.aggregate ? ` Aggregate MAE/RMSE (non-cyclic columns): ${fmt(cv.aggregate.mae)} | ${fmt(cv.aggregate.rmse)}` : "",
|
|
238
|
+
cv.aggregate?.excluded_cyclic_columns
|
|
239
|
+
? ` Excluded cyclic columns from aggregate: ${cv.aggregate.excluded_cyclic_columns}`
|
|
240
|
+
: "",
|
|
241
|
+
" See quality.csv — linear: mae/rmse/r2; cyclic_pairs: circ_mae_rad + mean_resultant_length only.",
|
|
242
|
+
" k-fold is relative column difficulty, not external missing-cell ground truth.",
|
|
263
243
|
].filter(Boolean).join("\n") : "\nHeld-out accuracy: skipped (cv_folds=0). Set cv_folds=5 to validate imputation.",
|
|
264
244
|
"",
|
|
265
245
|
`Artifacts: ${(summary.files ?? []).filter((f) => f !== "summary.json").join(", ")}`,
|
|
266
246
|
"Next: results(action=download) for imputed.csv / quality.csv; inference(impute_column) with this job_id as parent for held-out columns.",
|
|
247
|
+
"Optional imputation_uncertainty.csv pool_std is prototype-neighbourhood dispersion (not calibrated predictive SD).",
|
|
267
248
|
].filter((l) => l !== "");
|
|
268
249
|
content.push({ type: "text", text: lines.join("\n") });
|
|
269
|
-
|
|
270
|
-
const cap = getCaptionForImage(name);
|
|
271
|
-
if (cap)
|
|
272
|
-
content.push({ type: "text", text: cap });
|
|
273
|
-
await tryAttachImage(content, job_id, name);
|
|
274
|
-
inlinedImages.add(name);
|
|
275
|
-
}
|
|
250
|
+
await attachResultsImages(content, job_id, jobType, summary, figuresArg, include_individual, inlinedImages);
|
|
276
251
|
}
|
|
277
252
|
else if (jobType === "reduce_spectral") {
|
|
278
253
|
const method = String(summary.method ?? "?");
|
|
@@ -349,13 +324,7 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
|
|
|
349
324
|
files.length > 0 ? `Artifacts: ${files.join(", ")}` : "",
|
|
350
325
|
].filter((l) => l !== "");
|
|
351
326
|
content.push({ type: "text", text: lines.join("\n") });
|
|
352
|
-
|
|
353
|
-
const cap = getCaptionForImage(name);
|
|
354
|
-
if (cap)
|
|
355
|
-
content.push({ type: "text", text: cap });
|
|
356
|
-
await tryAttachImage(content, job_id, name);
|
|
357
|
-
inlinedImages.add(name);
|
|
358
|
-
}
|
|
327
|
+
await attachResultsImages(content, job_id, jobType, summary, figuresArg, include_individual, inlinedImages);
|
|
359
328
|
}
|
|
360
329
|
else {
|
|
361
330
|
const grid = summary.grid ?? [0, 0];
|
|
@@ -438,14 +407,7 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
|
|
|
438
407
|
...((jobType === "train_som" || jobType === "train_siom" || jobType === "train_impute") ? ["", `Next: results_explorer(job_id="${job_id}") to browse all figures interactively; results(action=export, export_type=training_log) for learning curve; results(action=download) to save figures${jobType === "train_impute" ? " (includes imputed.csv)" : ""}; jobs(action=compare, job_ids=[...]) to compare runs; inference(action=predict) to score new data; inference(action=project_columns) to project other variables onto the map.`] : []),
|
|
439
408
|
].filter((l) => l !== "").join("\n");
|
|
440
409
|
content.push({ type: "text", text: textSummary });
|
|
441
|
-
|
|
442
|
-
for (const name of imagesToFetch) {
|
|
443
|
-
const cap = getCaptionForImage(name);
|
|
444
|
-
if (cap)
|
|
445
|
-
content.push({ type: "text", text: cap });
|
|
446
|
-
await tryAttachImage(content, job_id, name);
|
|
447
|
-
inlinedImages.add(name);
|
|
448
|
-
}
|
|
410
|
+
await attachResultsImages(content, job_id, jobType, summary, figuresArg, include_individual, inlinedImages);
|
|
449
411
|
}
|
|
450
412
|
const files = summary.files ?? [];
|
|
451
413
|
const jobType2 = summary.job_type ?? "train_som";
|
|
@@ -476,7 +438,16 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
|
|
|
476
438
|
: "";
|
|
477
439
|
content.push({ type: "text", text: `Available: ${files.join(", ")}. ${logicalNames}Use results(action=get, figures=[...]) for specific plots, figures=all for everything, or figures="none" for metrics-only.` });
|
|
478
440
|
}
|
|
479
|
-
|
|
441
|
+
const structuredPayload = {
|
|
442
|
+
job_id,
|
|
443
|
+
job_type: jobType2,
|
|
444
|
+
label: jobLabel,
|
|
445
|
+
summary,
|
|
446
|
+
files,
|
|
447
|
+
figures_inlined: [...inlinedImages],
|
|
448
|
+
};
|
|
449
|
+
const summaryText = content.filter((c) => c.type === "text").map((c) => c.text).join("\n\n");
|
|
450
|
+
return structuredTextResult(structuredPayload, summaryText, content);
|
|
480
451
|
}
|
|
481
452
|
if (action === "export") {
|
|
482
453
|
if (!exportType)
|
|
@@ -633,16 +604,36 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
|
|
|
633
604
|
}
|
|
634
605
|
await fs.mkdir(resolvedDir, { recursive: true });
|
|
635
606
|
const saved = [];
|
|
607
|
+
const missing = [];
|
|
636
608
|
for (const filename of toDownload) {
|
|
637
609
|
try {
|
|
638
610
|
const { data: buf } = await apiRawCall(`/v1/results/${job_id}/image/${filename}`);
|
|
639
611
|
await fs.writeFile(path.join(resolvedDir, filename), buf);
|
|
640
612
|
saved.push(filename);
|
|
641
613
|
}
|
|
642
|
-
catch {
|
|
614
|
+
catch {
|
|
615
|
+
missing.push(filename);
|
|
616
|
+
}
|
|
643
617
|
}
|
|
644
618
|
const savedDir = path.join(folder, jobSubfolder);
|
|
645
|
-
|
|
619
|
+
if (saved.length === 0) {
|
|
620
|
+
const missNote = missing.length > 0 ? ` Missing/failed: ${missing.join(", ")}.` : "";
|
|
621
|
+
return {
|
|
622
|
+
content: [{
|
|
623
|
+
type: "text",
|
|
624
|
+
text: `No files saved.${missNote} Check job_id and that the job is completed.`,
|
|
625
|
+
}],
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
const missNote = missing.length > 0
|
|
629
|
+
? ` Missing/failed (${missing.length}): ${missing.join(", ")}.`
|
|
630
|
+
: "";
|
|
631
|
+
return {
|
|
632
|
+
content: [{
|
|
633
|
+
type: "text",
|
|
634
|
+
text: `Saved ${saved.length} file(s) to ${savedDir}: ${saved.join(", ")}.${missNote}`,
|
|
635
|
+
}],
|
|
636
|
+
};
|
|
646
637
|
}
|
|
647
638
|
if (action === "recolor") {
|
|
648
639
|
if (!colormap)
|
|
@@ -651,11 +642,11 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
|
|
|
651
642
|
colormap,
|
|
652
643
|
figures: recolor_figures ?? ["combined"],
|
|
653
644
|
output_format: output_format ?? "png",
|
|
654
|
-
output_dpi:
|
|
645
|
+
output_dpi: resolveOutputDpi(output_dpi),
|
|
655
646
|
};
|
|
656
647
|
const data = (await apiCall("POST", `/v1/results/${job_id}/render`, body));
|
|
657
648
|
const newJobId = data.id;
|
|
658
|
-
const poll = await pollUntilComplete(newJobId,
|
|
649
|
+
const poll = await pollUntilComplete(newJobId, POLL_RECOLOR_MAX_MS);
|
|
659
650
|
if (poll.status === "completed") {
|
|
660
651
|
const content = [{ type: "text", text: `Re-rendered with colormap "${colormap}". New job_id: ${newJobId}.` }];
|
|
661
652
|
for (const fig of recolor_figures ?? ["combined"]) {
|