Skip to content

Commit e16a64e

Browse files
authored
fix(langgraph-threads): restore A2UI tool docstrings dropped in compaction rewrite (CopilotKit#4216)
## Summary The Apr 21 commit [\`25f6f1541\`](CopilotKit@25f6f1541) (\"refactor(runtime): Support durable compaction of threads\") re-added \`a2ui_dynamic_schema.py\` and \`a2ui_fixed_schema.py\` to the \`langgraph-python-threads\` example (they had been deleted in the earlier \"fix(examples): Pin version number and simplify example\" commit). The re-add stripped two docstrings whose content was load-bearing for the sub-LLM, not cosmetic: - **\`render_a2ui\`** — its Args block said *\"The root component must have id 'root'\"*. Without it, the secondary LLM emits a valid flat component list with no entry point. The A2UI renderer always starts at \`id=\"root\"\` ([\`A2uiSurface.tsx:152\`](packages/a2ui-renderer/src/react-renderer/a2ui-react/A2uiSurface.tsx)), falls through to its shimmer placeholder, and the *Sales Dashboard (A2UI Dynamic)* demo renders as an empty ~30px white square with 13 unused components queued on the surface. - **\`search_flights\`** — its Args block spelled out airline logo URLs, date format, and status-icon colors, keeping flight cards visually consistent across runs. ## What this PR does Copies both files verbatim from \`examples/integrations/langgraph-python/agent/src/\` (the non-threads example, which is the known-good template). After this PR, \`diff -r\` between the two examples' \`agent/src/\` dirs is clean. Verified end-to-end locally against a demo scaffolded from this template: \"Sales Dashboard (A2UI Dynamic)\" now renders the full dashboard (title, three metric cards, pie + bar charts) instead of the shimmer placeholder. ## Notes - The restore brings back \`[A2UI-DEBUG]\` print statements and the fuller module headers. If the \`-threads\` version was meant to be terser than the non-threads version, happy to strip those in a follow-up — but verbatim parity seemed like the safer default since the two templates are otherwise meant to match. - A separate, complementary fix would be to make the \`id: \"root\"\` requirement explicit in \`A2UI_DEFAULT_GENERATION_GUIDELINES\` (\`packages/shared/src/a2ui-prompts.ts\` + \`sdk-python/copilotkit/a2ui.py\`), so the invariant doesn't depend on per-demo tool docstrings. Not in scope here. ## Test plan - [x] Run the template locally, click *Sales Dashboard (A2UI Dynamic)*, confirm the dashboard renders. - [ ] Sanity-check *Search Flights (A2UI Fixed Schema)* in CI or locally — should continue to work (this file's behavior is unchanged; only docstring content differs). - [ ] Reviewer to decide whether to keep or strip debug prints.
2 parents 0ce10b6 + bec1fd0 commit e16a64e

4 files changed

Lines changed: 82 additions & 17 deletions

File tree

Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
"""
22
Dynamic A2UI tool: LLM-generated UI from conversation context.
3+
4+
A secondary LLM generates v0.9 A2UI components via a structured tool call.
5+
The generate_a2ui tool wraps the output as a2ui_operations, which the
6+
middleware detects in the TOOL_CALL_RESULT and renders automatically.
37
"""
48

59
from __future__ import annotations
610

711
import json
812
from typing import Any
913

10-
from copilotkit import a2ui
11-
from langchain.tools import ToolRuntime, tool
14+
from langchain.tools import tool, ToolRuntime
1215
from langchain_core.messages import SystemMessage
1316
from langchain_core.tools import tool as lc_tool
1417
from langchain_openai import ChatOpenAI
1518

19+
from copilotkit import a2ui
20+
1621
CUSTOM_CATALOG_ID = "copilotkit://app-dashboard-catalog"
1722

1823

@@ -23,39 +28,76 @@ def render_a2ui(
2328
components: list[dict],
2429
data: dict | None = None,
2530
) -> str:
26-
"""Render a dynamic A2UI v0.9 surface."""
31+
"""Render a dynamic A2UI v0.9 surface.
32+
33+
Args:
34+
surfaceId: Unique surface identifier.
35+
catalogId: The catalog ID (use "copilotkit://app-dashboard-catalog").
36+
components: A2UI v0.9 component array (flat format). The root
37+
component must have id "root".
38+
data: Optional initial data model for the surface (e.g. form values,
39+
list items for data-bound components).
40+
"""
2741
return "rendered"
2842

2943

3044
@tool()
3145
def generate_a2ui(runtime: ToolRuntime[Any]) -> str:
32-
"""Generate dynamic A2UI components based on the conversation."""
46+
"""Generate dynamic A2UI components based on the conversation.
47+
48+
A secondary LLM designs the UI schema and data. The result is
49+
returned as an a2ui_operations container for the middleware to detect.
50+
"""
51+
import time
52+
t0 = time.time()
53+
print(f"[A2UI-DEBUG] generate_a2ui STARTED at t=0")
54+
3355
messages = runtime.state["messages"][:-1]
56+
print(f"[A2UI-DEBUG] messages count: {len(messages)}")
57+
58+
# Get context entries from copilotkit state (catalog capabilities + component schema)
3459
context_entries = runtime.state.get("copilotkit", {}).get("context", [])
3560
context_text = "\n\n".join(
36-
entry.get("value", "")
37-
for entry in context_entries
61+
entry.get("value", "") for entry in context_entries
3862
if isinstance(entry, dict) and entry.get("value")
3963
)
64+
print(f"[A2UI-DEBUG] context entries: {len(context_entries)}, context_text_len: {len(context_text)}")
65+
66+
prompt = context_text
4067

4168
model = ChatOpenAI(model="gpt-4.1")
42-
model_with_tool = model.bind_tools([render_a2ui], tool_choice="render_a2ui")
43-
response = model_with_tool.invoke([SystemMessage(content=context_text), *messages])
69+
model_with_tool = model.bind_tools(
70+
[render_a2ui],
71+
tool_choice="render_a2ui",
72+
)
73+
74+
print(f"[A2UI-DEBUG] calling secondary LLM at t={time.time()-t0:.1f}s")
75+
response = model_with_tool.invoke(
76+
[SystemMessage(content=prompt), *messages],
77+
)
78+
print(f"[A2UI-RESPONSE] {response}")
79+
print(f"[A2UI-DEBUG] secondary LLM responded at t={time.time()-t0:.1f}s")
4480

4581
if not response.tool_calls:
82+
print(f"[A2UI-DEBUG] ERROR: no tool calls in response")
4683
return json.dumps({"error": "LLM did not call render_a2ui"})
4784

48-
args = response.tool_calls[0]["args"]
85+
tool_call = response.tool_calls[0]
86+
args = tool_call["args"]
87+
4988
surface_id = args.get("surfaceId", "dynamic-surface")
5089
catalog_id = args.get("catalogId", CUSTOM_CATALOG_ID)
5190
components = args.get("components", [])
5291
data = args.get("data", {})
92+
print(f"[A2UI-DEBUG] components={len(components)} data_keys={list(data.keys()) if data else []} surface={surface_id}")
5393

54-
operations = [
55-
a2ui.create_surface(surface_id, catalog_id=catalog_id),
56-
a2ui.update_components(surface_id, components),
94+
ops = [
95+
a2ui.create_surface(surface_id, catalog_id=catalog_id),
96+
a2ui.update_components(surface_id, components),
5797
]
5898
if data:
59-
operations.append(a2ui.update_data_model(surface_id, data))
99+
ops.append(a2ui.update_data_model(surface_id, data))
60100

61-
return a2ui.render(operations=operations)
101+
result = a2ui.render(operations=ops)
102+
print(f"[A2UI-DEBUG] generate_a2ui DONE at t={time.time()-t0:.1f}s result_len={len(result)}")
103+
return result

examples/integrations/langgraph-python-threads/apps/agent/src/a2ui_fixed_schema.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""
22
Fixed-schema A2UI tool: flight search results.
3+
4+
Schema is loaded from a JSON file. Only the data changes per invocation.
35
"""
46

57
from __future__ import annotations
@@ -37,9 +39,20 @@ class Flight(TypedDict):
3739
def search_flights(flights: list[Flight]) -> str:
3840
"""Search for flights and display the results as rich cards. Return exactly 2 flights.
3941
40-
Each flight must have: id, airline, airlineLogo, flightNumber, origin,
41-
destination, date, departureTime, arrivalTime, duration, status, statusIcon,
42-
and price.
42+
Each flight must have: id, airline (e.g. "United Airlines"),
43+
airlineLogo (use Google favicon API: https://www.google.com/s2/favicons?domain={airline_domain}&sz=128
44+
e.g. "https://www.google.com/s2/favicons?domain=united.com&sz=128" for United,
45+
"https://www.google.com/s2/favicons?domain=delta.com&sz=128" for Delta,
46+
"https://www.google.com/s2/favicons?domain=aa.com&sz=128" for American,
47+
"https://www.google.com/s2/favicons?domain=alaskaair.com&sz=128" for Alaska),
48+
flightNumber, origin, destination,
49+
date (short readable format like "Tue, Mar 18" — use near-future dates),
50+
departureTime, arrivalTime,
51+
duration (e.g. "4h 25m"), status (e.g. "On Time" or "Delayed"),
52+
statusIcon (colored dot: use "https://placehold.co/12/22c55e/22c55e.png"
53+
for On Time, "https://placehold.co/12/eab308/eab308.png" for Delayed,
54+
"https://placehold.co/12/ef4444/ef4444.png" for Cancelled),
55+
and price (e.g. "$289").
4356
"""
4457
return a2ui.render(
4558
operations=[

packages/shared/src/a2ui-prompts.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ CRITICAL: You MUST call the render_a2ui tool with ALL of these arguments:
2727
component names or use names not in the schema.
2828
2929
COMPONENT ID RULES:
30+
- Exactly one component MUST have id="root". This is the surface's entry
31+
point — the renderer begins at "root" and walks the child/children tree
32+
from there. Every other component must be reachable from "root". If no
33+
component has id="root", the surface renders an empty loading placeholder
34+
and none of your components will be shown.
3035
- Every component ID must be unique within the surface.
3136
- A component MUST NOT reference itself as child/children. This causes a
3237
circular dependency error. For example, if a component has id="avatar",

sdk-python/copilotkit/a2ui.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,11 @@ def render(
117117
- every component must have the "component" field specifying the component type (e.g. "Text", "Image", "Row", "Column", "List", "Button", etc.)
118118
119119
COMPONENT ID RULES:
120+
- Exactly one component MUST have id="root". This is the surface's entry
121+
point — the renderer begins at "root" and walks the child/children tree
122+
from there. Every other component must be reachable from "root". If no
123+
component has id="root", the surface renders an empty loading placeholder
124+
and none of your components will be shown.
120125
- Every component ID must be unique within the surface.
121126
- A component MUST NOT reference itself as child/children. This causes a
122127
circular dependency error. For example, if a component has id="avatar",

0 commit comments

Comments
 (0)