"""LangGraph agent backing the Beautiful Chat demo. Verbatim port of the canonical starter at /examples/integrations/langgraph-python. Reference structure (agent/main.py + agent/src/{todos,query,a2ui_fixed_schema, a2ui_dynamic_schema}.py) is inlined here into a single module to match the showcase cell's flat backend layout. Data files (db.csv + schemas/) live alongside this module under `beautiful_chat_data/` to keep the cell self-contained without polluting the shared `a2ui_schemas/` directory (which is owned by a2ui_fixed.py). """ from __future__ import annotations import csv import uuid from pathlib import Path from typing import Literal, TypedDict from copilotkit import ( CopilotKitMiddleware, StateItem, StateStreamingMiddleware, a2ui, ) from langchain.agents import AgentState as BaseAgentState from langchain.agents import create_agent from langchain.messages import ToolMessage from langchain.tools import ToolRuntime, tool from langchain_openai import ChatOpenAI from langgraph.types import Command # ─── Shared state schema ──────────────────────────────────────────── class Todo(TypedDict): id: str title: str description: str emoji: str status: Literal["pending", "completed"] class AgentState(BaseAgentState): todos: list[Todo] # ─── Todo tools ───────────────────────────────────────────────────── @tool def manage_todos(todos: list[Todo], runtime: ToolRuntime) -> Command: """ Manage the current todos. """ # Ensure all todos have IDs that are unique for todo in todos: if "id" not in todo or not todo["id"]: todo["id"] = str(uuid.uuid4()) # Update the state return Command( update={ "todos": todos, "messages": [ ToolMessage( content="Successfully updated todos", name="manage_todos", id=str(uuid.uuid4()), tool_call_id=runtime.tool_call_id, ) ], } ) @tool def get_todos(runtime: ToolRuntime): """ Get the current todos. """ return runtime.state.get("todos", []) todo_tools = [ manage_todos, get_todos, ] # ─── Data query tool ──────────────────────────────────────────────── # Read data at module load time to avoid file I/O issues in # LangGraph Cloud's sandboxed tool execution environment. _DATA_DIR = Path(__file__).parent / "beautiful_chat_data" _csv_path = _DATA_DIR / "db.csv" with open(_csv_path) as _f: _cached_data = list(csv.DictReader(_f)) @tool def query_data(query: str): """ Query the database, takes natural language. Always call before showing a chart or graph. """ return _cached_data # ─── A2UI fixed-schema tool: flight search ────────────────────────── CATALOG_ID = "copilotkit://app-dashboard-catalog" SURFACE_ID = "flight-search-results" class Flight(TypedDict, total=False): # All fields marked optional (`total=False`) so the LLM (or aimock fixture) # can omit auxiliary fields like `id` / `statusIcon` without tripping # langchain's tool-arg validation. Previously these were required and any # missing field surfaced as `Error invoking tool 'search_flights' with # kwargs ... flights.N.id: Field required` — the agent treated the error # string as the tool result and the surface never rendered. airline: str airlineLogo: str flightNumber: str origin: str destination: str date: str departureTime: str arrivalTime: str duration: str status: str price: str def _build_flight_components(flights: list[dict]) -> list[dict]: """Build a flat A2UI component tree with one literal FlightCard per flight. Avoids the structural-children template form (Row.children = { componentId, path }), which the GenericBinder only expands correctly for components whose schema declares STRUCTURAL children — sibling demos work because their schemas use literal-string-array children. Inlining the values per-flight sidesteps the template path entirely and renders identically. """ flight_card_ids: list[str] = [] components: list[dict] = [] for index, flight in enumerate(flights): card_id = f"flight-card-{index}" flight_card_ids.append(card_id) components.append( { "id": card_id, "component": "FlightCard", "airline": flight.get("airline", ""), "airlineLogo": flight.get("airlineLogo", ""), "flightNumber": flight.get("flightNumber", ""), "origin": flight.get("origin", ""), "destination": flight.get("destination", ""), "date": flight.get("date", ""), "departureTime": flight.get("departureTime", ""), "arrivalTime": flight.get("arrivalTime", ""), "duration": flight.get("duration", ""), "status": flight.get("status", ""), "price": flight.get("price", ""), } ) root: dict = { "id": "root", "component": "Row", "children": flight_card_ids, "gap": 16, } return [root, *components] @tool def search_flights(flights: list[Flight]) -> str: """Search for flights and display the results as rich cards. Return exactly 2 flights. Each flight must have: airline (e.g. "United Airlines"), airlineLogo (use Google favicon API: https://www.google.com/s2/favicons?domain={airline_domain}&sz=128 e.g. "https://www.google.com/s2/favicons?domain=united.com&sz=128" for United, "https://www.google.com/s2/favicons?domain=delta.com&sz=128" for Delta, "https://www.google.com/s2/favicons?domain=aa.com&sz=128" for American, "https://www.google.com/s2/favicons?domain=alaskaair.com&sz=128" for Alaska), flightNumber, origin, destination, date (short readable format like "Tue, Mar 18" — use near-future dates), departureTime, arrivalTime, duration (e.g. "4h 25m"), status (e.g. "On Time" or "Delayed"), and price (e.g. "$289"). """ return a2ui.render( operations=[ a2ui.create_surface(SURFACE_ID, catalog_id=CATALOG_ID), a2ui.update_components(SURFACE_ID, _build_flight_components(flights)), ], ) # ─── A2UI dynamic-schema tool: LLM-generated UI ───────────────────── CUSTOM_CATALOG_ID = "copilotkit://app-dashboard-catalog" # ─── Graph ────────────────────────────────────────────────────────── model = ChatOpenAI(model="gpt-5.4", model_kwargs={"parallel_tool_calls": False}) agent = create_agent( model=model, tools=[query_data, *todo_tools, search_flights], middleware=[ CopilotKitMiddleware(), StateStreamingMiddleware( StateItem(state_key="todos", tool="manage_todos", tool_argument="todos") ), ], state_schema=AgentState, system_prompt=""" You are a polished, professional demo assistant. Keep responses to 1-2 sentences. Tool guidance: - Flights: call search_flights to show flight cards with a pre-built schema. - Dashboards & rich UI: call generate_a2ui to create dashboard UIs with metrics, charts, tables, and cards. It handles rendering automatically. - Charts: call query_data first, then render with the chart component. - Todos: enable app mode first, then manage todos. - A2UI actions: when you see a log_a2ui_event result (e.g. "view_details"), respond with a brief confirmation. The UI already updated on the frontend. """, ) graph = agent