tinyagent-py 0.0.1__py3-none-any.whl → 0.0.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hooks/__init__.py +4 -0
- hooks/agno_storage_hook.py +128 -0
- hooks/gradio_callback.py +966 -0
- hooks/logging_manager.py +213 -0
- hooks/rich_ui_callback.py +559 -0
- storage/__init__.py +7 -0
- storage/agno_storage.py +114 -0
- storage/base.py +49 -0
- storage/json_file_storage.py +30 -0
- storage/postgres_storage.py +201 -0
- storage/redis_storage.py +48 -0
- storage/sqlite_storage.py +156 -0
- tinyagent_py-0.0.4.dist-info/METADATA +252 -0
- tinyagent_py-0.0.4.dist-info/RECORD +17 -0
- {tinyagent_py-0.0.1.dist-info → tinyagent_py-0.0.4.dist-info}/WHEEL +1 -1
- tinyagent_py-0.0.4.dist-info/top_level.txt +2 -0
- tinyagent/__init__.py +0 -4
- tinyagent/mcp_client.py +0 -52
- tinyagent/tiny_agent.py +0 -247
- tinyagent_py-0.0.1.dist-info/METADATA +0 -79
- tinyagent_py-0.0.1.dist-info/RECORD +0 -8
- tinyagent_py-0.0.1.dist-info/top_level.txt +0 -1
- {tinyagent_py-0.0.1.dist-info → tinyagent_py-0.0.4.dist-info}/licenses/LICENSE +0 -0
hooks/gradio_callback.py
ADDED
@@ -0,0 +1,966 @@
|
|
1
|
+
import asyncio
|
2
|
+
import json
|
3
|
+
import logging
|
4
|
+
import os
|
5
|
+
import re
|
6
|
+
import shutil
|
7
|
+
import time
|
8
|
+
from pathlib import Path
|
9
|
+
from typing import Any, Dict, List, Optional, Set, Union
|
10
|
+
|
11
|
+
import tiktoken
|
12
|
+
from tinyagent import TinyAgent
|
13
|
+
import gradio as gr
|
14
|
+
from gradio import ChatMessage
|
15
|
+
|
16
|
+
# Check if gradio is available
|
17
|
+
try:
|
18
|
+
import gradio as gr
|
19
|
+
|
20
|
+
except ImportError:
|
21
|
+
raise ModuleNotFoundError(
|
22
|
+
"Please install 'gradio' to use the GradioCallback: `pip install gradio`"
|
23
|
+
)
|
24
|
+
|
25
|
+
|
26
|
+
class GradioCallback:
|
27
|
+
"""
|
28
|
+
A callback for TinyAgent that provides a Gradio web interface.
|
29
|
+
This allows for interactive chat with the agent through a web UI.
|
30
|
+
"""
|
31
|
+
|
32
|
+
def __init__(
|
33
|
+
self,
|
34
|
+
file_upload_folder: Optional[str] = None,
|
35
|
+
allowed_file_types: Optional[List[str]] = None,
|
36
|
+
show_thinking: bool = True,
|
37
|
+
show_tool_calls: bool = True,
|
38
|
+
logger: Optional[logging.Logger] = None,
|
39
|
+
):
|
40
|
+
"""
|
41
|
+
Initialize the Gradio callback.
|
42
|
+
|
43
|
+
Args:
|
44
|
+
file_upload_folder: Optional folder to store uploaded files
|
45
|
+
allowed_file_types: List of allowed file extensions (default: [".pdf", ".docx", ".txt"])
|
46
|
+
show_thinking: Whether to show the thinking process
|
47
|
+
show_tool_calls: Whether to show tool calls
|
48
|
+
logger: Optional logger to use
|
49
|
+
"""
|
50
|
+
self.logger = logger or logging.getLogger(__name__)
|
51
|
+
self.show_thinking = show_thinking
|
52
|
+
self.show_tool_calls = show_tool_calls
|
53
|
+
|
54
|
+
# File upload settings
|
55
|
+
self.file_upload_folder = Path(file_upload_folder) if file_upload_folder else None
|
56
|
+
self.allowed_file_types = allowed_file_types or [".pdf", ".docx", ".txt"]
|
57
|
+
|
58
|
+
if self.file_upload_folder and not self.file_upload_folder.exists():
|
59
|
+
self.file_upload_folder.mkdir(parents=True, exist_ok=True)
|
60
|
+
self.logger.info(f"Created file upload folder: {self.file_upload_folder}")
|
61
|
+
|
62
|
+
# Initialize tiktoken encoder for token counting
|
63
|
+
try:
|
64
|
+
self.encoder = tiktoken.get_encoding("o200k_base")
|
65
|
+
self.logger.debug("Initialized tiktoken encoder with o200k_base encoding")
|
66
|
+
except Exception as e:
|
67
|
+
self.logger.error(f"Failed to initialize tiktoken encoder: {e}")
|
68
|
+
self.encoder = None
|
69
|
+
|
70
|
+
# State tracking for the current agent interaction
|
71
|
+
self.current_agent = None
|
72
|
+
self.current_user_input = ""
|
73
|
+
self.thinking_content = ""
|
74
|
+
self.tool_calls = []
|
75
|
+
self.tool_call_details = []
|
76
|
+
self.assistant_text_responses = []
|
77
|
+
self.token_usage = {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
|
78
|
+
self.is_running = False
|
79
|
+
self.last_update_yield_time = 0
|
80
|
+
|
81
|
+
# References to Gradio UI components (will be set in create_app)
|
82
|
+
self._chatbot_component = None
|
83
|
+
self._token_usage_component = None
|
84
|
+
|
85
|
+
self.logger.debug("GradioCallback initialized")
|
86
|
+
|
87
|
+
def count_tokens(self, text: str) -> int:
|
88
|
+
"""Count tokens in a string using tiktoken."""
|
89
|
+
if not self.encoder or not text:
|
90
|
+
return 0
|
91
|
+
try:
|
92
|
+
return len(self.encoder.encode(text))
|
93
|
+
except Exception as e:
|
94
|
+
self.logger.error(f"Error counting tokens: {e}")
|
95
|
+
return 0
|
96
|
+
|
97
|
+
async def __call__(self, event_name: str, agent: Any, **kwargs: Any) -> None:
|
98
|
+
"""
|
99
|
+
Process events from the TinyAgent.
|
100
|
+
|
101
|
+
Args:
|
102
|
+
event_name: The name of the event
|
103
|
+
agent: The TinyAgent instance
|
104
|
+
**kwargs: Additional event data
|
105
|
+
"""
|
106
|
+
self.logger.debug(f"Callback Event: {event_name}")
|
107
|
+
self.current_agent = agent
|
108
|
+
|
109
|
+
if event_name == "agent_start":
|
110
|
+
await self._handle_agent_start(agent, **kwargs)
|
111
|
+
elif event_name == "message_add":
|
112
|
+
await self._handle_message_add(agent, **kwargs)
|
113
|
+
elif event_name == "llm_start":
|
114
|
+
await self._handle_llm_start(agent, **kwargs)
|
115
|
+
elif event_name == "llm_end":
|
116
|
+
await self._handle_llm_end(agent, **kwargs)
|
117
|
+
elif event_name == "agent_end":
|
118
|
+
await self._handle_agent_end(agent, **kwargs)
|
119
|
+
|
120
|
+
async def _handle_agent_start(self, agent: Any, **kwargs: Any) -> None:
|
121
|
+
"""Handle the agent_start event. Reset state."""
|
122
|
+
self.logger.debug("Handling agent_start event")
|
123
|
+
self.current_user_input = kwargs.get("user_input", "")
|
124
|
+
self.thinking_content = ""
|
125
|
+
self.tool_calls = []
|
126
|
+
self.tool_call_details = []
|
127
|
+
self.assistant_text_responses = []
|
128
|
+
self.token_usage = {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
|
129
|
+
self.is_running = True
|
130
|
+
self.last_update_yield_time = 0
|
131
|
+
self.logger.debug(f"Agent started for input: {self.current_user_input[:50]}...")
|
132
|
+
|
133
|
+
async def _handle_message_add(self, agent: Any, **kwargs: Any) -> None:
|
134
|
+
"""Handle the message_add event. Store message details."""
|
135
|
+
message = kwargs.get("message", {})
|
136
|
+
role = message.get("role", "unknown")
|
137
|
+
self.logger.debug(f"Handling message_add event: {role}")
|
138
|
+
current_time = asyncio.get_event_loop().time()
|
139
|
+
|
140
|
+
if role == "assistant":
|
141
|
+
if "tool_calls" in message and message.get("tool_calls"):
|
142
|
+
self.logger.debug(f"Processing {len(message['tool_calls'])} tool calls")
|
143
|
+
for tool_call in message["tool_calls"]:
|
144
|
+
function_info = tool_call.get("function", {})
|
145
|
+
tool_name = function_info.get("name", "unknown")
|
146
|
+
args = function_info.get("arguments", "{}")
|
147
|
+
tool_id = tool_call.get("id", "unknown")
|
148
|
+
|
149
|
+
try:
|
150
|
+
# Attempt pretty formatting, fallback to raw string
|
151
|
+
parsed_args = json.loads(args)
|
152
|
+
formatted_args = json.dumps(parsed_args, indent=2)
|
153
|
+
except json.JSONDecodeError:
|
154
|
+
formatted_args = args # Keep as is if not valid JSON
|
155
|
+
|
156
|
+
token_count = self.count_tokens(f"{tool_name}({formatted_args})") # Count formatted
|
157
|
+
|
158
|
+
# Add to detailed tool call info if not already present by ID
|
159
|
+
if not any(tc['id'] == tool_id for tc in self.tool_call_details):
|
160
|
+
self.tool_call_details.append({
|
161
|
+
"id": tool_id,
|
162
|
+
"name": tool_name,
|
163
|
+
"arguments": formatted_args,
|
164
|
+
"result": None,
|
165
|
+
"token_count": token_count,
|
166
|
+
"result_token_count": 0,
|
167
|
+
"timestamp": current_time,
|
168
|
+
"result_timestamp": None
|
169
|
+
})
|
170
|
+
self.logger.debug(f"Added tool call detail: {tool_name} (ID: {tool_id}, Tokens: {token_count})")
|
171
|
+
|
172
|
+
# If this is a final_answer or ask_question tool, we'll handle it specially later
|
173
|
+
# when the result comes in
|
174
|
+
else:
|
175
|
+
self.logger.debug(f"Tool call detail already exists for ID: {tool_id}")
|
176
|
+
|
177
|
+
elif "content" in message and message.get("content"):
|
178
|
+
content = message["content"]
|
179
|
+
token_count = self.count_tokens(content)
|
180
|
+
self.assistant_text_responses.append({
|
181
|
+
"content": content,
|
182
|
+
"token_count": token_count,
|
183
|
+
"timestamp": current_time
|
184
|
+
})
|
185
|
+
self.logger.debug(f"Added assistant text response: {content[:50]}... (Tokens: {token_count})")
|
186
|
+
|
187
|
+
elif role == "tool":
|
188
|
+
tool_name = message.get("name", "unknown")
|
189
|
+
content = message.get("content", "")
|
190
|
+
tool_call_id = message.get("tool_call_id", None)
|
191
|
+
token_count = self.count_tokens(content)
|
192
|
+
|
193
|
+
if tool_call_id:
|
194
|
+
updated = False
|
195
|
+
for tool_detail in self.tool_call_details:
|
196
|
+
if tool_detail["id"] == tool_call_id:
|
197
|
+
tool_detail["result"] = content
|
198
|
+
tool_detail["result_token_count"] = token_count
|
199
|
+
tool_detail["result_timestamp"] = current_time
|
200
|
+
self.logger.debug(f"Updated tool call {tool_call_id} with result (Tokens: {token_count})")
|
201
|
+
|
202
|
+
# Special handling for final_answer and ask_question tools
|
203
|
+
# Add their results directly as assistant messages in the chat
|
204
|
+
if tool_detail["name"] in ["final_answer", "ask_question"]:
|
205
|
+
self.assistant_text_responses.append({
|
206
|
+
"content": content,
|
207
|
+
"token_count": token_count,
|
208
|
+
"timestamp": current_time,
|
209
|
+
"from_tool": True,
|
210
|
+
"tool_name": tool_detail["name"]
|
211
|
+
})
|
212
|
+
self.logger.debug(f"Added {tool_detail['name']} result as assistant message")
|
213
|
+
|
214
|
+
updated = True
|
215
|
+
break
|
216
|
+
if not updated:
|
217
|
+
self.logger.warning(f"Received tool result for unknown tool_call_id: {tool_call_id}")
|
218
|
+
else:
|
219
|
+
self.logger.warning(f"Received tool result without tool_call_id: {tool_name}")
|
220
|
+
|
221
|
+
async def _handle_llm_start(self, agent: Any, **kwargs: Any) -> None:
|
222
|
+
"""Handle the llm_start event."""
|
223
|
+
self.logger.debug("Handling llm_start event")
|
224
|
+
# Optionally clear previous thinking content if desired per LLM call
|
225
|
+
# self.thinking_content = ""
|
226
|
+
|
227
|
+
async def _handle_llm_end(self, agent: Any, **kwargs: Any) -> None:
|
228
|
+
"""Handle the llm_end event. Store thinking content and token usage."""
|
229
|
+
self.logger.debug("Handling llm_end event")
|
230
|
+
response = kwargs.get("response", {})
|
231
|
+
|
232
|
+
# Extract thinking content (often the raw message content before tool parsing)
|
233
|
+
try:
|
234
|
+
message = response.choices[0].message
|
235
|
+
# Only update thinking if there's actual content and no tool calls in this specific message
|
236
|
+
# Tool calls are handled separately via message_add
|
237
|
+
if hasattr(message, "content") and message.content and not getattr(message, "tool_calls", None):
|
238
|
+
# Check if this content is already in assistant_text_responses to avoid duplication
|
239
|
+
if not any(resp['content'] == message.content for resp in self.assistant_text_responses):
|
240
|
+
self.thinking_content = message.content # Store as potential thinking
|
241
|
+
self.logger.debug(f"Stored potential thinking content: {self.thinking_content[:50]}...")
|
242
|
+
else:
|
243
|
+
self.logger.debug("Content from llm_end already captured as assistant response.")
|
244
|
+
|
245
|
+
except (AttributeError, IndexError, TypeError) as e:
|
246
|
+
self.logger.debug(f"Could not extract thinking content from llm_end: {e}")
|
247
|
+
|
248
|
+
# Track token usage
|
249
|
+
try:
|
250
|
+
usage = response.usage
|
251
|
+
self.logger.debug(f"Token usage: {usage}")
|
252
|
+
if usage:
|
253
|
+
prompt_tokens = getattr(usage, "prompt_tokens", 0)
|
254
|
+
completion_tokens = getattr(usage, "completion_tokens", 0)
|
255
|
+
self.token_usage["prompt_tokens"] += prompt_tokens
|
256
|
+
self.token_usage["completion_tokens"] += completion_tokens
|
257
|
+
# Recalculate total based on potentially cumulative prompt/completion
|
258
|
+
self.token_usage["total_tokens"] = self.token_usage["prompt_tokens"] + self.token_usage["completion_tokens"]
|
259
|
+
self.logger.debug(f"Updated token usage: Prompt +{prompt_tokens}, Completion +{completion_tokens}. Total: {self.token_usage}")
|
260
|
+
except (AttributeError, TypeError) as e:
|
261
|
+
self.logger.debug(f"Could not extract token usage from llm_end: {e}")
|
262
|
+
|
263
|
+
async def _handle_agent_end(self, agent: Any, **kwargs: Any) -> None:
|
264
|
+
"""Handle the agent_end event. Mark agent as not running."""
|
265
|
+
self.logger.debug("Handling agent_end event")
|
266
|
+
self.is_running = False
|
267
|
+
# Final result is handled by interact_with_agent after agent.run completes
|
268
|
+
self.logger.debug(f"Agent finished. Final result: {kwargs.get('result', 'N/A')[:50]}...")
|
269
|
+
|
270
|
+
def upload_file(self, file, file_uploads_log):
|
271
|
+
"""
|
272
|
+
Handle file uploads in the Gradio interface.
|
273
|
+
|
274
|
+
Args:
|
275
|
+
file: The uploaded file
|
276
|
+
file_uploads_log: List of previously uploaded files
|
277
|
+
|
278
|
+
Returns:
|
279
|
+
Tuple of (status_message, updated_file_uploads_log)
|
280
|
+
"""
|
281
|
+
if file is None:
|
282
|
+
return gr.Textbox(value="No file uploaded", visible=True), file_uploads_log
|
283
|
+
|
284
|
+
file_ext = os.path.splitext(file.name)[1].lower()
|
285
|
+
if file_ext not in self.allowed_file_types:
|
286
|
+
return gr.Textbox("File type not allowed", visible=True), file_uploads_log
|
287
|
+
|
288
|
+
original_name = os.path.basename(file.name)
|
289
|
+
sanitized_name = re.sub(r"[^\w\-.]", "_", original_name)
|
290
|
+
|
291
|
+
file_path = os.path.join(self.file_upload_folder, sanitized_name)
|
292
|
+
shutil.copy(file.name, file_path)
|
293
|
+
|
294
|
+
return gr.Textbox(f"File uploaded: {file_path}", visible=True), file_uploads_log + [file_path]
|
295
|
+
|
296
|
+
def log_user_message(self, message, file_uploads_log):
|
297
|
+
"""
|
298
|
+
Process user message, add files, and update chatbot history.
|
299
|
+
This now ONLY prepares the input and adds the user message to the chat.
|
300
|
+
It disables the send button while processing.
|
301
|
+
|
302
|
+
Args:
|
303
|
+
message: User message text
|
304
|
+
file_uploads_log: List of uploaded files
|
305
|
+
|
306
|
+
Returns:
|
307
|
+
Tuple of (processed_message, initial_chatbot_state, disable_send_button)
|
308
|
+
"""
|
309
|
+
processed_message = message
|
310
|
+
# Check if there are file references to add to the message
|
311
|
+
if file_uploads_log and len(file_uploads_log) > 0:
|
312
|
+
file_list = "\n".join([f"- {os.path.basename(f)}" for f in file_uploads_log])
|
313
|
+
processed_message = f"{message}\n\nFiles available:\n{file_list}"
|
314
|
+
|
315
|
+
# Prepare the initial chatbot state for this turn
|
316
|
+
# Assumes chatbot history is passed correctly or managed via gr.State
|
317
|
+
# For simplicity, let's assume we get the history and append to it.
|
318
|
+
# We need the actual chatbot component value here.
|
319
|
+
# This part is tricky without direct access in this function signature.
|
320
|
+
# Let's modify interact_with_agent to handle this.
|
321
|
+
|
322
|
+
# Just return the processed message and disable the button
|
323
|
+
# The chatbot update will happen in interact_with_agent
|
324
|
+
return processed_message, gr.Button(interactive=False)
|
325
|
+
|
326
|
+
def _build_current_assistant_message(self) -> str:
|
327
|
+
"""
|
328
|
+
Construct the content for the assistant's message bubble based on current state.
|
329
|
+
Prioritizes: Latest Text Response > Tool Calls > Thinking Content.
|
330
|
+
"""
|
331
|
+
parts = []
|
332
|
+
display_content = "Thinking..." # Default if nothing else is available yet
|
333
|
+
|
334
|
+
# Sort details for consistent display order
|
335
|
+
sorted_tool_details = sorted(self.tool_call_details, key=lambda x: x.get("timestamp", 0))
|
336
|
+
sorted_text_responses = sorted(self.assistant_text_responses, key=lambda x: x.get("timestamp", 0))
|
337
|
+
|
338
|
+
# 1. Get the latest assistant text response (if any)
|
339
|
+
if sorted_text_responses:
|
340
|
+
display_content = sorted_text_responses[-1]["content"]
|
341
|
+
parts.append(display_content)
|
342
|
+
# If there's no text response yet, but we have tool calls or thinking, use a placeholder
|
343
|
+
elif sorted_tool_details or (self.show_thinking and self.thinking_content):
|
344
|
+
parts.append("Working on it...") # More informative than just "Thinking..."
|
345
|
+
|
346
|
+
# 2. Add Tool Call details (if enabled and available)
|
347
|
+
if self.show_tool_calls and sorted_tool_details:
|
348
|
+
for i, tool_detail in enumerate(sorted_tool_details):
|
349
|
+
tool_name = tool_detail["name"]
|
350
|
+
arguments = tool_detail["arguments"]
|
351
|
+
result = tool_detail["result"]
|
352
|
+
result_status = "⏳ Processing..." if result is None else "✅ Done"
|
353
|
+
input_tokens = tool_detail.get("token_count", 0)
|
354
|
+
output_tokens = tool_detail.get("result_token_count", 0)
|
355
|
+
|
356
|
+
# Special handling for final_answer and ask_question tools
|
357
|
+
#if tool_name in ["final_answer", "ask_question"] and result is not None:
|
358
|
+
# Don't add these as tool calls, they'll be shown as regular messages
|
359
|
+
#continue
|
360
|
+
|
361
|
+
# Create collapsible tool call section using Gradio's markdown format
|
362
|
+
parts.append(f"\n\n<details><summary>🛠️ **Tool: {tool_name}** ({result_status}) - {input_tokens+output_tokens} tokens</summary>")
|
363
|
+
parts.append(f"\n\n**Input Arguments:**\n```json\n{arguments}\n```")
|
364
|
+
|
365
|
+
if result is not None:
|
366
|
+
parts.append(f"\n\n**Output:** ({output_tokens} tokens)\n```\n{result}\n```")
|
367
|
+
|
368
|
+
parts.append("\n</details>")
|
369
|
+
|
370
|
+
# 3. Add Thinking Process (if enabled and available, and no text response yet)
|
371
|
+
# Only show thinking if there isn't a more concrete text response or tool call happening
|
372
|
+
if self.show_thinking and self.thinking_content and not sorted_text_responses and not sorted_tool_details:
|
373
|
+
parts.append("\n\n<details><summary>🧠 **Thinking Process**</summary>\n\n```\n" + self.thinking_content + "\n```\n</details>")
|
374
|
+
|
375
|
+
# If parts is empty after all checks, use the initial display_content
|
376
|
+
if not parts:
|
377
|
+
return display_content
|
378
|
+
else:
|
379
|
+
return "".join(parts)
|
380
|
+
|
381
|
+
def _get_token_usage_text(self) -> str:
|
382
|
+
"""Format the token usage string."""
|
383
|
+
if not any(self.token_usage.values()):
|
384
|
+
return "Tokens: 0"
|
385
|
+
return (f"Tokens: I {self.token_usage['prompt_tokens']} | " +
|
386
|
+
f"O {self.token_usage['completion_tokens']} | " +
|
387
|
+
f"Total {self.token_usage['total_tokens']}")
|
388
|
+
|
389
|
+
async def interact_with_agent(self, user_input_processed, chatbot_history):
|
390
|
+
"""
|
391
|
+
Process user input, interact with the agent, and stream updates to Gradio UI.
|
392
|
+
Each tool call and response will be shown as a separate message.
|
393
|
+
"""
|
394
|
+
self.logger.info(f"Starting interaction for: {user_input_processed[:50]}...")
|
395
|
+
|
396
|
+
# 1. Add user message to chatbot history as a ChatMessage
|
397
|
+
chatbot_history.append(
|
398
|
+
ChatMessage(role="user", content=user_input_processed)
|
399
|
+
)
|
400
|
+
|
401
|
+
# Initial yield to show user message
|
402
|
+
yield chatbot_history, self._get_token_usage_text()
|
403
|
+
|
404
|
+
# Kick off the agent in the background
|
405
|
+
loop = asyncio.get_event_loop()
|
406
|
+
agent_task = asyncio.create_task(self.current_agent.run(user_input_processed))
|
407
|
+
|
408
|
+
displayed_tool_calls = set()
|
409
|
+
displayed_text_responses = set()
|
410
|
+
thinking_message_added = False
|
411
|
+
update_interval = 0.3
|
412
|
+
min_yield_interval = 0.2
|
413
|
+
|
414
|
+
# Track tool calls that are in progress (showing "working...")
|
415
|
+
in_progress_tool_calls = {}
|
416
|
+
|
417
|
+
while not agent_task.done():
|
418
|
+
now = time.time()
|
419
|
+
if now - self.last_update_yield_time >= min_yield_interval:
|
420
|
+
sorted_tool_details = sorted(self.tool_call_details, key=lambda x: x.get("timestamp", 0))
|
421
|
+
sorted_text_responses = sorted(self.assistant_text_responses, key=lambda x: x.get("timestamp", 0))
|
422
|
+
|
423
|
+
# → New assistant text chunks
|
424
|
+
for resp in sorted_text_responses:
|
425
|
+
content = resp["content"]
|
426
|
+
if content not in displayed_text_responses:
|
427
|
+
chatbot_history.append(
|
428
|
+
ChatMessage(role="assistant", content=content)
|
429
|
+
)
|
430
|
+
displayed_text_responses.add(content)
|
431
|
+
self.logger.debug(f"Added new text response: {content[:50]}...")
|
432
|
+
|
433
|
+
# → Thinking placeholder (optional)
|
434
|
+
if self.show_thinking and self.thinking_content \
|
435
|
+
and not thinking_message_added \
|
436
|
+
and not displayed_text_responses:
|
437
|
+
thinking_msg = (
|
438
|
+
"Working on it...\n\n"
|
439
|
+
"```"
|
440
|
+
f"{self.thinking_content}"
|
441
|
+
"```"
|
442
|
+
)
|
443
|
+
chatbot_history.append(
|
444
|
+
ChatMessage(role="assistant", content=thinking_msg)
|
445
|
+
)
|
446
|
+
thinking_message_added = True
|
447
|
+
self.logger.debug("Added thinking message")
|
448
|
+
|
449
|
+
# → Show tool calls with "working..." status when they start
|
450
|
+
if self.show_tool_calls:
|
451
|
+
for tool in sorted_tool_details:
|
452
|
+
tid = tool["id"]
|
453
|
+
tname = tool["name"]
|
454
|
+
|
455
|
+
# If we haven't displayed this tool call yet
|
456
|
+
if tid not in displayed_tool_calls and tid not in in_progress_tool_calls:
|
457
|
+
in_tok = tool.get("token_count", 0)
|
458
|
+
# Create "working..." message for this tool call
|
459
|
+
body = (
|
460
|
+
f"**Input Arguments:**\n```json\n{tool['arguments']}\n```\n\n"
|
461
|
+
f"**Output:** ⏳ Working...\n"
|
462
|
+
)
|
463
|
+
# Add to chatbot with "working" status
|
464
|
+
msg = ChatMessage(
|
465
|
+
role="assistant",
|
466
|
+
content=body,
|
467
|
+
metadata={
|
468
|
+
"title": f"🛠️ {tname} — {in_tok} tokens",
|
469
|
+
"status": "pending"
|
470
|
+
}
|
471
|
+
)
|
472
|
+
chatbot_history.append(msg)
|
473
|
+
# Track this tool call as in progress
|
474
|
+
in_progress_tool_calls[tid] = len(chatbot_history) - 1
|
475
|
+
self.logger.debug(f"Added in-progress tool call: {tname}")
|
476
|
+
|
477
|
+
# If this tool call has completed and we're tracking it as in-progress
|
478
|
+
elif tid in in_progress_tool_calls and tool.get("result") is not None:
|
479
|
+
# Get the position in the chatbot history
|
480
|
+
pos = in_progress_tool_calls[tid]
|
481
|
+
in_tok = tool.get("token_count", 0)
|
482
|
+
out_tok = tool.get("result_token_count", 0)
|
483
|
+
tot_tok = in_tok + out_tok
|
484
|
+
|
485
|
+
# Update the message with completed status and result
|
486
|
+
body = (
|
487
|
+
f"**Input Arguments:**\n```json\n{tool['arguments']}\n```\n\n"
|
488
|
+
f"**Output:** ({out_tok} tokens)\n```json\n{tool['result']}\n```\n"
|
489
|
+
)
|
490
|
+
# Update the existing message
|
491
|
+
chatbot_history[pos] = ChatMessage(
|
492
|
+
role="assistant",
|
493
|
+
content=body,
|
494
|
+
metadata={
|
495
|
+
"title": f"🛠️ {tname} — {tot_tok} tokens ✅",
|
496
|
+
"status": "done"
|
497
|
+
}
|
498
|
+
)
|
499
|
+
# Mark as displayed and remove from in-progress
|
500
|
+
displayed_tool_calls.add(tid)
|
501
|
+
del in_progress_tool_calls[tid]
|
502
|
+
self.logger.debug(f"Updated tool call to completed: {tname}")
|
503
|
+
|
504
|
+
# yield updated history + token usage
|
505
|
+
token_text = self._get_token_usage_text()
|
506
|
+
yield chatbot_history, token_text
|
507
|
+
self.last_update_yield_time = now
|
508
|
+
|
509
|
+
await asyncio.sleep(update_interval)
|
510
|
+
|
511
|
+
# once the agent_task is done, add its final result if any
|
512
|
+
try:
|
513
|
+
final_text = await agent_task
|
514
|
+
except Exception as e:
|
515
|
+
final_text = f"Error: {e}"
|
516
|
+
self.is_running = False
|
517
|
+
|
518
|
+
if final_text not in displayed_text_responses:
|
519
|
+
chatbot_history.append(
|
520
|
+
ChatMessage(role="assistant", content=final_text)
|
521
|
+
)
|
522
|
+
self.logger.debug(f"Added final result: {final_text[:50]}...")
|
523
|
+
|
524
|
+
# final token usage
|
525
|
+
yield chatbot_history, self._get_token_usage_text()
|
526
|
+
|
527
|
+
def _format_response(self, response_text):
|
528
|
+
"""
|
529
|
+
Format the final response with thinking process, tool calls, and token usage.
|
530
|
+
|
531
|
+
Args:
|
532
|
+
response_text: The final response text from the agent
|
533
|
+
|
534
|
+
Returns:
|
535
|
+
Formatted response string with additional information in Markdown.
|
536
|
+
"""
|
537
|
+
formatted_parts = []
|
538
|
+
|
539
|
+
# Add the main response text
|
540
|
+
formatted_parts.append(response_text)
|
541
|
+
|
542
|
+
# Sort details for consistent display order
|
543
|
+
sorted_tool_details = sorted(self.tool_call_details, key=lambda x: x.get("timestamp", 0))
|
544
|
+
|
545
|
+
# Add tool calls if enabled and details exist
|
546
|
+
if self.show_tool_calls and sorted_tool_details:
|
547
|
+
formatted_parts.append("\n\n---\n")
|
548
|
+
|
549
|
+
for i, tool_detail in enumerate(sorted_tool_details):
|
550
|
+
tool_name = tool_detail["name"]
|
551
|
+
arguments = tool_detail["arguments"]
|
552
|
+
result = tool_detail["result"] or "No result captured."
|
553
|
+
input_tokens = tool_detail.get("token_count", 0)
|
554
|
+
output_tokens = tool_detail.get("result_token_count", 0)
|
555
|
+
|
556
|
+
# Skip final_answer and ask_question tools in the tool call section
|
557
|
+
# as they're already shown as regular messages
|
558
|
+
#if tool_name in ["final_answer", "ask_question"]:
|
559
|
+
# continue
|
560
|
+
|
561
|
+
formatted_parts.append(f"\n<details><summary>🛠️ **Tool {i+1}: {tool_name}** - {input_tokens+output_tokens} tokens</summary>\n")
|
562
|
+
formatted_parts.append(f"\n**Input Arguments:**\n```json\n{arguments}\n```")
|
563
|
+
formatted_parts.append(f"\n**Output:** ({output_tokens} tokens)\n```\n{result}\n```\n</details>")
|
564
|
+
|
565
|
+
# Add thinking process if enabled and content exists
|
566
|
+
if self.show_thinking and self.thinking_content:
|
567
|
+
# Avoid showing thinking if it's identical to the final response text
|
568
|
+
if self.thinking_content.strip() != response_text.strip():
|
569
|
+
formatted_parts.append("\n\n<details><summary>🧠 **Thinking Process**</summary>\n\n```\n" + self.thinking_content + "\n```\n</details>")
|
570
|
+
|
571
|
+
# Add token usage summary
|
572
|
+
if any(self.token_usage.values()):
|
573
|
+
formatted_parts.append("\n\n---\n")
|
574
|
+
formatted_parts.append(f"**Token Usage:** Prompt: {self.token_usage['prompt_tokens']} | " +
|
575
|
+
f"Completion: {self.token_usage['completion_tokens']} | " +
|
576
|
+
f"Total: {self.token_usage['total_tokens']}")
|
577
|
+
|
578
|
+
return "".join(formatted_parts)
|
579
|
+
|
580
|
+
def create_app(self, agent: TinyAgent, title: str = "TinyAgent Chat", description: str = None):
|
581
|
+
"""
|
582
|
+
Create a Gradio app for the agent.
|
583
|
+
|
584
|
+
Args:
|
585
|
+
agent: The TinyAgent instance
|
586
|
+
title: Title for the app
|
587
|
+
description: Optional description
|
588
|
+
|
589
|
+
Returns:
|
590
|
+
A Gradio Blocks application
|
591
|
+
"""
|
592
|
+
self.logger.debug("Creating Gradio app")
|
593
|
+
self.current_agent = agent # Store agent reference
|
594
|
+
|
595
|
+
with gr.Blocks(
|
596
|
+
title=title,
|
597
|
+
theme=gr.themes.Default(font=[gr.themes.GoogleFont("Inter"), "Arial", "sans-serif"])
|
598
|
+
) as app:
|
599
|
+
file_uploads_log = gr.State([])
|
600
|
+
|
601
|
+
with gr.Row():
|
602
|
+
# -- Left Sidebar --
|
603
|
+
with gr.Column(scale=1):
|
604
|
+
gr.Markdown(f"# {title}")
|
605
|
+
if description:
|
606
|
+
gr.Markdown(description)
|
607
|
+
|
608
|
+
# 1) Collapsible File Upload Section
|
609
|
+
if self.file_upload_folder:
|
610
|
+
with gr.Accordion("Upload Files", open=False):
|
611
|
+
gr.Markdown("Upload files to be used by the agent")
|
612
|
+
file_upload = gr.File(label="Choose a file")
|
613
|
+
upload_status = gr.Textbox(label="Upload Status", visible=False, interactive=False)
|
614
|
+
file_upload.change(
|
615
|
+
fn=self.upload_file,
|
616
|
+
inputs=[file_upload, file_uploads_log],
|
617
|
+
outputs=[upload_status, file_uploads_log]
|
618
|
+
)
|
619
|
+
|
620
|
+
# 2) Available Tools Section
|
621
|
+
tools = getattr(agent, "available_tools", [])
|
622
|
+
with gr.Accordion(f"Available Tools ({len(tools)})", open=True):
|
623
|
+
|
624
|
+
if not tools:
|
625
|
+
gr.Markdown("_No tools registered_")
|
626
|
+
else:
|
627
|
+
for tool_meta in tools:
|
628
|
+
fn = tool_meta.get("function", {})
|
629
|
+
tool_name = fn.get("name", "unknown")
|
630
|
+
with gr.Accordion(tool_name, open=False):
|
631
|
+
# Description
|
632
|
+
desc = fn.get("description")
|
633
|
+
if desc:
|
634
|
+
gr.Markdown(f"**Description:** {desc}")
|
635
|
+
|
636
|
+
# JSON schema for function calling
|
637
|
+
schema = fn.get("parameters")
|
638
|
+
if schema:
|
639
|
+
gr.JSON(value=schema, label="Function Calling Schema")
|
640
|
+
else:
|
641
|
+
gr.Markdown("_No schema available_")
|
642
|
+
|
643
|
+
# 3) Thinking / Tool‐call Toggles
|
644
|
+
with gr.Group():
|
645
|
+
gr.Markdown("## Display Options")
|
646
|
+
show_thinking_checkbox = gr.Checkbox(
|
647
|
+
label="Show thinking process",
|
648
|
+
value=self.show_thinking
|
649
|
+
)
|
650
|
+
show_tool_calls_checkbox = gr.Checkbox(
|
651
|
+
label="Show tool calls",
|
652
|
+
value=self.show_tool_calls
|
653
|
+
)
|
654
|
+
show_thinking_checkbox.change(
|
655
|
+
fn=lambda x: setattr(self, "show_thinking", x),
|
656
|
+
inputs=show_thinking_checkbox,
|
657
|
+
outputs=None
|
658
|
+
)
|
659
|
+
show_tool_calls_checkbox.change(
|
660
|
+
fn=lambda x: setattr(self, "show_tool_calls", x),
|
661
|
+
inputs=show_tool_calls_checkbox,
|
662
|
+
outputs=None
|
663
|
+
)
|
664
|
+
|
665
|
+
# 4) Token Usage Display
|
666
|
+
with gr.Group():
|
667
|
+
gr.Markdown("## Token Usage")
|
668
|
+
self._token_usage_component = gr.Textbox(
|
669
|
+
label="Token Usage",
|
670
|
+
interactive=False,
|
671
|
+
value=self._get_token_usage_text()
|
672
|
+
)
|
673
|
+
|
674
|
+
# Footer
|
675
|
+
gr.Markdown(
|
676
|
+
"<div style='text-align: center; margin-top: 20px;'>"
|
677
|
+
"Powered by <a href='https://github.com/askbudi/tinyagent' target='_blank'>TinyAgent</a>"
|
678
|
+
"</div>"
|
679
|
+
)
|
680
|
+
|
681
|
+
# -- Right Chat Column (unchanged) --
|
682
|
+
with gr.Column(scale=3):
|
683
|
+
# Chat interface - Assign component to self for updates
|
684
|
+
self._chatbot_component = gr.Chatbot(
|
685
|
+
[], # Start empty
|
686
|
+
label="Chat History",
|
687
|
+
height=600,
|
688
|
+
type="messages", # Use messages type for better formatting
|
689
|
+
bubble_full_width=False,
|
690
|
+
show_copy_button=True,
|
691
|
+
render_markdown=True # Enable markdown rendering
|
692
|
+
)
|
693
|
+
|
694
|
+
with gr.Row():
|
695
|
+
user_input = gr.Textbox(
|
696
|
+
placeholder="Type your message here...",
|
697
|
+
show_label=False,
|
698
|
+
container=False,
|
699
|
+
scale=9
|
700
|
+
)
|
701
|
+
submit_btn = gr.Button("Send", scale=1, variant="primary")
|
702
|
+
|
703
|
+
# Clear button
|
704
|
+
clear_btn = gr.Button("Clear Conversation")
|
705
|
+
|
706
|
+
# Store processed input temporarily between steps
|
707
|
+
processed_input_state = gr.State("")
|
708
|
+
|
709
|
+
# Event handlers - Chained logic
|
710
|
+
# 1. Process input, disable button
|
711
|
+
submit_action = submit_btn.click(
|
712
|
+
fn=self.log_user_message,
|
713
|
+
inputs=[user_input, file_uploads_log],
|
714
|
+
outputs=[processed_input_state, submit_btn], # Store processed input, disable btn
|
715
|
+
queue=False # Run quickly
|
716
|
+
).then(
|
717
|
+
# 2. Clear the raw input box
|
718
|
+
fn=lambda: gr.Textbox(value=""),
|
719
|
+
inputs=None,
|
720
|
+
outputs=[user_input],
|
721
|
+
queue=False # Run quickly
|
722
|
+
).then(
|
723
|
+
# 3. Run the main interaction loop (this yields updates)
|
724
|
+
fn=self.interact_with_agent,
|
725
|
+
inputs=[processed_input_state, self._chatbot_component],
|
726
|
+
outputs=[self._chatbot_component, self._token_usage_component], # Update chat and tokens
|
727
|
+
queue=True # Explicitly enable queue for this async generator
|
728
|
+
).then(
|
729
|
+
# 4. Re-enable the button after interaction finishes
|
730
|
+
fn=lambda: gr.Button(interactive=True),
|
731
|
+
inputs=None,
|
732
|
+
outputs=[submit_btn],
|
733
|
+
queue=False # Run quickly
|
734
|
+
)
|
735
|
+
|
736
|
+
# Also trigger on Enter key using the same chain
|
737
|
+
input_action = user_input.submit(
|
738
|
+
fn=self.log_user_message,
|
739
|
+
inputs=[user_input, file_uploads_log],
|
740
|
+
outputs=[processed_input_state, submit_btn], # Store processed input, disable btn
|
741
|
+
queue=False # Run quickly
|
742
|
+
).then(
|
743
|
+
# 2. Clear the raw input box
|
744
|
+
fn=lambda: gr.Textbox(value=""),
|
745
|
+
inputs=None,
|
746
|
+
outputs=[user_input],
|
747
|
+
queue=False # Run quickly
|
748
|
+
).then(
|
749
|
+
# 3. Run the main interaction loop (this yields updates)
|
750
|
+
fn=self.interact_with_agent,
|
751
|
+
inputs=[processed_input_state, self._chatbot_component],
|
752
|
+
outputs=[self._chatbot_component, self._token_usage_component], # Update chat and tokens
|
753
|
+
queue=True # Explicitly enable queue for this async generator
|
754
|
+
).then(
|
755
|
+
# 4. Re-enable the button after interaction finishes
|
756
|
+
fn=lambda: gr.Button(interactive=True),
|
757
|
+
inputs=None,
|
758
|
+
outputs=[submit_btn],
|
759
|
+
queue=False # Run quickly
|
760
|
+
)
|
761
|
+
|
762
|
+
# Clear conversation
|
763
|
+
clear_btn.click(
|
764
|
+
fn=self.clear_conversation,
|
765
|
+
inputs=None, # No inputs needed
|
766
|
+
# Outputs: Clear chatbot and reset token text
|
767
|
+
outputs=[self._chatbot_component, self._token_usage_component],
|
768
|
+
queue=False # Run quickly
|
769
|
+
)
|
770
|
+
|
771
|
+
self.logger.debug("Gradio app created")
|
772
|
+
return app
|
773
|
+
|
774
|
+
def clear_conversation(self):
|
775
|
+
"""Clear the conversation history (UI + agent), reset state, and update UI."""
|
776
|
+
self.logger.debug("Clearing conversation (UI + agent)")
|
777
|
+
# Reset UI‐side state
|
778
|
+
self.thinking_content = ""
|
779
|
+
self.tool_calls = []
|
780
|
+
self.tool_call_details = []
|
781
|
+
self.assistant_text_responses = []
|
782
|
+
self.token_usage = {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
|
783
|
+
self.is_running = False
|
784
|
+
|
785
|
+
# Also clear the agent's conversation history
|
786
|
+
try:
|
787
|
+
if self.current_agent and hasattr(self.current_agent, "clear_conversation"):
|
788
|
+
self.current_agent.clear_conversation()
|
789
|
+
self.logger.debug("Cleared TinyAgent internal conversation.")
|
790
|
+
except Exception as e:
|
791
|
+
self.logger.error(f"Failed to clear TinyAgent conversation: {e}")
|
792
|
+
|
793
|
+
# Return cleared UI components: empty chat + fresh token usage
|
794
|
+
return [], self._get_token_usage_text()
|
795
|
+
|
796
|
+
def launch(self, agent, title="TinyAgent Chat", description=None, share=False, **kwargs):
|
797
|
+
"""
|
798
|
+
Launch the Gradio app.
|
799
|
+
|
800
|
+
Args:
|
801
|
+
agent: The TinyAgent instance
|
802
|
+
title: Title for the app
|
803
|
+
description: Optional description
|
804
|
+
share: Whether to create a public link
|
805
|
+
**kwargs: Additional arguments to pass to gradio.launch()
|
806
|
+
|
807
|
+
Returns:
|
808
|
+
The Gradio app instance and launch URLs.
|
809
|
+
"""
|
810
|
+
self.logger.debug("Launching Gradio app")
|
811
|
+
# Ensure the agent has this callback added
|
812
|
+
if self not in agent.callbacks:
|
813
|
+
agent.add_callback(self)
|
814
|
+
self.logger.info("GradioCallback automatically added to the agent.")
|
815
|
+
|
816
|
+
app = self.create_app(agent, title, description)
|
817
|
+
|
818
|
+
# Use the same event loop for Gradio
|
819
|
+
launch_kwargs = {
|
820
|
+
"share": share,
|
821
|
+
"prevent_thread_lock": True # This is crucial - allows the main event loop to continue running
|
822
|
+
}
|
823
|
+
launch_kwargs.update(kwargs) # Allow overriding share/debug etc.
|
824
|
+
|
825
|
+
# Get the current event loop
|
826
|
+
loop = asyncio.get_event_loop()
|
827
|
+
self.logger.debug(f"Using event loop for Gradio: {loop}")
|
828
|
+
|
829
|
+
app.queue()
|
830
|
+
return app.launch(**launch_kwargs) # Return the app instance
|
831
|
+
|
832
|
+
|
833
|
+
from tinyagent.tiny_agent import tool
|
834
|
+
@tool(name="get_weather",description="Get the weather for a given city.")
|
835
|
+
def get_weather(city: str)->str:
|
836
|
+
"""Get the weather for a given city.
|
837
|
+
Args:
|
838
|
+
city: The city to get the weather for
|
839
|
+
|
840
|
+
Returns:
|
841
|
+
The weather for the given city
|
842
|
+
"""
|
843
|
+
|
844
|
+
return f"The weather in {city} is sunny."
|
845
|
+
|
846
|
+
async def run_example():
|
847
|
+
"""Example usage of GradioCallback with TinyAgent."""
|
848
|
+
import os
|
849
|
+
import sys
|
850
|
+
import tempfile
|
851
|
+
import shutil
|
852
|
+
import asyncio
|
853
|
+
from tinyagent import TinyAgent # Assuming TinyAgent is importable
|
854
|
+
from tinyagent.hooks.logging_manager import LoggingManager # Assuming LoggingManager exists
|
855
|
+
|
856
|
+
# --- Logging Setup (Simplified) ---
|
857
|
+
log_manager = LoggingManager(default_level=logging.INFO)
|
858
|
+
log_manager.set_levels({
|
859
|
+
'tinyagent.hooks.gradio_callback': logging.DEBUG,
|
860
|
+
'tinyagent.tiny_agent': logging.DEBUG,
|
861
|
+
'tinyagent.mcp_client': logging.DEBUG,
|
862
|
+
})
|
863
|
+
console_handler = logging.StreamHandler(sys.stdout)
|
864
|
+
log_manager.configure_handler(
|
865
|
+
console_handler,
|
866
|
+
format_string='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
867
|
+
level=logging.DEBUG
|
868
|
+
)
|
869
|
+
ui_logger = log_manager.get_logger('tinyagent.hooks.gradio_callback')
|
870
|
+
agent_logger = log_manager.get_logger('tinyagent.tiny_agent')
|
871
|
+
ui_logger.info("--- Starting GradioCallback Example ---")
|
872
|
+
# --- End Logging Setup ---
|
873
|
+
|
874
|
+
api_key = os.environ.get("OPENAI_API_KEY")
|
875
|
+
if not api_key:
|
876
|
+
ui_logger.error("OPENAI_API_KEY environment variable not set.")
|
877
|
+
return
|
878
|
+
|
879
|
+
# Create a temporary folder for file uploads
|
880
|
+
upload_folder = tempfile.mkdtemp(prefix="gradio_uploads_")
|
881
|
+
ui_logger.info(f"Created temporary upload folder: {upload_folder}")
|
882
|
+
|
883
|
+
# Ensure we're using a single event loop for everything
|
884
|
+
loop = asyncio.get_event_loop()
|
885
|
+
ui_logger.debug(f"Using event loop: {loop}")
|
886
|
+
|
887
|
+
# Initialize the agent
|
888
|
+
agent = TinyAgent(model="gpt-4.1-mini", api_key=api_key, logger=agent_logger)
|
889
|
+
|
890
|
+
agent.add_tool(get_weather)
|
891
|
+
|
892
|
+
# Create the Gradio callback
|
893
|
+
gradio_ui = GradioCallback(
|
894
|
+
file_upload_folder=upload_folder,
|
895
|
+
show_thinking=True,
|
896
|
+
show_tool_calls=True,
|
897
|
+
logger=ui_logger # Pass the specific logger
|
898
|
+
)
|
899
|
+
agent.add_callback(gradio_ui)
|
900
|
+
|
901
|
+
# Connect to MCP servers
|
902
|
+
try:
|
903
|
+
ui_logger.info("Connecting to MCP servers...")
|
904
|
+
# Use standard MCP servers as per contribution guide
|
905
|
+
await agent.connect_to_server("npx",["-y","@openbnb/mcp-server-airbnb","--ignore-robots-txt"])
|
906
|
+
await agent.connect_to_server("npx", ["-y", "@modelcontextprotocol/server-sequential-thinking"])
|
907
|
+
ui_logger.info("Connected to MCP servers.")
|
908
|
+
except Exception as e:
|
909
|
+
ui_logger.error(f"Failed to connect to MCP servers: {e}", exc_info=True)
|
910
|
+
# Continue without servers - we still have the local get_weather tool
|
911
|
+
|
912
|
+
# Create the Gradio app but don't launch it yet
|
913
|
+
#app = gradio_ui.create_app(
|
914
|
+
# agent,
|
915
|
+
# title="TinyAgent Chat Interface",
|
916
|
+
# description="Chat with TinyAgent. Try asking: 'Plan a trip to Toronto for 7 days in the next month.'",
|
917
|
+
#)
|
918
|
+
|
919
|
+
# Configure the queue without extra parameters
|
920
|
+
#app.queue()
|
921
|
+
|
922
|
+
# Launch the app in a way that doesn't block our event loop
|
923
|
+
ui_logger.info("Launching Gradio interface...")
|
924
|
+
try:
|
925
|
+
# Launch without blocking
|
926
|
+
#app.launch(
|
927
|
+
# share=False,
|
928
|
+
# prevent_thread_lock=True, # Critical to not block our event loop
|
929
|
+
# show_error=True
|
930
|
+
#)
|
931
|
+
gradio_ui.launch(
|
932
|
+
agent,
|
933
|
+
title="TinyAgent Chat Interface",
|
934
|
+
description="Chat with TinyAgent. Try asking: 'Plan a trip to Toronto for 7 days in the next month.'",
|
935
|
+
share=False,
|
936
|
+
prevent_thread_lock=True, # Critical to not block our event loop
|
937
|
+
show_error=True
|
938
|
+
)
|
939
|
+
ui_logger.info("Gradio interface launched (non-blocking).")
|
940
|
+
|
941
|
+
# Keep the main event loop running to handle both Gradio and MCP operations
|
942
|
+
# This is the key part - we need to keep our main event loop running
|
943
|
+
# but also allow it to process both Gradio and MCP client operations
|
944
|
+
while True:
|
945
|
+
await asyncio.sleep(1) # More efficient than an Event().wait()
|
946
|
+
|
947
|
+
except KeyboardInterrupt:
|
948
|
+
ui_logger.info("Received keyboard interrupt, shutting down...")
|
949
|
+
except Exception as e:
|
950
|
+
ui_logger.error(f"Failed to launch or run Gradio app: {e}", exc_info=True)
|
951
|
+
finally:
|
952
|
+
# Clean up
|
953
|
+
ui_logger.info("Cleaning up resources...")
|
954
|
+
if os.path.exists(upload_folder):
|
955
|
+
ui_logger.info(f"Removing temporary upload folder: {upload_folder}")
|
956
|
+
shutil.rmtree(upload_folder)
|
957
|
+
await agent.close()
|
958
|
+
ui_logger.info("--- GradioCallback Example Finished ---")
|
959
|
+
|
960
|
+
|
961
|
+
if __name__ == "__main__":
|
962
|
+
# Ensure asyncio event loop is handled correctly
|
963
|
+
try:
|
964
|
+
asyncio.run(run_example())
|
965
|
+
except KeyboardInterrupt:
|
966
|
+
print("\nExiting...")
|