Skip to content

Commit db1d7d0

Browse files
committed
fix(showcase): agno reasoning, ms-agent-python slots/multimodal, mastra subagents
- agno: custom _run_reasoning_agent handler emitting proper REASONING_MESSAGE AG-UI events (Agno's stock handler only emits STEP_STARTED/FINISHED which CopilotKit ignores); disable reasoning=True to avoid multi-call CoT loop that breaks aimock fixtures - ms-agent-python: wire chat-slots assistantMessage + disclaimer overrides; add missing public/demo-files/ (sample.png, sample.pdf) - mastra: register byocHashbrownAgent in main route; rewrite subagents e2e test to match actual page structure
1 parent 1db0bd7 commit db1d7d0

11 files changed

Lines changed: 361 additions & 82 deletions

File tree

showcase/integrations/agno/src/agent_server.py

Lines changed: 197 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,17 @@
2424
from ag_ui.core import (
2525
BaseEvent,
2626
EventType,
27+
ReasoningMessageContentEvent,
28+
ReasoningMessageEndEvent,
29+
ReasoningMessageStartEvent,
2730
RunAgentInput,
2831
RunErrorEvent,
2932
RunFinishedEvent,
3033
RunStartedEvent,
3134
StateSnapshotEvent,
35+
TextMessageContentEvent,
36+
TextMessageEndEvent,
37+
TextMessageStartEvent,
3238
)
3339
from ag_ui.core.types import Message as AGUIMessage
3440
from ag_ui.encoder import EventEncoder
@@ -451,6 +457,190 @@ async def _gen():
451457
app.post(route, name=f"agui_agent_config_{prefix.strip('/')}")(_handler)
452458

453459

460+
# ---------------------------------------------------------------------------
461+
# Reasoning-aware AGUI handler
462+
# ---------------------------------------------------------------------------
463+
#
464+
# Agno's stock AGUI handler emits STEP_STARTED/STEP_FINISHED events for
465+
# reasoning, not the REASONING_MESSAGE_* events that CopilotKit expects.
466+
# And reasoning=True triggers a multi-call CoT loop that breaks with
467+
# aimock/fixture environments.
468+
#
469+
# This custom handler:
470+
# 1. Runs the agent with reasoning=False (single LLM call)
471+
# 2. Collects the streamed text content
472+
# 3. Parses <reasoning>...</reasoning> tags from the text
473+
# 4. Emits REASONING_MESSAGE_* events for the reasoning block
474+
# 5. Emits TEXT_MESSAGE_* events for the answer
475+
#
476+
# If no <reasoning> tags are found, the entire response is emitted as a
477+
# text message (graceful fallback for aimock fixtures that return plain
478+
# text containing reasoning keywords).
479+
480+
import re
481+
482+
_REASONING_PATTERN = re.compile(
483+
r"<reasoning>(.*?)</reasoning>",
484+
re.DOTALL | re.IGNORECASE,
485+
)
486+
487+
488+
async def _run_reasoning_agent(
489+
agent: Union[Agent, RemoteAgent], run_input: RunAgentInput
490+
) -> AsyncIterator[BaseEvent]:
491+
"""Stream one reasoning agent run, synthesizing REASONING_MESSAGE events."""
492+
run_id = run_input.run_id or str(uuid.uuid4())
493+
thread_id = run_input.thread_id
494+
495+
try:
496+
user_input = extract_agui_user_input(run_input.messages or [])
497+
498+
yield RunStartedEvent(
499+
type=EventType.RUN_STARTED, thread_id=thread_id, run_id=run_id
500+
)
501+
502+
user_id: Optional[str] = None
503+
if run_input.forwarded_props and isinstance(run_input.forwarded_props, dict):
504+
user_id = run_input.forwarded_props.get("user_id")
505+
506+
session_state = validate_agui_state(run_input.state, thread_id) or {}
507+
508+
response_stream = agent.arun( # type: ignore[attr-defined]
509+
input=user_input,
510+
session_id=thread_id,
511+
stream=True,
512+
stream_events=True,
513+
user_id=user_id,
514+
session_state=session_state,
515+
run_id=run_id,
516+
)
517+
518+
# Collect the full text from the agent stream — we need to see the
519+
# complete response to split reasoning from answer. We still forward
520+
# tool-call events in real-time (important for the reasoning-chain
521+
# demo that interleaves reasoning with tool rendering).
522+
full_text = ""
523+
tool_events: list[BaseEvent] = []
524+
525+
async for event in async_stream_agno_response_as_agui_events(
526+
response_stream=response_stream, # type: ignore[arg-type]
527+
thread_id=thread_id,
528+
run_id=run_id,
529+
):
530+
if event.type in (EventType.RUN_STARTED, EventType.RUN_FINISHED):
531+
continue
532+
# Accumulate text content
533+
if event.type == EventType.TEXT_MESSAGE_CONTENT:
534+
full_text += event.delta # type: ignore[attr-defined]
535+
# Forward tool-call events immediately
536+
elif event.type in (
537+
EventType.TOOL_CALL_START,
538+
EventType.TOOL_CALL_ARGS,
539+
EventType.TOOL_CALL_END,
540+
):
541+
tool_events.append(event)
542+
# Skip text start/end — we'll re-emit with reasoning split
543+
544+
# Parse <reasoning>...</reasoning> tags
545+
match = _REASONING_PATTERN.search(full_text)
546+
547+
if match:
548+
reasoning_text = match.group(1).strip()
549+
answer_text = (
550+
full_text[: match.start()] + full_text[match.end() :]
551+
).strip()
552+
else:
553+
# Fallback: check for "Reasoning:" prefix pattern (aimock fixtures)
554+
lower = full_text.lower()
555+
if lower.startswith("reasoning:") or lower.startswith("reasoning step"):
556+
# Treat the whole text as containing reasoning — emit as
557+
# reasoning message so the ReasoningBlock renders, then emit
558+
# a short text answer.
559+
reasoning_text = full_text.strip()
560+
answer_text = ""
561+
else:
562+
reasoning_text = ""
563+
answer_text = full_text.strip()
564+
565+
# Emit reasoning message if we have reasoning content
566+
if reasoning_text:
567+
reasoning_msg_id = str(uuid.uuid4())
568+
yield ReasoningMessageStartEvent(
569+
type=EventType.REASONING_MESSAGE_START,
570+
message_id=reasoning_msg_id,
571+
role="reasoning",
572+
)
573+
yield ReasoningMessageContentEvent(
574+
type=EventType.REASONING_MESSAGE_CONTENT,
575+
message_id=reasoning_msg_id,
576+
delta=reasoning_text,
577+
)
578+
yield ReasoningMessageEndEvent(
579+
type=EventType.REASONING_MESSAGE_END,
580+
message_id=reasoning_msg_id,
581+
)
582+
583+
# Emit text message for the answer (or entire text if no reasoning)
584+
text_msg_id = str(uuid.uuid4())
585+
# Only emit a text message if there's actual content or tool events
586+
if answer_text or tool_events:
587+
yield TextMessageStartEvent(
588+
type=EventType.TEXT_MESSAGE_START,
589+
message_id=text_msg_id,
590+
role="assistant",
591+
)
592+
if answer_text:
593+
yield TextMessageContentEvent(
594+
type=EventType.TEXT_MESSAGE_CONTENT,
595+
message_id=text_msg_id,
596+
delta=answer_text,
597+
)
598+
yield TextMessageEndEvent(
599+
type=EventType.TEXT_MESSAGE_END,
600+
message_id=text_msg_id,
601+
)
602+
603+
# Emit any tool-call events that were collected
604+
for te in tool_events:
605+
yield te
606+
607+
yield RunFinishedEvent(
608+
type=EventType.RUN_FINISHED, thread_id=thread_id, run_id=run_id
609+
)
610+
611+
except asyncio.CancelledError: # noqa: TRY302
612+
raise
613+
except Exception as exc: # noqa: BLE001
614+
yield RunErrorEvent(type=EventType.RUN_ERROR, message=str(exc))
615+
616+
617+
def _attach_reasoning_route(
618+
app: FastAPI, agent: Agent, prefix: str
619+
) -> None:
620+
"""Mount a reasoning-aware AGUI POST endpoint at `<prefix>/agui`."""
621+
encoder = EventEncoder()
622+
route = f"{prefix.rstrip('/')}/agui"
623+
624+
async def _handler(run_input: RunAgentInput) -> StreamingResponse:
625+
async def _gen():
626+
async for event in _run_reasoning_agent(agent, run_input):
627+
yield encoder.encode(event)
628+
629+
return StreamingResponse(
630+
_gen(),
631+
media_type="text/event-stream",
632+
headers={
633+
"Cache-Control": "no-cache",
634+
"Connection": "keep-alive",
635+
"Access-Control-Allow-Origin": "*",
636+
"Access-Control-Allow-Methods": "POST, GET, OPTIONS",
637+
"Access-Control-Allow-Headers": "*",
638+
},
639+
)
640+
641+
app.post(route, name=f"agui_reasoning_{prefix.strip('/')}")(_handler)
642+
643+
454644
# ---------------------------------------------------------------------------
455645
# AgentOS bootstrap
456646
# ---------------------------------------------------------------------------
@@ -474,7 +664,8 @@ async def _gen():
474664
interfaces=[
475665
# main_agent is mounted separately below via _attach_hitl_aware_route
476666
# so it can forward tool results for HITL round-trips.
477-
AGUI(agent=reasoning_agent, prefix="/reasoning"), # -> /reasoning/agui
667+
# reasoning_agent is mounted separately below via _attach_reasoning_route
668+
# so it can emit proper REASONING_MESSAGE_* AG-UI events.
478669
# No-tools agent for the MCP Apps cell. The CopilotKit runtime's
479670
# `mcpApps.servers` middleware injects MCP server tools at request
480671
# time, so the LLM only sees the MCP-provided toolset.
@@ -521,6 +712,11 @@ async def _gen():
521712
_attach_state_aware_route(app, shared_state_rw_agent, "/shared-state-rw")
522713
_attach_state_aware_route(app, subagents_supervisor, "/subagents")
523714

715+
# Reasoning-aware route — emits proper REASONING_MESSAGE_* AG-UI events
716+
# that CopilotKit renders via the reasoningMessage slot. Replaces the stock
717+
# AGUI(agent=reasoning_agent, prefix="/reasoning") interface.
718+
_attach_reasoning_route(app, reasoning_agent, "/reasoning")
719+
524720
# Agent Config Object cell — builds a per-request Agno Agent whose system
525721
# prompt is composed from the CopilotKit provider's forwarded properties
526722
# (tone / expertise / responseLength).

showcase/integrations/agno/src/agents/reasoning_agent.py

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,16 @@
88
Mirrors `showcase/integrations/langgraph-python/src/agents/reasoning_agent.py`
99
(shared across the three reasoning demos there).
1010
11-
Agno's AGUI interface emits REASONING_MESSAGE_* events (see
12-
`agno/os/interfaces/agui/utils.py`) whenever the agent produces reasoning
13-
steps. Setting `reasoning=True` on the Agent enables Agno's built-in
14-
agentic reasoning loop, which generates step-wise thinking visible to the
15-
frontend via those events.
11+
Uses reasoning=False with a custom AGUI handler in agent_server.py that
12+
synthesizes REASONING_MESSAGE_* AG-UI events from <reasoning>...</reasoning>
13+
XML tags in the model output. This avoids Agno's multi-call CoT loop
14+
(which breaks aimock fixtures) while still producing the proper AG-UI
15+
events that CopilotKit's frontend renders via the reasoningMessage slot.
1616
1717
For the reasoning-chain demo we also expose the same shared backend tools
1818
(`get_weather`, `search_flights`, `get_stock_price`, `roll_dice`) as the
1919
primary agent so the catch-all tool renderer can observe a full
20-
reasoning tool call reasoning tool call chain.
20+
reasoning -> tool call -> reasoning -> tool call chain.
2121
"""
2222

2323
from __future__ import annotations
@@ -107,13 +107,30 @@ def roll_dice(sides: int = 6):
107107
return json.dumps({"sides": sides, "result": randint(1, max(2, sides))})
108108

109109

110-
# A reasoning-enabled agent emits REASONING_MESSAGE_* events through the
111-
# AGUI interface. gpt-4o-mini is cheap + fast and good enough for the
112-
# step-by-step demonstration the showcase cell is after.
110+
# NOTE: reasoning=False (the default) is used here intentionally.
111+
#
112+
# Agno's reasoning=True triggers a multi-call Chain-of-Thought loop that
113+
# makes up to `reasoning_max_steps` sequential LLM calls. This breaks in
114+
# proxy/fixture environments (aimock, D5 probes) where only the first
115+
# call matches a fixture — subsequent calls don't match and either fall
116+
# through to the real API (slow, non-deterministic) or fail entirely.
117+
#
118+
# Instead, the custom AGUI handler in agent_server.py synthesizes
119+
# REASONING_MESSAGE_* AG-UI events from the agent's response text. The
120+
# system prompt instructs the model to prefix its answer with a reasoning
121+
# block delimited by <reasoning>...</reasoning> tags. The custom handler
122+
# parses those tags and emits proper AG-UI reasoning events that
123+
# CopilotKit's frontend renders via the reasoningMessage slot.
124+
#
125+
# This approach:
126+
# - Works with aimock (single LLM call)
127+
# - Emits proper AG-UI REASONING_MESSAGE_* events (unlike Agno's stock
128+
# AGUI handler which only emits STEP_STARTED/STEP_FINISHED)
129+
# - Keeps the demo visually identical to native reasoning models
113130
agent = Agent(
114131
model=OpenAIChat(id="gpt-4o-mini", timeout=120),
115132
tools=[get_weather, search_flights, get_stock_price, roll_dice],
116-
reasoning=True,
133+
reasoning=False,
117134
tool_call_limit=10,
118135
description=(
119136
"You are a helpful assistant. For each user question, first think "
@@ -122,9 +139,18 @@ def roll_dice(sides: int = 6):
122139
),
123140
instructions="""
124141
REASONING STYLE:
125-
Always reason step-by-step before answering. Keep thinking concise —
126-
two to four short steps is plenty for most questions. Do not repeat
127-
the final answer inside the reasoning block.
142+
Always begin your response with a reasoning block wrapped in
143+
<reasoning>...</reasoning> XML tags. Inside the tags, think
144+
step-by-step (two to four short steps is plenty). After the closing
145+
tag, give your concise final answer. Example:
146+
147+
<reasoning>
148+
Step 1: Identify what the user is asking.
149+
Step 2: Consider which tool to use.
150+
Step 3: Formulate the answer.
151+
</reasoning>
152+
153+
Here is my answer...
128154
129155
TOOLS (reasoning-chain cell):
130156
- get_weather: use when the user asks about weather.

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,8 @@ export type LocalMastraAgentName =
106106
| "subagentsSupervisorAgent"
107107
| "interruptAgent"
108108
| "multimodalAgent"
109-
| "mcpAppsAgent";
109+
| "mcpAppsAgent"
110+
| "byocHashbrownAgent";
110111

111112
export type BuiltAgents = Record<
112113
DemoAgentName | LocalMastraAgentName,
@@ -174,6 +175,11 @@ export function buildAgents(
174175
"mcpAppsAgent missing from Mastra config — required for /demos/mcp-apps",
175176
);
176177
}
178+
if (!baseLocalAgents.byocHashbrownAgent) {
179+
throw new Error(
180+
"byocHashbrownAgent missing from Mastra config — required for /demos/byoc-hashbrown",
181+
);
182+
}
177183
const headlessCompleteAgentInstance = getLocalAgent({
178184
mastra: mastraInstance,
179185
agentId: "headlessCompleteAgent",
@@ -224,6 +230,14 @@ export function buildAgents(
224230
if (!mcpAppsAgentInstance) {
225231
throw new Error("getLocalAgent returned null for mcpAppsAgent");
226232
}
233+
const byocHashbrownAgentInstance = getLocalAgent({
234+
mastra: mastraInstance,
235+
agentId: "byocHashbrownAgent",
236+
resourceId: "mastra-byocHashbrownAgent",
237+
});
238+
if (!byocHashbrownAgentInstance) {
239+
throw new Error("getLocalAgent returned null for byocHashbrownAgent");
240+
}
227241
const localAgents = {
228242
weatherAgent: baseLocalAgents.weatherAgent,
229243
headlessCompleteAgent: headlessCompleteAgentInstance,
@@ -232,6 +246,7 @@ export function buildAgents(
232246
interruptAgent: interruptAgentInstance,
233247
multimodalAgent: multimodalAgentInstance,
234248
mcpAppsAgent: mcpAppsAgentInstance,
249+
byocHashbrownAgent: byocHashbrownAgentInstance,
235250
};
236251

237252
// Guard against silent shadowing: if Mastra ever registers a local agent
@@ -278,6 +293,7 @@ export function buildAgents(
278293
resourceIdByAgent.set("interruptAgent", "mastra-interruptAgent");
279294
resourceIdByAgent.set("multimodalAgent", "mastra-multimodalAgent");
280295
resourceIdByAgent.set("mcpAppsAgent", "mastra-mcpAppsAgent");
296+
resourceIdByAgent.set("byocHashbrownAgent", "mastra-byocHashbrownAgent");
281297

282298
const demoAliases: Record<
283299
string,

0 commit comments

Comments
 (0)