Skip to content

Commit f0952e7

Browse files
committed
fix(showcase): fix llamaindex D5 failures for tool-rendering, gen-ui-headless, hitl-steps
Three D5 probe failures fixed: tool-rendering: get_weather was a backend tool so the workflow emitted TOOL_CALL_CHUNK but no TOOL_CALL_RESULT, leaving useRenderTool stuck in loading state forever. Move get_weather to frontend_tools so CopilotKit manages the tool call lifecycle. Add ToolCallResultWorkflowEvent (which subclasses ToolCallEndWorkflowEvent to pass the AG_UI_EVENTS isinstance filter) and override aggregate_tool_calls to emit TOOL_CALL_RESULT for render-only tools. The WeatherCard component now always renders with data-testid="weather-card" even during loading, instead of switching to a separate div that lacks the testid. gen-ui-headless: the shared agent was missing a show_card frontend tool stub, so the workflow never emitted TOOL_CALL_CHUNK for it. Add the stub and register it in frontend_tools. hitl-steps: human_in_the_loop was routed to the hitl-in-chat specialized agent (which only has book_call), not the shared agent (which has generate_task_steps). Move human_in_the_loop to sharedAgentNames so it routes through the default agent. Introduce render_only_tool_names set to distinguish render-only tools (get_weather) from interactive tools (generate_task_steps, book_call, show_card) — only render-only tools get premature TOOL_CALL_RESULT; interactive tools let CopilotKit manage the result lifecycle to keep buttons enabled.
1 parent d44de79 commit f0952e7

4 files changed

Lines changed: 166 additions & 43 deletions

File tree

showcase/integrations/llamaindex/src/agents/agent.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,14 @@ def book_call(
7171
return f"Booking call about {topic} with {attendee}"
7272

7373

74+
def show_card(
75+
title: Annotated[str, "Short heading for the card."],
76+
body: Annotated[str, "Body text for the card."],
77+
) -> str:
78+
"""Display a titled card with a short body of text. Rendered on the frontend via useComponent."""
79+
return f"Displayed card: {title}"
80+
81+
7482
# --- Backend tools (executed server-side, using shared implementations) ---
7583

7684
# @region[weather-tool-backend]
@@ -187,22 +195,27 @@ async def generate_a2ui(
187195
"- Generate dynamic A2UI dashboards from conversation context (via generate_a2ui tool)\n"
188196
"- Generate step-by-step plans for user review (human-in-the-loop)\n"
189197
"- Book calls with people (via book_call frontend tool)\n"
198+
"- Show titled cards with a body of text (via show_card frontend tool)\n"
190199
"When asked about weather, always use the get_weather tool. "
191200
"When asked about financial data or charts, use query_data first. "
192201
"When asked to book a call, use the book_call tool with topic and name."
193202
)
194203

195204

196205
async def _agent_workflow_factory():
197-
return FixedAGUIChatWorkflow(
206+
wf = FixedAGUIChatWorkflow(
198207
llm=OpenAI(model="gpt-4.1", **_openai_kwargs),
199-
frontend_tools=[change_background, generate_haiku, generate_task_steps, book_call],
200-
backend_tools=[get_weather, query_data, manage_sales_todos, get_sales_todos_tool, schedule_meeting, search_flights, generate_a2ui],
208+
frontend_tools=[change_background, generate_haiku, generate_task_steps, book_call, show_card, get_weather],
209+
backend_tools=[query_data, manage_sales_todos, get_sales_todos_tool, schedule_meeting, search_flights, generate_a2ui],
201210
system_prompt=_AGENT_SYSTEM_PROMPT,
202211
initial_state={
203212
"todos": [],
204213
},
205214
)
215+
# Tools that use useRenderTool on the frontend — emit
216+
# TOOL_CALL_RESULT so the render transitions to "complete".
217+
wf.render_only_tool_names = {"get_weather"}
218+
return wf
206219

207220

208221
agent_router = get_ag_ui_workflow_router(

showcase/integrations/llamaindex/src/agents/hitl_in_chat_agent.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,15 @@
5252
LoopEvent,
5353
ToolCallEvent,
5454
)
55+
from llama_index.protocols.ag_ui.agent import ToolCallResultEvent
5556
from llama_index.protocols.ag_ui.events import (
5657
MessagesSnapshotWorkflowEvent,
5758
StateSnapshotWorkflowEvent,
5859
TextMessageChunkWorkflowEvent,
5960
ToolCallChunkWorkflowEvent,
61+
ToolCallEndWorkflowEvent,
6062
)
63+
from ag_ui.core import EventType
6164
from llama_index.protocols.ag_ui.router import get_ag_ui_workflow_router
6265
from llama_index.protocols.ag_ui.utils import (
6366
ag_ui_message_to_llama_index_message,
@@ -113,12 +116,37 @@ def _fix_tool_messages(chat_history: List[ChatMessage]) -> None:
113116
msg.role = MessageRole.TOOL
114117

115118

119+
class ToolCallResultWorkflowEvent(ToolCallEndWorkflowEvent):
120+
"""Emit a TOOL_CALL_RESULT AG-UI event for backend tools.
121+
122+
llama-index-protocols-ag-ui v0.2.2 has no built-in workflow event for
123+
TOOL_CALL_RESULT — ToolCallChunkWorkflowEvent emits the call but the
124+
result is never forwarded to the frontend. Without this event,
125+
CopilotKit's useRenderTool never transitions from "executing" to
126+
"complete" and rendered tool cards stay in their loading state.
127+
128+
Subclasses ToolCallEndWorkflowEvent so it passes the AG_UI_EVENTS
129+
isinstance filter in the router's stream_events loop.
130+
"""
131+
message_id: str = ""
132+
content: str = ""
133+
role: Optional[str] = "tool"
134+
type: EventType = EventType.TOOL_CALL_RESULT
135+
136+
116137
class FixedAGUIChatWorkflow(AGUIChatWorkflow):
117138
"""AGUIChatWorkflow that fixes duplicate tool-call rendering and
118139
tool-result message formatting.
119140
120141
See module docstring for the three upstream bugs this addresses.
142+
143+
render_only_tool_names: set of tool names that use useRenderTool
144+
on the frontend. For these tools, aggregate_tool_calls emits a
145+
TOOL_CALL_RESULT event so the render callback transitions to
146+
status "complete". Interactive tools (useHumanInTheLoop,
147+
useComponent) must NOT be in this set.
121148
"""
149+
render_only_tool_names: set = set()
122150

123151
def _snapshot_messages(
124152
self, ctx: Context, chat_history: List[ChatMessage]
@@ -303,6 +331,87 @@ async def chat(
303331

304332
return StopEvent()
305333

334+
@step
335+
async def aggregate_tool_calls(
336+
self, ctx: Context, ev: ToolCallResultEvent
337+
) -> Optional[Union[StopEvent, LoopEvent]]:
338+
"""Override to emit TOOL_CALL_RESULT events for backend tools.
339+
340+
The upstream aggregate_tool_calls processes backend tool results
341+
internally (adding to chat_history + MESSAGES_SNAPSHOT) but never
342+
emits a TOOL_CALL_RESULT AG-UI event. Without it, CopilotKit's
343+
useRenderTool never transitions to "complete" and tool cards stay
344+
in their loading state forever.
345+
"""
346+
num_tool_calls = await ctx.store.get("num_tool_calls")
347+
tool_call_results: List[ToolCallResultEvent] = ctx.collect_events(
348+
ev, [ToolCallResultEvent] * num_tool_calls
349+
)
350+
if tool_call_results is None:
351+
return None
352+
353+
frontend_tool_calls = [
354+
r for r in tool_call_results
355+
if r.tool_name in self.frontend_tools
356+
]
357+
backend_tool_calls = [
358+
r for r in tool_call_results
359+
if r.tool_name in self.backend_tools
360+
]
361+
362+
new_tool_messages = []
363+
for tool_result in backend_tool_calls:
364+
new_tool_messages.append(
365+
ChatMessage(
366+
role="tool",
367+
content=tool_result.tool_output.content,
368+
additional_kwargs={
369+
"tool_call_id": tool_result.tool_call_id,
370+
},
371+
)
372+
)
373+
# Emit TOOL_CALL_RESULT so useRenderTool transitions to "complete"
374+
ctx.write_event_to_stream(
375+
ToolCallResultWorkflowEvent(
376+
tool_call_id=tool_result.tool_call_id,
377+
message_id=str(uuid.uuid4()),
378+
content=tool_result.tool_output.content,
379+
role="tool",
380+
)
381+
)
382+
383+
chat_history = await ctx.store.get("chat_history")
384+
if new_tool_messages:
385+
chat_history.extend(new_tool_messages)
386+
self._snapshot_messages(ctx, [*chat_history])
387+
await ctx.store.set("chat_history", chat_history)
388+
389+
if len(frontend_tool_calls) > 0:
390+
# Emit TOOL_CALL_RESULT for render-only frontend tools (those
391+
# registered via useRenderTool, like get_weather). These tools
392+
# execute server-side and need the result forwarded so the
393+
# render callback transitions to status "complete".
394+
#
395+
# Do NOT emit for interactive frontend tools (those using
396+
# useHumanInTheLoop like generate_task_steps, book_call, or
397+
# useComponent like show_card). Those tools need CopilotKit to
398+
# manage the result lifecycle — premature TOOL_CALL_RESULT
399+
# would skip past the "executing" state and disable interactive
400+
# buttons.
401+
for tool_result in frontend_tool_calls:
402+
if tool_result.tool_name in self.render_only_tool_names:
403+
ctx.write_event_to_stream(
404+
ToolCallResultWorkflowEvent(
405+
tool_call_id=tool_result.tool_call_id,
406+
message_id=str(uuid.uuid4()),
407+
content=tool_result.tool_output.content,
408+
role="tool",
409+
)
410+
)
411+
return StopEvent()
412+
413+
return LoopEvent(messages=chat_history)
414+
306415

307416
async def _workflow_factory():
308417
return FixedAGUIChatWorkflow(

showcase/integrations/llamaindex/src/app/api/copilotkit/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ const sharedAgentNames = [
4040
"headless_simple",
4141
"headless_complete",
4242
"readonly_state_agent_context",
43+
"human_in_the_loop",
4344
];
4445

4546
// Specialized routers live at dedicated subpaths on the agent_server so the
@@ -53,7 +54,6 @@ const specializedAgents: Record<string, string> = {
5354
"shared-state-read-write": "/shared-state-read-write",
5455
"gen-ui-tool-based": "/gen-ui-tool-based",
5556
"beautiful-chat": "/beautiful-chat",
56-
human_in_the_loop: "/hitl-in-chat",
5757
hitl_in_app: "/hitl-in-app",
5858
subagents: "/subagents",
5959
};

showcase/integrations/llamaindex/src/app/demos/tool-rendering/page.tsx

Lines changed: 40 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,6 @@ function Chat() {
3636
}),
3737
render: ({ parameters, result, status }) => {
3838
const loading = status !== "complete";
39-
if (loading) {
40-
return (
41-
<div className="bg-[#667eea] text-white p-4 rounded-lg max-w-md">
42-
<span className="animate-spin">Retrieving weather...</span>
43-
</div>
44-
);
45-
}
46-
4739
const parsed = parseJsonResult<any>(result);
4840
const weatherResult: WeatherToolResult = {
4941
temperature: parsed?.temperature || 0,
@@ -53,10 +45,11 @@ function Chat() {
5345
feelsLike: parsed?.feels_like || parsed?.temperature || 0,
5446
};
5547

56-
const themeColor = getThemeColor(weatherResult.conditions);
48+
const themeColor = loading ? "#667eea" : getThemeColor(weatherResult.conditions);
5749

5850
return (
5951
<WeatherCard
52+
loading={loading}
6053
location={parameters?.location ?? ""}
6154
themeColor={themeColor}
6255
result={weatherResult}
@@ -121,10 +114,12 @@ function getThemeColor(conditions: string): string {
121114
}
122115

123116
function WeatherCard({
117+
loading,
124118
location,
125119
themeColor,
126120
result,
127121
}: {
122+
loading?: boolean;
128123
location?: string;
129124
themeColor: string;
130125
result: WeatherToolResult;
@@ -142,42 +137,48 @@ function WeatherCard({
142137
data-testid="weather-city"
143138
className="text-xl font-bold text-white capitalize"
144139
>
145-
{location}
140+
{location || "Weather"}
146141
</h3>
147-
<p className="text-white">Current Weather</p>
148-
</div>
149-
<WeatherIcon conditions={result.conditions} />
150-
</div>
151-
152-
<div className="mt-4 flex items-end justify-between">
153-
<div className="text-3xl font-bold text-white">
154-
<span>{result.temperature}&deg; C</span>
155-
<span className="text-sm text-white/50">
156-
{" / "}
157-
{((result.temperature * 9) / 5 + 32).toFixed(1)}&deg; F
158-
</span>
159-
</div>
160-
<div className="text-sm text-white capitalize">
161-
{result.conditions}
142+
<p className="text-white">
143+
{loading ? "Fetching weather..." : "Current Weather"}
144+
</p>
162145
</div>
146+
{!loading && <WeatherIcon conditions={result.conditions} />}
163147
</div>
164148

165-
<div className="mt-4 pt-4 border-t border-white">
166-
<div className="grid grid-cols-3 gap-2 text-center">
167-
<div data-testid="weather-humidity">
168-
<p className="text-white text-xs">Humidity</p>
169-
<p className="text-white font-medium">{result.humidity}%</p>
170-
</div>
171-
<div data-testid="weather-wind">
172-
<p className="text-white text-xs">Wind</p>
173-
<p className="text-white font-medium">{result.windSpeed} mph</p>
149+
{!loading && (
150+
<>
151+
<div className="mt-4 flex items-end justify-between">
152+
<div className="text-3xl font-bold text-white">
153+
<span>{result.temperature}&deg; C</span>
154+
<span className="text-sm text-white/50">
155+
{" / "}
156+
{((result.temperature * 9) / 5 + 32).toFixed(1)}&deg; F
157+
</span>
158+
</div>
159+
<div className="text-sm text-white capitalize">
160+
{result.conditions}
161+
</div>
174162
</div>
175-
<div data-testid="weather-feels-like">
176-
<p className="text-white text-xs">Feels Like</p>
177-
<p className="text-white font-medium">{result.feelsLike}&deg;</p>
163+
164+
<div className="mt-4 pt-4 border-t border-white">
165+
<div className="grid grid-cols-3 gap-2 text-center">
166+
<div data-testid="weather-humidity">
167+
<p className="text-white text-xs">Humidity</p>
168+
<p className="text-white font-medium">{result.humidity}%</p>
169+
</div>
170+
<div data-testid="weather-wind">
171+
<p className="text-white text-xs">Wind</p>
172+
<p className="text-white font-medium">{result.windSpeed} mph</p>
173+
</div>
174+
<div data-testid="weather-feels-like">
175+
<p className="text-white text-xs">Feels Like</p>
176+
<p className="text-white font-medium">{result.feelsLike}&deg;</p>
177+
</div>
178+
</div>
178179
</div>
179-
</div>
180-
</div>
180+
</>
181+
)}
181182
</div>
182183
</div>
183184
);

0 commit comments

Comments
 (0)