Skip to content

Commit ffdb3d0

Browse files
notSumit25claude
andcommitted
feat: dashboard chat reply is the agent's own words, not a canned string
The dashboard build contract forbade prose ("no prose"), and both the backend and the frontend hardcoded the same reply on every completed build: "Done — built and verified against your data." Every turn showed the identical line regardless of what the user asked — including turns that were corrections ("you're showing 3k, I expected 6k+"), where a blanket "built and verified" claimed success over a change that may not have landed. Worse than templated: a confident false claim. The contract now ends with a required ```dashboard-note``` fence — 1-3 sentences in plain business language stating what THIS turn changed, what it could not do or verify, and (on a correction) whether the disputed value actually moved. It is bound by the existing "never show internals" security rule, so no table/column/SQL/UUID leaks into the reply. - DashboardAgentService: NOTE_FENCE + extractSummary/stripNotes; the note is stripped BEFORE HTML extraction so prose can't be mistaken for the artifact; summary rides in config.summary. - SavedDashboardService.buildReplyText and the frontend store's buildReplyText both render the note, falling back to the (now shared) DEFAULT_BUILD_REPLY constant only when a turn produced no note — so an agent still on the old contract keeps working. Verified end-to-end against the live stack (real build + a correction turn + chat-only path), DB read-back, and browser: the reply now differs per turn, addresses corrections directly, leaks no internals, and survives reload. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 819c286 commit ffdb3d0

4 files changed

Lines changed: 75 additions & 7 deletions

File tree

CLAUDE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,8 @@ their native runners. See `desktop/README.md` for the full picture.
255255
Dashboards are **generated by the embedded DeepSQL Agent acting as a coding agent**
256256
(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.
257257

258-
- `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`.
258+
- `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`.
259+
- **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`.
259260
- 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).
260261
- **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.
261262
- 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.

backend/src/main/java/com/dbaagent/service/DashboardAgentService.java

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@
4343
* falls back to the older single-block contract if no shell/widget fences are
4444
* present at all, so a reply that predates this contract still works.
4545
*
46+
* <p>A final {@code dashboard-note} fence carries the agent's own account of the
47+
* turn — what changed, and what it could not do or verify — which becomes the
48+
* chat reply. Before this, the contract banned prose and the UI showed a
49+
* hardcoded "Done — built and verified against your data" on every build, so a
50+
* turn that ignored the user's correction still reported success. The note is
51+
* stripped before HTML extraction and falls back to that constant if absent.
52+
*
4653
* <p>The old JSON-spec contract (metrics/charts/tables + a {{placeholder}}
4754
* substitution engine + a fixed renderer) is gone: it couldn't express real SQL
4855
* (e.g. a Unix-epoch date filter) and boxed the agent in.
@@ -146,7 +153,8 @@ sessionId, buildTask(connectionId, prompt, currentConfig),
146153
"The agent couldn't build the dashboard: " + (reply.error() == null ? "it ended early" : reply.error()));
147154
}
148155

149-
String html = extractHtml(reply.text());
156+
String summary = extractSummary(reply.text());
157+
String html = extractHtml(stripNotes(reply.text()));
150158
if (html == null || html.isBlank()) {
151159
log.warn("Dashboard agent returned no HTML artifact. Reply head: {}",
152160
reply.text() == null ? "null" : reply.text().substring(0, Math.min(300, reply.text().length())));
@@ -165,6 +173,7 @@ sessionId, buildTask(connectionId, prompt, currentConfig),
165173
cfg.put("renderMode", "artifact");
166174
cfg.put("title", title);
167175
cfg.put("html", html);
176+
if (summary != null) cfg.put("summary", summary);
168177
cfg.put("trace", trace);
169178
return cfg;
170179
}
@@ -321,9 +330,22 @@ database jargon anywhere visible (titles, labels, descriptions, captions, errors
321330
--ds-shadow). Do NOT import fonts or set font-family. Stay monochrome; use a subtle soft color/gradient
322331
ONLY to highlight the 1–2 most important KPIs. Clean, minimal, lots of whitespace — not dark or neon.
323332
324-
Output your FINAL message as ONLY the fenced blocks described in step 5 above (one ```dashboard-shell```
325-
first, then one ```dashboard-widget id="..."``` per widget) — no prose, no tool calls after the last one.
326-
Do NOT wrap the whole thing in a single ```html block — the shell and each widget are SEPARATE fences.""");
333+
Output your FINAL message as ONLY fenced blocks (one ```dashboard-shell``` first, then one
334+
```dashboard-widget id="..."``` per widget, then the ```dashboard-note``` below) — no loose prose
335+
outside a fence, no tool calls after the last one.
336+
Do NOT wrap the whole thing in a single ```html block — the shell and each widget are SEPARATE fences.
337+
338+
7. END with a ```dashboard-note``` fence: 1-3 sentences to the person who asked, in the same plain
339+
business language as the two hard rules above (a note naming a table, a column, SQL, or the
340+
connection id breaks the same security requirement the dashboard itself is bound by).
341+
Say what THIS turn actually changed — not that a dashboard exists. Then, in the same note:
342+
- State anything you could NOT do, could not verify, or chose to skip, and why. A build that
343+
partly worked must say so. Never claim a number is correct because a query returned it.
344+
- If the user was correcting or disputing something (a wrong figure, a chart that didn't load),
345+
say plainly whether it is now fixed, and what the value/behaviour is now versus what they
346+
reported. If you could not reproduce or resolve their complaint, say THAT — do not answer a
347+
correction with a description of what you built.
348+
Write it as you would to a colleague: specific and short. Never open with "Done".""");
327349
return sb.toString();
328350
}
329351

@@ -337,6 +359,28 @@ database jargon anywhere visible (titles, labels, descriptions, captions, errors
337359
"```dashboard-(shell|widget)(?:\\s+id=\"([^\"]+)\")?\\s*\\n(.*?)\\n```",
338360
Pattern.DOTALL);
339361

362+
// The agent's own account of the turn. Stripped before HTML extraction runs:
363+
// prose mentioning a tag would otherwise be a candidate for extractHtml's
364+
// looks-like-markup fallback.
365+
private static final Pattern NOTE_FENCE = Pattern.compile(
366+
"```dashboard-note\\s*\\n(.*?)\\n```", Pattern.DOTALL);
367+
368+
private static final int MAX_SUMMARY_CHARS = 600;
369+
370+
/** The agent's summary of this turn, or null if it emitted no note. */
371+
private String extractSummary(String text) {
372+
if (text == null || text.isBlank()) return null;
373+
Matcher m = NOTE_FENCE.matcher(text);
374+
String note = null;
375+
while (m.find()) note = m.group(1).trim();
376+
if (note == null || note.isBlank()) return null;
377+
return trim(note, MAX_SUMMARY_CHARS);
378+
}
379+
380+
private String stripNotes(String text) {
381+
return text == null ? null : NOTE_FENCE.matcher(text).replaceAll("");
382+
}
383+
340384
/**
341385
* Assembles the final document from the agent's shell + widget chunks (the
342386
* progressive-render contract), substituting each widget's HTML+script into

backend/src/main/java/com/dbaagent/service/SavedDashboardService.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ public class SavedDashboardService {
3636
// unbounded snapshot table. Oldest beyond this count are pruned on each write.
3737
private static final int MAX_VERSIONS_PER_DASHBOARD = 50;
3838

39+
static final String DEFAULT_BUILD_REPLY =
40+
"Done — built and verified against your data. Saved as a draft — tell me what to change.";
41+
3942
@Autowired
4043
private SavedDashboardRepository savedDashboardRepository;
4144

@@ -409,11 +412,21 @@ public SavedDashboard completeBuildTurn(UUID dashboardId, Map<String, Object> co
409412
dashboard.setName(String.valueOf(title));
410413
}
411414
List<Map<String, Object>> messages = parseMessages(dashboard.getChatMessages());
412-
messages.add(chatMessage("agent", "Done — built and verified against your data. Saved as a draft — tell me what to change."));
415+
messages.add(chatMessage("agent", buildReplyText(config)));
413416
dashboard.setChatMessages(writeMessages(messages));
414417
return finishRunning(dashboard);
415418
}
416419

420+
// The agent's own dashboard-note. Falls back to the old constant only when a
421+
// turn produced no note, so an agent still on the previous contract works.
422+
private String buildReplyText(Map<String, Object> config) {
423+
Object summary = config == null ? null : config.get("summary");
424+
if (summary != null && !String.valueOf(summary).isBlank()) {
425+
return String.valueOf(summary).trim();
426+
}
427+
return DEFAULT_BUILD_REPLY;
428+
}
429+
417430
/** Turn finished as a real generation failure (not a client disconnect — see controller). */
418431
@Transactional
419432
public SavedDashboard appendErrorReply(UUID dashboardId, String errorText) {

src/lib/stores/useDashboardChatStore.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,16 @@ import { useShallow } from 'zustand/react/shallow'
33
import { generateDashboardStream } from '@/lib/dashboardGenerator'
44
import { savedDashboardsAPI } from '@/lib/api/client'
55

6+
// Mirrors SavedDashboardService.buildReplyText — this tab renders the reply
7+
// live, the backend persists its own copy, and the two must read the same.
8+
const DEFAULT_BUILD_REPLY =
9+
'Done — built and verified against your data. Saved as a draft — tell me what to change.'
10+
11+
const buildReplyText = (config) => {
12+
const summary = config?.summary
13+
return typeof summary === 'string' && summary.trim() ? summary.trim() : DEFAULT_BUILD_REPLY
14+
}
15+
616
// Keeps each dashboard workspace's in-flight generation (chat messages,
717
// streaming steps, the built config, the abort fn) alive in memory across
818
// component mount/unmount. DashboardWorkspace used to hold all of this in
@@ -282,7 +292,7 @@ export const useDashboardChatStore = create((set, get) => ({
282292
liveShell: null,
283293
liveWidget: null,
284294
liveWidgets: null,
285-
messages: [...cur.messages, { role: 'agent', text: 'Done — built and verified against your data. Saved as a draft — tell me what to change.' }],
295+
messages: [...cur.messages, { role: 'agent', text: buildReplyText(next) }],
286296
}
287297
})
288298
// completeBuildTurn (the backend's own persistence for this turn) just

0 commit comments

Comments
 (0)