kshitijthakkar commited on
Commit
4e4722b
Β·
1 Parent(s): ea9bb7d

feat: Add agent reasoning and tool call execution display to chat

Browse files

- Add helper functions to process ActionStep, PlanningStep, and FinalAnswerStep
- Implement stream_to_gradio() for streaming agent responses with ChatMessages
- Update chatbot to use type='messages' for rich display with collapsible sections
- Display agent reasoning (πŸ’­), tool calls (πŸ› οΈ), execution logs (πŸ“‹), and errors (⚠️)
- Add specific icons for TraceMind MCP tools (πŸ“Š leaderboard, πŸ” trace, πŸ’° cost)
- Show token usage and duration metrics for each step
- Remove separate reasoning panel in favor of inline display
- Update event handlers to support streaming responses

Similar to Outage Odyssey implementation for transparent agent behavior.

Files changed (2) hide show
  1. app.py +6 -6
  2. screens/chat.py +312 -50
app.py CHANGED
@@ -2122,23 +2122,23 @@ with gr.Blocks(title="TraceMind-AI", theme=theme) as app:
2122
  ]
2123
  )
2124
 
2125
- # Chat screen event handlers
2126
  chat_components['send_btn'].click(
2127
  fn=on_send_message,
2128
- inputs=[chat_components['message'], chat_components['chatbot'], chat_components['show_reasoning']],
2129
- outputs=[chat_components['chatbot'], chat_components['message'], chat_components['reasoning_display']]
2130
  )
2131
 
2132
  chat_components['message'].submit(
2133
  fn=on_send_message,
2134
- inputs=[chat_components['message'], chat_components['chatbot'], chat_components['show_reasoning']],
2135
- outputs=[chat_components['chatbot'], chat_components['message'], chat_components['reasoning_display']]
2136
  )
2137
 
2138
  chat_components['clear_btn'].click(
2139
  fn=on_clear_chat,
2140
  inputs=[],
2141
- outputs=[chat_components['chatbot'], chat_components['message'], chat_components['reasoning_display']]
2142
  )
2143
 
2144
  chat_components['quick_analyze'].click(
 
2122
  ]
2123
  )
2124
 
2125
+ # Chat screen event handlers (with streaming)
2126
  chat_components['send_btn'].click(
2127
  fn=on_send_message,
2128
+ inputs=[chat_components['message'], chat_components['chatbot']],
2129
+ outputs=[chat_components['chatbot'], chat_components['message']]
2130
  )
2131
 
2132
  chat_components['message'].submit(
2133
  fn=on_send_message,
2134
+ inputs=[chat_components['message'], chat_components['chatbot']],
2135
+ outputs=[chat_components['chatbot'], chat_components['message']]
2136
  )
2137
 
2138
  chat_components['clear_btn'].click(
2139
  fn=on_clear_chat,
2140
  inputs=[],
2141
+ outputs=[chat_components['chatbot']]
2142
  )
2143
 
2144
  chat_components['quick_analyze'].click(
screens/chat.py CHANGED
@@ -14,6 +14,10 @@ import yaml
14
  try:
15
  from smolagents import CodeAgent, InferenceClientModel, LiteLLMModel
16
  from smolagents.mcp_client import MCPClient
 
 
 
 
17
  SMOLAGENTS_AVAILABLE = True
18
  except ImportError:
19
  SMOLAGENTS_AVAILABLE = False
@@ -32,6 +36,235 @@ _global_agent = None
32
  _global_mcp_client = None
33
 
34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  def create_agent():
36
  """Create smolagents agent with MCP server tools (singleton pattern)"""
37
  global _global_agent, _global_mcp_client
@@ -146,52 +379,74 @@ def cleanup_agent():
146
  _global_agent = None
147
 
148
 
149
- def chat_with_agent(
150
- message: str,
151
- history: List[Tuple[str, str]],
152
- show_reasoning: bool = True
153
- ) -> Tuple[List[Tuple[str, str]], str]:
154
  """
155
- Process user message with agent
156
 
157
  Args:
158
  message: User's input message
159
- history: Chat history
160
- show_reasoning: Whether to show agent's reasoning steps
161
 
162
- Returns:
163
- Updated history and reasoning log
164
  """
165
 
166
  if not SMOLAGENTS_AVAILABLE:
167
  # Mock response for when smolagents isn't available
168
- history.append((message, "πŸ€– Agent not available (smolagents not installed). Install with: pip install smolagents"))
169
- return history, "No reasoning available"
 
 
 
 
 
 
170
 
171
  try:
172
  agent = create_agent()
173
  if agent is None:
174
- history.append((message, "❌ Failed to initialize agent"))
175
- return history, "Agent initialization failed"
176
-
177
- # Run agent
178
- response = agent.run(message)
179
-
180
- # Extract reasoning steps
181
- reasoning_log = ""
182
- if hasattr(agent, 'logs') and show_reasoning:
183
- for log in agent.logs:
184
- reasoning_log += f"**{log['role']}**: {log['content']}\n\n"
185
-
186
- # Add to history
187
- history.append((message, str(response)))
188
-
189
- return history, reasoning_log
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
 
191
  except Exception as e:
192
- error_msg = f"❌ Error: {str(e)}"
193
- history.append((message, error_msg))
194
- return history, f"Error during execution: {str(e)}"
 
 
 
 
 
195
 
196
 
197
  def create_chat_ui():
@@ -240,12 +495,17 @@ def create_chat_ui():
240
 
241
  with gr.Row():
242
  with gr.Column(scale=2):
243
- # Chat interface
244
  components['chatbot'] = gr.Chatbot(
245
  label="Agent Conversation",
 
246
  height=500,
247
  show_label=True,
248
- avatar_images=(None, "https://raw.githubusercontent.com/Mandark-droid/TraceMind-AI/assets/Logo.png")
 
 
 
 
249
  )
250
 
251
  with gr.Row():
@@ -260,19 +520,19 @@ def create_chat_ui():
260
 
261
  with gr.Row():
262
  components['clear_btn'] = gr.Button("πŸ—‘οΈ Clear Chat")
263
- components['show_reasoning'] = gr.Checkbox(
264
- label="Show Agent Reasoning",
265
- value=True,
266
- info="Display the agent's planning and tool usage steps"
267
- )
268
 
269
  with gr.Column(scale=1):
270
- # Reasoning panel
271
- gr.Markdown("### 🧠 Agent Reasoning")
272
- components['reasoning_display'] = gr.Markdown(
273
- "*Agent's reasoning steps will appear here...*",
274
- label="Reasoning Log"
275
- )
 
 
 
 
 
276
 
277
  # Quick actions
278
  gr.Markdown("### ⚑ Quick Actions")
@@ -283,20 +543,22 @@ def create_chat_ui():
283
  return chat_screen, components
284
 
285
 
286
- def on_send_message(message, history, show_reasoning):
287
- """Handle send button click"""
288
  if not message.strip():
289
- return history, "", "Please enter a message"
 
290
 
291
- updated_history, reasoning = chat_with_agent(message, history, show_reasoning)
292
- return updated_history, "", reasoning
 
293
 
294
 
295
  def on_clear_chat():
296
  """Handle clear button click and cleanup agent connection"""
297
  # Cleanup agent and MCP client connection
298
  cleanup_agent()
299
- return [], "", "*Agent's reasoning steps will appear here...*"
300
 
301
 
302
  def on_quick_action(action_type):
 
14
  try:
15
  from smolagents import CodeAgent, InferenceClientModel, LiteLLMModel
16
  from smolagents.mcp_client import MCPClient
17
+ from smolagents.agent_types import AgentAudio, AgentImage, AgentText
18
+ from smolagents.agents import MultiStepAgent, PlanningStep
19
+ from smolagents.memory import ActionStep, FinalAnswerStep
20
+ from smolagents.models import ChatMessageStreamDelta
21
  SMOLAGENTS_AVAILABLE = True
22
  except ImportError:
23
  SMOLAGENTS_AVAILABLE = False
 
36
  _global_mcp_client = None
37
 
38
 
39
+ # ============================================================================
40
+ # Helper Functions for Agent Step Processing
41
+ # ============================================================================
42
+
43
+ def get_step_footnote_content(step_log: ActionStep | PlanningStep, step_name: str) -> str:
44
+ """Get a footnote string for a step log with duration and token information"""
45
+ step_footnote = f"**{step_name}**"
46
+
47
+ # Check if token_usage attribute exists and is not None
48
+ if hasattr(step_log, 'token_usage') and step_log.token_usage is not None:
49
+ step_footnote += f" | Input tokens: {step_log.token_usage.input_tokens:,} | Output tokens: {step_log.token_usage.output_tokens:,}"
50
+
51
+ # Add duration information if available
52
+ if hasattr(step_log, 'timing') and step_log.timing and step_log.timing.duration:
53
+ step_footnote += f" | Duration: {round(float(step_log.timing.duration), 2)}s"
54
+
55
+ step_footnote_content = f"""<span style="color: #bbbbc2; font-size: 12px;">{step_footnote}</span> """
56
+ return step_footnote_content
57
+
58
+
59
+ def _clean_model_output(model_output: str) -> str:
60
+ """Clean up model output by removing trailing tags and extra backticks."""
61
+ if not model_output:
62
+ return ""
63
+ model_output = model_output.strip()
64
+ # Remove any trailing <end_code> and extra backticks, handling multiple possible formats
65
+ import re
66
+ model_output = re.sub(r"```\s*<end_code>", "```", model_output)
67
+ model_output = re.sub(r"<end_code>\s*```", "```", model_output)
68
+ model_output = re.sub(r"```\s*\n\s*<end_code>", "```", model_output)
69
+ return model_output.strip()
70
+
71
+
72
+ def _format_code_content(content: str) -> str:
73
+ """Format code content as Python code block if it's not already formatted."""
74
+ import re
75
+ content = content.strip()
76
+ # Remove existing code blocks and end_code tags
77
+ content = re.sub(r"```.*?\n", "", content)
78
+ content = re.sub(r"\s*<end_code>\s*", "", content)
79
+ content = content.strip()
80
+ # Add Python code block formatting if not already present
81
+ if not content.startswith("```python"):
82
+ content = f"```python\n{content}\n```"
83
+ return content
84
+
85
+
86
+ def _process_action_step(step_log: ActionStep, skip_model_outputs: bool = False):
87
+ """Process an ActionStep and yield appropriate Gradio ChatMessage objects."""
88
+ import re
89
+
90
+ # Output the step number
91
+ step_number = f"πŸ”§ Step {step_log.step_number}"
92
+ if not skip_model_outputs:
93
+ yield gr.ChatMessage(role="assistant", content=f"**{step_number}**", metadata={"status": "done"})
94
+
95
+ # First yield the thought/reasoning from the LLM
96
+ if not skip_model_outputs and getattr(step_log, "model_output", ""):
97
+ model_output = _clean_model_output(step_log.model_output)
98
+ # Format as thinking/reasoning
99
+ formatted_output = f"πŸ’­ **Reasoning:**\n{model_output}"
100
+ yield gr.ChatMessage(role="assistant", content=formatted_output, metadata={"status": "done"})
101
+
102
+ # For tool calls, create a parent message
103
+ if getattr(step_log, "tool_calls", []):
104
+ first_tool_call = step_log.tool_calls[0]
105
+ used_code = first_tool_call.name in ["python_interpreter", "execute_code", "final_answer"]
106
+
107
+ # Process arguments based on type
108
+ args = first_tool_call.arguments
109
+ if isinstance(args, dict):
110
+ content = str(args.get("answer", str(args)))
111
+ else:
112
+ content = str(args).strip()
113
+
114
+ # Format code content if needed
115
+ if used_code and "```" not in content:
116
+ content = _format_code_content(content)
117
+
118
+ # Choose appropriate emoji and title based on tool
119
+ tool_emoji = "πŸ› οΈ"
120
+ tool_title = f"Used tool: {first_tool_call.name}"
121
+
122
+ # Specific tool icons for TraceMind MCP tools
123
+ if "leaderboard" in first_tool_call.name.lower():
124
+ tool_emoji = "πŸ“Š"
125
+ tool_title = f"Analyzed Leaderboard using {first_tool_call.name}"
126
+ elif "trace" in first_tool_call.name.lower() or "debug" in first_tool_call.name.lower():
127
+ tool_emoji = "πŸ”"
128
+ tool_title = f"Debugged Trace using {first_tool_call.name}"
129
+ elif "cost" in first_tool_call.name.lower() or "estimate" in first_tool_call.name.lower():
130
+ tool_emoji = "πŸ’°"
131
+ tool_title = f"Estimated Cost using {first_tool_call.name}"
132
+ elif used_code:
133
+ tool_emoji = "πŸ’»"
134
+ tool_title = f"Executed Code using {first_tool_call.name}"
135
+
136
+ # Create the tool call message
137
+ parent_message_tool = gr.ChatMessage(
138
+ role="assistant",
139
+ content=content,
140
+ metadata={
141
+ "title": f"{tool_emoji} {tool_title}",
142
+ "status": "done",
143
+ },
144
+ )
145
+ yield parent_message_tool
146
+
147
+ # Display execution logs if they exist
148
+ if getattr(step_log, "observations", "") and step_log.observations.strip():
149
+ import re
150
+ log_content = step_log.observations.strip()
151
+ if log_content:
152
+ log_content = re.sub(r"^Execution logs:\s*", "", log_content)
153
+ yield gr.ChatMessage(
154
+ role="assistant",
155
+ content=f"```bash\n{log_content}\n```",
156
+ metadata={"title": "πŸ“‹ Execution Logs", "status": "done"},
157
+ )
158
+
159
+ # Handle errors
160
+ if getattr(step_log, "error", None):
161
+ error_msg = f"⚠️ **Error:** {str(step_log.error)}"
162
+ yield gr.ChatMessage(
163
+ role="assistant", content=error_msg, metadata={"title": "🚫 Error", "status": "done"}
164
+ )
165
+
166
+ # Add step footnote and separator
167
+ yield gr.ChatMessage(
168
+ role="assistant", content=get_step_footnote_content(step_log, step_number), metadata={"status": "done"}
169
+ )
170
+ yield gr.ChatMessage(role="assistant", content="---", metadata={"status": "done"})
171
+
172
+
173
+ def _process_planning_step(step_log: PlanningStep, skip_model_outputs: bool = False):
174
+ """Process a PlanningStep and yield appropriate gradio.ChatMessage objects."""
175
+ if not skip_model_outputs:
176
+ yield gr.ChatMessage(role="assistant", content="🧠 **Planning Phase**", metadata={"status": "done"})
177
+ yield gr.ChatMessage(role="assistant", content=step_log.plan, metadata={"status": "done"})
178
+ yield gr.ChatMessage(
179
+ role="assistant", content=get_step_footnote_content(step_log, "Planning Phase"), metadata={"status": "done"}
180
+ )
181
+ yield gr.ChatMessage(role="assistant", content="---", metadata={"status": "done"})
182
+
183
+
184
+ def _process_final_answer_step(step_log: FinalAnswerStep):
185
+ """Process a FinalAnswerStep and yield appropriate gradio.ChatMessage objects."""
186
+ # Try different possible attribute names for the final answer
187
+ final_answer = None
188
+ possible_attrs = ['output', 'answer', 'result', 'content', 'final_answer']
189
+
190
+ for attr in possible_attrs:
191
+ if hasattr(step_log, attr):
192
+ final_answer = getattr(step_log, attr)
193
+ break
194
+
195
+ # If no known attribute found, use string representation of the step
196
+ if final_answer is None:
197
+ yield gr.ChatMessage(
198
+ role="assistant",
199
+ content=f"**Final answer:** {str(step_log)}",
200
+ metadata={"status": "done"}
201
+ )
202
+ return
203
+
204
+ # Process the final answer based on its type
205
+ if isinstance(final_answer, AgentText):
206
+ yield gr.ChatMessage(
207
+ role="assistant",
208
+ content=final_answer.to_string(),
209
+ metadata={"status": "done", "title": "πŸ“œ Final Answer"},
210
+ )
211
+ elif isinstance(final_answer, AgentImage):
212
+ # Handle image if needed
213
+ yield gr.ChatMessage(
214
+ role="assistant",
215
+ content=f"![Image]({final_answer.to_string()})",
216
+ metadata={"status": "done", "title": "🎨 Image Result"},
217
+ )
218
+ elif isinstance(final_answer, AgentAudio):
219
+ yield gr.ChatMessage(
220
+ role="assistant",
221
+ content={"path": final_answer.to_string(), "mime_type": "audio/wav"},
222
+ metadata={"status": "done", "title": "πŸ”Š Audio Result"},
223
+ )
224
+ else:
225
+ # Assume markdown content and render as-is
226
+ yield gr.ChatMessage(
227
+ role="assistant",
228
+ content=str(final_answer),
229
+ metadata={"status": "done", "title": "πŸ“œ Final Answer"},
230
+ )
231
+
232
+
233
+ def pull_messages_from_step(step_log: ActionStep | PlanningStep | FinalAnswerStep, skip_model_outputs: bool = False):
234
+ """Extract Gradio ChatMessage objects from agent steps with proper nesting."""
235
+ if isinstance(step_log, ActionStep):
236
+ yield from _process_action_step(step_log, skip_model_outputs)
237
+ elif isinstance(step_log, PlanningStep):
238
+ yield from _process_planning_step(step_log, skip_model_outputs)
239
+ elif isinstance(step_log, FinalAnswerStep):
240
+ yield from _process_final_answer_step(step_log)
241
+ else:
242
+ raise ValueError(f"Unsupported step type: {type(step_log)}")
243
+
244
+
245
+ def stream_to_gradio(
246
+ agent,
247
+ task: str,
248
+ reset_agent_memory: bool = False,
249
+ ):
250
+ """Runs an agent with the given task and streams the messages from the agent as gradio ChatMessages."""
251
+ intermediate_text = ""
252
+
253
+ for event in agent.run(
254
+ task, stream=True, max_steps=20, reset=reset_agent_memory
255
+ ):
256
+ if isinstance(event, ActionStep | PlanningStep | FinalAnswerStep):
257
+ intermediate_text = ""
258
+ for message in pull_messages_from_step(
259
+ event,
260
+ skip_model_outputs=getattr(agent, "stream_outputs", False),
261
+ ):
262
+ yield message
263
+ elif isinstance(event, ChatMessageStreamDelta):
264
+ intermediate_text += event.content or ""
265
+ yield intermediate_text
266
+
267
+
268
  def create_agent():
269
  """Create smolagents agent with MCP server tools (singleton pattern)"""
270
  global _global_agent, _global_mcp_client
 
379
  _global_agent = None
380
 
381
 
382
+ def chat_with_agent(message: str, history: list):
 
 
 
 
383
  """
384
+ Process user message with agent using streaming
385
 
386
  Args:
387
  message: User's input message
388
+ history: Chat history (list of ChatMessage objects)
 
389
 
390
+ Yields:
391
+ Updated history with streaming agent responses
392
  """
393
 
394
  if not SMOLAGENTS_AVAILABLE:
395
  # Mock response for when smolagents isn't available
396
+ history.append(gr.ChatMessage(role="user", content=message, metadata={"status": "done"}))
397
+ history.append(gr.ChatMessage(
398
+ role="assistant",
399
+ content="πŸ€– Agent not available (smolagents not installed). Install with: pip install smolagents",
400
+ metadata={"status": "done"}
401
+ ))
402
+ yield history
403
+ return
404
 
405
  try:
406
  agent = create_agent()
407
  if agent is None:
408
+ history.append(gr.ChatMessage(role="user", content=message, metadata={"status": "done"}))
409
+ history.append(gr.ChatMessage(
410
+ role="assistant",
411
+ content="❌ Failed to initialize agent",
412
+ metadata={"status": "done"}
413
+ ))
414
+ yield history
415
+ return
416
+
417
+ # Add user message
418
+ history.append(gr.ChatMessage(role="user", content=message, metadata={"status": "done"}))
419
+ yield history
420
+
421
+ # Stream agent responses
422
+ for msg in stream_to_gradio(agent, task=message, reset_agent_memory=False):
423
+ if isinstance(msg, gr.ChatMessage):
424
+ # Mark previous message as done if it was pending
425
+ if history and history[-1].metadata.get("status") == "pending":
426
+ history[-1].metadata["status"] = "done"
427
+ history.append(msg)
428
+ elif isinstance(msg, str): # Streaming text delta
429
+ msg = msg.replace("<", r"\<").replace(">", r"\>") # HTML tags seem to break Gradio Chatbot
430
+ if history and history[-1].metadata.get("status") == "pending":
431
+ history[-1].content = msg
432
+ else:
433
+ history.append(gr.ChatMessage(role="assistant", content=msg, metadata={"status": "pending"}))
434
+ yield history
435
+
436
+ # Mark final message as done
437
+ if history and history[-1].metadata.get("status") == "pending":
438
+ history[-1].metadata["status"] = "done"
439
+ yield history
440
 
441
  except Exception as e:
442
+ import traceback
443
+ error_msg = f"❌ Error: {str(e)}\n\n```\n{traceback.format_exc()}\n```"
444
+ history.append(gr.ChatMessage(
445
+ role="assistant",
446
+ content=error_msg,
447
+ metadata={"title": "🚫 Error", "status": "done"}
448
+ ))
449
+ yield history
450
 
451
 
452
  def create_chat_ui():
 
495
 
496
  with gr.Row():
497
  with gr.Column(scale=2):
498
+ # Chat interface (using type="messages" for rich ChatMessage display)
499
  components['chatbot'] = gr.Chatbot(
500
  label="Agent Conversation",
501
+ type="messages",
502
  height=500,
503
  show_label=True,
504
+ show_copy_button=True,
505
+ avatar_images=(
506
+ "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/smolagents/mascot_smol.png",
507
+ "https://raw.githubusercontent.com/Mandark-droid/TraceMind-AI/assets/Logo.png"
508
+ )
509
  )
510
 
511
  with gr.Row():
 
520
 
521
  with gr.Row():
522
  components['clear_btn'] = gr.Button("πŸ—‘οΈ Clear Chat")
 
 
 
 
 
523
 
524
  with gr.Column(scale=1):
525
+ # Info panel
526
+ gr.Markdown("### ℹ️ Agent Status")
527
+ gr.Markdown("""
528
+ The agent's reasoning, tool calls, and execution logs are displayed inline in the chat.
529
+
530
+ **Look for:**
531
+ - πŸ’­ **Reasoning** - Agent's thought process
532
+ - πŸ› οΈ **Tool Calls** - MCP server invocations
533
+ - πŸ“‹ **Execution Logs** - Tool outputs
534
+ - πŸ“œ **Final Answer** - Agent's response
535
+ """)
536
 
537
  # Quick actions
538
  gr.Markdown("### ⚑ Quick Actions")
 
543
  return chat_screen, components
544
 
545
 
546
+ def on_send_message(message, history):
547
+ """Handle send button click - now uses streaming"""
548
  if not message.strip():
549
+ yield history, ""
550
+ return
551
 
552
+ # Stream agent responses
553
+ for updated_history in chat_with_agent(message, history):
554
+ yield updated_history, ""
555
 
556
 
557
  def on_clear_chat():
558
  """Handle clear button click and cleanup agent connection"""
559
  # Cleanup agent and MCP client connection
560
  cleanup_agent()
561
+ return []
562
 
563
 
564
  def on_quick_action(action_type):