Skip to content

Commit 5d3abc6

Browse files
committed
fix(showcase/langgraph-python): rename internal A2UI tool so middleware doesn't intercept
The A2UI middleware (`@ag-ui/a2ui-middleware`) defaults `a2uiToolNames` to `["render_a2ui"]` and synthesises ACTIVITY_SNAPSHOT events from the streaming tool-call args of any matching call — using the LLM's RAW catalogId and components verbatim, before the Python tool body has a chance to validate or normalise. Both `beautiful_chat.py` and `a2ui_dynamic.py` use a `generate_a2ui` tool that internally invokes a secondary LLM bound to a structured-output `render_a2ui` helper. Because the helper's name matched the middleware's default intercept list, the secondary LLM's hallucinated catalogId (`declarative-gen-ui-catalog` leaking into the beautiful-chat dashboard) and malformed root components (no `component` field on KPI dashboard) were emitted to the frontend bypass, surfacing as: - "A2UI render error: Catalog not found: declarative-gen-ui-catalog" on the beautiful-chat Sales Dashboard pill - "A2UI render error: Cannot create component root without a type" infinite-loop on the declarative-gen-ui KPI Dashboard pill The earlier force-pin of `catalog_id` and the defensive component sweep in `generate_a2ui` were correct but ran too late — they execute on the tool-result, after the middleware has already fired surface events from the streaming args. Fix: rename the internal helper to `_design_a2ui_surface` (and update `tool_choice`, prompt header, and module docstring) so it falls outside the middleware's intercept list. The explicit `a2ui.render(...)` ops the outer `generate_a2ui` returns are then the only path to the frontend, and our Python validation layer is authoritative. Verified locally: Beautiful Chat → Sales Dashboard pill renders Total Revenue / New Customers / etc. with no errors.
1 parent 3b3908e commit 5d3abc6

2 files changed

Lines changed: 37 additions & 18 deletions

File tree

showcase/integrations/langgraph-python/src/agents/a2ui_dynamic.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
`examples/integrations/langgraph-python/agent/src/a2ui_dynamic_schema.py`):
66
77
- The agent binds an explicit `generate_a2ui` tool. When called, `generate_a2ui`
8-
invokes a secondary LLM bound to `render_a2ui` (tool_choice forced) with the
9-
registered client catalog injected as `copilotkit.context`.
8+
invokes a secondary LLM bound to `_design_a2ui_surface` (tool_choice forced)
9+
with the registered client catalog injected as `copilotkit.context`. The
10+
internal tool is intentionally NOT named `render_a2ui` to avoid the A2UI
11+
middleware's default tool-call intercept (`a2uiToolNames`).
1012
- The tool result returns an `a2ui_operations` container which the A2UI
1113
middleware detects in the tool-call result and forwards to the frontend
1214
renderer.
@@ -37,14 +39,22 @@
3739
CUSTOM_CATALOG_ID = "declarative-gen-ui-catalog"
3840

3941

42+
# Internal tool bound only to the secondary LLM inside `generate_a2ui` for
43+
# structured output. Intentionally NOT named `render_a2ui` because the A2UI
44+
# middleware default-intercepts tool calls by that name from the run's event
45+
# stream and synthesises ACTIVITY_SNAPSHOT events from the LLM's RAW streaming
46+
# args (catalogId + components, before our Python code can validate). That
47+
# bypass is what surfaced the "Cannot create component root without a type"
48+
# infinite-loop on the deployed declarative-gen-ui demo. Renaming sidesteps
49+
# the middleware's intercept list (`a2uiToolNames`).
4050
@lc_tool
41-
def render_a2ui(
51+
def _design_a2ui_surface(
4252
surfaceId: str,
4353
catalogId: str,
4454
components: list[dict],
4555
data: dict | None = None,
4656
) -> str:
47-
"""Render a dynamic A2UI v0.9 surface.
57+
"""Design a dynamic A2UI v0.9 surface.
4858
4959
Args:
5060
surfaceId: Unique surface identifier.
@@ -53,12 +63,12 @@ def render_a2ui(
5363
component must have id "root".
5464
data: Optional initial data model for the surface.
5565
"""
56-
return "rendered"
66+
return "designed"
5767

5868

5969
_GENERATE_A2UI_PROMPT_HEADER = f"""\
60-
You are designing a dynamic A2UI v0.9 surface. Call the `render_a2ui` tool with
61-
a flat component array.
70+
You are designing a dynamic A2UI v0.9 surface. Call the `_design_a2ui_surface`
71+
tool with a flat component array.
6272
6373
Hard requirements (failing any of these breaks the renderer — be strict):
6474
- `catalogId` MUST be exactly: "{CUSTOM_CATALOG_ID}"
@@ -103,16 +113,16 @@ def generate_a2ui(runtime: ToolRuntime[Any]) -> str:
103113

104114
model = ChatOpenAI(model="gpt-4.1")
105115
model_with_tool = model.bind_tools(
106-
[render_a2ui],
107-
tool_choice="render_a2ui",
116+
[_design_a2ui_surface],
117+
tool_choice="_design_a2ui_surface",
108118
)
109119

110120
response = model_with_tool.invoke(
111121
[SystemMessage(content=prompt), *messages],
112122
)
113123

114124
if not response.tool_calls:
115-
return json.dumps({"error": "LLM did not call render_a2ui"})
125+
return json.dumps({"error": "LLM did not call _design_a2ui_surface"})
116126

117127
tool_call = response.tool_calls[0]
118128
args = tool_call["args"]

showcase/integrations/langgraph-python/src/agents/beautiful_chat.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -197,14 +197,23 @@ def search_flights(flights: list[Flight]) -> str:
197197
CUSTOM_CATALOG_ID = "copilotkit://app-dashboard-catalog"
198198

199199

200+
# Internal tool bound only to the secondary LLM inside `generate_a2ui` for
201+
# structured-output. Intentionally NOT named `render_a2ui` because the A2UI
202+
# middleware default-intercepts any tool call by that name from the run's
203+
# event stream and synthesises ACTIVITY_SNAPSHOT events from the LLM's RAW
204+
# streaming args (catalogId + components, before our Python code can validate
205+
# or normalise). That bypass is what surfaced the "Catalog not found:
206+
# declarative-gen-ui-catalog" hallucination on beautiful-chat and the
207+
# "Cannot create component root without a type" loop on declarative-gen-ui.
208+
# Renaming sidesteps the middleware's intercept list (`a2uiToolNames`).
200209
@lc_tool
201-
def render_a2ui(
210+
def _design_a2ui_surface(
202211
surfaceId: str,
203212
catalogId: str,
204213
components: list[dict],
205214
data: dict | None = None,
206215
) -> str:
207-
"""Render a dynamic A2UI v0.9 surface.
216+
"""Design a dynamic A2UI v0.9 surface.
208217
209218
Args:
210219
surfaceId: Unique surface identifier.
@@ -214,12 +223,12 @@ def render_a2ui(
214223
data: Optional initial data model for the surface (e.g. form values,
215224
list items for data-bound components).
216225
"""
217-
return "rendered"
226+
return "designed"
218227

219228

220229
_GENERATE_A2UI_PROMPT_HEADER = f"""\
221-
You are designing a dynamic A2UI v0.9 surface. Call the `render_a2ui` tool with
222-
a flat component array.
230+
You are designing a dynamic A2UI v0.9 surface. Call the `_design_a2ui_surface`
231+
tool with a flat component array.
223232
224233
Hard requirements (failing any of these breaks the renderer — be strict):
225234
- `catalogId` MUST be exactly: "{CUSTOM_CATALOG_ID}"
@@ -260,16 +269,16 @@ def generate_a2ui(runtime: ToolRuntime[Any]) -> str:
260269

261270
model = ChatOpenAI(model="gpt-4.1")
262271
model_with_tool = model.bind_tools(
263-
[render_a2ui],
264-
tool_choice="render_a2ui",
272+
[_design_a2ui_surface],
273+
tool_choice="_design_a2ui_surface",
265274
)
266275

267276
response = model_with_tool.invoke(
268277
[SystemMessage(content=prompt), *messages],
269278
)
270279

271280
if not response.tool_calls:
272-
return json.dumps({"error": "LLM did not call render_a2ui"})
281+
return json.dumps({"error": "LLM did not call _design_a2ui_surface"})
273282

274283
tool_call = response.tool_calls[0]
275284
args = tool_call["args"]

0 commit comments

Comments
 (0)