diff --git a/CLAUDE.md b/CLAUDE.md index b6b1011..2362039 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -255,7 +255,8 @@ their native runners. See `desktop/README.md` for the full picture. Dashboards are **generated by the embedded DeepSQL Agent acting as a coding agent** (customized Hermes runtime — see [`agent/README.md`](agent/README.md)) — it writes the whole dashboard as a single self-contained HTML document, not a JSON spec. The earlier spec+renderer model (metrics/charts/tables + a `{{placeholder}}` substitution engine + `DashboardBuilder.js`) was thrown away: the rigid `col BETWEEN {{name}}` convention couldn't express real SQL (e.g. a Unix-epoch date filter → `near '{range.start}'` syntax errors) and boxed the agent in. -- `DashboardAgentService` is a thin broker: `ensureProfileForUser` → `ensureSession` (fresh session) → `sendAndAwait` with an **artifact contract**. The agent grounds on the brain/schema, verifies every query with `execute_sql`, then emits ONE HTML doc (in a ` ```html ` block). The broker extracts the HTML and returns `{version:3, renderMode:"artifact", title, html, trace}`, stored verbatim in `saved_dashboards.dashboardConfig`. +- `DashboardAgentService` is a thin broker: `ensureProfileForUser` → `ensureSession` (fresh session) → `sendAndAwait` with an **artifact contract**. The agent grounds on the brain/schema, verifies every query with `execute_sql`, then emits ONE HTML doc (in a ` ```html ` block). The broker extracts the HTML and returns `{version:3, renderMode:"artifact", title, html, summary, trace}`, stored verbatim in `saved_dashboards.dashboardConfig`. +- **The chat reply is the agent's own words, not a template.** The build contract ends with a required ` ```dashboard-note ` fence (1–3 sentences): what THIS turn changed, plus anything it could not do/verify or deliberately skipped, and on a correction turn whether the disputed value actually changed. `DashboardAgentService.extractSummary` reads it (and `stripNotes` removes it *before* HTML extraction, so prose can't be mistaken for the artifact); it rides in `config.summary`. Both persistence (`SavedDashboardService.buildReplyText`) and the live tab (`useDashboardChatStore.buildReplyText`) render it, falling back to `DEFAULT_BUILD_REPLY` ("Done — built and verified…") only when a turn produced no note. Before this, the contract banned prose and both sites hardcoded that constant, so every build — including one answering a user's correction ("you're showing 3k, I expected 6k+") — reported the same confident "built and verified", which is worse than templated: it claimed success over an unverified change. **The two `buildReplyText` helpers must stay textually in sync** (backend Java + frontend JS) — the live bubble and the persisted reply are read from the same `summary`. - The agent loads the **`dashboard-design` skill** (`agent/skills/dashboard-design/SKILL.md`, v2 — artifact contract, the `deepsql.query` runtime, composition/UX rules, an **intent checklist**, and Unix-epoch date handling). - **Rendering + data access**: `DashboardArtifact.jsx` renders the HTML in a **sandboxed iframe** (`sandbox="allow-scripts"`, opaque origin + a strict CSP — no external network). The artifact fetches data only through an injected `deepsql.query(sql)` bridge that `postMessage`s to the parent; the parent calls **`POST /api/dashboards/query`** (`DashboardQueryController`), which is **read-only twice over** (`McpSqlGuardService.validateReadOnlySql` + `QueryExecutionContext.api` = `READ_ONLY_ONLY`) and access-scoped via `assertCanReadConnectionContent`. So the agent's code has full creative freedom while every query stays guarded and sandboxed. The bridge also auto-sizes the iframe and forwards runtime errors. - Generation endpoints unchanged (`POST /api/dashboards/generate` + `/generate/stream`). `DashboardBuilder.js`/`DashboardInputs.js` remain only because `tabs/Core/PreviewTab.js` still uses them — the dashboard *creation* path no longer touches them. diff --git a/backend/src/main/java/com/dbaagent/service/DashboardAgentService.java b/backend/src/main/java/com/dbaagent/service/DashboardAgentService.java index 0a85f08..027431b 100644 --- a/backend/src/main/java/com/dbaagent/service/DashboardAgentService.java +++ b/backend/src/main/java/com/dbaagent/service/DashboardAgentService.java @@ -43,6 +43,13 @@ * falls back to the older single-block contract if no shell/widget fences are * present at all, so a reply that predates this contract still works. * + *
A final {@code dashboard-note} fence carries the agent's own account of the + * turn — what changed, and what it could not do or verify — which becomes the + * chat reply. Before this, the contract banned prose and the UI showed a + * hardcoded "Done — built and verified against your data" on every build, so a + * turn that ignored the user's correction still reported success. The note is + * stripped before HTML extraction and falls back to that constant if absent. + * *
The old JSON-spec contract (metrics/charts/tables + a {{placeholder}}
* substitution engine + a fixed renderer) is gone: it couldn't express real SQL
* (e.g. a Unix-epoch date filter) and boxed the agent in.
@@ -146,7 +153,8 @@ sessionId, buildTask(connectionId, prompt, currentConfig),
"The agent couldn't build the dashboard: " + (reply.error() == null ? "it ended early" : reply.error()));
}
- String html = extractHtml(reply.text());
+ String summary = extractSummary(reply.text());
+ String html = extractHtml(stripNotes(reply.text()));
if (html == null || html.isBlank()) {
log.warn("Dashboard agent returned no HTML artifact. Reply head: {}",
reply.text() == null ? "null" : reply.text().substring(0, Math.min(300, reply.text().length())));
@@ -165,6 +173,7 @@ sessionId, buildTask(connectionId, prompt, currentConfig),
cfg.put("renderMode", "artifact");
cfg.put("title", title);
cfg.put("html", html);
+ if (summary != null) cfg.put("summary", summary);
cfg.put("trace", trace);
return cfg;
}
@@ -321,9 +330,22 @@ database jargon anywhere visible (titles, labels, descriptions, captions, errors
--ds-shadow). Do NOT import fonts or set font-family. Stay monochrome; use a subtle soft color/gradient
ONLY to highlight the 1–2 most important KPIs. Clean, minimal, lots of whitespace — not dark or neon.
- Output your FINAL message as ONLY the fenced blocks described in step 5 above (one ```dashboard-shell```
- first, then one ```dashboard-widget id="..."``` per widget) — no prose, no tool calls after the last one.
- Do NOT wrap the whole thing in a single ```html block — the shell and each widget are SEPARATE fences.""");
+ Output your FINAL message as ONLY fenced blocks (one ```dashboard-shell``` first, then one
+ ```dashboard-widget id="..."``` per widget, then the ```dashboard-note``` below) — no loose prose
+ outside a fence, no tool calls after the last one.
+ Do NOT wrap the whole thing in a single ```html block — the shell and each widget are SEPARATE fences.
+
+ 7. END with a ```dashboard-note``` fence: 1-3 sentences to the person who asked, in the same plain
+ business language as the two hard rules above (a note naming a table, a column, SQL, or the
+ connection id breaks the same security requirement the dashboard itself is bound by).
+ Say what THIS turn actually changed — not that a dashboard exists. Then, in the same note:
+ - State anything you could NOT do, could not verify, or chose to skip, and why. A build that
+ partly worked must say so. Never claim a number is correct because a query returned it.
+ - If the user was correcting or disputing something (a wrong figure, a chart that didn't load),
+ say plainly whether it is now fixed, and what the value/behaviour is now versus what they
+ reported. If you could not reproduce or resolve their complaint, say THAT — do not answer a
+ correction with a description of what you built.
+ Write it as you would to a colleague: specific and short. Never open with "Done".""");
return sb.toString();
}
@@ -337,6 +359,28 @@ database jargon anywhere visible (titles, labels, descriptions, captions, errors
"```dashboard-(shell|widget)(?:\\s+id=\"([^\"]+)\")?\\s*\\n(.*?)\\n```",
Pattern.DOTALL);
+ // The agent's own account of the turn. Stripped before HTML extraction runs:
+ // prose mentioning a tag would otherwise be a candidate for extractHtml's
+ // looks-like-markup fallback.
+ private static final Pattern NOTE_FENCE = Pattern.compile(
+ "```dashboard-note\\s*\\n(.*?)\\n```", Pattern.DOTALL);
+
+ private static final int MAX_SUMMARY_CHARS = 600;
+
+ /** The agent's summary of this turn, or null if it emitted no note. */
+ private String extractSummary(String text) {
+ if (text == null || text.isBlank()) return null;
+ Matcher m = NOTE_FENCE.matcher(text);
+ String note = null;
+ while (m.find()) note = m.group(1).trim();
+ if (note == null || note.isBlank()) return null;
+ return trim(note, MAX_SUMMARY_CHARS);
+ }
+
+ private String stripNotes(String text) {
+ return text == null ? null : NOTE_FENCE.matcher(text).replaceAll("");
+ }
+
/**
* Assembles the final document from the agent's shell + widget chunks (the
* progressive-render contract), substituting each widget's HTML+script into
diff --git a/backend/src/main/java/com/dbaagent/service/SavedDashboardService.java b/backend/src/main/java/com/dbaagent/service/SavedDashboardService.java
index beab8bd..1be80bd 100644
--- a/backend/src/main/java/com/dbaagent/service/SavedDashboardService.java
+++ b/backend/src/main/java/com/dbaagent/service/SavedDashboardService.java
@@ -36,6 +36,9 @@ public class SavedDashboardService {
// unbounded snapshot table. Oldest beyond this count are pruned on each write.
private static final int MAX_VERSIONS_PER_DASHBOARD = 50;
+ static final String DEFAULT_BUILD_REPLY =
+ "Done — built and verified against your data. Saved as a draft — tell me what to change.";
+
@Autowired
private SavedDashboardRepository savedDashboardRepository;
@@ -409,11 +412,21 @@ public SavedDashboard completeBuildTurn(UUID dashboardId, Map