Skip to content

Commit 112e53d

Browse files
tylerslatonclaude
andcommitted
fix(showcase/google-adk): wire AGUIToolset + strip nested-required so gen-UI demos render
Two compounding root causes were silently breaking every generative-UI demo that depended on frontend-registered tools (useFrontendTool, useComponent, useHumanInTheLoop): 1) Missing AGUIToolset() in bespoke agents The ag_ui_adk middleware injects the frontend's tool registrations by *replacing* AGUIToolset instances in the agent's `tools` list with a ClientProxyToolset that wraps `input.tools`. If no AGUIToolset is present, the frontend tools are dropped silently and the model never sees render_bar_chart / render_pie_chart / etc. Bespoke agents (the ones built directly with LlmAgent(...) instead of via build_simple_chat_agent in shared_chat.py) all had `tools=[]` or `tools=[backend_only]`. Appended AGUIToolset() to every bespoke agent. 2) Nested `required` in tool parameter schemas Gemini's function-calling API silently rejects function declarations whose parameter schema contains a `required` field below the top-level object — e.g. on the items.properties of an array, or on a nested property's own properties. The Zod-generated schema for render_bar_chart has exactly that shape (items.properties with required: [label, value]). The model emits no TOOL_CALL_* events, no TEXT_MESSAGE_*, just RUN_STARTED → STATE_SNAPSHOT → RUN_FINISHED. ag_ui_adk's _clean_schema_for_genai doesn't strip nested required. agent_server.py now monkey-patches _clean_schema_for_genai at startup so nested `required` is stripped before tool definitions reach the model. Top-level required is preserved (Gemini accepts it there). The patch is defensive and per-process — no upstream dependency change needed. Verification: curl test against /gen-ui-tool-based with the LP-shaped tool schema now emits TOOL_CALL_START + TOOL_CALL_ARGS containing the chart payload. Browser test renders the bar chart in the chat with Q1/Q2/Q3/Q4 data. Sanity-checked gen-ui-agent (set_steps state-streaming) — fully working, live "All 3 steps complete" card rendered. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5eac8ef commit 112e53d

19 files changed

Lines changed: 96 additions & 19 deletions

showcase/integrations/google-adk/src/agent_server.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import os
2626
import uvicorn
2727
from ag_ui_adk import ADKAgent, add_adk_fastapi_endpoint
28+
import ag_ui_adk.client_proxy_tool as _cpt
2829
from fastapi import FastAPI
2930
from fastapi.middleware.cors import CORSMiddleware
3031
from starlette.middleware.base import BaseHTTPMiddleware
@@ -35,6 +36,64 @@
3536

3637
load_dotenv()
3738

39+
40+
# Gemini's function-calling API silently rejects function declarations that
41+
# carry a `required` field inside nested object schemas (e.g. on
42+
# `items.properties` of an array, or on properties of an object property).
43+
# The model emits no TOOL_CALL_* events and no error — it just returns an
44+
# empty response, which manifests as the agent appearing to hang. Strip
45+
# `required` from any schema that isn't the top-level parameters object
46+
# before ag_ui_adk hands the tool definition to genai.
47+
#
48+
# Top-level `required` is preserved (Gemini supports it there).
49+
_original_clean_schema = _cpt._clean_schema_for_genai
50+
51+
52+
def _strip_nested_required(schema, _top_level=False):
53+
if isinstance(schema, dict):
54+
out = {}
55+
for k, v in schema.items():
56+
if k == "required" and not _top_level:
57+
continue
58+
if k in ("properties", "defs") and isinstance(v, dict):
59+
out[k] = {
60+
pn: _strip_nested_required(ps, _top_level=False)
61+
for pn, ps in v.items()
62+
}
63+
elif isinstance(v, (dict, list)):
64+
out[k] = _strip_nested_required(v, _top_level=False)
65+
else:
66+
out[k] = v
67+
return out
68+
if isinstance(schema, list):
69+
return [_strip_nested_required(item, _top_level=False) for item in schema]
70+
return schema
71+
72+
73+
def _patched_clean_schema_for_genai(schema):
74+
cleaned = _original_clean_schema(schema)
75+
# Walk the cleaned schema and remove `required` everywhere except the
76+
# outermost object (which Gemini does accept).
77+
if isinstance(cleaned, dict):
78+
top = {}
79+
for k, v in cleaned.items():
80+
if k == "required":
81+
top[k] = v # preserve top-level required
82+
elif k in ("properties", "defs") and isinstance(v, dict):
83+
top[k] = {
84+
pn: _strip_nested_required(ps, _top_level=False)
85+
for pn, ps in v.items()
86+
}
87+
elif isinstance(v, (dict, list)):
88+
top[k] = _strip_nested_required(v, _top_level=False)
89+
else:
90+
top[k] = v
91+
return top
92+
return cleaned
93+
94+
95+
_cpt._clean_schema_for_genai = _patched_clean_schema_for_genai
96+
3897
app = FastAPI(title="Google ADK Agent Server")
3998

4099

showcase/integrations/google-adk/src/agents/a2ui_fixed_agent.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from pathlib import Path
1313
from typing import Any
1414

15+
from ag_ui_adk import AGUIToolset
1516
from google.adk.agents import LlmAgent
1617
from google.adk.tools import ToolContext
1718

@@ -98,5 +99,5 @@ def display_flight(
9899
name="A2uiFixedAgent",
99100
model=get_model(),
100101
instruction=_INSTRUCTION,
101-
tools=[display_flight],
102+
tools=[display_flight, AGUIToolset()],
102103
)

showcase/integrations/google-adk/src/agents/agent_config_agent.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import logging
1313
from typing import Optional
1414

15+
from ag_ui_adk import AGUIToolset
1516
from google.adk.agents import LlmAgent
1617
from google.adk.agents.callback_context import CallbackContext
1718
from google.adk.models.llm_request import LlmRequest
@@ -144,6 +145,6 @@ def _inject_config(
144145
name="AgentConfigAgent",
145146
model=get_model(),
146147
instruction=_INSTRUCTION,
147-
tools=[],
148+
tools=[AGUIToolset()],
148149
before_model_callback=_inject_config,
149150
)

showcase/integrations/google-adk/src/agents/beautiful_chat_agent.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from __future__ import annotations
1010

11+
from ag_ui_adk import AGUIToolset
1112
from google.adk.agents import LlmAgent
1213
from google.adk.tools import ToolContext
1314

@@ -49,5 +50,5 @@ def schedule_meeting(
4950
name="BeautifulChatAgent",
5051
model=get_model(),
5152
instruction=_INSTRUCTION,
52-
tools=[query_data, search_flights, schedule_meeting],
53+
tools=[query_data, search_flights, schedule_meeting, AGUIToolset()],
5354
)

showcase/integrations/google-adk/src/agents/byoc_agents.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from __future__ import annotations
1010

11+
from ag_ui_adk import AGUIToolset
1112
from google.adk.agents import LlmAgent
1213
from google.adk.tools import ToolContext
1314

@@ -31,5 +32,5 @@ def query_data(tool_context: ToolContext, query: str) -> list:
3132
name="ByocAgent",
3233
model=get_model(),
3334
instruction=_INSTRUCTION,
34-
tools=[query_data],
35+
tools=[query_data, AGUIToolset()],
3536
)

showcase/integrations/google-adk/src/agents/declarative_gen_ui_agent.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from __future__ import annotations
1111

12+
from ag_ui_adk import AGUIToolset
1213
from google.adk.agents import LlmAgent
1314

1415
from agents.shared_chat import get_model
@@ -27,5 +28,5 @@
2728
name="DeclarativeGenUiAgent",
2829
model=get_model(),
2930
instruction=_INSTRUCTION,
30-
tools=[generate_a2ui],
31+
tools=[generate_a2ui, AGUIToolset()],
3132
)

showcase/integrations/google-adk/src/agents/gen_ui_agent.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from __future__ import annotations
1212

13+
from ag_ui_adk import AGUIToolset
1314
from google.adk.agents import LlmAgent
1415
from google.adk.tools import ToolContext
1516

@@ -55,5 +56,5 @@ def set_steps(tool_context: ToolContext, steps: list[dict]) -> dict:
5556
name="GenUiAgent",
5657
model=get_model(),
5758
instruction=_INSTRUCTION,
58-
tools=[set_steps],
59+
tools=[set_steps, AGUIToolset()],
5960
)

showcase/integrations/google-adk/src/agents/gen_ui_tool_based_agent.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from __future__ import annotations
99

1010
from google.adk.agents import LlmAgent
11+
from ag_ui_adk import AGUIToolset
1112

1213
from agents.shared_chat import get_model
1314

@@ -24,5 +25,5 @@
2425
name="GenUiToolBasedAgent",
2526
model=get_model(),
2627
instruction=_INSTRUCTION,
27-
tools=[],
28+
tools=[AGUIToolset()],
2829
)

showcase/integrations/google-adk/src/agents/mcp_apps_agent.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
from __future__ import annotations
1515

16+
from ag_ui_adk import AGUIToolset
1617
from google.adk.agents import LlmAgent
1718

1819
from agents.shared_chat import get_model
@@ -55,5 +56,5 @@
5556
name="McpAppsAgent",
5657
model=get_model(),
5758
instruction=_INSTRUCTION,
58-
tools=[],
59+
tools=[AGUIToolset()],
5960
)

showcase/integrations/google-adk/src/agents/multimodal_agent.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from __future__ import annotations
1010

11+
from ag_ui_adk import AGUIToolset
1112
from google.adk.agents import LlmAgent
1213

1314
from agents.shared_chat import get_model
@@ -24,5 +25,5 @@
2425
name="MultimodalAgent",
2526
model=get_model(),
2627
instruction=_INSTRUCTION,
27-
tools=[],
28+
tools=[AGUIToolset()],
2829
)

0 commit comments

Comments
 (0)