Skip to content

Commit 6450857

Browse files
committed
Improve AI SQL correction and stacked charts
1 parent f2a80ab commit 6450857

11 files changed

Lines changed: 296 additions & 39 deletions

Architecture/sql-ai-generation.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ This architecture governs:
6262
- Successful responses MAY surface the rationale inline to explain what was generated.
6363
- Successful responses MUST carry the visualization decision forward so the next manual execution of that same AI-generated SQL can auto-generate a chart for graph-worthy results.
6464

65+
## Database Error Correction Contract
66+
67+
- If the user manually executes SQL that was generated by AI and the database returns an execution error, Studio MUST feed the original natural-language request, the failed SQL, and the database error message back through the same `sql-generation` LLM transport.
68+
- The correction request MUST preserve the original explicit-execution contract: corrected SQL replaces the editor contents, updates the rationale and visualization decision, and focuses the editor, but Studio MUST NOT auto-run the corrected SQL.
69+
- Database-error correction MUST only run for editor contents that still match the pending AI-generated SQL. Manually edited SQL errors MUST stay as normal inline query errors and MUST NOT trigger hidden AI calls.
70+
- If database-error correction fails, Studio MUST keep the database error visible and surface the AI correction failure inline without discarding the user's current SQL.
71+
6572
## Embedder and Demo Contract
6673

6774
- Embedders own the actual provider call through one shared `llm({ task, prompt })` hook.
@@ -81,3 +88,5 @@ Changes to AI SQL generation MUST include tests covering:
8188
- preserving the visualization decision until the user manually runs the generated SQL
8289
- surfacing the visualization decision from the AI response
8390
- inline error rendering for failed AI generation
91+
- feeding execution errors from AI-generated SQL back into the model without auto-running the corrected SQL
92+
- leaving manually written SQL execution errors as normal inline query errors without invoking AI correction

Architecture/sql-result-visualization.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,10 @@ This architecture governs:
5757
- `title`: optional short display title
5858
- `data`: plain JSON objects with primitive field values only
5959
- `xKey` plus `series[]` for `bar` and `line` charts
60+
- optional `stacked: true` for `bar` charts only
6061
- `labelKey` plus `valueKey` for `pie` and `doughnut` charts
6162
- `line` charts MUST use date-like `xKey` values: ISO dates, ISO datetimes, or epoch milliseconds. Generic categorical data should use `bar`.
63+
- Stacked bar charts MUST use one data row per category and separate numeric series fields for each stack segment. Long/tidy SQL result rows MAY be pivoted by the AI visualization response into this Studio-owned chart config.
6264
- The supported chart types MUST be constrained to the known allowlist above.
6365
- The implementation MUST retry up to two times when the model returns malformed JSON or an invalid chart config, and each correction prompt MUST include the latest validation error.
6466

@@ -78,3 +80,4 @@ Changes to SQL result visualization MUST include tests covering:
7880
- automatic chart generation for AI-generated SQL results that request visualization
7981
- replacement of the action with a mounted chart
8082
- reset behavior when another query execution starts
83+
- validation and rendering support for stacked bar chart configs

FEATURES.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,12 +221,13 @@ The prompt context is built from live introspection metadata, including the conc
221221
AI responses must satisfy a strict JSON contract with generated SQL, a short rationale, and a yes/no visualization decision, and Studio retries once if the model returns malformed JSON.
222222
Submitted AI requests are also stored locally in the SQL-view TanStack collection, so an empty focused prompt field can browse older requests with `ArrowUp` / `ArrowDown` as placeholder-only previews before committing one back into the input for editing.
223223
Provider output-limit failures are surfaced explicitly and can feed into the next JSON-correction prompt instead of showing up as a vague parse failure. The visualization decision from AI generation is also preserved so the later manual run can still auto-chart graph-worthy results.
224+
If AI-generated SQL fails when the user runs it, Studio sends the original request, failed SQL, and database error back to the model, then replaces the editor with a corrected query without auto-running it.
224225

225226
## AI SQL Result Visualization
226227

227228
When SQL query results are visible and `llm` is configured, Studio can also turn the returned rows into an in-grid Bklit visualization.
228229
The visualization uses a minimal summary-row trigger labeled `Visualize data with AI`, right-aligned beside the query result count, and mounts the generated chart above the SQL result headers inside the shared scrollable grid without a regenerate control.
229-
Studio sends the executed SQL, the concrete database engine, and the full result row set to the model, and when the result came from `Generate SQL with AI` it also includes the original natural-language request for extra visualization context. The model is asked for a strict Bklit chart config for bar, line, pie, or doughnut charts, and Studio validates that schema before rendering.
230+
Studio sends the executed SQL, the concrete database engine, and the full result row set to the model, and when the result came from `Generate SQL with AI` it also includes the original natural-language request for extra visualization context. The model is asked for a strict Bklit chart config for bar, line, pie, doughnut, or stacked bar charts, and Studio validates that schema before rendering.
230231
Mounted charts sit inside an in-grid background band that stays tied to the visible result viewport instead of the total table width, while the chart itself stays centered and width-clamped between 300px and 1200px so wide result grids do not force giant charts.
231232
When SQL is generated through AI, the same model call also decides whether the expected result is graph-worthy; if it says yes, Studio auto-generates the chart after the user manually runs that generated SQL instead of waiting for a separate chart button click.
232233
If another query starts running, the visualization resets immediately so stale charts do not persist across changing result sets. Visualization generation also retries up to two times on malformed JSON, invalid chart configs, or explicit provider output-limit failures.

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ export function EmbeddedStudio() {
109109
- `queryInsights` is optional. Pass it only when your BFF implements the `query-insights` procedure; otherwise omit it so Studio hides the `Queries` view.
110110
- Studio does not render a built-in fullscreen header button. If your host needs fullscreen behavior, render that control at the host container level, as the local demo does.
111111

112-
Studio handles prompt construction, type-aware validation, correction retries, SQL execution retries, and conversion into the normal filter, SQL, and visualization surfaces. The host transport only needs to forward the prepared request to an LLM provider and return the typed result.
112+
Studio handles prompt construction, type-aware validation, correction retries, database-error correction for AI-generated SQL, and conversion into the normal filter, SQL, and visualization surfaces. The host transport only needs to forward the prepared request to an LLM provider and return the typed result.
113113

114114
## AI Contract
115115

@@ -136,7 +136,7 @@ type StudioLlmResponse =
136136
};
137137
```
138138

139-
Studio treats `output-limit-exceeded` as a first-class retry signal for SQL generation and visualization correction loops. All prompting and retry behavior live in Studio itself, so host implementations should stay transport-only.
139+
Studio treats `output-limit-exceeded` as a first-class retry signal for SQL generation and visualization correction loops. If AI-generated SQL fails after the user manually runs it, Studio also sends the failed SQL and database error back through the same `sql-generation` transport so the model can propose corrected SQL without auto-running it. All prompting and retry behavior live in Studio itself, so host implementations should stay transport-only.
140140

141141
## Integration Checklist
142142

ui/studio/views/sql/SqlResultVisualization.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,8 @@ function SqlResultBarChart({
212212
className="h-full min-h-64"
213213
data={config.data}
214214
margin={{ bottom: 38, left: 40, right: 24, top: 12 }}
215+
stacked={config.stacked === true}
216+
stackGap={config.stacked === true ? 1 : 0}
215217
xDataKey={config.xKey}
216218
>
217219
<Grid strokeDasharray="0" vertical={false} />

ui/studio/views/sql/SqlView.test.tsx

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -676,8 +676,9 @@ describe("SqlView", () => {
676676
harness.cleanup();
677677
});
678678

679-
it("surfaces query errors only after the user manually runs AI-generated SQL", async () => {
679+
it("feeds AI-generated SQL execution errors back into the model without auto-running the correction", async () => {
680680
const badSql = "select typeof(json_col) from public.organizations limit 5;";
681+
const correctedSql = "select id from public.organizations limit 5;";
681682
const raw: Adapter["raw"] = async (details) => {
682683
const error = new Error(
683684
"function typeof(json) does not exist",
@@ -689,10 +690,18 @@ describe("SqlView", () => {
689690
const studio = createStudioMock(adapter);
690691
const llmMock = vi
691692
.fn<(request: StudioLlmRequest) => Promise<string>>()
692-
.mockResolvedValue(
693+
.mockResolvedValueOnce(
693694
JSON.stringify({
694695
rationale: "Tried a typeof helper.",
695696
sql: badSql,
697+
shouldGenerateVisualization: false,
698+
}),
699+
)
700+
.mockResolvedValueOnce(
701+
JSON.stringify({
702+
rationale: "Uses plain selectable columns after the database error.",
703+
sql: correctedSql,
704+
shouldGenerateVisualization: true,
696705
}),
697706
);
698707
studio.llm = llmMock;
@@ -739,21 +748,75 @@ describe("SqlView", () => {
739748
runButton.dispatchEvent(new MouseEvent("click", { bubbles: true }));
740749
});
741750

751+
await waitFor(() => editor.value === correctedSql);
752+
753+
expect(llmMock).toHaveBeenCalledTimes(2);
754+
expect(llmMock.mock.calls[1]?.[0]).toMatchObject({
755+
task: "sql-generation",
756+
});
757+
expect(llmMock.mock.calls[1]?.[0].prompt).toContain(
758+
"Previous SQL statement: select typeof(json_col) from public.organizations limit 5",
759+
);
760+
expect(llmMock.mock.calls[1]?.[0].prompt).toContain(
761+
"Database error from that SQL: function typeof(json) does not exist",
762+
);
763+
expect(rawSpy).toHaveBeenCalledTimes(1);
764+
expect(rawSpy).toHaveBeenCalledWith(
765+
{ sql: "select typeof(json_col) from public.organizations limit 5" },
766+
expect.any(Object),
767+
);
768+
expect(harness.container.textContent).toContain(
769+
"Uses plain selectable columns after the database error.",
770+
);
771+
expect(harness.container.textContent).not.toContain("Query error:");
772+
773+
harness.cleanup();
774+
});
775+
776+
it("keeps normal manual SQL execution errors inline without asking AI for a correction", async () => {
777+
const raw: Adapter["raw"] = async (details) => {
778+
const error = new Error("relation missing_table does not exist") as AdapterError;
779+
error.query = { parameters: [], sql: details.sql };
780+
return [error];
781+
};
782+
const { adapter, rawSpy } = createAdapterMock({ raw });
783+
const studio = createStudioMock(adapter);
784+
const llmMock = vi.fn<(request: StudioLlmRequest) => Promise<string>>();
785+
studio.llm = llmMock;
786+
useStudioMock.mockReturnValue(studio);
787+
788+
const harness = renderSqlView();
789+
const editor = harness.container.querySelector<HTMLTextAreaElement>(
790+
'textarea[aria-label="SQL editor"]',
791+
);
792+
const runButton = [...harness.container.querySelectorAll("button")].find(
793+
(button) => button.textContent?.includes("Run SQL"),
794+
);
795+
796+
if (!editor || !runButton) {
797+
throw new Error("Expected SQL editor and Run SQL button");
798+
}
799+
800+
act(() => {
801+
editor.value = "select * from missing_table";
802+
mockCodeMirrorOnChange?.("select * from missing_table");
803+
});
804+
805+
act(() => {
806+
runButton.dispatchEvent(new MouseEvent("click", { bubbles: true }));
807+
});
808+
742809
await waitFor(() => {
743810
return (
744811
harness.container.textContent?.includes(
745-
"function typeof(json) does not exist",
812+
"relation missing_table does not exist",
746813
) ?? false
747814
);
748815
});
749816

750-
expect(llmMock).toHaveBeenCalledTimes(1);
817+
expect(llmMock).not.toHaveBeenCalled();
751818
expect(rawSpy).toHaveBeenCalledTimes(1);
752-
expect(rawSpy).toHaveBeenCalledWith(
753-
{ sql: "select typeof(json_col) from public.organizations limit 5" },
754-
expect.any(Object),
755-
);
756-
expect(harness.container.textContent).toContain("Tried a typeof helper.");
819+
expect(harness.container.textContent).toContain("Query error:");
757820

758821
harness.cleanup();
759822
});

0 commit comments

Comments
 (0)