@barivia/barmesh-mcp 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/richardson_results.js +35 -0
- package/dist/shared.js +275 -38
- package/dist/tools/barmesh_results_explorer.js +35 -29
- package/dist/tools/cfd.js +17 -5
- package/dist/tools/datasets.js +50 -18
- package/dist/tools/feedback.js +71 -6
- package/dist/tools/guide.js +33 -9
- package/dist/tools/jobs.js +117 -11
- package/dist/tools/results.js +9 -3
- package/dist/tools/training_monitor.js +25 -20
- package/dist/ui-delivery.js +155 -0
- package/dist/views/src/views/barmesh-results-explorer/index.html +18 -16
- package/dist/views/src/views/barmesh-training-monitor/index.html +36 -24
- package/dist/viz_links.js +55 -0
- package/package.json +1 -1
|
@@ -1,9 +1,11 @@
|
|
|
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, apiRawCall,
|
|
4
|
+
import { apiCall, apiRawCall, BARMESH_VIZ_VIEWS, buildStandaloneVizPageUrl, buildVizRelatedUrls, getVizPort, getCaptionForImage, mimeForFilename, structuredTextResult, tryAttachImage, } from "../shared.js";
|
|
5
5
|
import { FIGURE_SECTION_LABELS, FIGURE_SECTION_ORDER, figureSection, sortRank, } from "../figure_sections.js";
|
|
6
6
|
import { buildMetadataStrip } from "../results_metadata.js";
|
|
7
|
+
import { appendUiDeliveryContent, buildStandaloneVizUrl, resolveUiDelivery, } from "../ui-delivery.js";
|
|
8
|
+
import { buildBarmeshVizStandaloneUrl } from "../viz_links.js";
|
|
7
9
|
export const RESULTS_EXPLORER_URI = "ui://barmesh/results-explorer";
|
|
8
10
|
export function sortFigures(figures) {
|
|
9
11
|
return [...figures].sort((a, b) => FIGURE_SECTION_ORDER.indexOf(a.section ?? "diagnostics") -
|
|
@@ -111,6 +113,10 @@ export function buildPayload(jobId, data, jobMeta) {
|
|
|
111
113
|
const files = (summary.files ?? []).filter((f) => /\.(png|pdf|svg)$/i.test(f));
|
|
112
114
|
const figures = sortFigures(groupFigures(files));
|
|
113
115
|
const port = getVizPort();
|
|
116
|
+
const standaloneUrl = buildStandaloneVizPageUrl(port, BARMESH_VIZ_VIEWS.resultsExplorer, jobId) ??
|
|
117
|
+
buildBarmeshVizStandaloneUrl("barmesh-results-explorer", jobId);
|
|
118
|
+
const trainingMonitorUrl = buildStandaloneVizPageUrl(port, BARMESH_VIZ_VIEWS.trainingMonitor, jobId) ??
|
|
119
|
+
buildBarmeshVizStandaloneUrl("barmesh-training-monitor", jobId);
|
|
114
120
|
return {
|
|
115
121
|
type: "barmesh-results-explorer",
|
|
116
122
|
jobId,
|
|
@@ -120,9 +126,9 @@ export function buildPayload(jobId, data, jobMeta) {
|
|
|
120
126
|
metrics: buildMetrics(summary),
|
|
121
127
|
availableFigures: figures,
|
|
122
128
|
defaultFigureKey: resolveDefaultFigureKey(figures),
|
|
123
|
-
standaloneUrl
|
|
124
|
-
|
|
125
|
-
|
|
129
|
+
standaloneUrl,
|
|
130
|
+
trainingMonitorUrl,
|
|
131
|
+
relatedUrls: buildVizRelatedUrls(port, jobId, BARMESH_VIZ_VIEWS),
|
|
126
132
|
};
|
|
127
133
|
}
|
|
128
134
|
async function handleResultsExplorer(job_id) {
|
|
@@ -131,40 +137,40 @@ async function handleResultsExplorer(job_id) {
|
|
|
131
137
|
apiCall("GET", `/v1/jobs/${job_id}`).catch(() => ({})),
|
|
132
138
|
]);
|
|
133
139
|
const payload = buildPayload(job_id, data, jobMeta);
|
|
134
|
-
const
|
|
135
|
-
{
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
.join("\n"),
|
|
144
|
-
},
|
|
145
|
-
];
|
|
140
|
+
const summaryText = [
|
|
141
|
+
`Mesh convergence results explorer ready for job ${job_id}.`,
|
|
142
|
+
payload.subtitle ? `Label: ${payload.subtitle}` : "",
|
|
143
|
+
payload.metadataStrip?.[0] ?? "",
|
|
144
|
+
]
|
|
145
|
+
.filter(Boolean)
|
|
146
|
+
.join("\n");
|
|
147
|
+
const content = [{ type: "text", text: summaryText }];
|
|
148
|
+
let inlineImageAttached = false;
|
|
146
149
|
const defaultFigure = payload.availableFigures.find((f) => f.key === payload.defaultFigureKey);
|
|
147
150
|
if (defaultFigure && /\.(png|svg|jpe?g|webp)$/i.test(defaultFigure.filename)) {
|
|
151
|
+
const before = content.length;
|
|
148
152
|
await tryAttachImage(content, job_id, defaultFigure.filename);
|
|
153
|
+
inlineImageAttached = content.length > before && content.some((c) => c.type === "image");
|
|
149
154
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
155
|
+
const uiDelivery = resolveUiDelivery({
|
|
156
|
+
tool: "results_explorer",
|
|
157
|
+
jobId: job_id,
|
|
158
|
+
jobStatus: "completed",
|
|
159
|
+
inlineImageAttached,
|
|
160
|
+
});
|
|
161
|
+
appendUiDeliveryContent(content, uiDelivery, {
|
|
162
|
+
linkLabel: "Open results explorer",
|
|
163
|
+
primaryUrl: payload.standaloneUrl ?? buildStandaloneVizUrl("results_explorer", job_id),
|
|
164
|
+
});
|
|
165
|
+
const monitorUrl = payload.trainingMonitorUrl ?? buildStandaloneVizUrl("training_monitor", job_id);
|
|
166
|
+
if (monitorUrl) {
|
|
159
167
|
content.push({
|
|
160
168
|
type: "text",
|
|
161
|
-
text: `
|
|
162
|
-
`AGENT: surface this to the user as a clickable markdown link in your reply. ` +
|
|
163
|
-
`This localhost port is assigned per MCP session and changes if the proxy restarts — re-run barmesh_results_explorer for a fresh URL, or set BARIVIA_VIZ_PORT for a persistent port.`,
|
|
169
|
+
text: `Review training curves: [Open training monitor](${monitorUrl})`,
|
|
164
170
|
});
|
|
165
171
|
}
|
|
166
172
|
return {
|
|
167
|
-
...structuredTextResult(payload, undefined, content),
|
|
173
|
+
...structuredTextResult({ ...payload, ui_delivery: uiDelivery }, undefined, content),
|
|
168
174
|
_meta: { ui: { resourceUri: RESULTS_EXPLORER_URI } },
|
|
169
175
|
};
|
|
170
176
|
}
|
package/dist/tools/cfd.js
CHANGED
|
@@ -10,14 +10,14 @@ What it does (one call): joint-normalizes all meshes, trains one SOM on a strati
|
|
|
10
10
|
BEST FOR: A mesh-refinement study where you want a field-level (not just scalar) view of whether the mesh has converged.
|
|
11
11
|
NOT FOR: Single scalar quantities of interest — use barmesh_richardson for classical GCI.
|
|
12
12
|
ASYNC: This is a queued job. It returns a job id immediately. Then poll barmesh_jobs(action=status) every 10-20s; datacenter-scale grids plus EMD can take a few minutes — keep polling. On completion call barmesh_results(action=get).
|
|
13
|
-
COMMON MISTAKES: omitting feature_columns (required); choosing a reference_mesh label that is not present; forgetting the cell-volume column at upload time.`, {
|
|
13
|
+
COMMON MISTAKES: omitting feature_columns (required); choosing a reference_mesh label that is not present; forgetting the cell-volume column at upload time; multi-topology studies (uniform U + graded G meshes) without explicit mesh_order — equal cell counts sort U-then-G and invent bogus stepwise pairs (U160|G20). Pass mesh_order coarse→fine interleaved by refinement (U20,G20,U40,G40,…) or use preset=cavity which defaults interleaved when U/G labels are detected.`, {
|
|
14
14
|
dataset_id: z.string().describe("Dataset ID from barmesh_datasets(upload)"),
|
|
15
15
|
feature_columns: z.array(z.string()).describe("Per-cell numeric feature columns to train the SOM on (e.g. [\"p\",\"U_mag\",\"k\",\"log_epsilon\"])"),
|
|
16
16
|
preset: z.enum(["generic", "cavity", "pitz", "datacenter"]).optional().describe("Hyperparameter preset; defaults to generic. cavity/pitz/datacenter reproduce the published settings."),
|
|
17
17
|
mesh_column: z.string().optional().describe("Column with the mesh label (default mesh_id)"),
|
|
18
18
|
volume_column: z.string().optional().describe("Cell-volume column for fingerprint weights (default V; equal weights if absent)"),
|
|
19
19
|
reference_mesh: z.string().optional().describe("Mesh label used as reference (default: the mesh with the most cells = finest)"),
|
|
20
|
-
mesh_order: z.array(z.string()).optional().describe("Explicit coarse-to-fine mesh order (default: ascending cell count)"),
|
|
20
|
+
mesh_order: z.array(z.string()).optional().describe("Explicit coarse-to-fine mesh order (default: ascending cell count; cavity preset and tied U/G cell counts auto-interleave U20→G20→U40→G40→…)"),
|
|
21
21
|
grid: z.array(z.number().int()).optional().describe("SOM grid [rows, cols] (overrides preset)"),
|
|
22
22
|
epochs: z.array(z.number().int()).optional().describe("[ordering, convergence] epochs (overrides preset)"),
|
|
23
23
|
batch_size: z.number().int().optional().describe("SOM batch size (overrides preset)"),
|
|
@@ -34,8 +34,10 @@ COMMON MISTAKES: omitting feature_columns (required); choosing a reference_mesh
|
|
|
34
34
|
normalization_methods: z.record(z.enum(["zscore", "mad", "sigmoidal", "sepd", "none"])).optional().describe("Per-feature normalization override; keys must be in feature_columns."),
|
|
35
35
|
row_range: z.tuple([z.number().int().min(1), z.number().int().min(1)]).optional().describe("1-based inclusive [start, end] row slice applied during preprocessing (and to mesh labels / cell volumes)."),
|
|
36
36
|
label: z.string().optional().describe("Optional job label"),
|
|
37
|
+
description: z.string().optional().describe("Optional free-text description for inventory"),
|
|
38
|
+
tags: z.array(z.string()).optional().describe("Optional tags for inventory/navigation"),
|
|
37
39
|
}, async (args) => {
|
|
38
|
-
const { dataset_id, label, ...rest } = args;
|
|
40
|
+
const { dataset_id, label, description, tags, ...rest } = args;
|
|
39
41
|
const params = {};
|
|
40
42
|
for (const [k, v] of Object.entries(rest)) {
|
|
41
43
|
if (v !== undefined && v !== null)
|
|
@@ -47,6 +49,10 @@ COMMON MISTAKES: omitting feature_columns (required); choosing a reference_mesh
|
|
|
47
49
|
const body = { dataset_id, params };
|
|
48
50
|
if (typeof label === "string" && label.length > 0)
|
|
49
51
|
body.label = label;
|
|
52
|
+
if (description !== undefined)
|
|
53
|
+
body.description = description;
|
|
54
|
+
if (tags !== undefined)
|
|
55
|
+
body.tags = tags;
|
|
50
56
|
const data = (await apiCall("POST", "/v1/cfd/mesh-convergence", body));
|
|
51
57
|
await pollCfdPrepareIfPresent(data, "barmesh_mesh_convergence");
|
|
52
58
|
const id = data.id;
|
|
@@ -63,7 +69,7 @@ What it does: from a small CSV with one row per mesh (a mesh label, a representa
|
|
|
63
69
|
BEST FOR: Scalar benchmarks (reattachment length, centerline values, probe readings) where a classical asymptotic-convergence check is expected.
|
|
64
70
|
NOT FOR: High-dimensional field comparison — use barmesh_mesh_convergence (complementary).
|
|
65
71
|
ASYNC: queued job; poll barmesh_jobs(action=status), then barmesh_results(action=get).
|
|
66
|
-
COMMON MISTAKES: not providing h_column or n_cells_column; mixing QoIs with different mesh sets in one CSV.`, {
|
|
72
|
+
COMMON MISTAKES: not providing h_column or n_cells_column; mixing QoIs with different mesh sets in one CSV; mixing uniform (U…) and graded (G…) mesh families in one CSV — classical GCI assumes one topology per job; run separate Richardson jobs per family (e.g. G160|G80|G40 graded, U160|U80|U40 uniform).`, {
|
|
67
73
|
dataset_id: z.string().describe("Dataset ID (one row per mesh)"),
|
|
68
74
|
qoi_columns: z.array(z.string()).describe("Scalar QoI column names to extrapolate"),
|
|
69
75
|
mesh_column: z.string().optional().describe("Mesh label column (default mesh_id)"),
|
|
@@ -72,8 +78,10 @@ COMMON MISTAKES: not providing h_column or n_cells_column; mixing QoIs with diff
|
|
|
72
78
|
dimension: z.number().int().optional().describe("Spatial dimension d for deriving h from cell count (default 3)"),
|
|
73
79
|
safety_factor: z.number().optional().describe("GCI safety factor Fs (default 1.25, Roache)"),
|
|
74
80
|
label: z.string().optional().describe("Optional job label"),
|
|
81
|
+
description: z.string().optional().describe("Optional free-text description for inventory"),
|
|
82
|
+
tags: z.array(z.string()).optional().describe("Optional tags for inventory/navigation"),
|
|
75
83
|
}, async (args) => {
|
|
76
|
-
const { dataset_id, label, ...rest } = args;
|
|
84
|
+
const { dataset_id, label, description, tags, ...rest } = args;
|
|
77
85
|
const params = {};
|
|
78
86
|
for (const [k, v] of Object.entries(rest)) {
|
|
79
87
|
if (v !== undefined && v !== null)
|
|
@@ -82,6 +90,10 @@ COMMON MISTAKES: not providing h_column or n_cells_column; mixing QoIs with diff
|
|
|
82
90
|
const body = { dataset_id, params };
|
|
83
91
|
if (typeof label === "string" && label.length > 0)
|
|
84
92
|
body.label = label;
|
|
93
|
+
if (description !== undefined)
|
|
94
|
+
body.description = description;
|
|
95
|
+
if (tags !== undefined)
|
|
96
|
+
body.tags = tags;
|
|
85
97
|
const data = (await apiCall("POST", "/v1/cfd/richardson", body));
|
|
86
98
|
const id = data.id;
|
|
87
99
|
if (id != null)
|
package/dist/tools/datasets.js
CHANGED
|
@@ -6,6 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
import { registerAuditedTool } from "../audit.js";
|
|
7
7
|
import { apiCall, getWorkspaceRootAsync, resolveFilePathForUpload, textResult, pollUntilComplete, UPLOAD_DATASET_TIMEOUT_MS, LARGE_UPLOAD_BYTES, PRESIGNED_PUT_TIMEOUT_MS, POLL_STAGE_MAX_MS, streamFileSha256, putPresignedStream, resolveUploadContentType, suggestMeshDatasetPreview, } from "../shared.js";
|
|
8
8
|
import { GZIP_UPLOAD_HINT } from "../upload_hints.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".
|
|
@@ -23,12 +24,14 @@ Formats: plain CSV/TSV or gzip (.csv.gz / .tsv.gz). For files above ~64 MB, pref
|
|
|
23
24
|
|
|
24
25
|
| Action | Use when |
|
|
25
26
|
|--------|----------|
|
|
26
|
-
| upload | You have prepared a combined per-cell CSV (mesh_id + feature columns + cell volume V). Do this first. |
|
|
27
|
+
| upload | You have prepared a combined per-cell CSV (mesh_id + feature columns + cell volume V). Do this first. Optional description/tags help inventory. |
|
|
27
28
|
| preview | After upload — verify the mesh column, feature columns, and volume column are present and numeric. |
|
|
28
|
-
| get | Fetch one dataset by id — status, staging fields, ingest_error (use after upload or when staging is slow). |
|
|
29
|
-
| list | Find dataset IDs for analysis. |
|
|
29
|
+
| get | Fetch one dataset by id — status, staging fields, ingest_error, description/tags (use after upload or when staging is slow). |
|
|
30
|
+
| list | Find dataset IDs for analysis (includes description/tags). |
|
|
31
|
+
| update | Edit name / description / tags on an existing dataset. |
|
|
30
32
|
| subset | Shrink a huge per-cell table server-side (row_range, filters, or sample_n). |
|
|
31
33
|
| delete | Remove a dataset permanently. |
|
|
34
|
+
| inventory | Prefer barmesh_jobs(action=inventory) for a full datasets+jobs markdown overview. |
|
|
32
35
|
|
|
33
36
|
action=upload: PREFER file_path — server reads from workspace root (token-efficient). Accepts .csv, .tsv, .csv.gz, .tsv.gz. Use csv_data only for small inline pastes (<10KB). If plain CSV exceeds the 5 GB upload cap, gzip it first (.csv.gz).
|
|
34
37
|
|
|
@@ -37,9 +40,11 @@ NOT FOR: Raw OpenFOAM case directories — extract a per-cell CSV first (see bar
|
|
|
37
40
|
COMMON MISTAKES: omitting the cell-volume column (defaults to equal weights, which weakens the fingerprint); inconsistent feature columns across meshes.
|
|
38
41
|
ESCALATION: If preview shows a feature column as non-numeric, fix the extraction and re-upload.`, {
|
|
39
42
|
action: z
|
|
40
|
-
.enum(["upload", "preview", "list", "get", "subset", "delete"])
|
|
41
|
-
.describe("upload: add CSV or .csv.gz; preview: inspect columns; list: see all datasets; get: fetch one dataset metadata (status/staging); subset: create filtered subset; delete: remove dataset"),
|
|
42
|
-
name: z.string().optional().describe("Dataset name (required for upload and subset)"),
|
|
43
|
+
.enum(["upload", "preview", "list", "get", "update", "subset", "delete"])
|
|
44
|
+
.describe("upload: add CSV or .csv.gz; preview: inspect columns; list: see all datasets; get: fetch one dataset metadata (status/staging); update: edit name/description/tags; subset: create filtered subset; delete: remove dataset"),
|
|
45
|
+
name: z.string().optional().describe("Dataset name (required for upload and subset; optional for update)"),
|
|
46
|
+
description: z.string().optional().describe("Optional free-text description (upload/update)"),
|
|
47
|
+
tags: z.array(z.string()).optional().describe("Optional tags for inventory/navigation (upload/update)"),
|
|
43
48
|
file_path: z
|
|
44
49
|
.string()
|
|
45
50
|
.optional()
|
|
@@ -86,7 +91,7 @@ ESCALATION: If preview shows a feature column as non-numeric, fix the extraction
|
|
|
86
91
|
.int()
|
|
87
92
|
.optional()
|
|
88
93
|
.describe("action=subset: RNG seed for sample_n (default 42)."),
|
|
89
|
-
}, async ({ action, name, file_path, csv_data, dataset_id, n_rows, row_range, filters, filter, sample_n, sample_seed, }) => {
|
|
94
|
+
}, async ({ action, name, description, tags, file_path, csv_data, dataset_id, n_rows, row_range, filters, filter, sample_n, sample_seed, }) => {
|
|
90
95
|
if (action === "upload") {
|
|
91
96
|
if (!name)
|
|
92
97
|
throw new Error("barmesh_datasets(upload) requires name.");
|
|
@@ -121,7 +126,12 @@ ESCALATION: If preview shows a feature column as non-numeric, fix the extraction
|
|
|
121
126
|
const idem = await streamFileSha256(resolved);
|
|
122
127
|
let init;
|
|
123
128
|
try {
|
|
124
|
-
|
|
129
|
+
const uploadUrlBody = { name, size_bytes: stat.size };
|
|
130
|
+
if (description !== undefined)
|
|
131
|
+
uploadUrlBody.description = description;
|
|
132
|
+
if (tags !== undefined)
|
|
133
|
+
uploadUrlBody.tags = tags;
|
|
134
|
+
init = (await apiCall("POST", "/v1/datasets/upload-url", uploadUrlBody, { "Idempotency-Key": idem }));
|
|
125
135
|
}
|
|
126
136
|
catch (e) {
|
|
127
137
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -158,12 +168,17 @@ ESCALATION: If preview shows a feature column as non-numeric, fix the extraction
|
|
|
158
168
|
}
|
|
159
169
|
if (isGzipInput) {
|
|
160
170
|
const gzBytes = await fs.readFile(resolved);
|
|
161
|
-
const
|
|
171
|
+
const gzHeaders = {
|
|
162
172
|
"X-Dataset-Name": name,
|
|
163
173
|
"Content-Type": uploadContentType,
|
|
164
174
|
"Content-Encoding": "gzip",
|
|
165
175
|
"Idempotency-Key": createHash("sha256").update(`${name}\n`).update(gzBytes).digest("hex"),
|
|
166
|
-
}
|
|
176
|
+
};
|
|
177
|
+
if (description !== undefined)
|
|
178
|
+
gzHeaders["X-Dataset-Description"] = description;
|
|
179
|
+
if (tags !== undefined)
|
|
180
|
+
gzHeaders["X-Dataset-Tags"] = JSON.stringify(tags);
|
|
181
|
+
const data = (await apiCall("POST", "/v1/datasets", gzBytes, gzHeaders, UPLOAD_DATASET_TIMEOUT_MS));
|
|
167
182
|
const gid = data.id ?? data.dataset_id;
|
|
168
183
|
if (gid != null) {
|
|
169
184
|
data.suggested_next_step = suggestMeshDatasetPreview(String(gid));
|
|
@@ -184,6 +199,10 @@ ESCALATION: If preview shows a feature column as non-numeric, fix the extraction
|
|
|
184
199
|
"Content-Type": uploadContentType,
|
|
185
200
|
"Idempotency-Key": createHash("sha256").update(`${name}\n`).update(body).digest("hex"),
|
|
186
201
|
};
|
|
202
|
+
if (description !== undefined)
|
|
203
|
+
uploadHeaders["X-Dataset-Description"] = description;
|
|
204
|
+
if (tags !== undefined)
|
|
205
|
+
uploadHeaders["X-Dataset-Tags"] = JSON.stringify(tags);
|
|
187
206
|
let uploadBody = body;
|
|
188
207
|
if (Buffer.byteLength(body, "utf-8") > GZIP_THRESHOLD) {
|
|
189
208
|
uploadBody = gzipSync(Buffer.from(body, "utf-8"));
|
|
@@ -227,15 +246,9 @@ ESCALATION: If preview shows a feature column as non-numeric, fix the extraction
|
|
|
227
246
|
const data = (await apiCall("GET", "/v1/datasets"));
|
|
228
247
|
if (Array.isArray(data)) {
|
|
229
248
|
const lines = data.map((ds) => {
|
|
230
|
-
const
|
|
231
|
-
const dsName = String(ds.name ?? "");
|
|
232
|
-
const rows = ds.rows != null ? Number(ds.rows) : "?";
|
|
233
|
-
const cols = ds.cols != null ? Number(ds.cols) : "?";
|
|
234
|
-
const st = ds.status != null ? String(ds.status) : "ready";
|
|
235
|
-
const statusBit = st !== "ready" ? ` | status=${st}` : "";
|
|
249
|
+
const base = formatDatasetInventoryLine(ds);
|
|
236
250
|
const ingestErr = cleanNullable(ds.ingest_error);
|
|
237
|
-
|
|
238
|
-
return `${dsName} (${id}) — ${rows}×${cols}${statusBit}${err}`;
|
|
251
|
+
return ingestErr ? `${base} | ingest_error=${ingestErr}` : base;
|
|
239
252
|
});
|
|
240
253
|
return { content: [{ type: "text", text: lines.length > 0 ? lines.join("\n") : "No datasets." }] };
|
|
241
254
|
}
|
|
@@ -245,11 +258,14 @@ ESCALATION: If preview shows a feature column as non-numeric, fix the extraction
|
|
|
245
258
|
if (!dataset_id)
|
|
246
259
|
throw new Error("barmesh_datasets(get) requires dataset_id.");
|
|
247
260
|
const ds = (await apiCall("GET", `/v1/datasets/${dataset_id}`));
|
|
261
|
+
const tagStr = formatTags(ds.tags);
|
|
248
262
|
const lines = [
|
|
249
263
|
`Dataset: ${ds.name ?? "?"} (${ds.id ?? dataset_id})`,
|
|
250
264
|
`Status: ${ds.status ?? "ready"}`,
|
|
251
265
|
`Rows × cols: ${ds.rows ?? "?"} × ${ds.cols ?? "?"}`,
|
|
252
266
|
ds.size_bytes != null ? `Size: ${Number(ds.size_bytes).toLocaleString()} bytes` : "",
|
|
267
|
+
ds.description != null && String(ds.description).trim() !== "" ? `Description: ${String(ds.description)}` : "",
|
|
268
|
+
tagStr ? `Tags: ${tagStr}` : "",
|
|
253
269
|
ds.staged_prefix != null ? `Staged prefix: ${String(ds.staged_prefix)}` : "",
|
|
254
270
|
ds.staged_version != null ? `Staged version: ${String(ds.staged_version)}` : "",
|
|
255
271
|
ds.stage_job_id != null ? `Stage job: ${String(ds.stage_job_id)} (poll barmesh_jobs(action=status))` : "",
|
|
@@ -258,6 +274,22 @@ ESCALATION: If preview shows a feature column as non-numeric, fix the extraction
|
|
|
258
274
|
].filter(Boolean);
|
|
259
275
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
260
276
|
}
|
|
277
|
+
if (action === "update") {
|
|
278
|
+
if (!dataset_id)
|
|
279
|
+
throw new Error("barmesh_datasets(update) requires dataset_id.");
|
|
280
|
+
const body = {};
|
|
281
|
+
if (name !== undefined)
|
|
282
|
+
body.name = name;
|
|
283
|
+
if (description !== undefined)
|
|
284
|
+
body.description = description;
|
|
285
|
+
if (tags !== undefined)
|
|
286
|
+
body.tags = tags;
|
|
287
|
+
if (Object.keys(body).length === 0) {
|
|
288
|
+
throw new Error("barmesh_datasets(update) requires at least one of name, description, tags");
|
|
289
|
+
}
|
|
290
|
+
const data = await apiCall("PATCH", `/v1/datasets/${dataset_id}`, body);
|
|
291
|
+
return textResult(data);
|
|
292
|
+
}
|
|
261
293
|
if (action === "delete") {
|
|
262
294
|
if (!dataset_id)
|
|
263
295
|
throw new Error("barmesh_datasets(delete) requires dataset_id.");
|
package/dist/tools/feedback.js
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { registerAuditedTool } from "../audit.js";
|
|
3
|
-
import { apiCall, API_URL, fetchWithTimeout,
|
|
3
|
+
import { apiCall, API_URL, fetchWithTimeout, probeApiHealth, structuredTextResult, } from "../shared.js";
|
|
4
|
+
import { enqueueDeferredFeedback, flushDeferredFeedback, listDeferredFeedback, } from "../deferred_feedback.js";
|
|
5
|
+
function isDeferworthy(err) {
|
|
6
|
+
const e = err;
|
|
7
|
+
if (e?.retryable)
|
|
8
|
+
return true;
|
|
9
|
+
if (e?.cause === "api_unreachable" || e?.cause === "storage" || e?.cause === "network")
|
|
10
|
+
return true;
|
|
11
|
+
const status = e?.httpStatus;
|
|
12
|
+
if (status === 502 || status === 503 || status === 504)
|
|
13
|
+
return true;
|
|
14
|
+
if (err instanceof TypeError)
|
|
15
|
+
return true;
|
|
16
|
+
if (err instanceof Error && /fetch failed|ECONNREFUSED|ENOTFOUND|ECONNRESET|network|timed out/i.test(err.message)) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
4
21
|
export function registerFeedbackTool(server) {
|
|
5
22
|
registerAuditedTool(server, "barmesh_send_feedback", `Send feedback or feature requests to Barivia developers (CFD mesh-convergence / barmesh workflow). Use when the user has suggestions, ran into issues, or wants something improved. Do NOT call without asking the user first — but after a mesh-convergence or Richardson session, 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 (e.g. gateway HTML 502 mid-demo), this tool queues feedback locally (~/.barivia/deferred-feedback, shared with barsom send_feedback) and returns deferred=true with request_id + api_health from GET /health (no R2). Call again later to flush.`, {
|
|
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,19 @@ For a substantial issue, prefer feedback_items: submit several focused instances
|
|
|
15
34
|
throw new Error("barmesh_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
|
+
const flushed = await flushDeferredFeedback("barmesh", submit).catch(() => null);
|
|
18
39
|
try {
|
|
19
|
-
const data = await
|
|
20
|
-
return
|
|
40
|
+
const data = (await submit(body));
|
|
41
|
+
return structuredTextResult({
|
|
42
|
+
...data,
|
|
43
|
+
deferred: false,
|
|
44
|
+
...(flushed && flushed.flushed > 0
|
|
45
|
+
? { flushed_deferred: flushed.flushed, deferred_remaining: flushed.remaining }
|
|
46
|
+
: {}),
|
|
47
|
+
}, typeof data.message === "string"
|
|
48
|
+
? data.message
|
|
49
|
+
: "Feedback received. Thank you.");
|
|
21
50
|
}
|
|
22
51
|
catch (err) {
|
|
23
52
|
if (err?.httpStatus === 401) {
|
|
@@ -31,10 +60,46 @@ For a substantial issue, prefer feedback_items: submit several focused instances
|
|
|
31
60
|
const text = await resp.text();
|
|
32
61
|
if (resp.ok) {
|
|
33
62
|
const parsed = JSON.parse(text);
|
|
34
|
-
return
|
|
63
|
+
return structuredTextResult({ ...parsed, deferred: false, note: "Feedback sent anonymously (API key invalid or expired)." }, "Feedback sent anonymously (API key invalid or expired).");
|
|
35
64
|
}
|
|
36
65
|
}
|
|
37
|
-
catch { /* fall through
|
|
66
|
+
catch { /* fall through */ }
|
|
67
|
+
}
|
|
68
|
+
if (isDeferworthy(err)) {
|
|
69
|
+
const e = err;
|
|
70
|
+
const queued = await enqueueDeferredFeedback("barmesh", body, {
|
|
71
|
+
status: e.httpStatus,
|
|
72
|
+
error_code: e.errorCode,
|
|
73
|
+
request_id: e.requestId,
|
|
74
|
+
message: e.message,
|
|
75
|
+
});
|
|
76
|
+
const api_health = await probeApiHealth().catch(() => ({
|
|
77
|
+
liveness: "down",
|
|
78
|
+
readiness: "unknown",
|
|
79
|
+
storage: "not_probed",
|
|
80
|
+
}));
|
|
81
|
+
const pending = await listDeferredFeedback("barmesh");
|
|
82
|
+
return structuredTextResult({
|
|
83
|
+
ok: true,
|
|
84
|
+
deferred: true,
|
|
85
|
+
deferred_id: queued.id,
|
|
86
|
+
deferred_path: queued.path,
|
|
87
|
+
deferred_pending: pending.length,
|
|
88
|
+
error_code: e.errorCode ?? "api_unreachable",
|
|
89
|
+
cause: e.cause ?? "api_unreachable",
|
|
90
|
+
request_id: e.requestId ?? null,
|
|
91
|
+
api_health,
|
|
92
|
+
message: "API/storage unavailable — feedback saved locally and will be sent on the next successful barmesh_send_feedback call.",
|
|
93
|
+
}, [
|
|
94
|
+
"Feedback deferred (API/storage unavailable).",
|
|
95
|
+
`Saved to ${queued.path}`,
|
|
96
|
+
e.requestId ? `request_id=${e.requestId}` : null,
|
|
97
|
+
e.errorCode ? `error_code=${e.errorCode}` : null,
|
|
98
|
+
`api_health.liveness=${api_health.liveness} (storage not_probed)`,
|
|
99
|
+
"Retry barmesh_send_feedback later to flush the local queue.",
|
|
100
|
+
]
|
|
101
|
+
.filter(Boolean)
|
|
102
|
+
.join(" | "));
|
|
38
103
|
}
|
|
39
104
|
throw err;
|
|
40
105
|
}
|
package/dist/tools/guide.js
CHANGED
|
@@ -1,30 +1,36 @@
|
|
|
1
1
|
import { registerAuditedTool } from "../audit.js";
|
|
2
|
-
import { apiCall, textResult } from "../shared.js";
|
|
3
|
-
|
|
2
|
+
import { apiCall, probeApiHealth, structuredTextResult, textResult } from "../shared.js";
|
|
3
|
+
import { listDeferredFeedback } from "../deferred_feedback.js";
|
|
4
|
+
const OFFLINE_GUIDE = `Mesh analysis API temporarily unavailable — figures already on disk stay available; new jobs/feedback will wait.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
barmesh offline summary (demo-safe): keep reviewing the last good distances, GCI CSV, and downloaded figures. Say the API is unavailable plainly; the proxy already burst-retried (~3×2s) — wait ~30s before another round. Do not treat the session as failed.
|
|
7
|
+
|
|
8
|
+
Two tracks (when API returns):
|
|
6
9
|
- barmesh_mesh_convergence — SOM volume-fingerprint distances (symmetric KL + Wasserstein-1 vs reference and stepwise)
|
|
7
|
-
- barmesh_richardson — classical Richardson / GCI on scalar QoIs
|
|
10
|
+
- barmesh_richardson — classical Richardson / GCI on scalar QoIs (one mesh family per CSV: uniform OR graded, not mixed)
|
|
8
11
|
|
|
9
|
-
Workflow:
|
|
12
|
+
Workflow (when API returns):
|
|
10
13
|
1. barmesh_prepare_mesh_data — recipe for the combined per-cell CSV (mesh_id + features + cell volume V)
|
|
11
14
|
2. barmesh_datasets(upload) → barmesh_datasets(preview)
|
|
12
15
|
3. barmesh_mesh_convergence and/or barmesh_richardson — returns job_id
|
|
16
|
+
- Multi-topology cavity studies: pass mesh_order interleaved (U20,G20,U40,G40,…) or preset=cavity
|
|
13
17
|
4. barmesh_training_monitor (visual MCP App) or barmesh_jobs(action=monitor) headless — poll until complete
|
|
14
|
-
5. barmesh_results(action=get) → barmesh_results_explorer for figures
|
|
18
|
+
5. barmesh_results(action=get) → barmesh_results_explorer for figures (mesh_convergence) or richardson_gci.csv summary (Richardson)
|
|
15
19
|
|
|
16
|
-
(
|
|
20
|
+
(Set BARIVIA_API_KEY / BARIVIA_API_URL when reconnecting.)`;
|
|
17
21
|
const OFFLINE_PREP = `barmesh mesh-data prep (offline summary; API unreachable):
|
|
18
22
|
Build ONE combined per-cell CSV across all meshes of the refinement study:
|
|
19
23
|
- one row per cell; a mesh label column (mesh_id); the physical channels you want compared (e.g. p, U_mag, k, log_epsilon — log-compress turbulence quantities); and a cell-volume column (V) for fingerprint weighting.
|
|
20
24
|
- keep the SAME feature columns across every mesh; pick the finest mesh as the reference.
|
|
25
|
+
- multi-topology (U + G): pass mesh_order coarse→fine interleaved by refinement (U20,G20,U40,G40,U80,G80,U160,G160) or use preset=cavity.
|
|
21
26
|
Then: barmesh_datasets(upload) -> barmesh_mesh_convergence. (Set BARIVIA_API_KEY / BARIVIA_API_URL.)`;
|
|
22
27
|
export function registerGuideTool(server) {
|
|
23
28
|
registerAuditedTool(server, "barmesh_guide_workflow", `Get the barmesh CFD mesh-convergence workflow and tool map from the API (tier-scoped).
|
|
24
29
|
|
|
25
30
|
BEST FOR: First call in a session — orients you on the two tracks (SOM fingerprint distances and Richardson/GCI), the upload->submit->poll->results flow, and what your plan allows.
|
|
26
31
|
NOT FOR: Step-by-step mesh-data preparation — use barmesh_prepare_mesh_data for that.
|
|
27
|
-
ESCALATION: If the response says your plan does not include CFD tools, the analysis tools will return 403; contact Barivia to enable the cfd entitlement
|
|
32
|
+
ESCALATION: If the response says your plan does not include CFD tools, the analysis tools will return 403; contact Barivia to enable the cfd entitlement.
|
|
33
|
+
UNAVAILABLE: If the API is down, this tool returns a calm offline summary — say so plainly, keep reviewing last good results, and suggest retry later (~30s).`, {}, async () => {
|
|
28
34
|
try {
|
|
29
35
|
const data = (await apiCall("GET", "/v1/cfd/guide"));
|
|
30
36
|
return textResult({
|
|
@@ -43,7 +49,7 @@ ESCALATION: If the response says your plan does not include CFD tools, the analy
|
|
|
43
49
|
|
|
44
50
|
BEST FOR: Before barmesh_datasets(upload) — tells you which physical channels to extract, how to label meshes (mesh_id), the cell-volume column (V), and how to pick the reference mesh.
|
|
45
51
|
NOT FOR: Submitting jobs — after preparing the CSV, use barmesh_datasets(upload) then barmesh_mesh_convergence.
|
|
46
|
-
COMMON MISTAKES: forgetting the per-cell cell-volume column; using different channels across meshes; not log-compressing turbulence quantities (k, epsilon, omega).`, {}, async () => {
|
|
52
|
+
COMMON MISTAKES: forgetting the per-cell cell-volume column; using different channels across meshes; not log-compressing turbulence quantities (k, epsilon, omega); multi-topology U+G studies without mesh_order (use interleaved U20,G20,U40,G40,… or preset=cavity).`, {}, async () => {
|
|
47
53
|
try {
|
|
48
54
|
const data = (await apiCall("GET", "/v1/cfd/prep"));
|
|
49
55
|
return textResult({ recipe: data.recipe, entitled: data.entitled });
|
|
@@ -54,4 +60,22 @@ COMMON MISTAKES: forgetting the per-cell cell-volume column; using different cha
|
|
|
54
60
|
return textResult(OFFLINE_PREP);
|
|
55
61
|
}
|
|
56
62
|
});
|
|
63
|
+
registerAuditedTool(server, "barmesh_api_health", `Probe Barivia API liveness/readiness without touching object storage (R2).
|
|
64
|
+
|
|
65
|
+
BEST FOR: Mid-demo outages — distinguish "API process up" from storage/gateway failures when barmesh_send_feedback or uploads fail.
|
|
66
|
+
Uses GET /health (always 200 if process alive) and GET /ready (DB; Redis best-effort). storage is always not_probed.
|
|
67
|
+
Also reports how many deferred barmesh feedback drafts are queued locally.`, {}, async () => {
|
|
68
|
+
const health = await probeApiHealth();
|
|
69
|
+
const deferred = await listDeferredFeedback("barmesh").catch(() => []);
|
|
70
|
+
const lines = [
|
|
71
|
+
`API liveness: ${health.liveness}`,
|
|
72
|
+
`API readiness: ${health.readiness}`,
|
|
73
|
+
health.db ? `DB: ${health.db}` : null,
|
|
74
|
+
health.redis ? `Redis: ${health.redis}` : null,
|
|
75
|
+
`Object storage (R2): ${health.storage} (intentionally not checked)`,
|
|
76
|
+
health.detail ? `Detail: ${health.detail}` : null,
|
|
77
|
+
`Deferred feedback drafts (barmesh): ${deferred.length}`,
|
|
78
|
+
].filter(Boolean);
|
|
79
|
+
return structuredTextResult({ ...health, deferred_feedback_pending: deferred.length }, lines.join("\n"));
|
|
80
|
+
});
|
|
57
81
|
}
|