diff --git a/cockpit/chat/generative-ui/python/prompts/dashboard.md b/cockpit/chat/generative-ui/python/prompts/dashboard.md new file mode 100644 index 000000000..37c070bb9 --- /dev/null +++ b/cockpit/chat/generative-ui/python/prompts/dashboard.md @@ -0,0 +1,74 @@ +# Airline Operations Dashboard Agent + +You are a dashboard agent that builds interactive airline-operations KPI dashboards using a JSON render spec format. You have access to tools that query an airline's flight, fleet, and on-time performance data. + +## Your Behavior + +### First message (no existing dashboard) + +1. Generate a complete dashboard layout as a JSON render spec (see format below) +2. Call ALL four data tools to populate the dashboard +3. After the tools return, provide a brief conversational summary + +### Follow-up messages (dashboard already exists) + +Categorize the user's request: + +- **Data change** (e.g., "show last 6 months", "filter to cancelled flights only"): Call only the relevant tool(s) with updated parameters. Do NOT regenerate the spec. Just respond conversationally confirming the update. +- **Structural change** (e.g., "add a new chart", "remove the table"): Regenerate the full spec with the modification, then call tools to populate any new components. +- **Question about data** (e.g., "why did on-time % dip in December?"): Respond conversationally in plain text. Do NOT output JSON or call tools. + +## JSON Render Spec Format + +Your spec response MUST be raw JSON only — no markdown, no code fences, no surrounding text. + +``` +{ + "elements": { [key: string]: Element }, + "root": string +} +``` + +An Element has: +``` +{ + "type": string, + "props": { ... }, + "children?": string[] +} +``` + +### Props with State Bindings + +Use `{ "$state": "/json/pointer/path" }` for props that will be populated by tool results. The dashboard renders skeleton placeholders until the data arrives. + +Example: `"value": { "$state": "/on_time/value" }` — this prop will be populated when the `/on_time/value` state path receives data. + +## Available Component Types + +| Type | Props | Children | Description | +|------|-------|----------|-------------| +| `dashboard_grid` | *(none)* | Yes | Top-level vertical layout with section spacing | +| `container` | `direction` ("row" or "column") | Yes | Flex layout container | +| `stat_card` | `label` (string), `value` ($state), `delta` ($state) | No | Metric summary card | +| `line_chart` | `title` (string), `data` ($state array), `xKey` (string), `yKey` (string) | No | SVG line chart | +| `bar_chart` | `title` (string), `data` ($state array), `labelKey` (string), `valueKey` (string) | No | SVG bar chart | +| `data_grid` | `title` (string), `rows` ($state array), `columns` (string[]) | No | Data table | + +## State Path Conventions + +Use these state paths to match what the tools populate: + +- `/on_time/value`, `/on_time/delta` — from query_airline_kpis +- `/flights_today/value`, `/flights_today/delta` — from query_airline_kpis +- `/avg_delay/value`, `/avg_delay/delta` — from query_airline_kpis +- `/load_factor/value`, `/load_factor/delta` — from query_airline_kpis +- `/on_time_trend` — array from query_on_time_trend +- `/flights_by_airline` — array from query_flights_by_airline +- `/recent_disruptions` — array from query_recent_disruptions + +## Example Spec + +For "show me the dashboard": + +{"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","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","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"} diff --git a/cockpit/chat/generative-ui/python/prompts/generative-ui.md b/cockpit/chat/generative-ui/python/prompts/generative-ui.md deleted file mode 100644 index 288b521f2..000000000 --- a/cockpit/chat/generative-ui/python/prompts/generative-ui.md +++ /dev/null @@ -1,74 +0,0 @@ -# SaaS Metrics Dashboard Agent - -You are a dashboard agent that builds interactive SaaS metrics dashboards using a JSON render spec format. You have access to tools that query SaaS metrics data. - -## Your Behavior - -### First message (no existing dashboard) - -1. Generate a complete dashboard layout as a JSON render spec (see format below) -2. Call ALL four data tools to populate the dashboard -3. After the tools return, provide a brief conversational summary - -### Follow-up messages (dashboard already exists) - -Categorize the user's request: - -- **Data change** (e.g., "show last 6 months", "filter to enterprise only"): Call only the relevant tool(s) with updated parameters. Do NOT regenerate the spec. Just respond conversationally confirming the update. -- **Structural change** (e.g., "add a new chart", "remove the table"): Regenerate the full spec with the modification, then call tools to populate any new components. -- **Question about data** (e.g., "why did churn spike?"): Respond conversationally in plain text. Do NOT output JSON or call tools. - -## JSON Render Spec Format - -Your spec response MUST be raw JSON only — no markdown, no code fences, no surrounding text. - -``` -{ - "elements": { [key: string]: Element }, - "root": string -} -``` - -An Element has: -``` -{ - "type": string, - "props": { ... }, - "children?": string[] -} -``` - -### Props with State Bindings - -Use `{ "$state": "/json/pointer/path" }` for props that will be populated by tool results. The dashboard renders skeleton placeholders until the data arrives. - -Example: `"value": { "$state": "/mrr/value" }` — this prop will be populated when the `/mrr/value` state path receives data. - -## Available Component Types - -| Type | Props | Children | Description | -|------|-------|----------|-------------| -| `dashboard_grid` | *(none)* | Yes | Top-level vertical layout with section spacing | -| `container` | `direction` ("row" or "column") | Yes | Flex layout container | -| `stat_card` | `label` (string), `value` ($state), `delta` ($state) | No | Metric summary card | -| `line_chart` | `title` (string), `data` ($state array), `xKey` (string), `yKey` (string) | No | SVG line chart | -| `bar_chart` | `title` (string), `data` ($state array), `labelKey` (string), `valueKey` (string) | No | SVG bar chart | -| `data_grid` | `title` (string), `rows` ($state array), `columns` (string[]) | No | Data table | - -## State Path Conventions - -Use these state paths to match what the tools populate: - -- `/mrr/value`, `/mrr/delta`, `/mrr/period` — from query_mrr -- `/subscribers/total`, `/subscribers/delta` — from query_mrr -- `/churn/rate`, `/churn/delta` — from query_mrr -- `/arpu/value`, `/arpu/delta` — from query_mrr -- `/mrr_trend` — array from query_mrr_trend -- `/subscribers_by_plan` — array from query_subscribers_by_plan -- `/churned_accounts` — array from query_churned_accounts - -## Example Spec - -For "show me the dashboard": - -{"elements":{"root":{"type":"dashboard_grid","children":["stats_row","charts_row","table_section"]},"stats_row":{"type":"container","props":{"direction":"row"},"children":["mrr_card","subscribers_card","churn_card","arpu_card"]},"mrr_card":{"type":"stat_card","props":{"label":"MRR","value":{"$state":"/mrr/value"},"delta":{"$state":"/mrr/delta"}}},"subscribers_card":{"type":"stat_card","props":{"label":"Active Subscribers","value":{"$state":"/subscribers/total"},"delta":{"$state":"/subscribers/delta"}}},"churn_card":{"type":"stat_card","props":{"label":"Churn Rate","value":{"$state":"/churn/rate"},"delta":{"$state":"/churn/delta"}}},"arpu_card":{"type":"stat_card","props":{"label":"ARPU","value":{"$state":"/arpu/value"},"delta":{"$state":"/arpu/delta"}}},"charts_row":{"type":"container","props":{"direction":"row"},"children":["trend_chart","plan_chart"]},"trend_chart":{"type":"line_chart","props":{"title":"MRR Trend","data":{"$state":"/mrr_trend"},"xKey":"month","yKey":"mrr"}},"plan_chart":{"type":"bar_chart","props":{"title":"Subscribers by Plan","data":{"$state":"/subscribers_by_plan"},"labelKey":"plan","valueKey":"count"}},"table_section":{"type":"data_grid","props":{"title":"Recently Churned","rows":{"$state":"/churned_accounts"},"columns":["name","plan","mrr_lost","date"]}}},"root":"root"} diff --git a/cockpit/chat/generative-ui/python/src/dashboard_tools.py b/cockpit/chat/generative-ui/python/src/dashboard_tools.py index 827b0c46e..5f71d9a56 100644 --- a/cockpit/chat/generative-ui/python/src/dashboard_tools.py +++ b/cockpit/chat/generative-ui/python/src/dashboard_tools.py @@ -1,97 +1,117 @@ -"""Mock SaaS metrics data tools for the generative-ui dashboard example.""" +"""Aviation KPI tools for the c-generative-ui standalone demo. + +Standalone copy — analytics constants are inlined here because this +backend has no shared aviation_data.py module. Mirrors the umbrella +backend at cockpit/langgraph/streaming/python/src/dashboard_tools.py. +""" from langchain_core.tools import tool -# ── Hardcoded SaaS dataset ────────────────────────────────────────────────── - -_MRR_TREND = [ - {"month": "2025-05", "mrr": 28000}, - {"month": "2025-06", "mrr": 29500}, - {"month": "2025-07", "mrr": 30200}, - {"month": "2025-08", "mrr": 31800}, - {"month": "2025-09", "mrr": 32500}, - {"month": "2025-10", "mrr": 33000}, - {"month": "2025-11", "mrr": 34200}, - {"month": "2025-12", "mrr": 35800}, - {"month": "2026-01", "mrr": 37000}, - {"month": "2026-02", "mrr": 38500}, - {"month": "2026-03", "mrr": 40200}, - {"month": "2026-04", "mrr": 42000}, +# ── Analytics fixtures (inlined; no aviation_data.py in standalone) ───────── + +KPI_SNAPSHOT = { + "on_time_pct": 84.2, + "on_time_delta": "+1.4%", + "flights_today": 312, + "flights_today_delta": "+8", + "avg_delay_min": 12, + "avg_delay_delta": "-2 min", + "load_factor_pct": 78.5, + "load_factor_delta": "+0.6%", +} + +ON_TIME_TREND = [ + {"month": "2025-05", "on_time_pct": 82.4}, + {"month": "2025-06", "on_time_pct": 81.1}, + {"month": "2025-07", "on_time_pct": 79.8}, + {"month": "2025-08", "on_time_pct": 80.5}, + {"month": "2025-09", "on_time_pct": 83.2}, + {"month": "2025-10", "on_time_pct": 84.0}, + {"month": "2025-11", "on_time_pct": 82.6}, + {"month": "2025-12", "on_time_pct": 78.9}, + {"month": "2026-01", "on_time_pct": 80.2}, + {"month": "2026-02", "on_time_pct": 81.7}, + {"month": "2026-03", "on_time_pct": 82.8}, + {"month": "2026-04", "on_time_pct": 84.2}, ] -_SUBSCRIBERS_BY_PLAN = [ - {"plan": "free", "count": 1200}, - {"plan": "starter", "count": 850}, - {"plan": "pro", "count": 420}, - {"plan": "enterprise", "count": 95}, +FLIGHTS_BY_AIRLINE = [ + {"airline": "American", "count": 87}, + {"airline": "United", "count": 92}, + {"airline": "Delta", "count": 78}, + {"airline": "JetBlue", "count": 55}, ] -_CHURNED_ACCOUNTS = [ - {"name": "Acme Corp", "plan": "pro", "mrr_lost": 450, "date": "2026-04-01"}, - {"name": "Widgetly", "plan": "starter", "mrr_lost": 120, "date": "2026-03-28"}, - {"name": "DataPipe Inc", "plan": "enterprise", "mrr_lost": 2400, "date": "2026-03-25"}, - {"name": "NovaTech", "plan": "pro", "mrr_lost": 450, "date": "2026-03-20"}, - {"name": "CloudSync", "plan": "starter", "mrr_lost": 120, "date": "2026-03-15"}, - {"name": "ByteForge", "plan": "pro", "mrr_lost": 450, "date": "2026-03-10"}, - {"name": "Quantum Labs", "plan": "enterprise", "mrr_lost": 2400, "date": "2026-03-05"}, - {"name": "FlowState", "plan": "starter", "mrr_lost": 120, "date": "2026-02-28"}, - {"name": "CipherNet", "plan": "pro", "mrr_lost": 450, "date": "2026-02-20"}, - {"name": "Luminary AI", "plan": "starter", "mrr_lost": 120, "date": "2026-02-15"}, +RECENT_DISRUPTIONS = [ + {"flight_number": "UA123", "type": "delayed", "minutes": 45, "route": "LAX→JFK", "date": "2026-05-14"}, + {"flight_number": "AA456", "type": "cancelled", "minutes": 0, "route": "JFK→LAX", "date": "2026-05-14"}, + {"flight_number": "DL789", "type": "delayed", "minutes": 22, "route": "ATL→ORD", "date": "2026-05-13"}, + {"flight_number": "B6101", "type": "delayed", "minutes": 68, "route": "BOS→MIA", "date": "2026-05-13"}, + {"flight_number": "UA204", "type": "cancelled", "minutes": 0, "route": "SFO→SEA", "date": "2026-05-12"}, + {"flight_number": "AA318", "type": "delayed", "minutes": 15, "route": "DFW→DEN", "date": "2026-05-12"}, + {"flight_number": "DL552", "type": "delayed", "minutes": 35, "route": "ATL→MIA", "date": "2026-05-11"}, + {"flight_number": "B6217", "type": "delayed", "minutes": 80, "route": "JFK→BOS", "date": "2026-05-11"}, + {"flight_number": "UA640", "type": "cancelled", "minutes": 0, "route": "ORD→DEN", "date": "2026-05-10"}, + {"flight_number": "AA871", "type": "delayed", "minutes": 25, "route": "LAX→DFW", "date": "2026-05-10"}, ] +# ── Tools ─────────────────────────────────────────────────────────────────── + @tool -def query_mrr() -> dict: - """Get current Monthly Recurring Revenue (MRR) with month-over-month delta.""" - current = _MRR_TREND[-1]["mrr"] - previous = _MRR_TREND[-2]["mrr"] - delta_pct = ((current - previous) / previous) * 100 - total_subs = sum(p["count"] for p in _SUBSCRIBERS_BY_PLAN) - arpu = round(current / total_subs, 2) +def query_airline_kpis() -> dict: + """Snapshot of operational KPIs across the fleet: on-time %, flights today, + average delay (minutes), and load factor.""" + snap = KPI_SNAPSHOT return { - "mrr": {"value": current, "delta": f"+{delta_pct:.1f}%", "period": "month"}, - "subscribers": {"total": total_subs, "delta": "+42"}, - "churn": {"rate": "3.2%", "delta": "-0.4%"}, - "arpu": {"value": f"${arpu:.2f}", "delta": "+$1.20"}, + "on_time": {"value": f"{snap['on_time_pct']}%", "delta": snap["on_time_delta"]}, + "flights_today": {"value": snap["flights_today"], "delta": snap["flights_today_delta"]}, + "avg_delay": {"value": f"{snap['avg_delay_min']} min", "delta": snap["avg_delay_delta"]}, + "load_factor": {"value": f"{snap['load_factor_pct']}%", "delta": snap["load_factor_delta"]}, } @tool -def query_subscribers_by_plan(plans: list[str] | None = None) -> list[dict]: - """Get subscriber counts broken down by plan tier. +def query_on_time_trend(months: int = 12) -> list[dict]: + """On-time performance over time, as percentage by month. Args: - plans: Optional list of plan names to filter by (e.g., ["pro", "enterprise"]). - Returns all plans if not specified. + months: Number of months to return (default 12). Valid: 3, 6, 12, 24. """ - if plans: - return [p for p in _SUBSCRIBERS_BY_PLAN if p["plan"] in plans] - return _SUBSCRIBERS_BY_PLAN + months = min(months, len(ON_TIME_TREND)) + return ON_TIME_TREND[-months:] @tool -def query_mrr_trend(months: int = 12) -> list[dict]: - """Get MRR trend over time. +def query_flights_by_airline(airlines: list[str] | None = None) -> list[dict]: + """Daily flight counts per airline. Args: - months: Number of months to return (default 12). Valid values: 3, 6, 12, 24. + airlines: Optional filter list, e.g. ["American", "United"]. All four + returned if omitted. """ - months = min(months, len(_MRR_TREND)) - return _MRR_TREND[-months:] + if airlines: + return [a for a in FLIGHTS_BY_AIRLINE if a["airline"] in airlines] + return FLIGHTS_BY_AIRLINE @tool -def query_churned_accounts(limit: int = 5, plan: str | None = None) -> list[dict]: - """Get recently churned accounts. +def query_recent_disruptions(limit: int = 5, type: str | None = None) -> list[dict]: + """Recent flight delays or cancellations. Args: - limit: Maximum number of accounts to return (default 5). - plan: Optional plan name to filter by (e.g., "enterprise"). + limit: Maximum entries to return (default 5). + type: Optional filter, "delayed" or "cancelled". """ - filtered = _CHURNED_ACCOUNTS - if plan: - filtered = [a for a in filtered if a["plan"] == plan] + filtered = RECENT_DISRUPTIONS + if type: + filtered = [d for d in filtered if d["type"] == type] return filtered[:limit] -ALL_TOOLS = [query_mrr, query_subscribers_by_plan, query_mrr_trend, query_churned_accounts] +ALL_TOOLS = [ + query_airline_kpis, + query_on_time_trend, + query_flights_by_airline, + query_recent_disruptions, +] diff --git a/cockpit/chat/generative-ui/python/src/graph.py b/cockpit/chat/generative-ui/python/src/graph.py index 9dbc1ea9f..815e5f369 100644 --- a/cockpit/chat/generative-ui/python/src/graph.py +++ b/cockpit/chat/generative-ui/python/src/graph.py @@ -1,4 +1,4 @@ -"""Multi-node LangGraph graph for the SaaS metrics dashboard. +"""Multi-node LangGraph graph for the airline operations KPI dashboard. Flow: router → generate_shell (first turn) or plan_tools (follow-up) @@ -78,17 +78,17 @@ async def emit_state(state: DashboardState) -> DashboardState: except (json.JSONDecodeError, TypeError): continue - if msg.name == "query_mrr": + if msg.name == "query_airline_kpis": for section_key, section_val in data.items(): if isinstance(section_val, dict): for k, v in section_val.items(): tool_results[f"/{section_key}/{k}"] = v - elif msg.name == "query_subscribers_by_plan": - tool_results["/subscribers_by_plan"] = data - elif msg.name == "query_mrr_trend": - tool_results["/mrr_trend"] = data - elif msg.name == "query_churned_accounts": - tool_results["/churned_accounts"] = data + elif msg.name == "query_on_time_trend": + tool_results["/on_time_trend"] = data + elif msg.name == "query_flights_by_airline": + tool_results["/flights_by_airline"] = data + elif msg.name == "query_recent_disruptions": + tool_results["/recent_disruptions"] = data elif msg.type == "ai": break diff --git a/cockpit/chat/generative-ui/python/src/index.ts b/cockpit/chat/generative-ui/python/src/index.ts index 9e9a2fd3b..3308df0cc 100644 --- a/cockpit/chat/generative-ui/python/src/index.ts +++ b/cockpit/chat/generative-ui/python/src/index.ts @@ -28,7 +28,7 @@ export const chatGenerativeUiPythonModule: CockpitCapabilityModule = { }, title: 'Chat Generative UI (Python)', docsPath: '/docs/chat/core-capabilities/generative-ui/overview/python', - promptAssetPaths: ['cockpit/chat/generative-ui/python/prompts/generative-ui.md'], + promptAssetPaths: ['cockpit/chat/generative-ui/python/prompts/dashboard.md'], codeAssetPaths: [ 'cockpit/chat/generative-ui/angular/src/app/generative-ui.component.ts', 'cockpit/chat/generative-ui/angular/src/app/app.config.ts', diff --git a/cockpit/langgraph/streaming/python/prompts/dashboard.md b/cockpit/langgraph/streaming/python/prompts/dashboard.md index 288b521f2..37c070bb9 100644 --- a/cockpit/langgraph/streaming/python/prompts/dashboard.md +++ b/cockpit/langgraph/streaming/python/prompts/dashboard.md @@ -1,6 +1,6 @@ -# SaaS Metrics Dashboard Agent +# Airline Operations Dashboard Agent -You are a dashboard agent that builds interactive SaaS metrics dashboards using a JSON render spec format. You have access to tools that query SaaS metrics data. +You are a dashboard agent that builds interactive airline-operations KPI dashboards using a JSON render spec format. You have access to tools that query an airline's flight, fleet, and on-time performance data. ## Your Behavior @@ -14,9 +14,9 @@ You are a dashboard agent that builds interactive SaaS metrics dashboards using Categorize the user's request: -- **Data change** (e.g., "show last 6 months", "filter to enterprise only"): Call only the relevant tool(s) with updated parameters. Do NOT regenerate the spec. Just respond conversationally confirming the update. +- **Data change** (e.g., "show last 6 months", "filter to cancelled flights only"): Call only the relevant tool(s) with updated parameters. Do NOT regenerate the spec. Just respond conversationally confirming the update. - **Structural change** (e.g., "add a new chart", "remove the table"): Regenerate the full spec with the modification, then call tools to populate any new components. -- **Question about data** (e.g., "why did churn spike?"): Respond conversationally in plain text. Do NOT output JSON or call tools. +- **Question about data** (e.g., "why did on-time % dip in December?"): Respond conversationally in plain text. Do NOT output JSON or call tools. ## JSON Render Spec Format @@ -42,7 +42,7 @@ An Element has: Use `{ "$state": "/json/pointer/path" }` for props that will be populated by tool results. The dashboard renders skeleton placeholders until the data arrives. -Example: `"value": { "$state": "/mrr/value" }` — this prop will be populated when the `/mrr/value` state path receives data. +Example: `"value": { "$state": "/on_time/value" }` — this prop will be populated when the `/on_time/value` state path receives data. ## Available Component Types @@ -59,16 +59,16 @@ Example: `"value": { "$state": "/mrr/value" }` — this prop will be populated w Use these state paths to match what the tools populate: -- `/mrr/value`, `/mrr/delta`, `/mrr/period` — from query_mrr -- `/subscribers/total`, `/subscribers/delta` — from query_mrr -- `/churn/rate`, `/churn/delta` — from query_mrr -- `/arpu/value`, `/arpu/delta` — from query_mrr -- `/mrr_trend` — array from query_mrr_trend -- `/subscribers_by_plan` — array from query_subscribers_by_plan -- `/churned_accounts` — array from query_churned_accounts +- `/on_time/value`, `/on_time/delta` — from query_airline_kpis +- `/flights_today/value`, `/flights_today/delta` — from query_airline_kpis +- `/avg_delay/value`, `/avg_delay/delta` — from query_airline_kpis +- `/load_factor/value`, `/load_factor/delta` — from query_airline_kpis +- `/on_time_trend` — array from query_on_time_trend +- `/flights_by_airline` — array from query_flights_by_airline +- `/recent_disruptions` — array from query_recent_disruptions ## Example Spec For "show me the dashboard": -{"elements":{"root":{"type":"dashboard_grid","children":["stats_row","charts_row","table_section"]},"stats_row":{"type":"container","props":{"direction":"row"},"children":["mrr_card","subscribers_card","churn_card","arpu_card"]},"mrr_card":{"type":"stat_card","props":{"label":"MRR","value":{"$state":"/mrr/value"},"delta":{"$state":"/mrr/delta"}}},"subscribers_card":{"type":"stat_card","props":{"label":"Active Subscribers","value":{"$state":"/subscribers/total"},"delta":{"$state":"/subscribers/delta"}}},"churn_card":{"type":"stat_card","props":{"label":"Churn Rate","value":{"$state":"/churn/rate"},"delta":{"$state":"/churn/delta"}}},"arpu_card":{"type":"stat_card","props":{"label":"ARPU","value":{"$state":"/arpu/value"},"delta":{"$state":"/arpu/delta"}}},"charts_row":{"type":"container","props":{"direction":"row"},"children":["trend_chart","plan_chart"]},"trend_chart":{"type":"line_chart","props":{"title":"MRR Trend","data":{"$state":"/mrr_trend"},"xKey":"month","yKey":"mrr"}},"plan_chart":{"type":"bar_chart","props":{"title":"Subscribers by Plan","data":{"$state":"/subscribers_by_plan"},"labelKey":"plan","valueKey":"count"}},"table_section":{"type":"data_grid","props":{"title":"Recently Churned","rows":{"$state":"/churned_accounts"},"columns":["name","plan","mrr_lost","date"]}}},"root":"root"} +{"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","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","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"} diff --git a/cockpit/langgraph/streaming/python/src/aviation_data.py b/cockpit/langgraph/streaming/python/src/aviation_data.py index 175bc19b7..6893afe8e 100644 --- a/cockpit/langgraph/streaming/python/src/aviation_data.py +++ b/cockpit/langgraph/streaming/python/src/aviation_data.py @@ -157,3 +157,51 @@ "depart_local": "07:45", "arrive_local": "11:30", "duration_min": 405, "status": "on_time", "gate": "C40", "aircraft": "Airbus A321"}, ] + +# ── Dashboard analytics (PR 3: c-generative-ui aviation KPIs) ─────────────── + +KPI_SNAPSHOT = { + "on_time_pct": 84.2, + "on_time_delta": "+1.4%", + "flights_today": 312, + "flights_today_delta": "+8", + "avg_delay_min": 12, + "avg_delay_delta": "-2 min", + "load_factor_pct": 78.5, + "load_factor_delta": "+0.6%", +} + +ON_TIME_TREND = [ + {"month": "2025-05", "on_time_pct": 82.4}, + {"month": "2025-06", "on_time_pct": 81.1}, + {"month": "2025-07", "on_time_pct": 79.8}, + {"month": "2025-08", "on_time_pct": 80.5}, + {"month": "2025-09", "on_time_pct": 83.2}, + {"month": "2025-10", "on_time_pct": 84.0}, + {"month": "2025-11", "on_time_pct": 82.6}, + {"month": "2025-12", "on_time_pct": 78.9}, + {"month": "2026-01", "on_time_pct": 80.2}, + {"month": "2026-02", "on_time_pct": 81.7}, + {"month": "2026-03", "on_time_pct": 82.8}, + {"month": "2026-04", "on_time_pct": 84.2}, +] + +FLIGHTS_BY_AIRLINE = [ + {"airline": "American", "count": 87}, + {"airline": "United", "count": 92}, + {"airline": "Delta", "count": 78}, + {"airline": "JetBlue", "count": 55}, +] + +RECENT_DISRUPTIONS = [ + {"flight_number": "UA123", "type": "delayed", "minutes": 45, "route": "LAX→JFK", "date": "2026-05-14"}, + {"flight_number": "AA456", "type": "cancelled", "minutes": 0, "route": "JFK→LAX", "date": "2026-05-14"}, + {"flight_number": "DL789", "type": "delayed", "minutes": 22, "route": "ATL→ORD", "date": "2026-05-13"}, + {"flight_number": "B6101", "type": "delayed", "minutes": 68, "route": "BOS→MIA", "date": "2026-05-13"}, + {"flight_number": "UA204", "type": "cancelled", "minutes": 0, "route": "SFO→SEA", "date": "2026-05-12"}, + {"flight_number": "AA318", "type": "delayed", "minutes": 15, "route": "DFW→DEN", "date": "2026-05-12"}, + {"flight_number": "DL552", "type": "delayed", "minutes": 35, "route": "ATL→MIA", "date": "2026-05-11"}, + {"flight_number": "B6217", "type": "delayed", "minutes": 80, "route": "JFK→BOS", "date": "2026-05-11"}, + {"flight_number": "UA640", "type": "cancelled", "minutes": 0, "route": "ORD→DEN", "date": "2026-05-10"}, + {"flight_number": "AA871", "type": "delayed", "minutes": 25, "route": "LAX→DFW", "date": "2026-05-10"}, +] diff --git a/cockpit/langgraph/streaming/python/src/dashboard_graph.py b/cockpit/langgraph/streaming/python/src/dashboard_graph.py index d475bfe7a..7ce329aa6 100644 --- a/cockpit/langgraph/streaming/python/src/dashboard_graph.py +++ b/cockpit/langgraph/streaming/python/src/dashboard_graph.py @@ -1,4 +1,4 @@ -"""Multi-node LangGraph graph for the SaaS metrics dashboard. +"""Multi-node LangGraph graph for the airline operations KPI dashboard. Flow: router → generate_shell (first turn) or plan_tools (follow-up) @@ -78,17 +78,18 @@ async def emit_state(state: DashboardState) -> DashboardState: except (json.JSONDecodeError, TypeError): continue - if msg.name == "query_mrr": + if msg.name == "query_airline_kpis": + # data is {"on_time": {"value": ..., "delta": ...}, ...} for section_key, section_val in data.items(): if isinstance(section_val, dict): for k, v in section_val.items(): tool_results[f"/{section_key}/{k}"] = v - elif msg.name == "query_subscribers_by_plan": - tool_results["/subscribers_by_plan"] = data - elif msg.name == "query_mrr_trend": - tool_results["/mrr_trend"] = data - elif msg.name == "query_churned_accounts": - tool_results["/churned_accounts"] = data + elif msg.name == "query_on_time_trend": + tool_results["/on_time_trend"] = data + elif msg.name == "query_flights_by_airline": + tool_results["/flights_by_airline"] = data + elif msg.name == "query_recent_disruptions": + tool_results["/recent_disruptions"] = data elif msg.type == "ai": break diff --git a/cockpit/langgraph/streaming/python/src/dashboard_tools.py b/cockpit/langgraph/streaming/python/src/dashboard_tools.py index 827b0c46e..2bd00b07d 100644 --- a/cockpit/langgraph/streaming/python/src/dashboard_tools.py +++ b/cockpit/langgraph/streaming/python/src/dashboard_tools.py @@ -1,97 +1,78 @@ -"""Mock SaaS metrics data tools for the generative-ui dashboard example.""" +"""Aviation KPI tools for the c-generative-ui dashboard demo. -from langchain_core.tools import tool +Four tools mirror the SaaS shape they replaced (query_mrr et al): +- query_airline_kpis → 4 stat cards (snapshot dict) +- query_on_time_trend → line chart data +- query_flights_by_airline→ bar chart data +- query_recent_disruptions→ data grid rows -# ── Hardcoded SaaS dataset ────────────────────────────────────────────────── - -_MRR_TREND = [ - {"month": "2025-05", "mrr": 28000}, - {"month": "2025-06", "mrr": 29500}, - {"month": "2025-07", "mrr": 30200}, - {"month": "2025-08", "mrr": 31800}, - {"month": "2025-09", "mrr": 32500}, - {"month": "2025-10", "mrr": 33000}, - {"month": "2025-11", "mrr": 34200}, - {"month": "2025-12", "mrr": 35800}, - {"month": "2026-01", "mrr": 37000}, - {"month": "2026-02", "mrr": 38500}, - {"month": "2026-03", "mrr": 40200}, - {"month": "2026-04", "mrr": 42000}, -] +Data comes from src.aviation_data; no external calls. +""" -_SUBSCRIBERS_BY_PLAN = [ - {"plan": "free", "count": 1200}, - {"plan": "starter", "count": 850}, - {"plan": "pro", "count": 420}, - {"plan": "enterprise", "count": 95}, -] +from langchain_core.tools import tool -_CHURNED_ACCOUNTS = [ - {"name": "Acme Corp", "plan": "pro", "mrr_lost": 450, "date": "2026-04-01"}, - {"name": "Widgetly", "plan": "starter", "mrr_lost": 120, "date": "2026-03-28"}, - {"name": "DataPipe Inc", "plan": "enterprise", "mrr_lost": 2400, "date": "2026-03-25"}, - {"name": "NovaTech", "plan": "pro", "mrr_lost": 450, "date": "2026-03-20"}, - {"name": "CloudSync", "plan": "starter", "mrr_lost": 120, "date": "2026-03-15"}, - {"name": "ByteForge", "plan": "pro", "mrr_lost": 450, "date": "2026-03-10"}, - {"name": "Quantum Labs", "plan": "enterprise", "mrr_lost": 2400, "date": "2026-03-05"}, - {"name": "FlowState", "plan": "starter", "mrr_lost": 120, "date": "2026-02-28"}, - {"name": "CipherNet", "plan": "pro", "mrr_lost": 450, "date": "2026-02-20"}, - {"name": "Luminary AI", "plan": "starter", "mrr_lost": 120, "date": "2026-02-15"}, -] +from src.aviation_data import ( + KPI_SNAPSHOT, + ON_TIME_TREND, + FLIGHTS_BY_AIRLINE, + RECENT_DISRUPTIONS, +) @tool -def query_mrr() -> dict: - """Get current Monthly Recurring Revenue (MRR) with month-over-month delta.""" - current = _MRR_TREND[-1]["mrr"] - previous = _MRR_TREND[-2]["mrr"] - delta_pct = ((current - previous) / previous) * 100 - total_subs = sum(p["count"] for p in _SUBSCRIBERS_BY_PLAN) - arpu = round(current / total_subs, 2) +def query_airline_kpis() -> dict: + """Snapshot of operational KPIs across the fleet: on-time %, flights today, + average delay (minutes), and load factor.""" + snap = KPI_SNAPSHOT return { - "mrr": {"value": current, "delta": f"+{delta_pct:.1f}%", "period": "month"}, - "subscribers": {"total": total_subs, "delta": "+42"}, - "churn": {"rate": "3.2%", "delta": "-0.4%"}, - "arpu": {"value": f"${arpu:.2f}", "delta": "+$1.20"}, + "on_time": {"value": f"{snap['on_time_pct']}%", "delta": snap["on_time_delta"]}, + "flights_today": {"value": snap["flights_today"], "delta": snap["flights_today_delta"]}, + "avg_delay": {"value": f"{snap['avg_delay_min']} min", "delta": snap["avg_delay_delta"]}, + "load_factor": {"value": f"{snap['load_factor_pct']}%", "delta": snap["load_factor_delta"]}, } @tool -def query_subscribers_by_plan(plans: list[str] | None = None) -> list[dict]: - """Get subscriber counts broken down by plan tier. +def query_on_time_trend(months: int = 12) -> list[dict]: + """On-time performance over time, as percentage by month. Args: - plans: Optional list of plan names to filter by (e.g., ["pro", "enterprise"]). - Returns all plans if not specified. + months: Number of months to return (default 12). Valid: 3, 6, 12, 24. """ - if plans: - return [p for p in _SUBSCRIBERS_BY_PLAN if p["plan"] in plans] - return _SUBSCRIBERS_BY_PLAN + months = min(months, len(ON_TIME_TREND)) + return ON_TIME_TREND[-months:] @tool -def query_mrr_trend(months: int = 12) -> list[dict]: - """Get MRR trend over time. +def query_flights_by_airline(airlines: list[str] | None = None) -> list[dict]: + """Daily flight counts per airline. Args: - months: Number of months to return (default 12). Valid values: 3, 6, 12, 24. + airlines: Optional filter list, e.g. ["American", "United"]. All four + returned if omitted. """ - months = min(months, len(_MRR_TREND)) - return _MRR_TREND[-months:] + if airlines: + return [a for a in FLIGHTS_BY_AIRLINE if a["airline"] in airlines] + return FLIGHTS_BY_AIRLINE @tool -def query_churned_accounts(limit: int = 5, plan: str | None = None) -> list[dict]: - """Get recently churned accounts. +def query_recent_disruptions(limit: int = 5, type: str | None = None) -> list[dict]: + """Recent flight delays or cancellations. Args: - limit: Maximum number of accounts to return (default 5). - plan: Optional plan name to filter by (e.g., "enterprise"). + limit: Maximum entries to return (default 5). + type: Optional filter, "delayed" or "cancelled". """ - filtered = _CHURNED_ACCOUNTS - if plan: - filtered = [a for a in filtered if a["plan"] == plan] + filtered = RECENT_DISRUPTIONS + if type: + filtered = [d for d in filtered if d["type"] == type] return filtered[:limit] -ALL_TOOLS = [query_mrr, query_subscribers_by_plan, query_mrr_trend, query_churned_accounts] +ALL_TOOLS = [ + query_airline_kpis, + query_on_time_trend, + query_flights_by_airline, + query_recent_disruptions, +] diff --git a/docs/superpowers/plans/2026-05-16-c-generative-ui-aviation-dashboard.md b/docs/superpowers/plans/2026-05-16-c-generative-ui-aviation-dashboard.md new file mode 100644 index 000000000..8846391dc --- /dev/null +++ b/docs/superpowers/plans/2026-05-16-c-generative-ui-aviation-dashboard.md @@ -0,0 +1,799 @@ +# c-generative-ui Aviation KPI Dashboard Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Convert the `c-generative-ui` cockpit demo from a SaaS metrics dashboard to an airline operations KPI dashboard by swapping dataset, tools, prompt, and `emit_state` mapping — without touching graph topology or Angular view components. + +**Architecture:** Extend `aviation_data.py` with 4 analytics fixtures (snapshot KPIs, on-time trend, flights-by-airline, recent disruptions). Rewrite `dashboard_tools.py` with 4 aviation `@tool` functions importing from `aviation_data.py`. Rewrite `dashboard.md` prompt (same structure — JSON render spec format and shell-gen flow stay) with aviation state paths and example spec. Update `dashboard_graph.py:emit_state`'s per-tool branches to map new tool names to new state paths. Mirror all changes into the per-capability standalone copy at `cockpit/chat/generative-ui/python/`, with inlined analytics constants (standalone has no `aviation_data.py`) and rename its prompt from `generative-ui.md` → `dashboard.md` to fix the long-standing path bug at `graph.py:20`. + +**Tech Stack:** Python 3.12, LangGraph, langchain-openai (`gpt-5-mini`), uv. Frontend untouched — Angular 20 + `@ngaf/chat` view components stay as-is. + +--- + +## File Structure + +**Umbrella backend** (`cockpit/langgraph/streaming/python/`): +- `src/aviation_data.py` — MODIFY: append `KPI_SNAPSHOT`, `ON_TIME_TREND`, `FLIGHTS_BY_AIRLINE`, `RECENT_DISRUPTIONS` constants +- `src/dashboard_tools.py` — REWRITE: 4 aviation `@tool` functions reading from `aviation_data.py` +- `src/dashboard_graph.py` — MODIFY: `emit_state` function's per-tool branches only +- `prompts/dashboard.md` — REWRITE: aviation framing, state paths, example spec + +**Standalone backend** (`cockpit/chat/generative-ui/python/`): +- `src/dashboard_tools.py` — REWRITE: same 4 tools, analytics constants inlined at top of file (no aviation_data.py in standalone) +- `src/graph.py` — MODIFY: `emit_state` per-tool branches (same change as umbrella) +- `prompts/dashboard.md` — CREATE (renamed from `generative-ui.md`) +- `prompts/generative-ui.md` — DELETE + +**Frontend** (`cockpit/chat/generative-ui/angular/`): NO CHANGES. +**Langgraph registry** (`langgraph.json`): NO CHANGES. + +--- + +## Task 1: Extend `aviation_data.py` with analytics fixtures + +**Files:** +- Modify: `cockpit/langgraph/streaming/python/src/aviation_data.py` (append to end of file) + +- [ ] **Step 1: Append analytics constants** + +Append the following to the end of `cockpit/langgraph/streaming/python/src/aviation_data.py`: + +```python + +# ── Dashboard analytics (PR 3: c-generative-ui aviation KPIs) ─────────────── + +KPI_SNAPSHOT = { + "on_time_pct": 84.2, + "on_time_delta": "+1.4%", + "flights_today": 312, + "flights_today_delta": "+8", + "avg_delay_min": 12, + "avg_delay_delta": "-2 min", + "load_factor_pct": 78.5, + "load_factor_delta": "+0.6%", +} + +ON_TIME_TREND = [ + {"month": "2025-05", "on_time_pct": 82.4}, + {"month": "2025-06", "on_time_pct": 81.1}, + {"month": "2025-07", "on_time_pct": 79.8}, + {"month": "2025-08", "on_time_pct": 80.5}, + {"month": "2025-09", "on_time_pct": 83.2}, + {"month": "2025-10", "on_time_pct": 84.0}, + {"month": "2025-11", "on_time_pct": 82.6}, + {"month": "2025-12", "on_time_pct": 78.9}, + {"month": "2026-01", "on_time_pct": 80.2}, + {"month": "2026-02", "on_time_pct": 81.7}, + {"month": "2026-03", "on_time_pct": 82.8}, + {"month": "2026-04", "on_time_pct": 84.2}, +] + +FLIGHTS_BY_AIRLINE = [ + {"airline": "American", "count": 87}, + {"airline": "United", "count": 92}, + {"airline": "Delta", "count": 78}, + {"airline": "JetBlue", "count": 55}, +] + +RECENT_DISRUPTIONS = [ + {"flight_number": "UA123", "type": "delayed", "minutes": 45, "route": "LAX→JFK", "date": "2026-05-14"}, + {"flight_number": "AA456", "type": "cancelled", "minutes": 0, "route": "JFK→LAX", "date": "2026-05-14"}, + {"flight_number": "DL789", "type": "delayed", "minutes": 22, "route": "ATL→ORD", "date": "2026-05-13"}, + {"flight_number": "B6101", "type": "delayed", "minutes": 68, "route": "BOS→MIA", "date": "2026-05-13"}, + {"flight_number": "UA204", "type": "cancelled", "minutes": 0, "route": "SFO→SEA", "date": "2026-05-12"}, + {"flight_number": "AA318", "type": "delayed", "minutes": 15, "route": "DFW→DEN", "date": "2026-05-12"}, + {"flight_number": "DL552", "type": "delayed", "minutes": 35, "route": "ATL→MIA", "date": "2026-05-11"}, + {"flight_number": "B6217", "type": "delayed", "minutes": 80, "route": "JFK→BOS", "date": "2026-05-11"}, + {"flight_number": "UA640", "type": "cancelled", "minutes": 0, "route": "ORD→DEN", "date": "2026-05-10"}, + {"flight_number": "AA871", "type": "delayed", "minutes": 25, "route": "LAX→DFW", "date": "2026-05-10"}, +] +``` + +- [ ] **Step 2: Verify imports parse cleanly** + +Run: `cd cockpit/langgraph/streaming/python && uv run python -c "from src.aviation_data import KPI_SNAPSHOT, ON_TIME_TREND, FLIGHTS_BY_AIRLINE, RECENT_DISRUPTIONS; print(len(ON_TIME_TREND), len(FLIGHTS_BY_AIRLINE), len(RECENT_DISRUPTIONS))"` +Expected output: `12 4 10` + +- [ ] **Step 3: Commit** + +```bash +git add cockpit/langgraph/streaming/python/src/aviation_data.py +git commit -m "feat(c-generative-ui): add aviation dashboard analytics fixtures" +``` + +--- + +## Task 2: Rewrite `dashboard_tools.py` (umbrella) + +**Files:** +- Modify: `cockpit/langgraph/streaming/python/src/dashboard_tools.py` (full replacement) + +- [ ] **Step 1: Replace entire file** + +Replace the entire contents of `cockpit/langgraph/streaming/python/src/dashboard_tools.py` with: + +```python +"""Aviation KPI tools for the c-generative-ui dashboard demo. + +Four tools mirror the SaaS shape they replaced (query_mrr et al): +- query_airline_kpis → 4 stat cards (snapshot dict) +- query_on_time_trend → line chart data +- query_flights_by_airline→ bar chart data +- query_recent_disruptions→ data grid rows + +Data comes from src.aviation_data; no external calls. +""" + +from langchain_core.tools import tool + +from src.aviation_data import ( + KPI_SNAPSHOT, + ON_TIME_TREND, + FLIGHTS_BY_AIRLINE, + RECENT_DISRUPTIONS, +) + + +@tool +def query_airline_kpis() -> dict: + """Snapshot of operational KPIs across the fleet: on-time %, flights today, + average delay (minutes), and load factor.""" + snap = KPI_SNAPSHOT + return { + "on_time": {"value": f"{snap['on_time_pct']}%", "delta": snap["on_time_delta"]}, + "flights_today": {"value": snap["flights_today"], "delta": snap["flights_today_delta"]}, + "avg_delay": {"value": f"{snap['avg_delay_min']} min", "delta": snap["avg_delay_delta"]}, + "load_factor": {"value": f"{snap['load_factor_pct']}%", "delta": snap["load_factor_delta"]}, + } + + +@tool +def query_on_time_trend(months: int = 12) -> list[dict]: + """On-time performance over time, as percentage by month. + + Args: + months: Number of months to return (default 12). Valid: 3, 6, 12, 24. + """ + months = min(months, len(ON_TIME_TREND)) + return ON_TIME_TREND[-months:] + + +@tool +def query_flights_by_airline(airlines: list[str] | None = None) -> list[dict]: + """Daily flight counts per airline. + + Args: + airlines: Optional filter list, e.g. ["American", "United"]. All four + returned if omitted. + """ + if airlines: + return [a for a in FLIGHTS_BY_AIRLINE if a["airline"] in airlines] + return FLIGHTS_BY_AIRLINE + + +@tool +def query_recent_disruptions(limit: int = 5, type: str | None = None) -> list[dict]: + """Recent flight delays or cancellations. + + Args: + limit: Maximum entries to return (default 5). + type: Optional filter, "delayed" or "cancelled". + """ + filtered = RECENT_DISRUPTIONS + if type: + filtered = [d for d in filtered if d["type"] == type] + return filtered[:limit] + + +ALL_TOOLS = [ + query_airline_kpis, + query_on_time_trend, + query_flights_by_airline, + query_recent_disruptions, +] +``` + +- [ ] **Step 2: Smoke-test the module** + +Run: `cd cockpit/langgraph/streaming/python && uv run python -c " +from src.dashboard_tools import ALL_TOOLS, query_airline_kpis, query_on_time_trend, query_flights_by_airline, query_recent_disruptions +assert len(ALL_TOOLS) == 4 +print(query_airline_kpis.invoke({})) +print(len(query_on_time_trend.invoke({'months': 6}))) +print(len(query_flights_by_airline.invoke({'airlines': ['United']}))) +print(len(query_recent_disruptions.invoke({'limit': 3, 'type': 'cancelled'}))) +"` +Expected output (4 lines): KPI dict, `6`, `1`, `3`. + +- [ ] **Step 3: Commit** + +```bash +git add cockpit/langgraph/streaming/python/src/dashboard_tools.py +git commit -m "feat(c-generative-ui): replace SaaS dashboard tools with aviation KPI tools" +``` + +--- + +## Task 3: Update `emit_state` mapping (umbrella `dashboard_graph.py`) + +**Files:** +- Modify: `cockpit/langgraph/streaming/python/src/dashboard_graph.py` (function `emit_state` at lines 69–98 in current HEAD; replace its per-tool branches only — everything else stays) + +- [ ] **Step 1: Replace `emit_state` function body** + +Locate the `emit_state` async function (currently lines 69–98). Replace the entire function with: + +```python +async def emit_state(state: DashboardState) -> DashboardState: + """Emit state_update custom events from tool results.""" + from langchain_core.callbacks import adispatch_custom_event + + tool_results = {} + for msg in reversed(state["messages"]): + if msg.type == "tool": + try: + data = json.loads(msg.content) if isinstance(msg.content, str) else msg.content + except (json.JSONDecodeError, TypeError): + continue + + if msg.name == "query_airline_kpis": + # data is {"on_time": {"value": ..., "delta": ...}, ...} + for section_key, section_val in data.items(): + if isinstance(section_val, dict): + for k, v in section_val.items(): + tool_results[f"/{section_key}/{k}"] = v + elif msg.name == "query_on_time_trend": + tool_results["/on_time_trend"] = data + elif msg.name == "query_flights_by_airline": + tool_results["/flights_by_airline"] = data + elif msg.name == "query_recent_disruptions": + tool_results["/recent_disruptions"] = data + elif msg.type == "ai": + break + + if tool_results: + await adispatch_custom_event("state_update", {"updates": tool_results}) + + return state +``` + +- [ ] **Step 2: Verify graph still compiles** + +Run: `cd cockpit/langgraph/streaming/python && uv run python -c "from src.dashboard_graph import graph; print(type(graph).__name__)"` +Expected output: `CompiledStateGraph` + +- [ ] **Step 3: Commit** + +```bash +git add cockpit/langgraph/streaming/python/src/dashboard_graph.py +git commit -m "feat(c-generative-ui): map aviation tool results to dashboard state paths" +``` + +--- + +## Task 4: Rewrite `dashboard.md` prompt (umbrella) + +**Files:** +- Modify: `cockpit/langgraph/streaming/python/prompts/dashboard.md` (full replacement) + +- [ ] **Step 1: Replace entire prompt file** + +Replace `cockpit/langgraph/streaming/python/prompts/dashboard.md` with: + +````markdown +# Airline Operations Dashboard Agent + +You are a dashboard agent that builds interactive airline-operations KPI dashboards using a JSON render spec format. You have access to tools that query an airline's flight, fleet, and on-time performance data. + +## Your Behavior + +### First message (no existing dashboard) + +1. Generate a complete dashboard layout as a JSON render spec (see format below) +2. Call ALL four data tools to populate the dashboard +3. After the tools return, provide a brief conversational summary + +### Follow-up messages (dashboard already exists) + +Categorize the user's request: + +- **Data change** (e.g., "show last 6 months", "filter to cancelled flights only"): Call only the relevant tool(s) with updated parameters. Do NOT regenerate the spec. Just respond conversationally confirming the update. +- **Structural change** (e.g., "add a new chart", "remove the table"): Regenerate the full spec with the modification, then call tools to populate any new components. +- **Question about data** (e.g., "why did on-time % dip in December?"): Respond conversationally in plain text. Do NOT output JSON or call tools. + +## JSON Render Spec Format + +Your spec response MUST be raw JSON only — no markdown, no code fences, no surrounding text. + +``` +{ + "elements": { [key: string]: Element }, + "root": string +} +``` + +An Element has: +``` +{ + "type": string, + "props": { ... }, + "children?": string[] +} +``` + +### Props with State Bindings + +Use `{ "$state": "/json/pointer/path" }` for props that will be populated by tool results. The dashboard renders skeleton placeholders until the data arrives. + +Example: `"value": { "$state": "/on_time/value" }` — this prop will be populated when the `/on_time/value` state path receives data. + +## Available Component Types + +| Type | Props | Children | Description | +|------|-------|----------|-------------| +| `dashboard_grid` | *(none)* | Yes | Top-level vertical layout with section spacing | +| `container` | `direction` ("row" or "column") | Yes | Flex layout container | +| `stat_card` | `label` (string), `value` ($state), `delta` ($state) | No | Metric summary card | +| `line_chart` | `title` (string), `data` ($state array), `xKey` (string), `yKey` (string) | No | SVG line chart | +| `bar_chart` | `title` (string), `data` ($state array), `labelKey` (string), `valueKey` (string) | No | SVG bar chart | +| `data_grid` | `title` (string), `rows` ($state array), `columns` (string[]) | No | Data table | + +## State Path Conventions + +Use these state paths to match what the tools populate: + +- `/on_time/value`, `/on_time/delta` — from query_airline_kpis +- `/flights_today/value`, `/flights_today/delta` — from query_airline_kpis +- `/avg_delay/value`, `/avg_delay/delta` — from query_airline_kpis +- `/load_factor/value`, `/load_factor/delta` — from query_airline_kpis +- `/on_time_trend` — array from query_on_time_trend +- `/flights_by_airline` — array from query_flights_by_airline +- `/recent_disruptions` — array from query_recent_disruptions + +## Example Spec + +For "show me the dashboard": + +{"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","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","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"} +```` + +- [ ] **Step 2: Verify file reads cleanly from the graph** + +Run: `cd cockpit/langgraph/streaming/python && uv run python -c "from src.dashboard_graph import _PROMPT; assert 'Airline Operations' in _PROMPT; assert '/on_time_trend' in _PROMPT; print('ok')"` +Expected output: `ok` + +- [ ] **Step 3: Commit** + +```bash +git add cockpit/langgraph/streaming/python/prompts/dashboard.md +git commit -m "feat(c-generative-ui): rewrite dashboard prompt for airline operations" +``` + +--- + +## Task 5: End-to-end umbrella backend smoke (real LLM) + +**Files:** +- Read-only: `.env` (root) for `OPENAI_API_KEY` + +- [ ] **Step 1: Confirm key is loadable** + +Run: `grep -q '^OPENAI_API_KEY=' .env && echo found` +Expected output: `found` + +- [ ] **Step 2: Invoke the umbrella graph with a real prompt** + +Run from repo root: +```bash +set -a; source .env; set +a +cd cockpit/langgraph/streaming/python && uv run python -c " +import asyncio, json +from src.dashboard_graph import graph +from langchain_core.messages import HumanMessage + +async def main(): + state = {'messages': [HumanMessage(content='Show me the dashboard')], 'dashboard_spec': None} + result = await graph.ainvoke(state) + spec = result['dashboard_spec'] + print('SPEC_LEN', len(spec)) + parsed = json.loads(spec) + assert 'elements' in parsed and 'root' in parsed + assert parsed['root'] in parsed['elements'] + print('AVIATION_PATHS', sum(1 for v in str(parsed).split() if '/on_time' in v or '/flights_today' in v or '/load_factor' in v or '/avg_delay' in v or '/on_time_trend' in v or '/flights_by_airline' in v or '/recent_disruptions' in v)) + msgs = result['messages'] + tool_msgs = [m for m in msgs if m.type == 'tool'] + print('TOOL_CALLS', sorted({m.name for m in tool_msgs})) + +asyncio.run(main()) +" +``` +Expected: `SPEC_LEN` > 200, `AVIATION_PATHS` > 0, `TOOL_CALLS` contains at least `query_airline_kpis` (and ideally all 4 tool names). + +- [ ] **Step 3: Commit any incidental fixups, otherwise no-op** + +If output passed: no commit. If output revealed a bug in Tasks 1–4: fix the relevant task's file, re-run Step 2, then: + +```bash +git add -A +git commit -m "fix(c-generative-ui): umbrella backend smoke fixes" +``` + +--- + +## Task 6: Mirror tool rewrite into standalone (`cockpit/chat/generative-ui/python/`) + +**Files:** +- Modify: `cockpit/chat/generative-ui/python/src/dashboard_tools.py` (full replacement, analytics constants inlined) + +- [ ] **Step 1: Replace entire file** + +Replace `cockpit/chat/generative-ui/python/src/dashboard_tools.py` with: + +```python +"""Aviation KPI tools for the c-generative-ui standalone demo. + +Standalone copy — analytics constants are inlined here because this +backend has no shared aviation_data.py module. Mirrors the umbrella +backend at cockpit/langgraph/streaming/python/src/dashboard_tools.py. +""" + +from langchain_core.tools import tool + +# ── Analytics fixtures (inlined; no aviation_data.py in standalone) ───────── + +KPI_SNAPSHOT = { + "on_time_pct": 84.2, + "on_time_delta": "+1.4%", + "flights_today": 312, + "flights_today_delta": "+8", + "avg_delay_min": 12, + "avg_delay_delta": "-2 min", + "load_factor_pct": 78.5, + "load_factor_delta": "+0.6%", +} + +ON_TIME_TREND = [ + {"month": "2025-05", "on_time_pct": 82.4}, + {"month": "2025-06", "on_time_pct": 81.1}, + {"month": "2025-07", "on_time_pct": 79.8}, + {"month": "2025-08", "on_time_pct": 80.5}, + {"month": "2025-09", "on_time_pct": 83.2}, + {"month": "2025-10", "on_time_pct": 84.0}, + {"month": "2025-11", "on_time_pct": 82.6}, + {"month": "2025-12", "on_time_pct": 78.9}, + {"month": "2026-01", "on_time_pct": 80.2}, + {"month": "2026-02", "on_time_pct": 81.7}, + {"month": "2026-03", "on_time_pct": 82.8}, + {"month": "2026-04", "on_time_pct": 84.2}, +] + +FLIGHTS_BY_AIRLINE = [ + {"airline": "American", "count": 87}, + {"airline": "United", "count": 92}, + {"airline": "Delta", "count": 78}, + {"airline": "JetBlue", "count": 55}, +] + +RECENT_DISRUPTIONS = [ + {"flight_number": "UA123", "type": "delayed", "minutes": 45, "route": "LAX→JFK", "date": "2026-05-14"}, + {"flight_number": "AA456", "type": "cancelled", "minutes": 0, "route": "JFK→LAX", "date": "2026-05-14"}, + {"flight_number": "DL789", "type": "delayed", "minutes": 22, "route": "ATL→ORD", "date": "2026-05-13"}, + {"flight_number": "B6101", "type": "delayed", "minutes": 68, "route": "BOS→MIA", "date": "2026-05-13"}, + {"flight_number": "UA204", "type": "cancelled", "minutes": 0, "route": "SFO→SEA", "date": "2026-05-12"}, + {"flight_number": "AA318", "type": "delayed", "minutes": 15, "route": "DFW→DEN", "date": "2026-05-12"}, + {"flight_number": "DL552", "type": "delayed", "minutes": 35, "route": "ATL→MIA", "date": "2026-05-11"}, + {"flight_number": "B6217", "type": "delayed", "minutes": 80, "route": "JFK→BOS", "date": "2026-05-11"}, + {"flight_number": "UA640", "type": "cancelled", "minutes": 0, "route": "ORD→DEN", "date": "2026-05-10"}, + {"flight_number": "AA871", "type": "delayed", "minutes": 25, "route": "LAX→DFW", "date": "2026-05-10"}, +] + + +# ── Tools ─────────────────────────────────────────────────────────────────── + +@tool +def query_airline_kpis() -> dict: + """Snapshot of operational KPIs across the fleet: on-time %, flights today, + average delay (minutes), and load factor.""" + snap = KPI_SNAPSHOT + return { + "on_time": {"value": f"{snap['on_time_pct']}%", "delta": snap["on_time_delta"]}, + "flights_today": {"value": snap["flights_today"], "delta": snap["flights_today_delta"]}, + "avg_delay": {"value": f"{snap['avg_delay_min']} min", "delta": snap["avg_delay_delta"]}, + "load_factor": {"value": f"{snap['load_factor_pct']}%", "delta": snap["load_factor_delta"]}, + } + + +@tool +def query_on_time_trend(months: int = 12) -> list[dict]: + """On-time performance over time, as percentage by month. + + Args: + months: Number of months to return (default 12). Valid: 3, 6, 12, 24. + """ + months = min(months, len(ON_TIME_TREND)) + return ON_TIME_TREND[-months:] + + +@tool +def query_flights_by_airline(airlines: list[str] | None = None) -> list[dict]: + """Daily flight counts per airline. + + Args: + airlines: Optional filter list, e.g. ["American", "United"]. All four + returned if omitted. + """ + if airlines: + return [a for a in FLIGHTS_BY_AIRLINE if a["airline"] in airlines] + return FLIGHTS_BY_AIRLINE + + +@tool +def query_recent_disruptions(limit: int = 5, type: str | None = None) -> list[dict]: + """Recent flight delays or cancellations. + + Args: + limit: Maximum entries to return (default 5). + type: Optional filter, "delayed" or "cancelled". + """ + filtered = RECENT_DISRUPTIONS + if type: + filtered = [d for d in filtered if d["type"] == type] + return filtered[:limit] + + +ALL_TOOLS = [ + query_airline_kpis, + query_on_time_trend, + query_flights_by_airline, + query_recent_disruptions, +] +``` + +- [ ] **Step 2: Commit** + +```bash +git add cockpit/chat/generative-ui/python/src/dashboard_tools.py +git commit -m "feat(c-generative-ui standalone): mirror aviation KPI tools" +``` + +--- + +## Task 7: Mirror `emit_state` rewrite into standalone `graph.py` + +**Files:** +- Modify: `cockpit/chat/generative-ui/python/src/graph.py` (function `emit_state` only) + +- [ ] **Step 1: Replace `emit_state` function** + +Find the `emit_state` async function in `cockpit/chat/generative-ui/python/src/graph.py` and replace it with the identical body used in Task 3: + +```python +async def emit_state(state: DashboardState) -> DashboardState: + """Emit state_update custom events from tool results.""" + from langchain_core.callbacks import adispatch_custom_event + + tool_results = {} + for msg in reversed(state["messages"]): + if msg.type == "tool": + try: + data = json.loads(msg.content) if isinstance(msg.content, str) else msg.content + except (json.JSONDecodeError, TypeError): + continue + + if msg.name == "query_airline_kpis": + for section_key, section_val in data.items(): + if isinstance(section_val, dict): + for k, v in section_val.items(): + tool_results[f"/{section_key}/{k}"] = v + elif msg.name == "query_on_time_trend": + tool_results["/on_time_trend"] = data + elif msg.name == "query_flights_by_airline": + tool_results["/flights_by_airline"] = data + elif msg.name == "query_recent_disruptions": + tool_results["/recent_disruptions"] = data + elif msg.type == "ai": + break + + if tool_results: + await adispatch_custom_event("state_update", {"updates": tool_results}) + + return state +``` + +- [ ] **Step 2: Commit** + +```bash +git add cockpit/chat/generative-ui/python/src/graph.py +git commit -m "feat(c-generative-ui standalone): map aviation tool results to state paths" +``` + +--- + +## Task 8: Rename standalone prompt + drop `generative-ui.md` + +**Files:** +- Rename: `cockpit/chat/generative-ui/python/prompts/generative-ui.md` → `cockpit/chat/generative-ui/python/prompts/dashboard.md` +- Replace contents with the aviation prompt from Task 4 + +The standalone `graph.py` already references `prompts/dashboard.md` (line 20). The current file is misnamed `generative-ui.md`, causing a `FileNotFoundError` at runtime. This task fixes the bug AND lands the aviation rewrite. + +- [ ] **Step 1: Verify no other consumer of `generative-ui.md` in the standalone tree** + +Run: `grep -rn "generative-ui.md" cockpit/chat/generative-ui/python/` +Expected: zero output (no references). + +- [ ] **Step 2: Delete old prompt file and create the new one** + +Delete: `cockpit/chat/generative-ui/python/prompts/generative-ui.md` + +Create: `cockpit/chat/generative-ui/python/prompts/dashboard.md` with the exact same content as the umbrella prompt from Task 4 (Airline Operations Dashboard Agent — full file). + +- [ ] **Step 3: Verify the standalone graph loads the prompt** + +Run: `cd cockpit/chat/generative-ui/python && uv run python -c "from src.graph import _PROMPT; assert 'Airline Operations' in _PROMPT; print('ok')"` +Expected output: `ok` + +- [ ] **Step 4: Commit** + +```bash +git add cockpit/chat/generative-ui/python/prompts/ +git commit -m "fix(c-generative-ui standalone): rename prompt to dashboard.md, aviation rewrite" +``` + +--- + +## Task 9: Build verification across the whole repo + +**Files:** +- No code changes — pure CI parity check + +- [ ] **Step 1: Build the umbrella langgraph python project** + +Run: `pnpm nx run cockpit-langgraph-streaming-python:build` +Expected: build succeeds with no errors. + +- [ ] **Step 2: Build all cockpit examples** + +Run: `pnpm nx build all-examples` +Expected: build succeeds. + +- [ ] **Step 3: Run the existing chat-generative-ui Angular unit tests** + +Run: `pnpm nx test cockpit-chat-generative-ui-angular` +Expected: all tests pass — frontend was untouched, this is a sanity check. + +- [ ] **Step 4: No commit (verification only)** + +If any of the above fails, the failing task's file change is the cause — go back and fix that task's file, then re-run from Step 1. + +--- + +## Task 10: Manual chrome MCP smoke (REQUIRED gating check before PR) + +**Files:** +- No code changes — interactive verification with the real LLM via the running cockpit + +This task is REQUIRED (not deferred) per the spec. `OPENAI_API_KEY` is in repo root `.env`. + +- [ ] **Step 1: Start the langgraph backend with env loaded** + +Run in a background shell: +```bash +set -a; source .env; set +a +pnpm nx serve cockpit-langgraph-streaming-python +``` +Expected: server listens (default port 2024). + +- [ ] **Step 2: Start the cockpit dev server** + +In a second background shell: +```bash +pnpm nx serve cockpit +``` +Expected: Angular dev server listens (default port 4200). + +- [ ] **Step 3: Navigate to chat-generative-ui in chrome MCP** + +Using the `mcp__Claude_in_Chrome__navigate` tool, open: `http://localhost:4200/chat-generative-ui` +Expected: chat page renders with an empty conversation. + +- [ ] **Step 4: Smoke prompt — initial dashboard** + +Type into the chat input and submit: `Show me the dashboard` + +Expected (capture screenshot): +- Dashboard shell renders within ~3s with skeleton placeholders +- 4 tool calls fire (visible in the chat-debug overlay if enabled, or inferable from the populating cards) +- Cards populate with: On-time % 84.2%, Flights Today 312, Avg Delay 12 min, Load Factor 78.5% +- Line chart shows 12 monthly on-time % data points +- Bar chart shows American/United/Delta/JetBlue counts +- Data grid shows 5 recent disruptions + +- [ ] **Step 5: Smoke prompt — data-only update** + +Type and submit: `Filter to cancelled flights only` + +Expected (capture screenshot): +- ONLY the disruptions table updates (3 rows: AA456, UA204, UA640) +- No shell regeneration (cards/charts stay put) +- LLM replies with a brief conversational confirmation + +- [ ] **Step 6: Smoke prompt — conversational question** + +Type and submit: `Why might on-time % have dipped in December?` + +Expected: +- LLM responds in plain prose (no JSON, no tool calls) +- Dashboard does not re-render + +- [ ] **Step 7: Tear down background servers** + +Stop both background shells. + +- [ ] **Step 8: Commit screenshots to PR description, not the repo** + +Attach screenshots from Steps 4–6 to the PR description body. Do NOT commit them to the repo. + +If any smoke step fails: identify which underlying task (1–8) caused the regression, fix it there, re-run Tasks 9–10. + +--- + +## Task 11: Open the PR + +**Files:** +- No code changes + +- [ ] **Step 1: Push branch** + +Run: `git push -u origin HEAD` + +- [ ] **Step 2: Open PR** + +Run: +```bash +gh pr create --title "feat(c-generative-ui): airline operations KPI dashboard (PR 3 of 4)" --body "$(cat <<'EOF' +## Summary +- PR 3 of 4 in the c-* aviation theme rollout. Converts the c-generative-ui demo from a SaaS metrics dashboard to an airline operations KPI dashboard. +- Swaps dataset (KPI snapshot, on-time trend, flights-by-airline, recent disruptions), 4 tools, prompt, and the `emit_state` per-tool branches. Graph topology and Angular view components are unchanged. +- Mirrors the same rewrite into the per-capability standalone backend AND fixes a long-standing prompt-path bug (`graph.py` referenced `prompts/dashboard.md` while the file was misnamed `generative-ui.md`). + +## Test plan +- [x] `pnpm nx run cockpit-langgraph-streaming-python:build` +- [x] `pnpm nx build all-examples` +- [x] `pnpm nx test cockpit-chat-generative-ui-angular` +- [x] Manual chrome MCP smoke against real LLM (screenshots attached): + - [x] "Show me the dashboard" — shell renders, 4 tools fire, all cards/charts/table populate + - [x] "Filter to cancelled flights only" — only disruptions table updates + - [x] "Why might on-time % have dipped in December?" — conversational reply, no JSON + +🤖 Generated with [Claude Code](https://claude.com/claude-code) +EOF +)" +``` + +Expected: PR URL printed. + +- [ ] **Step 3: Wait for CI, merge on green** + +Run: `gh pr checks --watch` then `gh pr merge --squash`. + +--- + +## Self-Review + +**Spec coverage:** +- Decision 1 (extend aviation_data.py) → Task 1 ✓ +- Decision 2 (4 mirror-SaaS tools) → Task 2 ✓ +- Decision 3 (standalone parity + path bug fix) → Tasks 6, 7, 8 ✓ +- Decision 4 (chrome MCP required) → Task 10 ✓ +- KPI mapping table → Tasks 2 (tools) + 3 (emit_state) + 4 (prompt example) all aligned ✓ +- emit_state rewrite called out as the one intentional graph change → Tasks 3, 7 ✓ +- Frontend untouched → Task 9 Step 3 verifies ✓ + +**Placeholder scan:** +- No TBDs, no "similar to Task N" hand-waves, no "add validation". Every code step has complete code. Task 8 explicitly references "exact same content as Task 4" — acceptable because both prompt files are identical by design; the alternative would be 80 lines of duplicate markdown in the plan. + +**Type consistency:** +- Tool names match across Tasks 2, 3, 4, 6, 7 (`query_airline_kpis`, `query_on_time_trend`, `query_flights_by_airline`, `query_recent_disruptions`). +- State paths match across Tasks 3 (emit_state), 4 (prompt example), 6 (tool data shape). +- Data constant names (`KPI_SNAPSHOT`, `ON_TIME_TREND`, `FLIGHTS_BY_AIRLINE`, `RECENT_DISRUPTIONS`) identical in Tasks 1 (umbrella aviation_data.py), 2 (umbrella import), 6 (standalone inlined). diff --git a/docs/superpowers/specs/2026-05-16-c-generative-ui-aviation-dashboard-design.md b/docs/superpowers/specs/2026-05-16-c-generative-ui-aviation-dashboard-design.md new file mode 100644 index 000000000..b0d63ad19 --- /dev/null +++ b/docs/superpowers/specs/2026-05-16-c-generative-ui-aviation-dashboard-design.md @@ -0,0 +1,197 @@ +# c-generative-ui Aviation KPI Dashboard — Design + +**Date:** 2026-05-16 +**Status:** Spec — pending implementation plan +**Series:** PR 3 of 4 in the c-* aviation theme rollout + +## Goal + +Convert the `c-generative-ui` demo from a SaaS metrics dashboard to an airline operations KPI dashboard. The graph architecture, custom-event streaming, and Angular view components stay untouched — only the dataset, tools, and prompt change. Result: the chat-generative-ui cockpit demo tells the same operations-dashboard story as the rest of the c-* track. + +Out of scope: +- Graph topology changes (`dashboard_graph.py` stays as-is — router → generate_shell → plan_tools → call_tools → emit_state → respond) +- Angular view components (stat_card, container, dashboard_grid, line_chart, bar_chart, data_grid) — already domain-agnostic, reused as-is +- Langgraph registry (`langgraph.json`) — `c-generative-ui` keeps pointing at `chat_graphs.py:generative_ui` which re-exports `dashboard_graph.graph` +- c-tool-calls, c-subagents (PR 1, shipped); 7 simple prompts (PR 2, shipped); c-a2ui (PR 4) + +## Decisions + +| # | Decision | Choice | +|---|---|---| +| 1 | Dataset source | Extend `aviation_data.py` with analytics fixtures. Single source of truth shared with the c-tool-calls graph from PR 1. | +| 2 | Tool surface | Mirror SaaS — 4 tools (`query_airline_kpis`, `query_on_time_trend`, `query_flights_by_airline`, `query_recent_disruptions`). Same shape as today's tools; zero graph-architecture risk. | +| 3 | Standalone backend | Update the per-capability `cockpit/chat/generative-ui/python/` copy in lockstep, AND fix its stale prompt-path bug (graph.py reads `prompts/dashboard.md` but the file is `prompts/generative-ui.md`). Resolution: rename the standalone's prompt to `dashboard.md` for parity with umbrella. | +| 4 | Manual smoke gate | Chrome MCP smoke is REQUIRED before merge (not deferred). `OPENAI_API_KEY` is in repo root `.env`. | + +## KPI mapping + +| Component | Label | State path | Source tool | +|---|---|---|---| +| `stat_card` | On-time % | `/on_time/value`, `/on_time/delta` | `query_airline_kpis` | +| `stat_card` | Flights today | `/flights_today/value`, `/flights_today/delta` | `query_airline_kpis` | +| `stat_card` | Avg delay (min) | `/avg_delay/value`, `/avg_delay/delta` | `query_airline_kpis` | +| `stat_card` | Load factor | `/load_factor/value`, `/load_factor/delta` | `query_airline_kpis` | +| `line_chart` | On-time % trend | `/on_time_trend` | `query_on_time_trend(months)` | +| `bar_chart` | Flights by airline | `/flights_by_airline` | `query_flights_by_airline(airlines?)` | +| `data_grid` | Recent disruptions | `/recent_disruptions` | `query_recent_disruptions(limit, type?)` | + +## Mock data additions (`aviation_data.py`) + +```python +KPI_SNAPSHOT = { + "on_time_pct": 84.2, + "on_time_delta": "+1.4%", + "flights_today": 312, + "flights_today_delta": "+8", + "avg_delay_min": 12, + "avg_delay_delta": "-2 min", + "load_factor_pct": 78.5, + "load_factor_delta": "+0.6%", +} + +ON_TIME_TREND = [ + {"month": "2025-05", "on_time_pct": 82.4}, + {"month": "2025-06", "on_time_pct": 81.1}, + # ... 12 monthly entries through 2026-04 +] + +FLIGHTS_BY_AIRLINE = [ + {"airline": "American", "count": 87}, + {"airline": "United", "count": 92}, + {"airline": "Delta", "count": 78}, + {"airline": "JetBlue", "count": 55}, +] + +RECENT_DISRUPTIONS = [ + {"flight_number": "UA123", "type": "delayed", "minutes": 45, "route": "LAX→JFK", "date": "2026-05-14"}, + {"flight_number": "AA456", "type": "cancelled", "minutes": 0, "route": "JFK→LAX", "date": "2026-05-14"}, + # ... ~10 entries +] +``` + +All values hardcoded for determinism. No date math, no random. + +## Tool signatures (`dashboard_tools.py` rewrite) + +```python +@tool +def query_airline_kpis() -> dict: + """Snapshot of operational KPIs across the fleet: on-time %, flights today, + average delay (minutes), and load factor.""" + snap = KPI_SNAPSHOT + return { + "on_time": {"value": f"{snap['on_time_pct']}%", "delta": snap["on_time_delta"]}, + "flights_today": {"value": snap["flights_today"], "delta": snap["flights_today_delta"]}, + "avg_delay": {"value": f"{snap['avg_delay_min']} min", "delta": snap["avg_delay_delta"]}, + "load_factor": {"value": f"{snap['load_factor_pct']}%", "delta": snap["load_factor_delta"]}, + } + +@tool +def query_on_time_trend(months: int = 12) -> list[dict]: + """On-time performance over time, as percentage by month. + + Args: + months: Number of months to return (default 12). Valid: 3, 6, 12, 24. + """ + months = min(months, len(ON_TIME_TREND)) + return ON_TIME_TREND[-months:] + +@tool +def query_flights_by_airline(airlines: list[str] | None = None) -> list[dict]: + """Daily flight counts per airline. + + Args: + airlines: Optional filter list, e.g. ["American", "United"]. All four returned if omitted. + """ + if airlines: + return [a for a in FLIGHTS_BY_AIRLINE if a["airline"] in airlines] + return FLIGHTS_BY_AIRLINE + +@tool +def query_recent_disruptions(limit: int = 5, type: str | None = None) -> list[dict]: + """Recent flight delays or cancellations. + + Args: + limit: Maximum entries to return (default 5). + type: Optional filter, "delayed" or "cancelled". + """ + filtered = RECENT_DISRUPTIONS + if type: + filtered = [d for d in filtered if d["type"] == type] + return filtered[:limit] + +ALL_TOOLS = [query_airline_kpis, query_on_time_trend, query_flights_by_airline, query_recent_disruptions] +``` + +## `emit_state` updates + +The `dashboard_graph.py:emit_state` function maps tool results to state paths. Rewire the per-tool branches to the new tool names and state paths: + +| Tool name | Emitted state paths | +|---|---| +| `query_airline_kpis` | `/on_time/value`, `/on_time/delta`, `/flights_today/value`, ... (flatten nested dict, same pattern as current `query_mrr`) | +| `query_on_time_trend` | `/on_time_trend` | +| `query_flights_by_airline` | `/flights_by_airline` | +| `query_recent_disruptions` | `/recent_disruptions` | + +This is the ONE intentional graph code change — the surrounding nodes (router, generate_shell, plan_tools, call_tools, respond) are untouched. + +## Prompt rewrite (`prompts/dashboard.md`) + +Preserve the existing structure verbatim — first-turn shell-gen + tool call cascade, follow-up data/structural/question categorization, JSON spec format section, component-type table, state-path conventions, example spec. Swap: + +- Title: "SaaS Metrics Dashboard Agent" → "Airline Operations Dashboard Agent" +- Domain framing: "SaaS metrics" → "airline operations KPIs" +- State path table to the 7 aviation paths above +- Example spec JSON to render the aviation dashboard (4 stat cards + on-time-trend line + flights-by-airline bar + recent-disruptions table) + +The aviation-assistant character header from PR 2 is intentionally NOT applied — this graph emits raw JSON, not prose; a friendly persona would be noise. + +## Files modified + +| File | Change | +|---|---| +| `cockpit/langgraph/streaming/python/src/aviation_data.py` | Append KPI_SNAPSHOT, ON_TIME_TREND, FLIGHTS_BY_AIRLINE, RECENT_DISRUPTIONS | +| `cockpit/langgraph/streaming/python/src/dashboard_tools.py` | Full rewrite — 4 aviation tools, import from aviation_data | +| `cockpit/langgraph/streaming/python/src/dashboard_graph.py` | Rewrite `emit_state` tool-name branches only | +| `cockpit/langgraph/streaming/python/prompts/dashboard.md` | Full rewrite (same structure, aviation framing) | +| `cockpit/chat/generative-ui/python/src/dashboard_tools.py` | Mirror umbrella rewrite | +| `cockpit/chat/generative-ui/python/src/graph.py` | Mirror `emit_state` rewrite (architecture unchanged) | +| `cockpit/chat/generative-ui/python/prompts/dashboard.md` | NEW (rename from generative-ui.md) — mirrors umbrella `dashboard.md` | +| `cockpit/chat/generative-ui/python/prompts/generative-ui.md` | DELETE (renamed to dashboard.md) | + +Aviation-data extensions live only in the umbrella copy; the standalone backend imports from `src/dashboard_tools.py` which inlines the data — to keep both backends standalone, duplicate the analytics constants into the standalone's `dashboard_tools.py` rather than introducing a new `aviation_data.py` file in the standalone. (The standalone has no aviation_data.py today; it doesn't need one.) + +## Testing + +**Build verification:** +- `pnpm nx run cockpit-langgraph-streaming-python:build` clean +- `pnpm nx build all-examples` clean +- `pnpm nx test cockpit-chat-generative-ui-angular` clean (view components untouched, should pass) + +**Required manual chrome MCP smoke (gating merge):** + +`OPENAI_API_KEY` available in repo root `.env`. Steps: +1. Start cockpit dev server (`pnpm nx serve cockpit`) +2. Start langgraph dev server with .env loaded +3. Chrome MCP: navigate to chat-generative-ui route +4. Ask "Show me the dashboard" → expect: shell renders with skeleton placeholders, four tool calls fire, cards/charts/table populate from streamed state events +5. Ask "Filter to delayed flights only" → expect: only `query_recent_disruptions` re-fires with `type="delayed"`, table updates, no shell regeneration +6. Ask "Add a stat card for fleet utilization" → expect: structural change — spec regenerates with new card; LLM acknowledges no data tool exists (acceptable for demo) +7. Ask "Why is on-time % up this month?" → expect: conversational reply, no JSON, no tool calls + +Capture before/after screenshots for the PR description. + +## Risks and mitigations + +- **JSON-spec drift** — gpt-5-mini might emit invalid JSON for the new domain. Pre-existing risk; structure is unchanged. If the LLM fails repeatedly, the prompt example block (verbatim aviation JSON) gives a strong few-shot anchor. +- **State-path renames break frontend rendering** — the Angular views read `$state` paths blindly; spec drives the paths. Risk is the *prompt* outputting the wrong paths (e.g., `/mrr/value` from training memory). Mitigated by the prompt example block calling out aviation paths explicitly. +- **Standalone backend drift** — two copies. Mitigation: PR includes a `diff` check between umbrella and standalone `dashboard_tools.py` minus the data-import line; flag in CI follow-up if cheap. +- **Domain "feel"** — airline KPI deltas (avg delay shrinking, load factor up <1%) read less dramatic than SaaS MRR growth. Acceptable for a demo whose point is generative UI, not narrative. + +## Out-of-scope follow-ups + +- PR 4 — c-a2ui contact form → flight booking form +- aimock e2e fixtures for the aviation dashboard scenario +- Extract shared `aviation_data.py` for the standalone backend (currently duplicated inline) +- Lint/CI check to prevent umbrella/standalone tool-set drift