@barivia/barsom-mcp 0.22.1 → 0.22.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -3
- package/dist/index.js +1 -1
- package/dist/job_status_format.js +23 -0
- package/dist/prepare_training_prompt.js +15 -5
- package/dist/shared.js +62 -7
- package/dist/tools/account.js +16 -77
- package/dist/tools/datasets.js +20 -8
- package/dist/tools/explore_map.js +37 -18
- package/dist/tools/results.js +7 -1
- package/dist/tools/train.js +11 -4
- package/dist/tools/training_core.js +6 -1
- package/dist/tools/training_monitor.js +80 -37
- package/dist/train_submit_message.js +5 -7
- package/dist/training_monitor_curve.js +32 -0
- package/dist/ui-delivery.js +185 -0
- package/dist/views/src/views/results-explorer/index.html +16 -13
- package/dist/views/src/views/training-monitor/index.html +30 -18
- package/dist/viz_links.js +25 -0
- package/package.json +1 -1
package/dist/tools/train.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { registerAuditedTool } from "../audit.js";
|
|
3
|
-
import { apiCall
|
|
3
|
+
import { apiCall } from "../shared.js";
|
|
4
4
|
import { buildTrainSubmitExtras } from "../train_submit_message.js";
|
|
5
|
+
import { TRAINING_MONITOR_URI, buildTrainingMonitorResult, } from "./training_monitor.js";
|
|
5
6
|
import { TRAINING_INPUT_SCHEMA, buildTrainMapParams, buildFloopParams, fetchTrainingPresets, assertValidNormalizationArgs, } from "./training_core.js";
|
|
6
7
|
export const TRAIN_DESCRIPTION = `Submit a self-organizing map training job. Returns a job_id; poll with jobs(action=status) and then results(action=get). All actions are async.
|
|
7
8
|
|
|
@@ -89,7 +90,12 @@ async function submitTrainJob(opts) {
|
|
|
89
90
|
}
|
|
90
91
|
data.message = msg;
|
|
91
92
|
data.suggested_next_step = extras.suggested_next_step;
|
|
92
|
-
|
|
93
|
+
const monitor = await buildTrainingMonitorResult(newJobId, { preamble: msg });
|
|
94
|
+
return {
|
|
95
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }, ...monitor.content],
|
|
96
|
+
structuredContent: monitor.structuredContent,
|
|
97
|
+
_meta: { ui: { resourceUri: TRAINING_MONITOR_URI } },
|
|
98
|
+
};
|
|
93
99
|
}
|
|
94
100
|
/**
|
|
95
101
|
* Core training dispatcher. Shared by the `train` tool and the deprecated
|
|
@@ -98,7 +104,7 @@ async function submitTrainJob(opts) {
|
|
|
98
104
|
export async function runTrain(action, args) {
|
|
99
105
|
const dataset_id = args.dataset_id;
|
|
100
106
|
if (action === "map" || action === "siom_map" || action === "impute") {
|
|
101
|
-
const { preset, grid_x, grid_y, epochs, model, periodic, columns, cyclic_features, cyclic_pairs, temporal_features, feature_weights, transforms, auto_log_transforms, time_delay_embeddings, categorical_features, normalize, normalization_methods, sigma_f, learning_rate, batch_size, quality_metrics, backend, output_format, output_dpi, colormap, row_range, gamma, gamma_f, siom_decay, siom_penalty, penalty_alpha, reset_per_epoch, siom_feature_geometry, siom_qe_backend, siom_qe_batch_size, label, cv_folds, viz_mode, viz_top_components, emit_cell_uncertainty, } = args;
|
|
107
|
+
const { preset, grid_x, grid_y, epochs, model, periodic, columns, cyclic_features, cyclic_pairs, temporal_features, feature_weights, transforms, auto_log_transforms, time_delay_embeddings, categorical_features, normalize, normalization_methods, sigma_f, learning_rate, batch_size, quality_metrics, backend, output_format, output_dpi, colormap, row_range, gamma, gamma_f, siom_decay, siom_penalty, penalty_alpha, reset_per_epoch, siom_feature_geometry, siom_qe_backend, siom_qe_batch_size, label, cv_folds, viz_mode, viz_top_components, viz_upload_cyclic_components, emit_cell_uncertainty, } = args;
|
|
102
108
|
let PRESETS = {};
|
|
103
109
|
try {
|
|
104
110
|
PRESETS = await fetchTrainingPresets();
|
|
@@ -123,7 +129,8 @@ export async function runTrain(action, args) {
|
|
|
123
129
|
temporal_features, feature_weights, transforms, auto_log_transforms,
|
|
124
130
|
time_delay_embeddings, categorical_features,
|
|
125
131
|
normalize, normalization_methods, sigma_f, learning_rate, batch_size, quality_metrics, backend,
|
|
126
|
-
output_format, output_dpi, colormap, row_range,
|
|
132
|
+
output_format, output_dpi, colormap, row_range, siom_feature_geometry, siom_qe_backend, siom_qe_batch_size,
|
|
133
|
+
viz_upload_cyclic_components,
|
|
127
134
|
}, PRESETS);
|
|
128
135
|
if (action === "impute") {
|
|
129
136
|
params._job_type = "train_impute";
|
|
@@ -113,6 +113,8 @@ export const TRAINING_INPUT_SCHEMA = {
|
|
|
113
113
|
.describe("Visualization density; auto-capped when feature count > 40"),
|
|
114
114
|
viz_top_components: z.number().int().min(0).max(64).optional()
|
|
115
115
|
.describe("With viz_mode=summary_plus_top: upload top-N component maps by variance (default 8)"),
|
|
116
|
+
viz_upload_cyclic_components: z.boolean().optional()
|
|
117
|
+
.describe("When true with individual component PNGs, also publish cos/sin cyclic expansion planes (default false; recovered hour/month panels stay in combined.png)"),
|
|
116
118
|
emit_cell_uncertainty: z.boolean().optional()
|
|
117
119
|
.describe("impute: write imputation_uncertainty.csv (pool_std = prototype-neighbourhood dispersion, not calibrated SD; pool_n = contributing nodes)"),
|
|
118
120
|
gamma: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional())
|
|
@@ -245,7 +247,7 @@ export function buildFloopParams(args) {
|
|
|
245
247
|
return { params, paramSummary };
|
|
246
248
|
}
|
|
247
249
|
export function buildTrainMapParams(args, presets) {
|
|
248
|
-
const { preset, grid_x, grid_y, epochs, model, periodic, columns, cyclic_features, cyclic_pairs, temporal_features, feature_weights, transforms, auto_log_transforms, time_delay_embeddings, categorical_features, normalize, normalization_methods, sigma_f, learning_rate, batch_size, quality_metrics, backend, output_format, output_dpi, colormap, row_range, siom_feature_geometry, siom_qe_backend, siom_qe_batch_size, } = args;
|
|
250
|
+
const { preset, grid_x, grid_y, epochs, model, periodic, columns, cyclic_features, cyclic_pairs, temporal_features, feature_weights, transforms, auto_log_transforms, time_delay_embeddings, categorical_features, normalize, normalization_methods, sigma_f, learning_rate, batch_size, quality_metrics, backend, output_format, output_dpi, colormap, row_range, siom_feature_geometry, siom_qe_backend, siom_qe_batch_size, viz_upload_cyclic_components, } = args;
|
|
249
251
|
const p = preset ? presets[preset] : undefined;
|
|
250
252
|
const params = {
|
|
251
253
|
model: model ?? "SOM",
|
|
@@ -308,6 +310,9 @@ export function buildTrainMapParams(args, presets) {
|
|
|
308
310
|
params.siom_qe_backend = siom_qe_backend;
|
|
309
311
|
if (siom_qe_batch_size !== undefined)
|
|
310
312
|
params.siom_qe_batch_size = siom_qe_batch_size;
|
|
313
|
+
if (viz_upload_cyclic_components !== undefined) {
|
|
314
|
+
params.viz_upload_cyclic_components = viz_upload_cyclic_components;
|
|
315
|
+
}
|
|
311
316
|
const effectiveGrid = params.grid;
|
|
312
317
|
const effectiveEpochs = params.epochs;
|
|
313
318
|
const effectiveBatch = params.batch_size;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
|
|
3
3
|
import { runMcpToolAudit } from "../audit.js";
|
|
4
|
-
import { apiCall,
|
|
4
|
+
import { apiCall, BARSOM_VIZ_VIEWS, buildStandaloneVizPageUrl, buildVizRelatedUrls, getVizPort, structuredTextResult, } from "../shared.js";
|
|
5
|
+
import { appendUiDeliveryContent, beginInlineFallback, resolveUiDelivery, tryAttachLearningCurve, } from "../ui-delivery.js";
|
|
5
6
|
import { lastEpochTeFromCurves } from "../training_monitor_curve.js";
|
|
7
|
+
import { buildVizStandaloneUrl, resultsExplorerStandaloneLinkText, } from "../viz_links.js";
|
|
6
8
|
export const TRAINING_MONITOR_URI = "ui://barsom/training-monitor";
|
|
7
9
|
export const TRAINING_MONITOR_REFRESH_MS = 5000;
|
|
8
10
|
function hasCurveArrays(data) {
|
|
@@ -14,8 +16,12 @@ function hasCurveArrays(data) {
|
|
|
14
16
|
];
|
|
15
17
|
return keys.some((k) => Array.isArray(data[k]) && data[k].length > 0);
|
|
16
18
|
}
|
|
17
|
-
function buildStructuredContent(job_id, data) {
|
|
19
|
+
function buildStructuredContent(job_id, data, port = getVizPort()) {
|
|
18
20
|
const id = String(data.id ?? job_id);
|
|
21
|
+
const standaloneUrl = buildStandaloneVizPageUrl(port, BARSOM_VIZ_VIEWS.trainingMonitor, id) ??
|
|
22
|
+
buildVizStandaloneUrl("training-monitor", id);
|
|
23
|
+
const resultsExplorerUrl = buildStandaloneVizPageUrl(port, BARSOM_VIZ_VIEWS.resultsExplorer, id) ??
|
|
24
|
+
buildVizStandaloneUrl("results-explorer", id);
|
|
19
25
|
return {
|
|
20
26
|
...data,
|
|
21
27
|
type: "training-monitor",
|
|
@@ -23,6 +29,9 @@ function buildStructuredContent(job_id, data) {
|
|
|
23
29
|
id,
|
|
24
30
|
job_id: id,
|
|
25
31
|
refresh_interval_ms: TRAINING_MONITOR_REFRESH_MS,
|
|
32
|
+
standaloneUrl,
|
|
33
|
+
resultsExplorerUrl,
|
|
34
|
+
relatedUrls: buildVizRelatedUrls(port, id, BARSOM_VIZ_VIEWS),
|
|
26
35
|
};
|
|
27
36
|
}
|
|
28
37
|
function hasTeCurveArrays(data) {
|
|
@@ -81,6 +90,71 @@ async function enrichWithTrainingLog(job_id, data) {
|
|
|
81
90
|
return data;
|
|
82
91
|
}
|
|
83
92
|
}
|
|
93
|
+
export async function buildTrainingMonitorResult(job_id, opts) {
|
|
94
|
+
const { fetch_training_log, preamble } = opts ?? {};
|
|
95
|
+
let data = (await apiCall("GET", `/v1/jobs/${job_id}`));
|
|
96
|
+
const jobStatus = String(data.status ?? "");
|
|
97
|
+
if (fetch_training_log || needsTrainingLogEnrichment(data)) {
|
|
98
|
+
data = await enrichWithTrainingLog(job_id, data);
|
|
99
|
+
}
|
|
100
|
+
const port = getVizPort();
|
|
101
|
+
const structuredContent = buildStructuredContent(job_id, data, port);
|
|
102
|
+
const status = jobStatus;
|
|
103
|
+
const progress = (data.progress ?? 0) * 100;
|
|
104
|
+
const etaSec = data.training_eta_sec != null ? Number(data.training_eta_sec) : null;
|
|
105
|
+
const elapsedSec = data.training_elapsed_sec != null ? Number(data.training_elapsed_sec) : null;
|
|
106
|
+
const epoch = data.epoch != null ? Number(data.epoch) : null;
|
|
107
|
+
const totalEpochs = data.total_epochs != null ? Number(data.total_epochs) : null;
|
|
108
|
+
const timingParts = [];
|
|
109
|
+
if (elapsedSec != null && elapsedSec >= 0)
|
|
110
|
+
timingParts.push(`elapsed ${Math.round(elapsedSec)}s`);
|
|
111
|
+
if (etaSec != null && etaSec > 0)
|
|
112
|
+
timingParts.push(`ETA ~${Math.round(etaSec)}s`);
|
|
113
|
+
if (epoch != null && totalEpochs != null)
|
|
114
|
+
timingParts.push(`epoch ${epoch}/${totalEpochs}`);
|
|
115
|
+
const timingNote = timingParts.length > 0 ? ` ${timingParts.join(", ")}.` : "";
|
|
116
|
+
const text = (preamble ? `${preamble}\n\n` : "") +
|
|
117
|
+
`Training monitor (live when embedded, refreshes every ${TRAINING_MONITOR_REFRESH_MS / 1000}s): job ${job_id} — ${status} (${progress.toFixed(1)}%).${timingNote} ` +
|
|
118
|
+
`Optional UI; jobs(action=status) is enough to finish the workflow headless.`;
|
|
119
|
+
const content = [{ type: "text", text }];
|
|
120
|
+
const terminal = status === "completed" || status === "failed" || status === "cancelled";
|
|
121
|
+
let inlineImageAttached = false;
|
|
122
|
+
if (terminal) {
|
|
123
|
+
inlineImageAttached = await tryAttachLearningCurve(content, job_id);
|
|
124
|
+
}
|
|
125
|
+
const uiDelivery = resolveUiDelivery({
|
|
126
|
+
tool: "training_monitor",
|
|
127
|
+
jobId: job_id,
|
|
128
|
+
jobStatus: status,
|
|
129
|
+
inlineImageAttached,
|
|
130
|
+
});
|
|
131
|
+
appendUiDeliveryContent(content, uiDelivery, {
|
|
132
|
+
linkLabel: "Open training monitor",
|
|
133
|
+
primaryUrl: structuredContent.standaloneUrl,
|
|
134
|
+
});
|
|
135
|
+
if (status === "completed" && uiDelivery.urls?.length) {
|
|
136
|
+
const resultsUrl = uiDelivery.urls.find((u) => u.includes("results-explorer"));
|
|
137
|
+
if (resultsUrl) {
|
|
138
|
+
content.push({
|
|
139
|
+
type: "text",
|
|
140
|
+
text: `Job completed — browse map figures: [Open results explorer](${resultsUrl})`,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
structuredContent.ui_delivery = uiDelivery;
|
|
145
|
+
return { structuredContent, content, text };
|
|
146
|
+
}
|
|
147
|
+
/** After terminal + finalize, append results explorer open hint (monitor completion path). */
|
|
148
|
+
export function appendResultsExplorerCompletionHint(content, job_id) {
|
|
149
|
+
const link = resultsExplorerStandaloneLinkText(job_id);
|
|
150
|
+
if (link) {
|
|
151
|
+
content.push({
|
|
152
|
+
type: "text",
|
|
153
|
+
text: `${link}\n` +
|
|
154
|
+
`AGENT: when the job is fully complete (finalize done if any), call results_explorer(job_id="${job_id}") in the same turn and surface this link — do not wait for the user to ask.`,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
84
158
|
export function registerTrainingMonitorTool(server) {
|
|
85
159
|
registerAppTool(server, "training_monitor", {
|
|
86
160
|
title: "Training Monitor",
|
|
@@ -94,42 +168,11 @@ export function registerTrainingMonitorTool(server) {
|
|
|
94
168
|
},
|
|
95
169
|
_meta: { ui: { resourceUri: TRAINING_MONITOR_URI } },
|
|
96
170
|
}, async (args) => runMcpToolAudit("training_monitor", "default", args, async () => {
|
|
171
|
+
beginInlineFallback();
|
|
97
172
|
const { job_id, fetch_training_log } = args;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
data = await enrichWithTrainingLog(job_id, data);
|
|
102
|
-
}
|
|
103
|
-
const structuredContent = buildStructuredContent(job_id, data);
|
|
104
|
-
const status = jobStatus;
|
|
105
|
-
const progress = (data.progress ?? 0) * 100;
|
|
106
|
-
const etaSec = data.training_eta_sec != null ? Number(data.training_eta_sec) : null;
|
|
107
|
-
const elapsedSec = data.training_elapsed_sec != null ? Number(data.training_elapsed_sec) : null;
|
|
108
|
-
const epoch = data.epoch != null ? Number(data.epoch) : null;
|
|
109
|
-
const totalEpochs = data.total_epochs != null ? Number(data.total_epochs) : null;
|
|
110
|
-
const timingParts = [];
|
|
111
|
-
if (elapsedSec != null && elapsedSec >= 0)
|
|
112
|
-
timingParts.push(`elapsed ${Math.round(elapsedSec)}s`);
|
|
113
|
-
if (etaSec != null && etaSec > 0)
|
|
114
|
-
timingParts.push(`ETA ~${Math.round(etaSec)}s`);
|
|
115
|
-
if (epoch != null && totalEpochs != null)
|
|
116
|
-
timingParts.push(`epoch ${epoch}/${totalEpochs}`);
|
|
117
|
-
const timingNote = timingParts.length > 0 ? ` ${timingParts.join(", ")}.` : "";
|
|
118
|
-
const text = `Training monitor (live when embedded, refreshes every ${TRAINING_MONITOR_REFRESH_MS / 1000}s): job ${job_id} — ${status} (${progress.toFixed(1)}%).${timingNote} ` +
|
|
119
|
-
`Optional UI; jobs(action=status) is enough to finish the workflow.`;
|
|
120
|
-
const content = [{ type: "text", text }];
|
|
121
|
-
const port = getVizPort();
|
|
122
|
-
const standaloneUrl = port
|
|
123
|
-
? `http://localhost:${port}/viz/training-monitor?mode=standalone&job_id=${encodeURIComponent(job_id)}`
|
|
124
|
-
: undefined;
|
|
125
|
-
if (!getClientSupportsMcpApps() && standaloneUrl) {
|
|
126
|
-
content.push({
|
|
127
|
-
type: "text",
|
|
128
|
-
text: `Live training monitor: [Open training monitor](${standaloneUrl})\n` +
|
|
129
|
-
`AGENT: surface this to the user as a clickable markdown link (e.g. "[Open training monitor](${standaloneUrl})") in your reply — do not leave it buried in tool output. Re-post the link after the job completes so the user can return to the curves.\n` +
|
|
130
|
-
`This localhost port is assigned per MCP session and changes if the proxy restarts — if the page stays at "running 0%" or stops updating, the link is stale: re-run training_monitor for a fresh URL, or set BARIVIA_VIZ_PORT in your MCP env for a persistent port. Health check: http://localhost:${port}/api/health?job_id=${encodeURIComponent(job_id)}`,
|
|
131
|
-
});
|
|
132
|
-
}
|
|
173
|
+
const { structuredContent, content, text } = await buildTrainingMonitorResult(job_id, {
|
|
174
|
+
fetch_training_log,
|
|
175
|
+
});
|
|
133
176
|
return {
|
|
134
177
|
...structuredTextResult(structuredContent, text, content),
|
|
135
178
|
_meta: { ui: { resourceUri: TRAINING_MONITOR_URI } },
|
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { buildVizStandaloneUrl } from "./viz_links.js";
|
|
2
2
|
export function buildTrainSubmitExtras(opts) {
|
|
3
3
|
const { newJobId, totalRows, hasTransforms, prepareJobId, variantPrefix, paramSummary, impute, resultsHint } = opts;
|
|
4
|
-
const monitorUrl =
|
|
5
|
-
? `http://localhost:${getVizPort()}/viz/training-monitor?mode=standalone&job_id=${encodeURIComponent(newJobId)}`
|
|
6
|
-
: null;
|
|
4
|
+
const monitorUrl = buildVizStandaloneUrl("training-monitor", newJobId);
|
|
7
5
|
let msg = `Job submitted (${variantPrefix}, ${paramSummary}). `;
|
|
8
6
|
if (prepareJobId) {
|
|
9
7
|
msg += `Preprocessing job prepare_training_matrix (${prepareJobId}) runs on worker-io first; train job ${newJobId} starts after it completes. Poll jobs(action=status, job_id="${prepareJobId}") until completed, then poll the train job. `;
|
|
10
8
|
}
|
|
11
|
-
msg += `
|
|
9
|
+
msg += `Training monitor opened for job ${newJobId}`;
|
|
12
10
|
if (monitorUrl)
|
|
13
|
-
msg += `
|
|
14
|
-
msg +=
|
|
11
|
+
msg += ` — standalone: ${monitorUrl}`;
|
|
12
|
+
msg += `. Poll jobs(action=status, job_id="${newJobId}") every 60–120s for large jobs if needed. `;
|
|
15
13
|
msg += `When status is completed, check jobs(action=status) for finalize_job_id — if present, poll that finalize_training job before results(action=get). `;
|
|
16
14
|
msg += `Then use results(action=get, job_id="${newJobId}")`;
|
|
17
15
|
if (resultsHint) {
|
|
@@ -83,3 +83,35 @@ export function lastEpochTeFromCurves(data) {
|
|
|
83
83
|
}
|
|
84
84
|
return null;
|
|
85
85
|
}
|
|
86
|
+
export function isTerminalJobStatus(status) {
|
|
87
|
+
return status === "completed" || status === "failed" || status === "cancelled";
|
|
88
|
+
}
|
|
89
|
+
/** True when compute + finalize (if any) are done — safe to open results_explorer. */
|
|
90
|
+
export function isFullyComplete(data) {
|
|
91
|
+
const status = String(data.status ?? "");
|
|
92
|
+
if (status !== "completed")
|
|
93
|
+
return isTerminalJobStatus(status);
|
|
94
|
+
const finalizeId = data.finalize_job_id != null && String(data.finalize_job_id) !== ""
|
|
95
|
+
? String(data.finalize_job_id)
|
|
96
|
+
: null;
|
|
97
|
+
if (!finalizeId)
|
|
98
|
+
return true;
|
|
99
|
+
return String(data.finalize_status ?? "") === "completed";
|
|
100
|
+
}
|
|
101
|
+
/** Keep polling while finalize is still running after parent compute completed. */
|
|
102
|
+
export function shouldKeepPollingJob(data) {
|
|
103
|
+
const status = String(data.status ?? "");
|
|
104
|
+
if (!isTerminalJobStatus(status))
|
|
105
|
+
return true;
|
|
106
|
+
if (status !== "completed")
|
|
107
|
+
return false;
|
|
108
|
+
if (data.finalize_failed === true)
|
|
109
|
+
return false;
|
|
110
|
+
const finalizeId = data.finalize_job_id != null && String(data.finalize_job_id) !== ""
|
|
111
|
+
? String(data.finalize_job_id)
|
|
112
|
+
: null;
|
|
113
|
+
const finalizeStatus = data.finalize_status != null && String(data.finalize_status) !== ""
|
|
114
|
+
? String(data.finalize_status)
|
|
115
|
+
: null;
|
|
116
|
+
return finalizeId != null && finalizeStatus !== "completed";
|
|
117
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-capability adaptation for MCP App tools (training_monitor, results_explorer).
|
|
3
|
+
* Respects BARIVIA_UI_DELIVERY override and emits a structured block agents can rely on.
|
|
4
|
+
*/
|
|
5
|
+
import { apiRawCall, getClientSupportsMcpApps, getVizPort, mimeForFilename, resetInlineAttachBudget, } from "./shared.js";
|
|
6
|
+
import { buildVizStandaloneUrl } from "./viz_links.js";
|
|
7
|
+
export function parseUiDeliveryOverride() {
|
|
8
|
+
const raw = (process.env.BARIVIA_UI_DELIVERY ?? "auto").trim().toLowerCase();
|
|
9
|
+
if (raw === "apps" || raw === "localhost" || raw === "inline" || raw === "auto") {
|
|
10
|
+
return raw;
|
|
11
|
+
}
|
|
12
|
+
return "auto";
|
|
13
|
+
}
|
|
14
|
+
function vizPortPinned() {
|
|
15
|
+
return Boolean(process.env.BARIVIA_VIZ_PORT?.trim());
|
|
16
|
+
}
|
|
17
|
+
function viewForTool(tool) {
|
|
18
|
+
return tool === "training_monitor" ? "training-monitor" : "results-explorer";
|
|
19
|
+
}
|
|
20
|
+
/** Prefer viz_links (single URL builder) so train/monitor/explorer stay consistent. */
|
|
21
|
+
export function buildStandaloneVizUrl(view, jobId) {
|
|
22
|
+
return buildVizStandaloneUrl(viewForTool(view), jobId);
|
|
23
|
+
}
|
|
24
|
+
export function buildHealthUrl(jobId) {
|
|
25
|
+
const port = getVizPort();
|
|
26
|
+
if (!port)
|
|
27
|
+
return undefined;
|
|
28
|
+
const base = `http://localhost:${port}/api/health`;
|
|
29
|
+
return jobId ? `${base}?job_id=${encodeURIComponent(jobId)}` : base;
|
|
30
|
+
}
|
|
31
|
+
function hintForTier(tier, tool, opts) {
|
|
32
|
+
const label = tool === "training_monitor" ? "training monitor" : "results explorer";
|
|
33
|
+
switch (tier) {
|
|
34
|
+
case "embedded":
|
|
35
|
+
return `The ${label} MCP App panel is embedded in this client. Also mention the standalone URL if the user may open a browser tab.`;
|
|
36
|
+
case "localhost":
|
|
37
|
+
return opts.standaloneUrl
|
|
38
|
+
? `AGENT: Post a clickable markdown link to the user (e.g. "[Open ${label}](${opts.standaloneUrl})") — do not bury it in tool output or use broken workspace file paths. Re-post after the job completes if status was running.`
|
|
39
|
+
: `AGENT: Viz server unavailable — use jobs(status) and results(get) for progress and figures.`;
|
|
40
|
+
case "inline_image":
|
|
41
|
+
return `AGENT: Inline raster figure(s) are attached below. Summarize metrics in your reply; offer results_explorer or results(get) for additional figures.`;
|
|
42
|
+
case "text_only":
|
|
43
|
+
return `AGENT: No embedded panel or localhost viz — use jobs(status) for training progress and results(get) or results_explorer for figures.`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function resolveUiDelivery(options) {
|
|
47
|
+
const override = parseUiDeliveryOverride();
|
|
48
|
+
const mcpApps = getClientSupportsMcpApps();
|
|
49
|
+
const port = getVizPort() || null;
|
|
50
|
+
const standaloneUrl = buildStandaloneVizUrl(options.tool, options.jobId);
|
|
51
|
+
let delivery;
|
|
52
|
+
if (override === "apps") {
|
|
53
|
+
delivery = mcpApps ? "embedded" : port ? "localhost" : "text_only";
|
|
54
|
+
}
|
|
55
|
+
else if (override === "localhost") {
|
|
56
|
+
delivery = port ? "localhost" : options.inlineImageAttached ? "inline_image" : "text_only";
|
|
57
|
+
}
|
|
58
|
+
else if (override === "inline") {
|
|
59
|
+
delivery = options.inlineImageAttached ? "inline_image" : "text_only";
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
// auto
|
|
63
|
+
if (mcpApps) {
|
|
64
|
+
delivery = "embedded";
|
|
65
|
+
}
|
|
66
|
+
else if (port) {
|
|
67
|
+
delivery = "localhost";
|
|
68
|
+
}
|
|
69
|
+
else if (options.inlineImageAttached) {
|
|
70
|
+
delivery = "inline_image";
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
delivery = "text_only";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const urls = [];
|
|
77
|
+
if (standaloneUrl && (delivery === "embedded" || delivery === "localhost")) {
|
|
78
|
+
urls.push(standaloneUrl);
|
|
79
|
+
}
|
|
80
|
+
const healthUrl = buildHealthUrl(options.jobId);
|
|
81
|
+
if (healthUrl && delivery === "localhost") {
|
|
82
|
+
urls.push(healthUrl);
|
|
83
|
+
}
|
|
84
|
+
const crossLink = options.tool === "training_monitor" && options.jobStatus === "completed"
|
|
85
|
+
? buildStandaloneVizUrl("results_explorer", options.jobId)
|
|
86
|
+
: options.tool === "results_explorer"
|
|
87
|
+
? buildStandaloneVizUrl("training_monitor", options.jobId)
|
|
88
|
+
: undefined;
|
|
89
|
+
if (crossLink && !urls.includes(crossLink)) {
|
|
90
|
+
urls.push(crossLink);
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
delivery,
|
|
94
|
+
urls: urls.length > 0 ? urls : undefined,
|
|
95
|
+
hint_for_agent: hintForTier(delivery, options.tool, { standaloneUrl, jobStatus: options.jobStatus }),
|
|
96
|
+
mcp_apps_supported: mcpApps,
|
|
97
|
+
viz_port: port,
|
|
98
|
+
port_pinned: vizPortPinned(),
|
|
99
|
+
override,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Prepend prominent standalone URL (hosts may advertise Apps but not mount them),
|
|
104
|
+
* then structured ui_delivery JSON + agent hint.
|
|
105
|
+
*/
|
|
106
|
+
export function appendUiDeliveryContent(content, block, options) {
|
|
107
|
+
const primary = options?.primaryUrl ?? block.urls?.[0];
|
|
108
|
+
const label = options?.linkLabel ?? "Open interactive view";
|
|
109
|
+
const prefix = [];
|
|
110
|
+
if (primary) {
|
|
111
|
+
const embedNote = block.delivery === "embedded"
|
|
112
|
+
? "Embedded panel if your host supports MCP Apps — otherwise open the standalone URL below (expected on some Cursor builds)."
|
|
113
|
+
: "Open the standalone URL below when no inline panel appears.";
|
|
114
|
+
prefix.push({
|
|
115
|
+
type: "text",
|
|
116
|
+
text: `${embedNote}\n` +
|
|
117
|
+
`Standalone URL (copy if Open is blocked):\n${primary}\n` +
|
|
118
|
+
`[${label}](${primary})\n` +
|
|
119
|
+
`${block.hint_for_agent}`,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
prefix.push({
|
|
123
|
+
type: "text",
|
|
124
|
+
text: "```json\n" + JSON.stringify({ ui_delivery: block }, null, 2) + "\n```",
|
|
125
|
+
});
|
|
126
|
+
if (primary && !block.port_pinned && block.delivery !== "text_only") {
|
|
127
|
+
prefix.push({
|
|
128
|
+
type: "text",
|
|
129
|
+
text: "Tip: set BARIVIA_VIZ_PORT in MCP env for a session-stable localhost port; re-run this tool if the page stops updating.",
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
content.unshift(...prefix);
|
|
133
|
+
}
|
|
134
|
+
export async function tryAttachLearningCurve(content, jobId) {
|
|
135
|
+
for (const ext of ["png", "svg"]) {
|
|
136
|
+
try {
|
|
137
|
+
const filename = `learning_curve.${ext}`;
|
|
138
|
+
const { data: buf } = await apiRawCall(`/v1/results/${jobId}/image/${filename}`);
|
|
139
|
+
content.push({
|
|
140
|
+
type: "image",
|
|
141
|
+
data: buf.toString("base64"),
|
|
142
|
+
mimeType: mimeForFilename(filename),
|
|
143
|
+
annotations: { audience: ["user"], priority: 0.75 },
|
|
144
|
+
});
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
export function getMcpClientDiagnostics() {
|
|
154
|
+
const override = parseUiDeliveryOverride();
|
|
155
|
+
const mcpApps = getClientSupportsMcpApps();
|
|
156
|
+
const port = getVizPort() || null;
|
|
157
|
+
let activeTier = "text_only";
|
|
158
|
+
if (override === "apps") {
|
|
159
|
+
activeTier = mcpApps ? "embedded" : port ? "localhost" : "text_only";
|
|
160
|
+
}
|
|
161
|
+
else if (override === "localhost") {
|
|
162
|
+
activeTier = port ? "localhost" : "text_only";
|
|
163
|
+
}
|
|
164
|
+
else if (override === "inline") {
|
|
165
|
+
activeTier = "inline_image";
|
|
166
|
+
}
|
|
167
|
+
else if (mcpApps) {
|
|
168
|
+
activeTier = "embedded";
|
|
169
|
+
}
|
|
170
|
+
else if (port) {
|
|
171
|
+
activeTier = "localhost";
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
mcp_apps_supported: mcpApps,
|
|
175
|
+
viz_port: port,
|
|
176
|
+
viz_port_pinned: vizPortPinned(),
|
|
177
|
+
ui_delivery_override: override,
|
|
178
|
+
active_delivery_tier: activeTier,
|
|
179
|
+
health_url: buildHealthUrl(),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/** Reset inline budget at tool entry when attaching fallback images. */
|
|
183
|
+
export function beginInlineFallback() {
|
|
184
|
+
resetInlineAttachBudget();
|
|
185
|
+
}
|