Skip to content

Commit 594c7c1

Browse files
committed
add example of gradio in streaming agent with mcp cases
1 parent 91c62c1 commit 594c7c1

File tree

7 files changed

+2271
-1224
lines changed

7 files changed

+2271
-1224
lines changed

examples/gradio_example/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Streaming Agent Gradio Example
2+
3+
This example demonstrates how to create a streaming agent interface using Gradio.
4+
5+
Run the example via:
6+
7+
```
8+
uv run --dev python examples/gradio_example/main.py
9+
```
10+
11+
## Details
12+
13+
This example shows how to integrate streaming agents with Gradio's chat interface, allowing real-time conversation with AI agents through a web UI.
14+
15+
This demo snapshow is showed below:
16+
17+
![Demo](demo.png)
18+
19+
We using EventHandler to process each stream event and show a ChatMessage with metadata to explain the current process part of agent.

examples/gradio_example/__init__.py

Whitespace-only changes.

examples/gradio_example/demo.png

161 KB
Loading
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
# Abstract event handler
2+
from abc import ABC, abstractmethod
3+
from typing import Optional
4+
5+
from gradio.components import ChatMessage
6+
from openai.types.responses import ResponseTextDeltaEvent
7+
8+
from agents import StreamEvent, ToolCallItem, ToolCallOutputItem
9+
10+
11+
class EventHandler(ABC):
12+
"""Abstract event handler"""
13+
14+
def __init__(self):
15+
self._next_handler: Optional[EventHandler] = None
16+
17+
def set_next(self, handler: 'EventHandler') -> 'EventHandler':
18+
"""Set next handler"""
19+
self._next_handler = handler
20+
return handler
21+
22+
def handle(self, event:StreamEvent, messages: list, context: dict) -> bool:
23+
"""
24+
Process event, return True if handled, False if not handled
25+
"""
26+
if self.can_handle(event):
27+
return self._process(event, messages, context)
28+
elif self._next_handler:
29+
return self._next_handler.handle(event, messages, context)
30+
return False
31+
32+
@abstractmethod
33+
def can_handle(self, event:StreamEvent) -> bool:
34+
"""Determine if this event can be handled"""
35+
pass
36+
37+
@abstractmethod
38+
def _process(self, event:StreamEvent, messages: list, context: dict) -> bool:
39+
"""Specific processing logic"""
40+
pass
41+
42+
# Tool call handler
43+
class ToolCallHandler(EventHandler):
44+
"""Handle tool call events"""
45+
46+
def can_handle(self, event) -> bool:
47+
return (hasattr(event, 'type') and event.type == "run_item_stream_event"
48+
and hasattr(event, 'name') and event.name == "tool_called"
49+
and isinstance(event.item, ToolCallItem))
50+
51+
52+
def _process(self, event, messages: list, context: dict) -> bool:
53+
tool_name = getattr(event.item.raw_item, "name", "unknown_tool")
54+
getattr(event.item.raw_item, "server_label", "unknown_server")
55+
arguments = getattr(event.item.raw_item, "arguments", {})
56+
# Regular tool call
57+
tool_msg = f"🛠️ {tool_name} parameters: {arguments}"
58+
tool_title = "🛠️ Tool Call"
59+
60+
# Add to message list in context
61+
context["current_messages"].append(ChatMessage(
62+
role="assistant",
63+
content=tool_msg,
64+
metadata={"title": tool_title}
65+
))
66+
return True
67+
68+
# Tool output handler
69+
class ToolOutputHandler(EventHandler):
70+
"""Handle tool output events"""
71+
72+
def can_handle(self, event) -> bool:
73+
return (hasattr(event, 'type') and event.type == "run_item_stream_event"
74+
and hasattr(event, 'name') and event.name == "tool_output"
75+
and isinstance(event.item, ToolCallOutputItem))
76+
77+
78+
def _process(self, event, messages: list, context: dict) -> bool:
79+
tool_output = getattr(event.item, "output", "unknown_output")
80+
# Regular tool output
81+
tool_msg = f"🛠️ {tool_output}"
82+
tool_title = "🛠️ Tool Output"
83+
84+
# Add to message list in context
85+
context["current_messages"].append(ChatMessage(
86+
role="assistant",
87+
content=tool_msg,
88+
metadata={"title": tool_title}
89+
))
90+
return True
91+
92+
# Agent Update handler
93+
class AgentUpdateHandler(EventHandler):
94+
"""Handle agent update events"""
95+
96+
def can_handle(self, event) -> bool:
97+
return (hasattr(event, 'type') and event.type == "agent_updated_stream_event")
98+
99+
def _process(self, event, messages: list, context: dict) -> bool:
100+
agent_name = getattr(event.new_agent, "name", "unknown_agent")
101+
agent_update_msg = f"🤖 Agent {agent_name} updated"
102+
103+
# Add to message list in context
104+
context["current_messages"].append(ChatMessage(
105+
role="assistant",
106+
content=agent_update_msg,
107+
metadata={"title": "🤖 Agent Update"}
108+
))
109+
return True
110+
111+
# Handoff handler
112+
class HandoffHandler(EventHandler):
113+
"""Handle agent handoff request events"""
114+
115+
def can_handle(self, event) -> bool:
116+
return (hasattr(event, 'type') and event.type == "run_item_stream_event"
117+
and hasattr(event, 'name') and event.name == "handoff_requested")
118+
119+
def _process(self, event, messages: list, context: dict) -> bool:
120+
handoff_from_func = getattr(event.item.raw_item, "name", "unknown")
121+
handoff_msg = f"🤝 Handoff requested from {handoff_from_func}"
122+
123+
# Add to message list in context
124+
context["current_messages"].append(ChatMessage(
125+
role="assistant",
126+
content=handoff_msg,
127+
metadata={"title": "🤝 Handoff Requested"}
128+
))
129+
return True
130+
131+
# Handoff completion handler
132+
class HandoffOccuredHandler(EventHandler):
133+
"""Handle agent handoff completion events"""
134+
135+
def can_handle(self, event) -> bool:
136+
return (hasattr(event, 'type') and event.type == "run_item_stream_event"
137+
and hasattr(event, 'name') and event.name == "handoff_occured")
138+
139+
def _process(self, event, messages: list, context: dict) -> bool:
140+
agent_from = getattr(event.item.source_agent, "name", "unknown")
141+
agent_to = getattr(event.item.target_agent, "name", "unknown")
142+
handoff_msg = f"🤝 Handoff completed from {agent_from} to {agent_to}"
143+
144+
# Add to message list in context
145+
context["current_messages"].append(ChatMessage(
146+
role="assistant",
147+
content=handoff_msg,
148+
metadata={"title": "🤝 Handoff Completed"}
149+
))
150+
return True
151+
152+
# Reasoning handler
153+
class ReasoningHandler(EventHandler):
154+
"""Handle reasoning events"""
155+
156+
def can_handle(self, event) -> bool:
157+
return (hasattr(event, 'type') and event.type == "run_item_stream_event"
158+
and hasattr(event, 'name') and event.name == "reasoning_item_created")
159+
160+
def _process(self, event, messages: list, context: dict) -> bool:
161+
reasoning = getattr(event.item.raw_item, "summary", None)
162+
if reasoning:
163+
summary_text = "\n".join([s.text for s in reasoning])
164+
else:
165+
summary_text = str(event.item.raw_item)
166+
print(f"🧠 Reasoning: {summary_text}")
167+
168+
# Add to message list in context
169+
context["current_messages"].append(ChatMessage(
170+
role="assistant",
171+
content=summary_text,
172+
metadata={"title": "🧠 Reasoning"}
173+
))
174+
return True
175+
176+
177+
178+
# MCP approval handler
179+
class MCPApprovalHandler(EventHandler):
180+
"""Handle MCP approval events"""
181+
182+
def can_handle(self, event) -> bool:
183+
return (hasattr(event, 'type') and event.type == "run_item_stream_event"
184+
and hasattr(event, 'name') and event.name == "mcp_approval_requested")
185+
186+
def _process(self, event, messages: list, context: dict) -> bool:
187+
mcp_approval_args = getattr(event.item.raw_item, "arguments", "unknown_arguments")
188+
mcp_approval_name = getattr(event.item.raw_item, "name", "unknown_name")
189+
mcp_approval_msg = f"🔧 MCP approval requested: {mcp_approval_args} for {mcp_approval_name}"
190+
mcp_approval_title = f"🔧 MCP approval requested: {mcp_approval_name}"
191+
print(mcp_approval_msg)
192+
193+
# Add to message list in context
194+
context["current_messages"].append(ChatMessage(
195+
role="assistant",
196+
content=mcp_approval_msg,
197+
metadata={"title": mcp_approval_title}
198+
))
199+
return True
200+
201+
# MCP tool list handler
202+
class MCPListToolsHandler(EventHandler):
203+
"""Handle MCP tool list events"""
204+
205+
def can_handle(self, event) -> bool:
206+
return (hasattr(event, 'type') and event.type == "run_item_stream_event"
207+
and hasattr(event, 'name') and event.name == "mcp_list_tools")
208+
209+
def _process(self, event, messages: list, context: dict) -> bool:
210+
mcp_list_tools_name_list = [tool.name for tool in event.item.raw_item.tools]
211+
[f"tool: {tool.name} with description {tool.description} \n" for tool in event.item.raw_item.tools]
212+
mcp_list_tools_msg = f"🔧 MCP Tools: {', '.join(mcp_list_tools_name_list)}"
213+
mcp_list_tools_title = f"🔧 MCP Tools: {mcp_list_tools_name_list}"
214+
print(mcp_list_tools_msg)
215+
216+
# Add to message list in context
217+
context["current_messages"].append(ChatMessage(
218+
role="assistant",
219+
content=mcp_list_tools_msg,
220+
metadata={"title": mcp_list_tools_title}
221+
))
222+
return True
223+
224+
# Text response handler
225+
class TextResponseHandler(EventHandler):
226+
"""Handle text response events"""
227+
228+
def can_handle(self, event) -> bool:
229+
return (hasattr(event, 'type') and event.type == "raw_response_event"
230+
and isinstance(event.data, ResponseTextDeltaEvent))
231+
232+
def _process(self, event, messages: list, context: dict) -> bool:
233+
response_buffer = context.get("response_buffer", "")
234+
response_buffer += event.data.delta
235+
context["response_buffer"] = response_buffer
236+
237+
# Find or create text response message
238+
text_message_found = False
239+
for i, msg in enumerate(context["current_messages"]):
240+
if (hasattr(msg, 'role') and msg.role == "assistant"
241+
and (not hasattr(msg, 'metadata') or not msg.metadata)):
242+
# Update existing text message
243+
context["current_messages"][i] = ChatMessage(
244+
role="assistant",
245+
content=response_buffer
246+
)
247+
text_message_found = True
248+
break
249+
250+
# If no text message found, create a new one
251+
if not text_message_found:
252+
context["current_messages"].append(ChatMessage(
253+
role="assistant",
254+
content=response_buffer
255+
))
256+
257+
return True
258+
259+
# Event handler chain
260+
class EventHandlerChain:
261+
"""Event handler chain manager"""
262+
263+
def __init__(self):
264+
self._first_handler: Optional[EventHandler] = None
265+
self._setup_chain()
266+
267+
def _setup_chain(self):
268+
"""Setup handler chain"""
269+
self._first_handler = ToolCallHandler()
270+
self._first_handler.set_next(ToolOutputHandler()) \
271+
.set_next(AgentUpdateHandler()) \
272+
.set_next(HandoffHandler()) \
273+
.set_next(HandoffOccuredHandler()) \
274+
.set_next(MCPApprovalHandler()) \
275+
.set_next(MCPListToolsHandler()) \
276+
.set_next(ReasoningHandler()) \
277+
.set_next(TextResponseHandler())
278+
279+
def process_event(self, event, messages: list, context: dict) -> bool:
280+
"""Process event"""
281+
if self._first_handler:
282+
return self._first_handler.handle(event, messages, context)
283+
return False

0 commit comments

Comments
 (0)