Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 33 additions & 33 deletions apps/website/content/docs/chat/guides/generative-ui.mdx

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions apps/website/e2e/react-chat-generative-ui-preview.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,17 @@ test('native Chat Generative UI keeps canonical Docs, Code and Run aligned', asy
await expect(page.getByRole('tabpanel')).toContainText(
'createConnectedApplication'
);
for (const name of ['graph.py', 'dashboard_tools.py', 'operations.py', 'dashboard_contract.py'] as const) {
const file = page.getByRole('complementary', { name: 'File tree' }).getByRole('button', { name, exact: true });
await expect(file).toBeVisible();
await file.click();
await expect(page.getByRole('tabpanel')).toContainText({
'graph.py': 'plan_dashboard_operation',
'dashboard_tools.py': 'query_recent_disruptions',
'operations.py': 'def admit(',
'dashboard_contract.py': 'def validate_spec(',
}[name]!);
}
await expect(page).toHaveURL(/(?:\?|&)mode=code(?:&|$)/);
await page
.locator('[data-workspace-desktop-navigation]')
Expand Down
4 changes: 3 additions & 1 deletion cockpit/chat/footprint.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ describe('Chat footprint', () => {

expect(graphSource).toContain('async def generate_title');
expect(graphSource).toContain('metadata={"title": title}');
expect(graphSource).toContain('add_node("generate_title", generate_title)');
const explicitRegistration = graphSource.includes('add_node("generate_title", generate_title)');
const loopRegistration = /for name, node in \([^\n]*\("generate_title", generate_title\)[^\n]*\):\n _builder\.add_node\(name, node\)/.test(graphSource);
expect(explicitRegistration || loopRegistration).toBe(true);
expect(graphSource).toContain('add_edge("generate_title", END)');
}
});
Expand Down
39 changes: 34 additions & 5 deletions cockpit/chat/generative-ui/angular/e2e/c-generative-ui.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { test, expect } from '@playwright/test';
import { submitAndWaitForResponse } from '@threadplane-internal/e2e-harness';

test('c-generative-ui: dashboard prompt renders chat-generative-ui surface', async ({ page }) => {
await submitAndWaitForResponse(page, 'Show me a dashboard of airline operations.');
test('c-generative-ui: dashboard prompt renders chat-generative-ui surface', async ({
page,
}) => {
await submitAndWaitForResponse(
page,
'Show me a dashboard of airline operations.'
);

// The render_spec tool call returns a dashboard JSON spec which the
// content-classifier in @threadplane/chat mounts as a tree of
Expand All @@ -14,13 +19,37 @@ test('c-generative-ui: dashboard prompt renders chat-generative-ui surface', asy
await expect(page.locator('chat-generative-ui')).not.toHaveCount(0);
});

test('c-generative-ui: filter prompt produces assistant turn', async ({ page }) => {
await submitAndWaitForResponse(page, 'Filter to only the cancelled flights.');
test('c-generative-ui: filter prompt produces assistant turn', async ({
page,
}) => {
const runThreads: string[] = [];
page.on('request', (request) => {
const run = /\/threads\/([^/]+)\/runs\/stream/.exec(request.url());
if (request.method() === 'POST' && run) runThreads.push(run[1]);
});
await submitAndWaitForResponse(
page,
'Show me a dashboard of airline operations.'
);
await expect(page.locator('chat-generative-ui').first()).toBeVisible();
// Keep the accepted dashboard's conversation; the one-turn helper navigates
// on every call and would make this a rejected first-turn filter.
const send = page.getByRole('button', { name: /send message/i });
await page
.getByRole('textbox', { name: /message|prompt/i })
.fill('Filter to only the cancelled flights.');
await expect(send).toBeEnabled();
await send.click();

// The query_recent_disruptions tool returns data the assistant uses to
// narrow the dashboard. Distinctive surface here is just that the
// assistant turn finalized — the dashboard view update is internal state.
await expect(
page.locator('chat-message[data-role="assistant"]').last(),
page.locator('chat-message[data-role="assistant"]').last()
).toBeVisible();
await expect(
page.locator('chat-message[data-role="assistant"]').last()
).toContainText('Stored 3 cancelled disruption rows (limit 5).');
expect(runThreads).toHaveLength(2);
expect(new Set(runThreads).size).toBe(1);
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
"response": {
"toolCalls": [
{
"name": "render_spec",
"arguments": "{\"elements\":{\"root\":{\"type\":\"dashboard_grid\",\"children\":[\"stats_row\",\"charts_row\",\"table_section\"]},\"stats_row\":{\"type\":\"container\",\"props\":{\"direction\":\"row\"},\"children\":[\"on_time_card\",\"flights_card\",\"delay_card\",\"load_card\"]},\"on_time_card\":{\"type\":\"stat_card\",\"props\":{\"label\":\"On-time %\",\"value\":{\"$state\":\"/on_time/value\"},\"delta\":{\"$state\":\"/on_time/delta\"}}},\"flights_card\":{\"type\":\"stat_card\",\"props\":{\"label\":\"Flights Today\",\"value\":{\"$state\":\"/flights_today/value\"},\"delta\":{\"$state\":\"/flights_today/delta\"}}},\"delay_card\":{\"type\":\"stat_card\",\"props\":{\"label\":\"Avg Delay (min)\",\"value\":{\"$state\":\"/avg_delay/value\"},\"delta\":{\"$state\":\"/avg_delay/delta\"}}},\"load_card\":{\"type\":\"stat_card\",\"props\":{\"label\":\"Load Factor\",\"value\":{\"$state\":\"/load_factor/value\"},\"delta\":{\"$state\":\"/load_factor/delta\"}}},\"charts_row\":{\"type\":\"container\",\"props\":{\"direction\":\"row\"},\"children\":[\"trend_chart\",\"airline_chart\"]},\"trend_chart\":{\"type\":\"line_chart\",\"props\":{\"title\":\"On-time % Trend\",\"data\":{\"$state\":\"/on_time_trend\"},\"xKey\":\"month\",\"yKey\":\"on_time_pct\"}},\"airline_chart\":{\"type\":\"bar_chart\",\"props\":{\"title\":\"Flights by Airline (Daily)\",\"data\":{\"$state\":\"/flights_by_airline\"},\"labelKey\":\"airline\",\"valueKey\":\"count\"}},\"table_section\":{\"type\":\"data_grid\",\"props\":{\"title\":\"Recent Disruptions\",\"rows\":{\"$state\":\"/recent_disruptions\"},\"columns\":[\"flight_number\",\"type\",\"minutes\",\"route\",\"date\"]}}},\"root\":\"root\"}",
"name": "plan_dashboard_operation",
"arguments": "{\"kind\":\"create\",\"calls\":[{\"name\":\"render_spec\",\"args\":{\"elements\":{\"root\":{\"type\":\"dashboard_grid\",\"children\":[\"stats_row\",\"charts_row\",\"table_section\"]},\"stats_row\":{\"type\":\"container\",\"props\":{\"direction\":\"row\"},\"children\":[\"on_time_card\",\"flights_card\",\"delay_card\",\"load_card\"]},\"on_time_card\":{\"type\":\"stat_card\",\"props\":{\"label\":\"On-time %\",\"value\":{\"$state\":\"/on_time/value\"},\"delta\":{\"$state\":\"/on_time/delta\"}}},\"flights_card\":{\"type\":\"stat_card\",\"props\":{\"label\":\"Flights Today\",\"value\":{\"$state\":\"/flights_today/value\"},\"delta\":{\"$state\":\"/flights_today/delta\"}}},\"delay_card\":{\"type\":\"stat_card\",\"props\":{\"label\":\"Avg Delay (min)\",\"value\":{\"$state\":\"/avg_delay/value\"},\"delta\":{\"$state\":\"/avg_delay/delta\"}}},\"load_card\":{\"type\":\"stat_card\",\"props\":{\"label\":\"Load Factor\",\"value\":{\"$state\":\"/load_factor/value\"},\"delta\":{\"$state\":\"/load_factor/delta\"}}},\"charts_row\":{\"type\":\"container\",\"props\":{\"direction\":\"row\"},\"children\":[\"trend_chart\",\"airline_chart\"]},\"trend_chart\":{\"type\":\"line_chart\",\"props\":{\"title\":\"On-time % Trend\",\"data\":{\"$state\":\"/on_time_trend\"},\"xKey\":\"month\",\"yKey\":\"on_time_pct\"}},\"airline_chart\":{\"type\":\"bar_chart\",\"props\":{\"title\":\"Flights by Airline (Daily)\",\"data\":{\"$state\":\"/flights_by_airline\"},\"labelKey\":\"airline\",\"valueKey\":\"count\"}},\"table_section\":{\"type\":\"data_grid\",\"props\":{\"title\":\"Recent Disruptions\",\"rows\":{\"$state\":\"/recent_disruptions\"},\"columns\":[\"flight_number\",\"type\",\"minutes\",\"route\",\"date\"]}}},\"root\":\"root\"}},{\"name\":\"query_airline_kpis\",\"args\":{}},{\"name\":\"query_on_time_trend\",\"args\":{}},{\"name\":\"query_flights_by_airline\",\"args\":{}},{\"name\":\"query_recent_disruptions\",\"args\":{}}],\"interpretation\":\"\"}",
"id": "call_offZhaDPm8tbblqv1sNHarnc"
}
]
Expand All @@ -21,12 +21,12 @@
"response": {
"toolCalls": [
{
"name": "query_recent_disruptions",
"arguments": "{\"limit\":5,\"type\":\"cancelled\"}",
"name": "plan_dashboard_operation",
"arguments": "{\"kind\":\"data_update\",\"calls\":[{\"name\":\"query_recent_disruptions\",\"args\":{\"limit\":5,\"type\":\"cancelled\"}}],\"interpretation\":\"\"}",
"id": "call_ivGTFO6CX8sl4bkOprs633gH"
}
]
}
}
]
}
}
23 changes: 14 additions & 9 deletions cockpit/chat/generative-ui/python/prompts/generative-ui.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Airline Operations Dashboard Agent

You are a dashboard agent that builds interactive airline-operations KPI dashboards. You have five tools:
You choose one operation for an interactive airline-operations dashboard. You must call `plan_dashboard_operation(kind, calls, interpretation)` exactly once. That is your only directly available tool. Do not write an action promise or a summary in your response. Your decision is independently validated before the graph executes any action.

`kind` is exactly one of `create`, `data_update`, `restructure`, `interpret`, or `unsupported`. `calls` is a list of objects containing exactly `name` and `args` (no IDs). Use an empty string for `interpretation` on action and unsupported decisions; for `interpret`, provide the bounded question/interpretation text. Read-only kinds have an empty calls list. Decisions are bounded to five calls, with unique names and at most one render.

The graph can execute these five actions after admission:

- `render_spec(elements, root)` — Author or update the dashboard layout. `elements` is a dict keyed by component id (each value has `type`, optional `props`, optional `children`); `root` is the id of the top-level component. See the schema below.
- `query_airline_kpis()` — Snapshot of operational KPIs: on-time %, flights today, avg delay, load factor.
Expand All @@ -10,19 +14,20 @@ You are a dashboard agent that builds interactive airline-operations KPI dashboa

## Workflow

### When no dashboard exists yet (first turn)
### For a request to create/show a dashboard when none exists

1. Call `render_spec` ONCE with a complete dashboard layout — stat cards, charts, table — using `$state` bindings to the slots the data tools populate (see "State Path Conventions" below).
2. In the SAME turn (same tool_calls array), call EACH data tool that backs a component in your spec. Do NOT call tools whose data your spec doesn't reference.
3. After the tools return, return WITHOUT any further tool calls. A separate node will write a brief conversational summary. **Critical: if `render_spec` and the data tools have already been called this turn, you are DONE. Return with no tool_calls. Do NOT call `render_spec` again. Do NOT re-call the data tools.**
Choose `create`. Include exactly one `render_spec` call with a complete layout and exactly the queries required for its `$state` bindings. Literal or null data needs no query. The graph will execute this batch once, assess every result, and publish the entire operation atomically. You will not receive another planning turn or produce its completion summary. Interpretive and unsupported requests remain read-only regardless of whether a layout exists; do not force creation solely because the checkpoint lacks one. `data_update` and `restructure` require an accepted existing layout.

### When the dashboard exists (follow-up turn)

Categorize the user's request and act ONCE. DO NOT ask clarifying questions — pick the most reasonable interpretation and act.
The confirmed checkpoint appended to this prompt provides the accepted layout and owned data. Existing dashboard means an accepted layout, including a literal-only layout with empty data. Choose one decision without asking clarifying questions.

- **Filter / scope** (e.g. "filter to cancelled flights only", "last 6 months", "top 3"): choose `data_update` with exactly one query and the requested arguments. Do not include a render call. A cancelled filter means `query_recent_disruptions` with `{"type":"cancelled"}`; the graph validates returned rows before applying them.
- **Structural change** (e.g. "add a card for X", "remove the table"): choose `restructure` with one modified render layout. Reuse resolved checkpoint bindings; include queries only for genuinely missing binding paths. A stored null array resolves its array pointer; a null metric does not resolve `/value` or `/delta`.
- **Interpretive question** (e.g. "why is on-time % low?"): choose `interpret`, no calls, and the question in `interpretation`. The graph produces read-only prose and an explicit no-change status.
- **Unsupported request**: choose `unsupported`, no calls. Never claim that a change occurred.

- **Filter / scope** (e.g. "filter to cancelled flights only", "last 6 months", "top 3"): call EXACTLY ONE data tool — the one that backs the affected component — with the new parameters. Do NOT call `render_spec`.
- **Structural change** (e.g. "add a card for X", "remove the table"): call `render_spec` with the modified layout, then call data tools only for the NEW components.
- **Interpretive question** that no tool could resolve (e.g. "why is on-time % low?"): respond in plain prose with no tool calls.
Query arguments use only their named fields. `months` and `limit` are integers from 1 through 100 (defaults 12 and 5); null is invalid. `airlines` may be absent/null/empty for all airlines or a list of at most 100 nonempty strings. Disruption `type` may be absent/null for all or exactly `delayed`/`cancelled`. All dashboard text is nonempty and at most 512 UTF-16 characters. Component IDs are at most 80 characters and cannot be `__proto__`, `prototype`, or `constructor`. Layouts contain 1–100 elements and form one connected tree without duplicate children, shared nodes, or cycles. Use only the six component types below and their exact documented props. Chart keys must be `month`/`on_time_pct` or `airline`/`count`; grid columns come from `flight_number`, `type`, `minutes`, `route`, `date`, without duplicates. Stat values/deltas and chart/grid data may be literal values or null; safe state bindings use only the paths below.

## JSON Render Spec Format

Expand Down
Loading
Loading