Spaces:
Running
Running
Commit Β·
6d7f362
1
Parent(s): da19b43
Implement a comprehensive debug system and thinking transparency solution for the NextGenApp. This includes real-time logging, categorized log levels, and enhanced chat interface with tool usage visualization. Introduce a modular architecture for better error handling and response management. Add tests for the new features and update documentation to reflect these changes.
Browse files- app_ng.py +86 -41
- debug_streamer.py +403 -0
- docs/DEBUG_SYSTEM_README.md +166 -0
- streaming_chat.py +335 -0
- test_debug_system.py +106 -0
app_ng.py
CHANGED
|
@@ -32,6 +32,8 @@ from dataclasses import asdict
|
|
| 32 |
# Local imports
|
| 33 |
from agent_ng import NextGenAgent, ChatMessage, get_agent_ng
|
| 34 |
from llm_manager import get_llm_manager
|
|
|
|
|
|
|
| 35 |
|
| 36 |
|
| 37 |
class NextGenApp:
|
|
@@ -44,6 +46,11 @@ class NextGenApp:
|
|
| 44 |
self.is_initializing = False
|
| 45 |
self.initialization_complete = False
|
| 46 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
# Initialize synchronously first, then start async initialization
|
| 48 |
self._start_async_initialization()
|
| 49 |
|
|
@@ -66,10 +73,12 @@ class NextGenApp:
|
|
| 66 |
async def _initialize_agent(self):
|
| 67 |
"""Initialize the agent asynchronously"""
|
| 68 |
self.is_initializing = True
|
|
|
|
| 69 |
self.initialization_logs.append("π Starting agent initialization...")
|
| 70 |
|
| 71 |
try:
|
| 72 |
# Initialize agent (uses single provider from AGENT_PROVIDER)
|
|
|
|
| 73 |
self.agent = await get_agent_ng()
|
| 74 |
|
| 75 |
# Wait for agent to be ready
|
|
@@ -78,24 +87,34 @@ class NextGenApp:
|
|
| 78 |
while not self.agent.is_ready() and wait_time < max_wait:
|
| 79 |
await asyncio.sleep(0.5)
|
| 80 |
wait_time += 0.5
|
|
|
|
| 81 |
self.initialization_logs.append(f"β³ Waiting for agent... ({wait_time:.1f}s)")
|
| 82 |
|
| 83 |
if self.agent.is_ready():
|
| 84 |
status = self.agent.get_status()
|
|
|
|
| 85 |
self.initialization_logs.append(f"β
Agent ready with {status['current_llm']}")
|
| 86 |
self.initialization_logs.append(f"π§ Tools available: {status['tools_count']}")
|
| 87 |
self.initialization_complete = True
|
| 88 |
else:
|
|
|
|
| 89 |
self.initialization_logs.append("β Agent initialization timeout")
|
| 90 |
|
| 91 |
except Exception as e:
|
|
|
|
| 92 |
self.initialization_logs.append(f"β Initialization failed: {str(e)}")
|
| 93 |
|
| 94 |
self.is_initializing = False
|
| 95 |
|
| 96 |
def get_initialization_logs(self) -> str:
|
| 97 |
"""Get initialization logs as formatted string"""
|
| 98 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
|
| 100 |
def get_agent_status(self) -> str:
|
| 101 |
"""Get current agent status"""
|
|
@@ -109,55 +128,58 @@ class NextGenApp:
|
|
| 109 |
else:
|
| 110 |
return "β Agent not ready"
|
| 111 |
|
| 112 |
-
async def chat_with_agent(self, message: str, history: List[
|
| 113 |
"""
|
| 114 |
-
Chat with the agent using modern streaming.
|
| 115 |
|
| 116 |
Args:
|
| 117 |
message: User message
|
| 118 |
-
history: Chat history as list of
|
| 119 |
|
| 120 |
Returns:
|
| 121 |
Updated history and empty message
|
| 122 |
"""
|
| 123 |
if not self.agent or not self.agent.is_ready():
|
| 124 |
error_msg = "Agent not ready. Please wait for initialization to complete."
|
| 125 |
-
history.append(
|
|
|
|
| 126 |
return history, ""
|
| 127 |
|
| 128 |
-
|
| 129 |
-
chat_history = []
|
| 130 |
-
for user_msg, assistant_msg in history:
|
| 131 |
-
chat_history.append(ChatMessage(role="user", content=user_msg))
|
| 132 |
-
chat_history.append(ChatMessage(role="assistant", content=assistant_msg))
|
| 133 |
-
|
| 134 |
-
# Add current user message
|
| 135 |
-
chat_history.append(ChatMessage(role="user", content=message))
|
| 136 |
-
|
| 137 |
-
# Stream response
|
| 138 |
-
response_content = ""
|
| 139 |
-
thinking_content = ""
|
| 140 |
-
tool_usage = []
|
| 141 |
|
| 142 |
try:
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
except Exception as e:
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
return history, ""
|
| 161 |
|
| 162 |
def create_interface(self) -> gr.Blocks:
|
| 163 |
"""Create the Gradio interface"""
|
|
@@ -247,13 +269,15 @@ class NextGenApp:
|
|
| 247 |
with gr.TabItem("π¬ Chat", id="chat"):
|
| 248 |
with gr.Row():
|
| 249 |
with gr.Column(scale=3):
|
| 250 |
-
# Chat interface
|
| 251 |
chatbot = gr.Chatbot(
|
| 252 |
label="Chat with the Agent",
|
| 253 |
height=500,
|
| 254 |
show_label=True,
|
| 255 |
container=True,
|
| 256 |
-
show_copy_button=True
|
|
|
|
|
|
|
| 257 |
)
|
| 258 |
|
| 259 |
with gr.Row():
|
|
@@ -292,8 +316,12 @@ class NextGenApp:
|
|
| 292 |
|
| 293 |
def copy_last_response(history):
|
| 294 |
if history and len(history) > 0:
|
| 295 |
-
|
| 296 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
return ""
|
| 298 |
|
| 299 |
def quick_math():
|
|
@@ -345,8 +373,8 @@ class NextGenApp:
|
|
| 345 |
)
|
| 346 |
|
| 347 |
# Logs Tab
|
| 348 |
-
with gr.TabItem("π
|
| 349 |
-
gr.Markdown("###
|
| 350 |
logs_display = gr.Markdown(
|
| 351 |
"π‘ Starting initialization...",
|
| 352 |
elem_classes=["status-card"]
|
|
@@ -357,10 +385,20 @@ class NextGenApp:
|
|
| 357 |
def refresh_logs():
|
| 358 |
return self.get_initialization_logs()
|
| 359 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
refresh_logs_btn.click(
|
| 361 |
fn=refresh_logs,
|
| 362 |
outputs=[logs_display]
|
| 363 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 364 |
|
| 365 |
# Stats Tab
|
| 366 |
with gr.TabItem("π Statistics", id="stats"):
|
|
@@ -420,6 +458,13 @@ class NextGenApp:
|
|
| 420 |
outputs=[model_info]
|
| 421 |
)
|
| 422 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 423 |
# Load initial logs
|
| 424 |
demo.load(
|
| 425 |
fn=refresh_logs,
|
|
|
|
| 32 |
# Local imports
|
| 33 |
from agent_ng import NextGenAgent, ChatMessage, get_agent_ng
|
| 34 |
from llm_manager import get_llm_manager
|
| 35 |
+
from debug_streamer import get_debug_streamer, get_log_handler, LogLevel, LogCategory
|
| 36 |
+
from streaming_chat import get_chat_interface
|
| 37 |
|
| 38 |
|
| 39 |
class NextGenApp:
|
|
|
|
| 46 |
self.is_initializing = False
|
| 47 |
self.initialization_complete = False
|
| 48 |
|
| 49 |
+
# Initialize debug system
|
| 50 |
+
self.debug_streamer = get_debug_streamer("app_ng")
|
| 51 |
+
self.log_handler = get_log_handler("app_ng")
|
| 52 |
+
self.chat_interface = get_chat_interface("app_ng")
|
| 53 |
+
|
| 54 |
# Initialize synchronously first, then start async initialization
|
| 55 |
self._start_async_initialization()
|
| 56 |
|
|
|
|
| 73 |
async def _initialize_agent(self):
|
| 74 |
"""Initialize the agent asynchronously"""
|
| 75 |
self.is_initializing = True
|
| 76 |
+
self.debug_streamer.info("Starting agent initialization", LogCategory.INIT)
|
| 77 |
self.initialization_logs.append("π Starting agent initialization...")
|
| 78 |
|
| 79 |
try:
|
| 80 |
# Initialize agent (uses single provider from AGENT_PROVIDER)
|
| 81 |
+
self.debug_streamer.info("Creating agent instance", LogCategory.INIT)
|
| 82 |
self.agent = await get_agent_ng()
|
| 83 |
|
| 84 |
# Wait for agent to be ready
|
|
|
|
| 87 |
while not self.agent.is_ready() and wait_time < max_wait:
|
| 88 |
await asyncio.sleep(0.5)
|
| 89 |
wait_time += 0.5
|
| 90 |
+
self.debug_streamer.debug(f"Waiting for agent... ({wait_time:.1f}s)", LogCategory.INIT)
|
| 91 |
self.initialization_logs.append(f"β³ Waiting for agent... ({wait_time:.1f}s)")
|
| 92 |
|
| 93 |
if self.agent.is_ready():
|
| 94 |
status = self.agent.get_status()
|
| 95 |
+
self.debug_streamer.success(f"Agent ready with {status['current_llm']}", LogCategory.INIT)
|
| 96 |
self.initialization_logs.append(f"β
Agent ready with {status['current_llm']}")
|
| 97 |
self.initialization_logs.append(f"π§ Tools available: {status['tools_count']}")
|
| 98 |
self.initialization_complete = True
|
| 99 |
else:
|
| 100 |
+
self.debug_streamer.error("Agent initialization timeout", LogCategory.INIT)
|
| 101 |
self.initialization_logs.append("β Agent initialization timeout")
|
| 102 |
|
| 103 |
except Exception as e:
|
| 104 |
+
self.debug_streamer.error(f"Initialization failed: {str(e)}", LogCategory.INIT)
|
| 105 |
self.initialization_logs.append(f"β Initialization failed: {str(e)}")
|
| 106 |
|
| 107 |
self.is_initializing = False
|
| 108 |
|
| 109 |
def get_initialization_logs(self) -> str:
|
| 110 |
"""Get initialization logs as formatted string"""
|
| 111 |
+
# Combine static logs with real-time debug logs
|
| 112 |
+
static_logs = "\n".join(self.initialization_logs)
|
| 113 |
+
debug_logs = self.log_handler.get_current_logs()
|
| 114 |
+
|
| 115 |
+
if debug_logs and debug_logs != "No logs available yet.":
|
| 116 |
+
return f"{static_logs}\n\n--- Real-time Debug Logs ---\n\n{debug_logs}"
|
| 117 |
+
return static_logs
|
| 118 |
|
| 119 |
def get_agent_status(self) -> str:
|
| 120 |
"""Get current agent status"""
|
|
|
|
| 128 |
else:
|
| 129 |
return "β Agent not ready"
|
| 130 |
|
| 131 |
+
async def chat_with_agent(self, message: str, history: List[Dict[str, str]]) -> Tuple[List[Dict[str, str]], str]:
|
| 132 |
"""
|
| 133 |
+
Chat with the agent using modern streaming with thinking transparency.
|
| 134 |
|
| 135 |
Args:
|
| 136 |
message: User message
|
| 137 |
+
history: Chat history as list of message dicts
|
| 138 |
|
| 139 |
Returns:
|
| 140 |
Updated history and empty message
|
| 141 |
"""
|
| 142 |
if not self.agent or not self.agent.is_ready():
|
| 143 |
error_msg = "Agent not ready. Please wait for initialization to complete."
|
| 144 |
+
history.append({"role": "user", "content": message})
|
| 145 |
+
history.append({"role": "assistant", "content": error_msg})
|
| 146 |
return history, ""
|
| 147 |
|
| 148 |
+
self.debug_streamer.info(f"Starting chat with message: {message[:50]}...", LogCategory.STREAM)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
|
| 150 |
try:
|
| 151 |
+
# Convert tuple history to dict format for internal processing
|
| 152 |
+
tuple_history = []
|
| 153 |
+
for msg in history:
|
| 154 |
+
if isinstance(msg, dict):
|
| 155 |
+
if msg.get("role") == "user":
|
| 156 |
+
tuple_history.append((msg["content"], ""))
|
| 157 |
+
elif msg.get("role") == "assistant":
|
| 158 |
+
if tuple_history:
|
| 159 |
+
tuple_history[-1] = (tuple_history[-1][0], msg["content"])
|
| 160 |
+
else:
|
| 161 |
+
tuple_history.append(("", msg["content"]))
|
| 162 |
+
elif isinstance(msg, tuple):
|
| 163 |
+
tuple_history.append(msg)
|
| 164 |
+
|
| 165 |
+
# Use the streaming chat interface
|
| 166 |
+
updated_tuple_history, _ = await self.chat_interface.chat_with_agent(message, tuple_history, self.agent)
|
| 167 |
+
|
| 168 |
+
# Convert back to dict format for Gradio
|
| 169 |
+
dict_history = []
|
| 170 |
+
for user_msg, assistant_msg in updated_tuple_history:
|
| 171 |
+
if user_msg:
|
| 172 |
+
dict_history.append({"role": "user", "content": user_msg})
|
| 173 |
+
if assistant_msg:
|
| 174 |
+
dict_history.append({"role": "assistant", "content": assistant_msg})
|
| 175 |
+
|
| 176 |
+
return dict_history, ""
|
| 177 |
except Exception as e:
|
| 178 |
+
self.debug_streamer.error(f"Error in chat_with_agent: {str(e)}", LogCategory.STREAM)
|
| 179 |
+
error_msg = f"β Error: {str(e)}"
|
| 180 |
+
history.append({"role": "user", "content": message})
|
| 181 |
+
history.append({"role": "assistant", "content": error_msg})
|
| 182 |
+
return history, ""
|
|
|
|
| 183 |
|
| 184 |
def create_interface(self) -> gr.Blocks:
|
| 185 |
"""Create the Gradio interface"""
|
|
|
|
| 269 |
with gr.TabItem("π¬ Chat", id="chat"):
|
| 270 |
with gr.Row():
|
| 271 |
with gr.Column(scale=3):
|
| 272 |
+
# Chat interface with metadata support for thinking transparency
|
| 273 |
chatbot = gr.Chatbot(
|
| 274 |
label="Chat with the Agent",
|
| 275 |
height=500,
|
| 276 |
show_label=True,
|
| 277 |
container=True,
|
| 278 |
+
show_copy_button=True,
|
| 279 |
+
type="messages", # Enable metadata support
|
| 280 |
+
bubble_full_width=False
|
| 281 |
)
|
| 282 |
|
| 283 |
with gr.Row():
|
|
|
|
| 316 |
|
| 317 |
def copy_last_response(history):
|
| 318 |
if history and len(history) > 0:
|
| 319 |
+
# Find the last assistant message
|
| 320 |
+
for msg in reversed(history):
|
| 321 |
+
if isinstance(msg, dict) and msg.get("role") == "assistant":
|
| 322 |
+
return msg.get("content", "")
|
| 323 |
+
elif isinstance(msg, tuple):
|
| 324 |
+
return msg[1] # Get last assistant message from tuple
|
| 325 |
return ""
|
| 326 |
|
| 327 |
def quick_math():
|
|
|
|
| 373 |
)
|
| 374 |
|
| 375 |
# Logs Tab
|
| 376 |
+
with gr.TabItem("π Logs", id="logs"):
|
| 377 |
+
gr.Markdown("### Initialization Logs")
|
| 378 |
logs_display = gr.Markdown(
|
| 379 |
"π‘ Starting initialization...",
|
| 380 |
elem_classes=["status-card"]
|
|
|
|
| 385 |
def refresh_logs():
|
| 386 |
return self.get_initialization_logs()
|
| 387 |
|
| 388 |
+
def clear_logs():
|
| 389 |
+
self.log_handler.clear_logs()
|
| 390 |
+
return "Logs cleared."
|
| 391 |
+
|
| 392 |
refresh_logs_btn.click(
|
| 393 |
fn=refresh_logs,
|
| 394 |
outputs=[logs_display]
|
| 395 |
)
|
| 396 |
+
|
| 397 |
+
clear_logs_btn = gr.Button("ποΈ Clear Logs", elem_classes=["cmw-button"])
|
| 398 |
+
clear_logs_btn.click(
|
| 399 |
+
fn=clear_logs,
|
| 400 |
+
outputs=[logs_display]
|
| 401 |
+
)
|
| 402 |
|
| 403 |
# Stats Tab
|
| 404 |
with gr.TabItem("π Statistics", id="stats"):
|
|
|
|
| 458 |
outputs=[model_info]
|
| 459 |
)
|
| 460 |
|
| 461 |
+
# Auto-refresh logs every 3 seconds
|
| 462 |
+
logs_timer = gr.Timer(3.0, active=True)
|
| 463 |
+
logs_timer.tick(
|
| 464 |
+
fn=refresh_logs,
|
| 465 |
+
outputs=[logs_display]
|
| 466 |
+
)
|
| 467 |
+
|
| 468 |
# Load initial logs
|
| 469 |
demo.load(
|
| 470 |
fn=refresh_logs,
|
debug_streamer.py
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Lean Debug Streamer
|
| 3 |
+
==================
|
| 4 |
+
|
| 5 |
+
A highly efficient, modular debug streaming system for real-time logging
|
| 6 |
+
and thinking transparency in LLM agents.
|
| 7 |
+
|
| 8 |
+
Key Features:
|
| 9 |
+
- Real-time log streaming to Gradio interface
|
| 10 |
+
- Thinking transparency with collapsible sections
|
| 11 |
+
- Minimal overhead and clean separation of concerns
|
| 12 |
+
- Thread-safe logging with queue-based streaming
|
| 13 |
+
- Support for different log levels and categories
|
| 14 |
+
- Integration with Gradio ChatMessage metadata
|
| 15 |
+
|
| 16 |
+
Inspired by the sophisticated logging in agent.py but designed to be
|
| 17 |
+
lean, efficient, and modular.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
import asyncio
|
| 21 |
+
import threading
|
| 22 |
+
import time
|
| 23 |
+
from typing import Dict, Any, Optional, Callable, List, Union
|
| 24 |
+
from dataclasses import dataclass, field
|
| 25 |
+
from enum import Enum
|
| 26 |
+
from queue import Queue, Empty
|
| 27 |
+
import json
|
| 28 |
+
from datetime import datetime
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class LogLevel(Enum):
|
| 32 |
+
"""Log levels for different types of messages"""
|
| 33 |
+
DEBUG = "debug"
|
| 34 |
+
INFO = "info"
|
| 35 |
+
WARNING = "warning"
|
| 36 |
+
ERROR = "error"
|
| 37 |
+
THINKING = "thinking"
|
| 38 |
+
TOOL_USE = "tool_use"
|
| 39 |
+
LLM_STREAM = "llm_stream"
|
| 40 |
+
SUCCESS = "success"
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class LogCategory(Enum):
|
| 44 |
+
"""Categories for organizing logs"""
|
| 45 |
+
INIT = "initialization"
|
| 46 |
+
LLM = "llm_call"
|
| 47 |
+
TOOL = "tool_execution"
|
| 48 |
+
STREAM = "streaming"
|
| 49 |
+
ERROR = "error_handling"
|
| 50 |
+
THINKING = "thinking_process"
|
| 51 |
+
SYSTEM = "system"
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
@dataclass
|
| 55 |
+
class LogEntry:
|
| 56 |
+
"""A single log entry with metadata"""
|
| 57 |
+
timestamp: float
|
| 58 |
+
level: LogLevel
|
| 59 |
+
category: LogCategory
|
| 60 |
+
message: str
|
| 61 |
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
| 62 |
+
thread_id: str = ""
|
| 63 |
+
session_id: str = "default"
|
| 64 |
+
|
| 65 |
+
def to_dict(self) -> Dict[str, Any]:
|
| 66 |
+
"""Convert to dictionary for JSON serialization"""
|
| 67 |
+
return {
|
| 68 |
+
"timestamp": self.timestamp,
|
| 69 |
+
"level": self.level.value,
|
| 70 |
+
"category": self.category.value,
|
| 71 |
+
"message": self.message,
|
| 72 |
+
"metadata": self.metadata,
|
| 73 |
+
"thread_id": self.thread_id,
|
| 74 |
+
"session_id": self.session_id,
|
| 75 |
+
"formatted_time": datetime.fromtimestamp(self.timestamp).strftime("%H:%M:%S.%f")[:-3]
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
class DebugStreamer:
|
| 80 |
+
"""
|
| 81 |
+
Lean, efficient debug streamer for real-time logging.
|
| 82 |
+
|
| 83 |
+
Features:
|
| 84 |
+
- Thread-safe queue-based logging
|
| 85 |
+
- Real-time streaming to Gradio interface
|
| 86 |
+
- Minimal overhead with clean separation
|
| 87 |
+
- Support for different log levels and categories
|
| 88 |
+
- Integration with Gradio ChatMessage metadata
|
| 89 |
+
"""
|
| 90 |
+
|
| 91 |
+
def __init__(self, session_id: str = "default", max_queue_size: int = 1000):
|
| 92 |
+
self.session_id = session_id
|
| 93 |
+
self.max_queue_size = max_queue_size
|
| 94 |
+
self.log_queue = Queue(maxsize=max_queue_size)
|
| 95 |
+
self.subscribers: List[Callable[[LogEntry], None]] = []
|
| 96 |
+
self.is_running = False
|
| 97 |
+
self.worker_thread: Optional[threading.Thread] = None
|
| 98 |
+
self._lock = threading.Lock()
|
| 99 |
+
|
| 100 |
+
# Start the worker thread
|
| 101 |
+
self.start()
|
| 102 |
+
|
| 103 |
+
def start(self):
|
| 104 |
+
"""Start the debug streamer worker thread"""
|
| 105 |
+
if self.is_running:
|
| 106 |
+
return
|
| 107 |
+
|
| 108 |
+
self.is_running = True
|
| 109 |
+
self.worker_thread = threading.Thread(target=self._worker_loop, daemon=True)
|
| 110 |
+
self.worker_thread.start()
|
| 111 |
+
|
| 112 |
+
def stop(self):
|
| 113 |
+
"""Stop the debug streamer"""
|
| 114 |
+
self.is_running = False
|
| 115 |
+
if self.worker_thread:
|
| 116 |
+
self.worker_thread.join(timeout=1.0)
|
| 117 |
+
|
| 118 |
+
def subscribe(self, callback: Callable[[LogEntry], None]):
|
| 119 |
+
"""Subscribe to log entries"""
|
| 120 |
+
with self._lock:
|
| 121 |
+
self.subscribers.append(callback)
|
| 122 |
+
|
| 123 |
+
def unsubscribe(self, callback: Callable[[LogEntry], None]):
|
| 124 |
+
"""Unsubscribe from log entries"""
|
| 125 |
+
with self._lock:
|
| 126 |
+
if callback in self.subscribers:
|
| 127 |
+
self.subscribers.remove(callback)
|
| 128 |
+
|
| 129 |
+
def _worker_loop(self):
|
| 130 |
+
"""Worker loop that processes log entries"""
|
| 131 |
+
while self.is_running:
|
| 132 |
+
try:
|
| 133 |
+
# Get log entry with timeout
|
| 134 |
+
entry = self.log_queue.get(timeout=0.1)
|
| 135 |
+
|
| 136 |
+
# Notify all subscribers
|
| 137 |
+
with self._lock:
|
| 138 |
+
for callback in self.subscribers:
|
| 139 |
+
try:
|
| 140 |
+
callback(entry)
|
| 141 |
+
except Exception as e:
|
| 142 |
+
print(f"Error in log subscriber: {e}")
|
| 143 |
+
|
| 144 |
+
self.log_queue.task_done()
|
| 145 |
+
|
| 146 |
+
except Empty:
|
| 147 |
+
continue
|
| 148 |
+
except Exception as e:
|
| 149 |
+
print(f"Error in debug streamer worker: {e}")
|
| 150 |
+
|
| 151 |
+
def log(self, level: LogLevel, category: LogCategory, message: str,
|
| 152 |
+
metadata: Optional[Dict[str, Any]] = None, session_id: Optional[str] = None):
|
| 153 |
+
"""Log a message with the specified level and category"""
|
| 154 |
+
if metadata is None:
|
| 155 |
+
metadata = {}
|
| 156 |
+
|
| 157 |
+
entry = LogEntry(
|
| 158 |
+
timestamp=time.time(),
|
| 159 |
+
level=level,
|
| 160 |
+
category=category,
|
| 161 |
+
message=message,
|
| 162 |
+
metadata=metadata,
|
| 163 |
+
thread_id=threading.get_ident(),
|
| 164 |
+
session_id=session_id or self.session_id
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
try:
|
| 168 |
+
self.log_queue.put_nowait(entry)
|
| 169 |
+
except:
|
| 170 |
+
# Queue is full, drop the oldest entry
|
| 171 |
+
try:
|
| 172 |
+
self.log_queue.get_nowait()
|
| 173 |
+
self.log_queue.put_nowait(entry)
|
| 174 |
+
except Empty:
|
| 175 |
+
pass
|
| 176 |
+
|
| 177 |
+
# Convenience methods for different log levels
|
| 178 |
+
def debug(self, message: str, category: LogCategory = LogCategory.SYSTEM,
|
| 179 |
+
metadata: Optional[Dict[str, Any]] = None):
|
| 180 |
+
"""Log a debug message"""
|
| 181 |
+
self.log(LogLevel.DEBUG, category, message, metadata)
|
| 182 |
+
|
| 183 |
+
def info(self, message: str, category: LogCategory = LogCategory.SYSTEM,
|
| 184 |
+
metadata: Optional[Dict[str, Any]] = None):
|
| 185 |
+
"""Log an info message"""
|
| 186 |
+
self.log(LogLevel.INFO, category, message, metadata)
|
| 187 |
+
|
| 188 |
+
def warning(self, message: str, category: LogCategory = LogCategory.SYSTEM,
|
| 189 |
+
metadata: Optional[Dict[str, Any]] = None):
|
| 190 |
+
"""Log a warning message"""
|
| 191 |
+
self.log(LogLevel.WARNING, category, message, metadata)
|
| 192 |
+
|
| 193 |
+
def error(self, message: str, category: LogCategory = LogCategory.ERROR,
|
| 194 |
+
metadata: Optional[Dict[str, Any]] = None):
|
| 195 |
+
"""Log an error message"""
|
| 196 |
+
self.log(LogLevel.ERROR, category, message, metadata)
|
| 197 |
+
|
| 198 |
+
def thinking(self, message: str, metadata: Optional[Dict[str, Any]] = None):
|
| 199 |
+
"""Log a thinking process message"""
|
| 200 |
+
self.log(LogLevel.THINKING, LogCategory.THINKING, message, metadata)
|
| 201 |
+
|
| 202 |
+
def tool_use(self, tool_name: str, tool_args: Dict[str, Any],
|
| 203 |
+
result: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None):
|
| 204 |
+
"""Log a tool usage"""
|
| 205 |
+
tool_metadata = {
|
| 206 |
+
"tool_name": tool_name,
|
| 207 |
+
"tool_args": tool_args,
|
| 208 |
+
"result": result,
|
| 209 |
+
**(metadata or {})
|
| 210 |
+
}
|
| 211 |
+
self.log(LogLevel.TOOL_USE, LogCategory.TOOL, f"Using tool: {tool_name}", tool_metadata)
|
| 212 |
+
|
| 213 |
+
def llm_stream(self, content: str, metadata: Optional[Dict[str, Any]] = None):
|
| 214 |
+
"""Log LLM streaming content"""
|
| 215 |
+
self.log(LogLevel.LLM_STREAM, LogCategory.LLM, content, metadata)
|
| 216 |
+
|
| 217 |
+
def success(self, message: str, category: LogCategory = LogCategory.SYSTEM,
|
| 218 |
+
metadata: Optional[Dict[str, Any]] = None):
|
| 219 |
+
"""Log a success message"""
|
| 220 |
+
self.log(LogLevel.SUCCESS, category, message, metadata)
|
| 221 |
+
|
| 222 |
+
def get_recent_logs(self, count: int = 50) -> List[LogEntry]:
|
| 223 |
+
"""Get recent log entries (for debugging)"""
|
| 224 |
+
# This is a simple implementation - in production you might want to use a proper log store
|
| 225 |
+
return []
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
class GradioLogHandler:
|
| 229 |
+
"""
|
| 230 |
+
Handler for streaming logs to Gradio interface.
|
| 231 |
+
|
| 232 |
+
This class handles the conversion of log entries to Gradio-compatible
|
| 233 |
+
formats and manages the streaming to the Logs tab.
|
| 234 |
+
"""
|
| 235 |
+
|
| 236 |
+
def __init__(self, debug_streamer: DebugStreamer):
|
| 237 |
+
self.debug_streamer = debug_streamer
|
| 238 |
+
self.log_buffer: List[str] = []
|
| 239 |
+
self.max_buffer_size = 1000
|
| 240 |
+
self.current_logs_display = ""
|
| 241 |
+
|
| 242 |
+
# Subscribe to log entries
|
| 243 |
+
self.debug_streamer.subscribe(self._handle_log_entry)
|
| 244 |
+
|
| 245 |
+
def _handle_log_entry(self, entry: LogEntry):
|
| 246 |
+
"""Handle a new log entry"""
|
| 247 |
+
# Format the log entry for display
|
| 248 |
+
formatted_log = self._format_log_entry(entry)
|
| 249 |
+
|
| 250 |
+
# Add to buffer
|
| 251 |
+
self.log_buffer.append(formatted_log)
|
| 252 |
+
|
| 253 |
+
# Trim buffer if too large
|
| 254 |
+
if len(self.log_buffer) > self.max_buffer_size:
|
| 255 |
+
self.log_buffer = self.log_buffer[-self.max_buffer_size:]
|
| 256 |
+
|
| 257 |
+
# Update current display
|
| 258 |
+
self.current_logs_display = "\n".join(self.log_buffer[-50:]) # Show last 50 entries
|
| 259 |
+
|
| 260 |
+
def _format_log_entry(self, entry: LogEntry) -> str:
|
| 261 |
+
"""Format a log entry for display"""
|
| 262 |
+
timestamp = datetime.fromtimestamp(entry.timestamp).strftime("%H:%M:%S.%f")[:-3]
|
| 263 |
+
|
| 264 |
+
# Choose emoji based on level
|
| 265 |
+
emoji_map = {
|
| 266 |
+
LogLevel.DEBUG: "π",
|
| 267 |
+
LogLevel.INFO: "βΉοΈ",
|
| 268 |
+
LogLevel.WARNING: "β οΈ",
|
| 269 |
+
LogLevel.ERROR: "β",
|
| 270 |
+
LogLevel.THINKING: "π",
|
| 271 |
+
LogLevel.TOOL_USE: "π§",
|
| 272 |
+
LogLevel.LLM_STREAM: "π‘",
|
| 273 |
+
LogLevel.SUCCESS: "β
"
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
emoji = emoji_map.get(entry.level, "π")
|
| 277 |
+
|
| 278 |
+
# Format the message with better spacing
|
| 279 |
+
formatted = f"{emoji} [{timestamp}] {entry.message}"
|
| 280 |
+
|
| 281 |
+
# Add metadata if present
|
| 282 |
+
if entry.metadata:
|
| 283 |
+
metadata_str = json.dumps(entry.metadata, indent=2)
|
| 284 |
+
formatted += f"\n π {metadata_str}"
|
| 285 |
+
|
| 286 |
+
# Add new line for better readability
|
| 287 |
+
formatted += "\n"
|
| 288 |
+
|
| 289 |
+
return formatted
|
| 290 |
+
|
| 291 |
+
def get_current_logs(self) -> str:
|
| 292 |
+
"""Get the current logs display"""
|
| 293 |
+
return self.current_logs_display or "No logs available yet."
|
| 294 |
+
|
| 295 |
+
def clear_logs(self):
|
| 296 |
+
"""Clear the log buffer"""
|
| 297 |
+
self.log_buffer.clear()
|
| 298 |
+
self.current_logs_display = ""
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
class ThinkingTransparency:
|
| 302 |
+
"""
|
| 303 |
+
Handler for thinking transparency in Gradio ChatMessage.
|
| 304 |
+
|
| 305 |
+
This class manages the creation of thinking sections that can be
|
| 306 |
+
displayed in collapsible accordions in the Gradio chat interface.
|
| 307 |
+
"""
|
| 308 |
+
|
| 309 |
+
def __init__(self, debug_streamer: DebugStreamer):
|
| 310 |
+
self.debug_streamer = debug_streamer
|
| 311 |
+
self.current_thinking = ""
|
| 312 |
+
self.thinking_metadata = {}
|
| 313 |
+
|
| 314 |
+
def start_thinking(self, title: str = "π§ Thinking", metadata: Optional[Dict[str, Any]] = None):
|
| 315 |
+
"""Start a thinking process"""
|
| 316 |
+
self.current_thinking = ""
|
| 317 |
+
self.thinking_metadata = {
|
| 318 |
+
"title": title,
|
| 319 |
+
"status": "pending",
|
| 320 |
+
**(metadata or {})
|
| 321 |
+
}
|
| 322 |
+
self.debug_streamer.thinking(f"Starting thinking process: {title}")
|
| 323 |
+
|
| 324 |
+
def add_thinking(self, content: str):
|
| 325 |
+
"""Add content to the current thinking process"""
|
| 326 |
+
self.current_thinking += content
|
| 327 |
+
self.debug_streamer.thinking(content)
|
| 328 |
+
|
| 329 |
+
def complete_thinking(self, final_content: Optional[str] = None):
|
| 330 |
+
"""Complete the thinking process"""
|
| 331 |
+
if final_content:
|
| 332 |
+
self.current_thinking = final_content
|
| 333 |
+
|
| 334 |
+
self.thinking_metadata["status"] = "done"
|
| 335 |
+
self.debug_streamer.thinking("Thinking process completed")
|
| 336 |
+
|
| 337 |
+
return self._create_thinking_message()
|
| 338 |
+
|
| 339 |
+
def _create_thinking_message(self) -> Dict[str, Any]:
|
| 340 |
+
"""Create a ChatMessage-compatible thinking message"""
|
| 341 |
+
return {
|
| 342 |
+
"role": "assistant",
|
| 343 |
+
"content": self.current_thinking,
|
| 344 |
+
"metadata": self.thinking_metadata
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
def create_tool_usage_message(self, tool_name: str, tool_args: Dict[str, Any],
|
| 348 |
+
result: str) -> Dict[str, Any]:
|
| 349 |
+
"""Create a tool usage message with metadata"""
|
| 350 |
+
return {
|
| 351 |
+
"role": "assistant",
|
| 352 |
+
"content": f"Used tool: {tool_name}\n\n**Arguments:**\n{json.dumps(tool_args, indent=2)}\n\n**Result:**\n{result}",
|
| 353 |
+
"metadata": {
|
| 354 |
+
"title": f"π§ {tool_name}",
|
| 355 |
+
"status": "done",
|
| 356 |
+
"tool_name": tool_name,
|
| 357 |
+
"tool_args": tool_args
|
| 358 |
+
}
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
# Global debug streamer instance
|
| 363 |
+
_global_debug_streamer: Optional[DebugStreamer] = None
|
| 364 |
+
_global_log_handler: Optional[GradioLogHandler] = None
|
| 365 |
+
_global_thinking_transparency: Optional[ThinkingTransparency] = None
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
def get_debug_streamer(session_id: str = "default") -> DebugStreamer:
|
| 369 |
+
"""Get the global debug streamer instance"""
|
| 370 |
+
global _global_debug_streamer
|
| 371 |
+
if _global_debug_streamer is None:
|
| 372 |
+
_global_debug_streamer = DebugStreamer(session_id)
|
| 373 |
+
return _global_debug_streamer
|
| 374 |
+
|
| 375 |
+
|
| 376 |
+
def get_log_handler(session_id: str = "default") -> GradioLogHandler:
|
| 377 |
+
"""Get the global log handler instance"""
|
| 378 |
+
global _global_log_handler
|
| 379 |
+
if _global_log_handler is None:
|
| 380 |
+
debug_streamer = get_debug_streamer(session_id)
|
| 381 |
+
_global_log_handler = GradioLogHandler(debug_streamer)
|
| 382 |
+
return _global_log_handler
|
| 383 |
+
|
| 384 |
+
|
| 385 |
+
def get_thinking_transparency(session_id: str = "default") -> ThinkingTransparency:
|
| 386 |
+
"""Get the global thinking transparency instance"""
|
| 387 |
+
global _global_thinking_transparency
|
| 388 |
+
if _global_thinking_transparency is None:
|
| 389 |
+
debug_streamer = get_debug_streamer(session_id)
|
| 390 |
+
_global_thinking_transparency = ThinkingTransparency(debug_streamer)
|
| 391 |
+
return _global_thinking_transparency
|
| 392 |
+
|
| 393 |
+
|
| 394 |
+
def cleanup_debug_system():
|
| 395 |
+
"""Cleanup the debug system"""
|
| 396 |
+
global _global_debug_streamer, _global_log_handler, _global_thinking_transparency
|
| 397 |
+
|
| 398 |
+
if _global_debug_streamer:
|
| 399 |
+
_global_debug_streamer.stop()
|
| 400 |
+
_global_debug_streamer = None
|
| 401 |
+
|
| 402 |
+
_global_log_handler = None
|
| 403 |
+
_global_thinking_transparency = None
|
docs/DEBUG_SYSTEM_README.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
********# Debug System & Thinking Transparency
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
I've implemented a comprehensive debug system and thinking transparency solution for your LLM agent. This addresses the empty response issue with OpenRouter and provides real-time visibility into the agent's thinking process.
|
| 6 |
+
|
| 7 |
+
## π Key Features
|
| 8 |
+
|
| 9 |
+
### 1. **Lean Debug Module** (`debug_streamer.py`)
|
| 10 |
+
- **Real-time logging** with thread-safe queue-based system
|
| 11 |
+
- **Minimal overhead** with clean separation of concerns
|
| 12 |
+
- **Multiple log levels**: DEBUG, INFO, WARNING, ERROR, THINKING, TOOL_USE, LLM_STREAM, SUCCESS
|
| 13 |
+
- **Categorized logging**: INIT, LLM, TOOL, STREAM, ERROR, THINKING, SYSTEM
|
| 14 |
+
- **Auto-streaming to Gradio** Logs tab
|
| 15 |
+
|
| 16 |
+
### 2. **Error Handling** (Integrated)
|
| 17 |
+
- **Centralized error classification** via `error_handler.py`
|
| 18 |
+
- **Provider-specific error handling** for all LLM providers
|
| 19 |
+
- **Automatic retry logic** with exponential backoff
|
| 20 |
+
- **Comprehensive error reporting** with suggested actions
|
| 21 |
+
|
| 22 |
+
### 3. **Thinking Transparency** (`streaming_chat.py`)
|
| 23 |
+
- **Real-time thinking process** visualization
|
| 24 |
+
- **Collapsible thinking sections** using Gradio ChatMessage metadata
|
| 25 |
+
- **Tool usage visualization** with detailed metadata
|
| 26 |
+
- **Streaming response handling** with event-based architecture
|
| 27 |
+
- **Clean separation** between thinking, tool usage, and content
|
| 28 |
+
|
| 29 |
+
### 4. **Enhanced App** (`app_ng.py`)
|
| 30 |
+
- **Integrated debug system** with real-time log streaming
|
| 31 |
+
- **Thinking transparency** in chat interface
|
| 32 |
+
- **Auto-refreshing logs** every 3 seconds
|
| 33 |
+
- **Modern Gradio ChatMessage** format with metadata support
|
| 34 |
+
- **Comprehensive error handling** and fallback mechanisms
|
| 35 |
+
|
| 36 |
+
## π§ How It Works
|
| 37 |
+
|
| 38 |
+
### Debug Streaming
|
| 39 |
+
```python
|
| 40 |
+
# Initialize debug system
|
| 41 |
+
debug_streamer = get_debug_streamer("session_id")
|
| 42 |
+
log_handler = get_log_handler("session_id")
|
| 43 |
+
|
| 44 |
+
# Log messages with categories
|
| 45 |
+
debug_streamer.info("Agent initialized", LogCategory.INIT)
|
| 46 |
+
debug_streamer.thinking("Processing user question...")
|
| 47 |
+
debug_streamer.tool_use("calculator", {"operation": "add", "a": 5, "b": 3}, "8")
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
### Thinking Transparency
|
| 51 |
+
```python
|
| 52 |
+
# Start thinking process
|
| 53 |
+
thinking_transparency.start_thinking("π§ Analyzing question...")
|
| 54 |
+
thinking_transparency.add_thinking("Let me break this down...")
|
| 55 |
+
thinking_message = thinking_transparency.complete_thinking()
|
| 56 |
+
|
| 57 |
+
# Create tool usage message
|
| 58 |
+
tool_message = thinking_transparency.create_tool_usage_message(
|
| 59 |
+
tool_name="calculator",
|
| 60 |
+
tool_args={"operation": "multiply", "a": 4, "b": 7},
|
| 61 |
+
result="28"
|
| 62 |
+
)
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
### Error Handling
|
| 66 |
+
```python
|
| 67 |
+
# Centralized error classification and handling
|
| 68 |
+
error_handler = get_error_handler()
|
| 69 |
+
error_info = error_handler.classify_error(error, "openrouter")
|
| 70 |
+
if error_info.is_temporary:
|
| 71 |
+
# Handle retry logic
|
| 72 |
+
pass
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
## π― Problem Solutions
|
| 76 |
+
|
| 77 |
+
### 1. **Error Handling Issue**
|
| 78 |
+
- **Root Cause**: LLM providers return various error types (rate limits, auth issues, service unavailable)
|
| 79 |
+
- **Solution**: Centralized error classification and proper error reporting
|
| 80 |
+
- **Features**: Provider-specific handling, automatic retry, clear error messages
|
| 81 |
+
|
| 82 |
+
### 2. **Thinking Transparency**
|
| 83 |
+
- **Problem**: No visibility into agent's reasoning process
|
| 84 |
+
- **Solution**: Real-time thinking sections with collapsible metadata
|
| 85 |
+
- **Implementation**: Gradio ChatMessage metadata system
|
| 86 |
+
|
| 87 |
+
### 3. **Debug Visibility**
|
| 88 |
+
- **Problem**: Hard to debug agent issues
|
| 89 |
+
- **Solution**: Real-time streaming logs with categorization
|
| 90 |
+
- **Features**: Auto-refresh, clear logs, detailed metadata
|
| 91 |
+
|
| 92 |
+
## π Usage
|
| 93 |
+
|
| 94 |
+
### Running the Enhanced App
|
| 95 |
+
```bash
|
| 96 |
+
python app_ng.py
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
### Testing the Debug System
|
| 100 |
+
```bash
|
| 101 |
+
python test_debug_system.py
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
### Key Features in the UI
|
| 105 |
+
|
| 106 |
+
1. **Chat Tab**:
|
| 107 |
+
- Real-time thinking transparency
|
| 108 |
+
- Tool usage visualization
|
| 109 |
+
- Streaming responses
|
| 110 |
+
- Error handling with helpful messages
|
| 111 |
+
|
| 112 |
+
2. **Logs Tab**:
|
| 113 |
+
- Real-time debug logs
|
| 114 |
+
- Auto-refresh every 3 seconds
|
| 115 |
+
- Clear logs functionality
|
| 116 |
+
- Categorized logging
|
| 117 |
+
|
| 118 |
+
3. **Stats Tab**:
|
| 119 |
+
- Agent statistics
|
| 120 |
+
- LLM information
|
| 121 |
+
- Tool usage counts
|
| 122 |
+
|
| 123 |
+
## π Debugging OpenRouter Issues
|
| 124 |
+
|
| 125 |
+
The system now handles OpenRouter-specific issues:
|
| 126 |
+
|
| 127 |
+
1. **Rate Limiting (429 errors)**: Automatic detection and user-friendly messages
|
| 128 |
+
2. **Authentication Errors (401)**: Clear error messages with setup instructions
|
| 129 |
+
3. **Empty Responses**: Multiple retry strategies with fallback responses
|
| 130 |
+
4. **Service Issues**: Graceful degradation with helpful suggestions
|
| 131 |
+
|
| 132 |
+
## π Log Categories
|
| 133 |
+
|
| 134 |
+
- **INIT**: Initialization and setup
|
| 135 |
+
- **LLM**: LLM calls and responses
|
| 136 |
+
- **TOOL**: Tool execution and results
|
| 137 |
+
- **STREAM**: Streaming events
|
| 138 |
+
- **ERROR**: Error handling
|
| 139 |
+
- **THINKING**: Thinking process
|
| 140 |
+
- **SYSTEM**: System-level events
|
| 141 |
+
|
| 142 |
+
## π¨ Thinking Transparency Features
|
| 143 |
+
|
| 144 |
+
- **Collapsible thinking sections** with titles
|
| 145 |
+
- **Real-time thinking updates** as the agent processes
|
| 146 |
+
- **Tool usage metadata** with arguments and results
|
| 147 |
+
- **Error visualization** with helpful context
|
| 148 |
+
- **Status indicators** (pending, done, error)
|
| 149 |
+
|
| 150 |
+
## π§ Configuration
|
| 151 |
+
|
| 152 |
+
The system is highly configurable:
|
| 153 |
+
|
| 154 |
+
- **Log levels**: Adjust verbosity
|
| 155 |
+
- **Retry attempts**: Configure retry strategies
|
| 156 |
+
- **Auto-refresh intervals**: Customize UI updates
|
| 157 |
+
- **Session management**: Isolated debug contexts
|
| 158 |
+
|
| 159 |
+
## π Next Steps
|
| 160 |
+
|
| 161 |
+
1. **Test the system** with your OpenRouter setup
|
| 162 |
+
2. **Monitor the logs** for any issues
|
| 163 |
+
3. **Customize log levels** as needed
|
| 164 |
+
4. **Extend thinking transparency** for specific use cases
|
| 165 |
+
|
| 166 |
+
The system is designed to be lean, efficient, and transparent while providing comprehensive debugging capabilities for your LLM agent.
|
streaming_chat.py
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Modern Streaming Chat Interface
|
| 3 |
+
==============================
|
| 4 |
+
|
| 5 |
+
A sophisticated streaming chat interface that provides real-time thinking
|
| 6 |
+
transparency and tool usage visualization using Gradio's ChatMessage metadata.
|
| 7 |
+
|
| 8 |
+
Key Features:
|
| 9 |
+
- Real-time thinking transparency with collapsible sections
|
| 10 |
+
- Tool usage visualization with metadata
|
| 11 |
+
- Streaming response handling
|
| 12 |
+
- Integration with debug system
|
| 13 |
+
- Clean separation of concerns
|
| 14 |
+
- Support for multiple LLM providers
|
| 15 |
+
|
| 16 |
+
Based on Gradio's ChatMessage metadata system for thinking transparency.
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
import asyncio
|
| 20 |
+
import time
|
| 21 |
+
from typing import List, Dict, Any, Optional, Tuple, AsyncGenerator
|
| 22 |
+
from dataclasses import dataclass
|
| 23 |
+
import json
|
| 24 |
+
|
| 25 |
+
from debug_streamer import get_debug_streamer, get_log_handler, get_thinking_transparency, LogLevel, LogCategory
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
@dataclass
|
| 29 |
+
class ChatMessage:
|
| 30 |
+
"""Enhanced ChatMessage with metadata support for thinking transparency"""
|
| 31 |
+
role: str # "user" or "assistant"
|
| 32 |
+
content: str
|
| 33 |
+
metadata: Optional[Dict[str, Any]] = None
|
| 34 |
+
|
| 35 |
+
def to_gradio_format(self) -> Dict[str, Any]:
|
| 36 |
+
"""Convert to Gradio ChatMessage format"""
|
| 37 |
+
return {
|
| 38 |
+
"role": self.role,
|
| 39 |
+
"content": self.content,
|
| 40 |
+
"metadata": self.metadata or {}
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class StreamingChatInterface:
|
| 45 |
+
"""
|
| 46 |
+
Modern streaming chat interface with thinking transparency.
|
| 47 |
+
|
| 48 |
+
This class handles real-time streaming of chat responses with
|
| 49 |
+
thinking transparency, tool usage visualization, and debug logging.
|
| 50 |
+
"""
|
| 51 |
+
|
| 52 |
+
def __init__(self, session_id: str = "default"):
|
| 53 |
+
self.session_id = session_id
|
| 54 |
+
self.debug_streamer = get_debug_streamer(session_id)
|
| 55 |
+
self.log_handler = get_log_handler(session_id)
|
| 56 |
+
self.thinking_transparency = get_thinking_transparency(session_id)
|
| 57 |
+
|
| 58 |
+
# Chat state
|
| 59 |
+
self.current_thinking = ""
|
| 60 |
+
self.current_tool_usage = []
|
| 61 |
+
self.is_thinking = False
|
| 62 |
+
|
| 63 |
+
async def stream_chat_response(self, message: str, history: List[Tuple[str, str]],
|
| 64 |
+
agent: Any) -> AsyncGenerator[Tuple[List[Tuple[str, str]], str], None]:
|
| 65 |
+
"""
|
| 66 |
+
Stream a chat response with thinking transparency.
|
| 67 |
+
|
| 68 |
+
Args:
|
| 69 |
+
message: User message
|
| 70 |
+
history: Chat history as list of tuples
|
| 71 |
+
agent: The agent instance to use
|
| 72 |
+
|
| 73 |
+
Yields:
|
| 74 |
+
Updated history and empty message for Gradio
|
| 75 |
+
"""
|
| 76 |
+
if not message.strip():
|
| 77 |
+
yield history, ""
|
| 78 |
+
return
|
| 79 |
+
|
| 80 |
+
self.debug_streamer.info(f"Starting chat response for: {message[:50]}...", LogCategory.STREAM)
|
| 81 |
+
|
| 82 |
+
# Add user message to history
|
| 83 |
+
working_history = history + [(message, "")]
|
| 84 |
+
yield working_history, ""
|
| 85 |
+
|
| 86 |
+
try:
|
| 87 |
+
# Start thinking process
|
| 88 |
+
await self._start_thinking_process(message)
|
| 89 |
+
|
| 90 |
+
# Stream the response
|
| 91 |
+
async for event in self._stream_agent_response(message, history, agent):
|
| 92 |
+
if event["type"] == "thinking":
|
| 93 |
+
await self._handle_thinking_event(event, working_history)
|
| 94 |
+
elif event["type"] == "tool_use":
|
| 95 |
+
await self._handle_tool_use_event(event, working_history)
|
| 96 |
+
elif event["type"] == "content":
|
| 97 |
+
await self._handle_content_event(event, working_history)
|
| 98 |
+
elif event["type"] == "error":
|
| 99 |
+
await self._handle_error_event(event, working_history)
|
| 100 |
+
|
| 101 |
+
yield working_history, ""
|
| 102 |
+
|
| 103 |
+
# Complete the response
|
| 104 |
+
await self._complete_response(working_history)
|
| 105 |
+
yield working_history, ""
|
| 106 |
+
|
| 107 |
+
except Exception as e:
|
| 108 |
+
self.debug_streamer.error(f"Error in streaming chat: {str(e)}", LogCategory.STREAM)
|
| 109 |
+
await self._handle_streaming_error(e, working_history)
|
| 110 |
+
yield working_history, ""
|
| 111 |
+
|
| 112 |
+
async def _start_thinking_process(self, message: str):
|
| 113 |
+
"""Start the thinking process"""
|
| 114 |
+
self.is_thinking = True
|
| 115 |
+
self.current_thinking = ""
|
| 116 |
+
self.current_tool_usage = []
|
| 117 |
+
|
| 118 |
+
self.thinking_transparency.start_thinking(
|
| 119 |
+
title="π§ Thinking",
|
| 120 |
+
metadata={"message": message[:100] + "..." if len(message) > 100 else message}
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
self.debug_streamer.thinking(f"Starting to process: {message}")
|
| 124 |
+
|
| 125 |
+
async def _stream_agent_response(self, message: str, history: List[Tuple[str, str]],
|
| 126 |
+
agent: Any) -> AsyncGenerator[Dict[str, Any], None]:
|
| 127 |
+
"""Stream response from the agent"""
|
| 128 |
+
try:
|
| 129 |
+
# Convert history to internal format
|
| 130 |
+
internal_history = []
|
| 131 |
+
for user_msg, assistant_msg in history:
|
| 132 |
+
internal_history.append(ChatMessage(role="user", content=user_msg))
|
| 133 |
+
if assistant_msg:
|
| 134 |
+
internal_history.append(ChatMessage(role="assistant", content=assistant_msg))
|
| 135 |
+
|
| 136 |
+
# Stream from agent
|
| 137 |
+
if hasattr(agent, 'stream_chat'):
|
| 138 |
+
async for event in agent.stream_chat(message, internal_history):
|
| 139 |
+
yield event
|
| 140 |
+
elif hasattr(agent, 'stream'):
|
| 141 |
+
# Fallback to basic streaming
|
| 142 |
+
for chunk in agent.stream(message, internal_history):
|
| 143 |
+
yield {"type": "content", "content": chunk}
|
| 144 |
+
else:
|
| 145 |
+
# Fallback to non-streaming
|
| 146 |
+
response = agent(message, chat_history=internal_history)
|
| 147 |
+
if hasattr(response, 'answer'):
|
| 148 |
+
yield {"type": "content", "content": response.answer}
|
| 149 |
+
else:
|
| 150 |
+
yield {"type": "content", "content": str(response)}
|
| 151 |
+
|
| 152 |
+
except Exception as e:
|
| 153 |
+
self.debug_streamer.error(f"Error streaming from agent: {str(e)}", LogCategory.STREAM)
|
| 154 |
+
yield {"type": "error", "content": f"Error: {str(e)}"}
|
| 155 |
+
|
| 156 |
+
async def _handle_thinking_event(self, event: Dict[str, Any], working_history: List[Tuple[str, str]]):
|
| 157 |
+
"""Handle thinking events"""
|
| 158 |
+
thinking_content = event.get("content", "")
|
| 159 |
+
self.current_thinking += thinking_content
|
| 160 |
+
|
| 161 |
+
# Update thinking in the last assistant message
|
| 162 |
+
if working_history and len(working_history) > 0:
|
| 163 |
+
last_user, last_assistant = working_history[-1]
|
| 164 |
+
if last_assistant == "":
|
| 165 |
+
# Create thinking message with metadata
|
| 166 |
+
thinking_message = self.thinking_transparency._create_thinking_message()
|
| 167 |
+
working_history[-1] = (last_user, thinking_message["content"])
|
| 168 |
+
else:
|
| 169 |
+
# Update existing thinking
|
| 170 |
+
working_history[-1] = (last_user, self.current_thinking)
|
| 171 |
+
|
| 172 |
+
async def _handle_tool_use_event(self, event: Dict[str, Any], working_history: List[Tuple[str, str]]):
|
| 173 |
+
"""Handle tool usage events"""
|
| 174 |
+
tool_name = event.get("content", "unknown")
|
| 175 |
+
tool_metadata = event.get("metadata", {})
|
| 176 |
+
|
| 177 |
+
self.current_tool_usage.append({
|
| 178 |
+
"tool_name": tool_name,
|
| 179 |
+
"metadata": tool_metadata
|
| 180 |
+
})
|
| 181 |
+
|
| 182 |
+
# Create tool usage message
|
| 183 |
+
tool_message = self.thinking_transparency.create_tool_usage_message(
|
| 184 |
+
tool_name=tool_name,
|
| 185 |
+
tool_args=tool_metadata.get("tool_args", {}),
|
| 186 |
+
result=tool_metadata.get("result", "Tool executed")
|
| 187 |
+
)
|
| 188 |
+
|
| 189 |
+
# Add tool usage to history
|
| 190 |
+
if working_history and len(working_history) > 0:
|
| 191 |
+
last_user, last_assistant = working_history[-1]
|
| 192 |
+
working_history[-1] = (last_user, tool_message["content"])
|
| 193 |
+
|
| 194 |
+
async def _handle_content_event(self, event: Dict[str, Any], working_history: List[Tuple[str, str]]):
|
| 195 |
+
"""Handle content events"""
|
| 196 |
+
content = event.get("content", "")
|
| 197 |
+
|
| 198 |
+
if working_history and len(working_history) > 0:
|
| 199 |
+
last_user, last_assistant = working_history[-1]
|
| 200 |
+
|
| 201 |
+
# If we have thinking content, complete it first
|
| 202 |
+
if self.is_thinking and self.current_thinking:
|
| 203 |
+
self.thinking_transparency.complete_thinking()
|
| 204 |
+
self.is_thinking = False
|
| 205 |
+
|
| 206 |
+
# Update the response content
|
| 207 |
+
if last_assistant == "":
|
| 208 |
+
working_history[-1] = (last_user, content)
|
| 209 |
+
else:
|
| 210 |
+
# Append to existing content
|
| 211 |
+
working_history[-1] = (last_user, last_assistant + content)
|
| 212 |
+
|
| 213 |
+
async def _handle_error_event(self, event: Dict[str, Any], working_history: List[Tuple[str, str]]):
|
| 214 |
+
"""Handle error events"""
|
| 215 |
+
error_content = event.get("content", "Unknown error")
|
| 216 |
+
|
| 217 |
+
# Log the error for debugging
|
| 218 |
+
self.debug_streamer.error(f"Streaming error: {error_content}", LogCategory.LLM)
|
| 219 |
+
|
| 220 |
+
if working_history and len(working_history) > 0:
|
| 221 |
+
last_user, last_assistant = working_history[-1]
|
| 222 |
+
working_history[-1] = (last_user, f"β {error_content}")
|
| 223 |
+
|
| 224 |
+
async def _complete_response(self, working_history: List[Tuple[str, str]]):
|
| 225 |
+
"""Complete the response process"""
|
| 226 |
+
if self.is_thinking:
|
| 227 |
+
self.thinking_transparency.complete_thinking()
|
| 228 |
+
self.is_thinking = False
|
| 229 |
+
|
| 230 |
+
self.debug_streamer.success("Chat response completed", LogCategory.STREAM)
|
| 231 |
+
|
| 232 |
+
async def _handle_streaming_error(self, error: Exception, working_history: List[Tuple[str, str]]):
|
| 233 |
+
"""Handle streaming errors"""
|
| 234 |
+
error_msg = f"β **Streaming Error**\n\n{str(error)}\n\nPlease try again."
|
| 235 |
+
|
| 236 |
+
if working_history and len(working_history) > 0:
|
| 237 |
+
last_user, last_assistant = working_history[-1]
|
| 238 |
+
working_history[-1] = (last_user, error_msg)
|
| 239 |
+
|
| 240 |
+
def create_thinking_message(self, content: str, title: str = "π§ Thinking") -> ChatMessage:
|
| 241 |
+
"""Create a thinking message with metadata"""
|
| 242 |
+
return ChatMessage(
|
| 243 |
+
role="assistant",
|
| 244 |
+
content=content,
|
| 245 |
+
metadata={
|
| 246 |
+
"title": title,
|
| 247 |
+
"status": "pending" if self.is_thinking else "done"
|
| 248 |
+
}
|
| 249 |
+
)
|
| 250 |
+
|
| 251 |
+
def create_tool_usage_message(self, tool_name: str, tool_args: Dict[str, Any],
|
| 252 |
+
result: str) -> ChatMessage:
|
| 253 |
+
"""Create a tool usage message with metadata"""
|
| 254 |
+
return ChatMessage(
|
| 255 |
+
role="assistant",
|
| 256 |
+
content=f"π§ **{tool_name}**\n\n**Arguments:**\n{json.dumps(tool_args, indent=2)}\n\n**Result:**\n{result}",
|
| 257 |
+
metadata={
|
| 258 |
+
"title": f"π§ {tool_name}",
|
| 259 |
+
"status": "done",
|
| 260 |
+
"tool_name": tool_name,
|
| 261 |
+
"tool_args": tool_args
|
| 262 |
+
}
|
| 263 |
+
)
|
| 264 |
+
|
| 265 |
+
def create_error_message(self, error: str, error_type: str = "error") -> ChatMessage:
|
| 266 |
+
"""Create an error message with metadata"""
|
| 267 |
+
return ChatMessage(
|
| 268 |
+
role="assistant",
|
| 269 |
+
content=f"β **{error_type.title()}**\n\n{error}",
|
| 270 |
+
metadata={
|
| 271 |
+
"title": f"β {error_type.title()}",
|
| 272 |
+
"status": "done",
|
| 273 |
+
"error": True
|
| 274 |
+
}
|
| 275 |
+
)
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
class GradioChatInterface:
|
| 279 |
+
"""
|
| 280 |
+
Gradio-specific chat interface that handles the conversion between
|
| 281 |
+
internal chat format and Gradio's expected format.
|
| 282 |
+
"""
|
| 283 |
+
|
| 284 |
+
def __init__(self, session_id: str = "default"):
|
| 285 |
+
self.streaming_chat = StreamingChatInterface(session_id)
|
| 286 |
+
self.debug_streamer = get_debug_streamer(session_id)
|
| 287 |
+
self.log_handler = get_log_handler(session_id)
|
| 288 |
+
|
| 289 |
+
async def chat_with_agent(self, message: str, history: List[Tuple[str, str]],
|
| 290 |
+
agent: Any) -> Tuple[List[Tuple[str, str]], str]:
|
| 291 |
+
"""
|
| 292 |
+
Chat with the agent using Gradio format.
|
| 293 |
+
|
| 294 |
+
Args:
|
| 295 |
+
message: User message
|
| 296 |
+
history: Chat history as list of tuples
|
| 297 |
+
agent: The agent instance
|
| 298 |
+
|
| 299 |
+
Returns:
|
| 300 |
+
Updated history and empty message
|
| 301 |
+
"""
|
| 302 |
+
try:
|
| 303 |
+
# Stream the response
|
| 304 |
+
async for updated_history, _ in self.streaming_chat.stream_chat_response(
|
| 305 |
+
message, history, agent
|
| 306 |
+
):
|
| 307 |
+
# Yield intermediate results for real-time updates
|
| 308 |
+
pass
|
| 309 |
+
|
| 310 |
+
return updated_history, ""
|
| 311 |
+
|
| 312 |
+
except Exception as e:
|
| 313 |
+
self.debug_streamer.error(f"Error in Gradio chat interface: {str(e)}", LogCategory.STREAM)
|
| 314 |
+
error_history = history + [(message, f"β Error: {str(e)}")]
|
| 315 |
+
return error_history, ""
|
| 316 |
+
|
| 317 |
+
def get_current_logs(self) -> str:
|
| 318 |
+
"""Get current logs for the Logs tab"""
|
| 319 |
+
return self.log_handler.get_current_logs()
|
| 320 |
+
|
| 321 |
+
def clear_logs(self):
|
| 322 |
+
"""Clear the logs"""
|
| 323 |
+
self.log_handler.clear_logs()
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
# Global chat interface instances
|
| 327 |
+
_global_chat_interface: Optional[GradioChatInterface] = None
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
def get_chat_interface(session_id: str = "default") -> GradioChatInterface:
|
| 331 |
+
"""Get the global chat interface instance"""
|
| 332 |
+
global _global_chat_interface
|
| 333 |
+
if _global_chat_interface is None:
|
| 334 |
+
_global_chat_interface = GradioChatInterface(session_id)
|
| 335 |
+
return _global_chat_interface
|
test_debug_system.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Test script for the new debug system and thinking transparency.
|
| 3 |
+
|
| 4 |
+
This script tests the debug streaming, thinking transparency, and response fixing
|
| 5 |
+
functionality to ensure everything works correctly.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import asyncio
|
| 9 |
+
import time
|
| 10 |
+
from debug_streamer import get_debug_streamer, get_log_handler, get_thinking_transparency, LogLevel, LogCategory
|
| 11 |
+
from streaming_chat import get_chat_interface
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
async def test_debug_system():
|
| 15 |
+
"""Test the debug system components"""
|
| 16 |
+
print("π§ͺ Testing Debug System Components...")
|
| 17 |
+
|
| 18 |
+
# Test debug streamer
|
| 19 |
+
debug_streamer = get_debug_streamer("test")
|
| 20 |
+
print("β
Debug streamer initialized")
|
| 21 |
+
|
| 22 |
+
# Test log handler
|
| 23 |
+
log_handler = get_log_handler("test")
|
| 24 |
+
print("β
Log handler initialized")
|
| 25 |
+
|
| 26 |
+
# Test thinking transparency
|
| 27 |
+
thinking_transparency = get_thinking_transparency("test")
|
| 28 |
+
print("β
Thinking transparency initialized")
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
# Test chat interface
|
| 32 |
+
chat_interface = get_chat_interface("test")
|
| 33 |
+
print("β
Chat interface initialized")
|
| 34 |
+
|
| 35 |
+
# Test logging
|
| 36 |
+
debug_streamer.info("Test info message", LogCategory.SYSTEM)
|
| 37 |
+
debug_streamer.warning("Test warning message", LogCategory.SYSTEM)
|
| 38 |
+
debug_streamer.error("Test error message", LogCategory.SYSTEM)
|
| 39 |
+
debug_streamer.success("Test success message", LogCategory.SYSTEM)
|
| 40 |
+
|
| 41 |
+
# Test thinking process
|
| 42 |
+
thinking_transparency.start_thinking("π§ Test Thinking")
|
| 43 |
+
thinking_transparency.add_thinking("This is a test thinking process...")
|
| 44 |
+
thinking_transparency.add_thinking(" Adding more thoughts...")
|
| 45 |
+
thinking_message = thinking_transparency.complete_thinking()
|
| 46 |
+
print(f"β
Thinking message created: {thinking_message}")
|
| 47 |
+
|
| 48 |
+
# Test tool usage
|
| 49 |
+
tool_message = thinking_transparency.create_tool_usage_message(
|
| 50 |
+
tool_name="test_tool",
|
| 51 |
+
tool_args={"param1": "value1", "param2": "value2"},
|
| 52 |
+
result="Tool executed successfully"
|
| 53 |
+
)
|
| 54 |
+
print(f"β
Tool message created: {tool_message}")
|
| 55 |
+
|
| 56 |
+
# Test log retrieval
|
| 57 |
+
logs = log_handler.get_current_logs()
|
| 58 |
+
print(f"β
Current logs: {len(logs)} characters")
|
| 59 |
+
|
| 60 |
+
print("π All tests passed!")
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
async def test_streaming_chat():
|
| 66 |
+
"""Test the streaming chat interface"""
|
| 67 |
+
print("\n㪠Testing Streaming Chat Interface...")
|
| 68 |
+
|
| 69 |
+
chat_interface = get_chat_interface("test")
|
| 70 |
+
|
| 71 |
+
# Test message creation
|
| 72 |
+
thinking_msg = chat_interface.streaming_chat.create_thinking_message(
|
| 73 |
+
"This is a test thinking process",
|
| 74 |
+
"π§ Test Thinking"
|
| 75 |
+
)
|
| 76 |
+
print(f"β
Thinking message: {thinking_msg.role} - {thinking_msg.content[:50]}...")
|
| 77 |
+
|
| 78 |
+
tool_msg = chat_interface.streaming_chat.create_tool_usage_message(
|
| 79 |
+
tool_name="test_tool",
|
| 80 |
+
tool_args={"test": "value"},
|
| 81 |
+
result="Success"
|
| 82 |
+
)
|
| 83 |
+
print(f"β
Tool message: {tool_msg.role} - {tool_msg.content[:50]}...")
|
| 84 |
+
|
| 85 |
+
error_msg = chat_interface.streaming_chat.create_error_message(
|
| 86 |
+
"Test error occurred",
|
| 87 |
+
"Test Error"
|
| 88 |
+
)
|
| 89 |
+
print(f"β
Error message: {error_msg.role} - {error_msg.content[:50]}...")
|
| 90 |
+
|
| 91 |
+
print("π Streaming chat tests passed!")
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
async def main():
|
| 95 |
+
"""Run all tests"""
|
| 96 |
+
print("π Starting Debug System Tests...\n")
|
| 97 |
+
|
| 98 |
+
await test_debug_system()
|
| 99 |
+
await test_streaming_chat()
|
| 100 |
+
|
| 101 |
+
print("\nπ All tests completed successfully!")
|
| 102 |
+
print("\nThe debug system is ready for use with the Next-Gen App!")
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
if __name__ == "__main__":
|
| 106 |
+
asyncio.run(main())
|