arterm-sedov commited on
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
Files changed (5) hide show
  1. app_ng.py +86 -41
  2. debug_streamer.py +403 -0
  3. docs/DEBUG_SYSTEM_README.md +166 -0
  4. streaming_chat.py +335 -0
  5. 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
- return "\n".join(self.initialization_logs)
 
 
 
 
 
 
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[Tuple[str, str]]) -> Tuple[List[Tuple[str, str]], str]:
113
  """
114
- Chat with the agent using modern streaming.
115
 
116
  Args:
117
  message: User message
118
- history: Chat history as list of tuples
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((message, error_msg))
 
126
  return history, ""
127
 
128
- # Convert history to ChatMessage format
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
- async for event in self.agent.stream_chat(message, chat_history[:-1]): # Exclude current message
144
- if event["type"] == "content":
145
- response_content += event["content"]
146
- elif event["type"] == "thinking":
147
- thinking_content = event["content"]
148
- elif event["type"] == "tool_use":
149
- tool_usage.append(event["content"])
150
- elif event["type"] == "error":
151
- response_content = f"❌ {event['content']}"
152
- break
153
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  except Exception as e:
155
- response_content = f"❌ Error: {str(e)}"
156
-
157
- # Add response to history
158
- history.append((message, response_content))
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
- last_response = history[-1][1] # Get last assistant message
296
- return last_response
 
 
 
 
297
  return ""
298
 
299
  def quick_math():
@@ -345,8 +373,8 @@ class NextGenApp:
345
  )
346
 
347
  # Logs Tab
348
- with gr.TabItem("πŸ“œ Initialization Logs", id="logs"):
349
- gr.Markdown("### Agent Initialization Logs")
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())