Files
copilotkit--copilotkit/showcase/integrations/langgraph-python/src/agents/beautiful_chat.py
T
2026-07-13 12:58:18 +08:00

233 lines
8.0 KiB
Python

"""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