mohammedelfeky-ai commited on
Commit
0cc5955
·
verified ·
1 Parent(s): 15869ad

Update Gradio_UI.py

Browse files
Files changed (1) hide show
  1. Gradio_UI.py +195 -116
Gradio_UI.py CHANGED
@@ -20,7 +20,7 @@ import shutil
20
  from typing import Optional
21
 
22
  from smolagents.agent_types import AgentAudio, AgentImage, AgentText, handle_agent_output_types
23
- from smolagents.agents import ActionStep, MultiStepAgent
24
  from smolagents.memory import MemoryStep
25
  from smolagents.utils import _is_package_available
26
 
@@ -51,10 +51,9 @@ def pull_messages_from_step(
51
  if hasattr(step_log, "tool_calls") and step_log.tool_calls is not None:
52
  first_tool_call = step_log.tool_calls[0]
53
  used_code = first_tool_call.name == "python_interpreter"
54
- parent_id = f"call_{len(step_log.tool_calls)}"
55
 
56
  # Tool call becomes the parent message with timing info
57
- # First we will handle arguments based on type
58
  args = first_tool_call.arguments
59
  if isinstance(args, dict):
60
  content = str(args.get("answer", str(args)))
@@ -62,12 +61,14 @@ def pull_messages_from_step(
62
  content = str(args).strip()
63
 
64
  if used_code:
65
- # Clean up the content by removing any end code tags
66
  content = re.sub(r"```.*?\n", "", content) # Remove existing code blocks
67
  content = re.sub(r"\s*<end_code>\s*", "", content) # Remove end_code tags
68
  content = content.strip()
69
- if not content.startswith("```python"):
70
  content = f"```python\n{content}\n```"
 
 
 
71
 
72
  parent_message_tool = gr.ChatMessage(
73
  role="assistant",
@@ -80,12 +81,11 @@ def pull_messages_from_step(
80
  )
81
  yield parent_message_tool
82
 
83
- # Nesting execution logs under the tool call if they exist
84
  if hasattr(step_log, "observations") and (
85
  step_log.observations is not None and step_log.observations.strip()
86
- ): # Only yield execution logs if there's actual content
87
  log_content = step_log.observations.strip()
88
- if log_content:
89
  log_content = re.sub(r"^Execution logs:\s*", "", log_content)
90
  yield gr.ChatMessage(
91
  role="assistant",
@@ -93,33 +93,34 @@ def pull_messages_from_step(
93
  metadata={"title": "📝 Execution Logs", "parent_id": parent_id, "status": "done"},
94
  )
95
 
96
- # Nesting any errors under the tool call
97
  if hasattr(step_log, "error") and step_log.error is not None:
98
  yield gr.ChatMessage(
99
  role="assistant",
100
  content=str(step_log.error),
101
  metadata={"title": "💥 Error", "parent_id": parent_id, "status": "done"},
102
  )
 
 
 
103
 
104
- # Update parent message metadata to done status without yielding a new message
105
- parent_message_tool.metadata["status"] = "done"
106
-
107
- # Handle standalone errors but not from tool calls
108
- elif hasattr(step_log, "error") and step_log.error is not None:
109
  yield gr.ChatMessage(role="assistant", content=str(step_log.error), metadata={"title": "💥 Error"})
110
 
111
- # Calculate duration and token information
112
- step_footnote = f"{step_number}"
113
- if hasattr(step_log, "input_token_count") and hasattr(step_log, "output_token_count"):
114
  token_str = (
115
  f" | Input-tokens:{step_log.input_token_count:,} | Output-tokens:{step_log.output_token_count:,}"
116
  )
117
- step_footnote += token_str
118
- if hasattr(step_log, "duration"):
119
- step_duration = f" | Duration: {round(float(step_log.duration), 2)}" if step_log.duration else None
120
- step_footnote += step_duration
121
- step_footnote = f"""<span style="color: #bbbbc2; font-size: 12px;">{step_footnote}</span> """
122
- yield gr.ChatMessage(role="assistant", content=f"{step_footnote}")
 
 
 
123
  yield gr.ChatMessage(role="assistant", content="-----")
124
 
125
 
@@ -136,43 +137,33 @@ def stream_to_gradio(
136
  )
137
  import gradio as gr
138
 
139
- total_input_tokens = 0
140
- total_output_tokens = 0
 
 
 
141
 
142
  for step_log in agent.run(task, stream=True, reset=reset_agent_memory, additional_args=additional_args):
143
- # Track tokens if model provides them
144
- if hasattr(agent.model, "last_input_token_count"):
145
- total_input_tokens += agent.model.last_input_token_count
146
- total_output_tokens += agent.model.last_output_token_count
147
- if isinstance(step_log, ActionStep):
148
  step_log.input_token_count = agent.model.last_input_token_count
149
  step_log.output_token_count = agent.model.last_output_token_count
150
 
151
- for message in pull_messages_from_step(
152
- step_log,
153
- ):
154
  yield message
155
 
156
- final_answer = step_log # Last log is the run's final_answer
157
- final_answer = handle_agent_output_types(final_answer)
 
158
 
159
- if isinstance(final_answer, AgentText):
160
- yield gr.ChatMessage(
161
- role="assistant",
162
- content=f"**Final answer:**\n{final_answer.to_string()}\n",
163
- )
164
- elif isinstance(final_answer, AgentImage):
165
- yield gr.ChatMessage(
166
- role="assistant",
167
- content={"path": final_answer.to_string(), "mime_type": "image/png"},
168
- )
169
- elif isinstance(final_answer, AgentAudio):
170
- yield gr.ChatMessage(
171
- role="assistant",
172
- content={"path": final_answer.to_string(), "mime_type": "audio/wav"},
173
- )
174
  else:
175
- yield gr.ChatMessage(role="assistant", content=f"**Final answer:** {str(final_answer)}")
176
 
177
 
178
  class GradioUI:
@@ -187,17 +178,51 @@ class GradioUI:
187
  self.file_upload_folder = file_upload_folder
188
  if self.file_upload_folder is not None:
189
  if not os.path.exists(file_upload_folder):
190
- os.mkdir(file_upload_folder)
191
-
192
- def interact_with_agent(self, prompt, messages):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  import gradio as gr
 
 
 
194
 
195
- messages.append(gr.ChatMessage(role="user", content=prompt))
196
- yield messages
197
  for msg in stream_to_gradio(self.agent, task=prompt, reset_agent_memory=False):
198
- messages.append(msg)
199
- yield messages
200
- yield messages
 
 
 
 
 
 
 
201
 
202
  def upload_file(
203
  self,
@@ -209,9 +234,6 @@ class GradioUI:
209
  "text/plain",
210
  ],
211
  ):
212
- """
213
- Handle file uploads, default allowed types are .pdf, .docx, and .txt
214
- """
215
  import gradio as gr
216
 
217
  if file is None:
@@ -219,78 +241,135 @@ class GradioUI:
219
 
220
  try:
221
  mime_type, _ = mimetypes.guess_type(file.name)
 
 
222
  except Exception as e:
223
  return gr.Textbox(f"Error: {e}", visible=True), file_uploads_log
224
-
225
  if mime_type not in allowed_file_types:
226
- return gr.Textbox("File type disallowed", visible=True), file_uploads_log
227
 
228
- # Sanitize file name
229
  original_name = os.path.basename(file.name)
230
- sanitized_name = re.sub(
231
- r"[^\w\-.]", "_", original_name
232
- ) # Replace any non-alphanumeric, non-dash, or non-dot characters with underscores
233
-
234
- type_to_ext = {}
235
- for ext, t in mimetypes.types_map.items():
236
- if t not in type_to_ext:
237
- type_to_ext[t] = ext
238
-
239
- # Ensure the extension correlates to the mime type
240
- sanitized_name = sanitized_name.split(".")[:-1]
241
- sanitized_name.append("" + type_to_ext[mime_type])
242
- sanitized_name = "".join(sanitized_name)
243
-
244
- # Save the uploaded file to the specified folder
245
- file_path = os.path.join(self.file_upload_folder, os.path.basename(sanitized_name))
246
- shutil.copy(file.name, file_path)
247
 
248
  return gr.Textbox(f"File uploaded: {file_path}", visible=True), file_uploads_log + [file_path]
249
 
250
  def log_user_message(self, text_input, file_uploads_log):
251
- return (
252
- text_input
253
- + (
 
 
254
  f"\nYou have been provided with these files, which might be helpful or not: {file_uploads_log}"
255
- if len(file_uploads_log) > 0
256
- else ""
257
- ),
258
- "",
259
- )
 
 
 
 
 
 
 
 
 
260
 
261
  def launch(self, **kwargs):
262
  import gradio as gr
263
 
264
- with gr.Blocks(fill_height=True) as demo:
265
- stored_messages = gr.State([])
266
- file_uploads_log = gr.State([])
267
- chatbot = gr.Chatbot(
268
- label="Agent",
269
- type="messages",
270
- avatar_images=(
271
- None,
272
- "https://huggingface.co/datasets/agents-course/course-images/resolve/main/en/communication/Alfred.png",
273
- ),
274
- resizeable=True,
275
- scale=1,
276
- )
277
- # If an upload folder is provided, enable the upload feature
278
- if self.file_upload_folder is not None:
279
- upload_file = gr.File(label="Upload a file")
280
- upload_status = gr.Textbox(label="Upload Status", interactive=False, visible=False)
281
- upload_file.change(
282
- self.upload_file,
283
- [upload_file, file_uploads_log],
284
- [upload_status, file_uploads_log],
285
- )
286
- text_input = gr.Textbox(lines=1, label="Chat Message")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
287
  text_input.submit(
288
  self.log_user_message,
289
  [text_input, file_uploads_log],
290
- [stored_messages, text_input],
291
- ).then(self.interact_with_agent, [stored_messages, chatbot], [chatbot])
 
 
 
 
 
 
 
 
 
 
 
292
 
293
- demo.launch(debug=True, share=True, **kwargs)
 
 
294
 
295
 
296
  __all__ = ["stream_to_gradio", "GradioUI"]
 
20
  from typing import Optional
21
 
22
  from smolagents.agent_types import AgentAudio, AgentImage, AgentText, handle_agent_output_types
23
+ from smolagents.agents import ActionStep, MultiStepAgent # Ensure MultiStepAgent is correctly referenced
24
  from smolagents.memory import MemoryStep
25
  from smolagents.utils import _is_package_available
26
 
 
51
  if hasattr(step_log, "tool_calls") and step_log.tool_calls is not None:
52
  first_tool_call = step_log.tool_calls[0]
53
  used_code = first_tool_call.name == "python_interpreter"
54
+ parent_id = f"call_{len(step_log.tool_calls)}_{step_log.step_number or 'x'}" # Make parent_id more unique
55
 
56
  # Tool call becomes the parent message with timing info
 
57
  args = first_tool_call.arguments
58
  if isinstance(args, dict):
59
  content = str(args.get("answer", str(args)))
 
61
  content = str(args).strip()
62
 
63
  if used_code:
 
64
  content = re.sub(r"```.*?\n", "", content) # Remove existing code blocks
65
  content = re.sub(r"\s*<end_code>\s*", "", content) # Remove end_code tags
66
  content = content.strip()
67
+ if not content.startswith("```python"): # Ensure it's a python block
68
  content = f"```python\n{content}\n```"
69
+ else: # If it is, ensure newlines are correct
70
+ content = content.replace("```python", "```python\n").replace("\n```", "\n```")
71
+
72
 
73
  parent_message_tool = gr.ChatMessage(
74
  role="assistant",
 
81
  )
82
  yield parent_message_tool
83
 
 
84
  if hasattr(step_log, "observations") and (
85
  step_log.observations is not None and step_log.observations.strip()
86
+ ):
87
  log_content = step_log.observations.strip()
88
+ if log_content: # Only yield if there's actual content
89
  log_content = re.sub(r"^Execution logs:\s*", "", log_content)
90
  yield gr.ChatMessage(
91
  role="assistant",
 
93
  metadata={"title": "📝 Execution Logs", "parent_id": parent_id, "status": "done"},
94
  )
95
 
 
96
  if hasattr(step_log, "error") and step_log.error is not None:
97
  yield gr.ChatMessage(
98
  role="assistant",
99
  content=str(step_log.error),
100
  metadata={"title": "💥 Error", "parent_id": parent_id, "status": "done"},
101
  )
102
+ # This direct update might not work as expected as yield creates new objects.
103
+ # Status update is visual; actual logic might be more complex.
104
+ parent_message_tool.metadata["status"] = "done"
105
 
106
+ elif hasattr(step_log, "error") and step_log.error is not None: # Standalone errors
 
 
 
 
107
  yield gr.ChatMessage(role="assistant", content=str(step_log.error), metadata={"title": "💥 Error"})
108
 
109
+ step_footnote_parts = [step_number]
110
+ if hasattr(step_log, "input_token_count") and step_log.input_token_count is not None and \
111
+ hasattr(step_log, "output_token_count") and step_log.output_token_count is not None:
112
  token_str = (
113
  f" | Input-tokens:{step_log.input_token_count:,} | Output-tokens:{step_log.output_token_count:,}"
114
  )
115
+ step_footnote_parts.append(token_str)
116
+ if hasattr(step_log, "duration") and step_log.duration is not None:
117
+ step_duration = f" | Duration: {round(float(step_log.duration), 2)}s"
118
+ step_footnote_parts.append(step_duration)
119
+
120
+ step_footnote_text = "".join(filter(None, step_footnote_parts))
121
+ if step_footnote_text:
122
+ step_footnote = f"""<span style="color: #bbbbc2; font-size: 12px;">{step_footnote_text}</span> """
123
+ yield gr.ChatMessage(role="assistant", content=f"{step_footnote}")
124
  yield gr.ChatMessage(role="assistant", content="-----")
125
 
126
 
 
137
  )
138
  import gradio as gr
139
 
140
+ # Reset interaction logs for the new run if the agent has this attribute
141
+ if hasattr(agent, 'interaction_logs'):
142
+ agent.interaction_logs.clear()
143
+ print("DEBUG: Cleared agent interaction_logs for new run.")
144
+
145
 
146
  for step_log in agent.run(task, stream=True, reset=reset_agent_memory, additional_args=additional_args):
147
+ if hasattr(agent.model, "last_input_token_count") and agent.model.last_input_token_count is not None: # Check for None
148
+ if isinstance(step_log, ActionStep): # Only add token counts to ActionSteps
 
 
 
149
  step_log.input_token_count = agent.model.last_input_token_count
150
  step_log.output_token_count = agent.model.last_output_token_count
151
 
152
+ for message in pull_messages_from_step(step_log):
 
 
153
  yield message
154
 
155
+ # After the loop, step_log holds the final answer or the last step's log
156
+ final_answer_content = step_log
157
+ final_answer_processed = handle_agent_output_types(final_answer_content)
158
 
159
+ if isinstance(final_answer_processed, AgentText):
160
+ yield gr.ChatMessage(role="assistant", content=f"**Final answer:**\n{final_answer_processed.to_string()}\n")
161
+ elif isinstance(final_answer_processed, AgentImage):
162
+ yield gr.ChatMessage(role="assistant", content={"path": final_answer_processed.to_string(), "mime_type": "image/png"})
163
+ elif isinstance(final_answer_processed, AgentAudio):
164
+ yield gr.ChatMessage(role="assistant", content={"path": final_answer_processed.to_string(), "mime_type": "audio/wav"})
 
 
 
 
 
 
 
 
 
165
  else:
166
+ yield gr.ChatMessage(role="assistant", content=f"**Final answer:** {str(final_answer_processed)}")
167
 
168
 
169
  class GradioUI:
 
178
  self.file_upload_folder = file_upload_folder
179
  if self.file_upload_folder is not None:
180
  if not os.path.exists(file_upload_folder):
181
+ os.makedirs(self.file_upload_folder, exist_ok=True) # Use makedirs
182
+
183
+ self._latest_file_path_for_download = None # For download button state
184
+
185
+ def _check_for_created_file(self):
186
+ """Helper function to check interaction logs for a created file path."""
187
+ self._latest_file_path_for_download = None # Reset
188
+ if hasattr(self.agent, 'interaction_logs') and self.agent.interaction_logs:
189
+ print(f"DEBUG UI: Checking {len(self.agent.interaction_logs)} interaction log entries.")
190
+ for log_entry in self.agent.interaction_logs:
191
+ if log_entry.get("tool_name") == "create_document":
192
+ tool_output_value = log_entry.get("tool_output")
193
+ print(f"DEBUG UI: Log for 'create_document', output: {tool_output_value}")
194
+ if tool_output_value and isinstance(tool_output_value, str):
195
+ if not tool_output_value.strip().startswith("ERROR:"):
196
+ normalized_path = os.path.normpath(tool_output_value)
197
+ if os.path.exists(normalized_path):
198
+ self._latest_file_path_for_download = normalized_path
199
+ print(f"DEBUG UI: File path for download set: {self._latest_file_path_for_download}")
200
+ return True # Found a valid file
201
+ else:
202
+ print(f"DEBUG UI: Path from log ('{normalized_path}') does not exist.")
203
+ else:
204
+ print(f"DEBUG UI: 'create_document' tool reported error: {tool_output_value}")
205
+ return False
206
+
207
+
208
+ def interact_with_agent(self, prompt, messages_history, download_btn_state, file_output_state):
209
  import gradio as gr
210
+
211
+ messages_history.append(gr.ChatMessage(role="user", content=prompt))
212
+ yield messages_history, gr.update(visible=False), gr.update(value=None, visible=False) # Hide download items initially
213
 
214
+ # Stream agent messages to chatbot
 
215
  for msg in stream_to_gradio(self.agent, task=prompt, reset_agent_memory=False):
216
+ messages_history.append(msg)
217
+ yield messages_history, gr.update(visible=False), gr.update(value=None, visible=False) # Keep hidden during streaming
218
+
219
+ # After streaming all agent messages, check for created file
220
+ file_found = self._check_for_created_file()
221
+
222
+ # Update UI based on whether a file was found
223
+ # Yielding final state for chatbot, download button, and file component
224
+ yield messages_history, gr.update(visible=file_found), gr.update(value=None, visible=False)
225
+
226
 
227
  def upload_file(
228
  self,
 
234
  "text/plain",
235
  ],
236
  ):
 
 
 
237
  import gradio as gr
238
 
239
  if file is None:
 
241
 
242
  try:
243
  mime_type, _ = mimetypes.guess_type(file.name)
244
+ if mime_type is None: # Fallback if guess_type returns None
245
+ mime_type = file.type # Gradio File object has a 'type' attribute
246
  except Exception as e:
247
  return gr.Textbox(f"Error: {e}", visible=True), file_uploads_log
248
+
249
  if mime_type not in allowed_file_types:
250
+ return gr.Textbox(f"File type '{mime_type}' disallowed", visible=True), file_uploads_log
251
 
 
252
  original_name = os.path.basename(file.name)
253
+ sanitized_name = re.sub(r"[^\w\-.]", "_", original_name)
254
+
255
+ # Ensure correct extension based on mime type, if possible
256
+ base_name, current_ext = os.path.splitext(sanitized_name)
257
+
258
+ type_to_ext_map = {
259
+ "application/pdf": ".pdf",
260
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
261
+ "text/plain": ".txt",
262
+ }
263
+ expected_ext = type_to_ext_map.get(mime_type)
264
+ if expected_ext and current_ext.lower() != expected_ext:
265
+ sanitized_name = base_name + expected_ext
266
+
267
+ file_path = os.path.join(self.file_upload_folder, sanitized_name)
268
+ shutil.copy(file.name, file_path) # file.name is the temp path of the uploaded file
 
269
 
270
  return gr.Textbox(f"File uploaded: {file_path}", visible=True), file_uploads_log + [file_path]
271
 
272
  def log_user_message(self, text_input, file_uploads_log):
273
+ # This function prepares the prompt that goes to the agent.
274
+ # It also clears the text_input box.
275
+ full_prompt = text_input
276
+ if file_uploads_log: # Check if list is not empty
277
+ full_prompt += (
278
  f"\nYou have been provided with these files, which might be helpful or not: {file_uploads_log}"
279
+ )
280
+ return full_prompt, "" # Return the full prompt and an empty string to clear input
281
+
282
+ def prepare_and_show_download_file(self):
283
+ import gradio as gr
284
+ if self._latest_file_path_for_download and os.path.exists(self._latest_file_path_for_download):
285
+ print(f"DEBUG UI: Preparing download for UI: {self._latest_file_path_for_download}")
286
+ return gr.File.update(value=self._latest_file_path_for_download,
287
+ label=os.path.basename(self._latest_file_path_for_download),
288
+ visible=True)
289
+ else:
290
+ print("DEBUG UI: No valid file path to prepare for download component.")
291
+ gr.Warning("No file available for download or path is invalid.")
292
+ return gr.File.update(visible=False)
293
 
294
  def launch(self, **kwargs):
295
  import gradio as gr
296
 
297
+ with gr.Blocks(fill_height=True, theme=gr.themes.Soft()) as demo: # Added a theme
298
+ # --- State Variables ---
299
+ # stored_messages is used to build the prompt for the agent, not directly for chatbot display here.
300
+ # The chatbot takes messages directly from interact_with_agent.
301
+ # We'll use chat_history_state for the chatbot's message list.
302
+ chat_history_state = gr.State([])
303
+ file_uploads_log = gr.State([]) # Tracks paths of uploaded files
304
+
305
+ # --- UI Layout ---
306
+ gr.Markdown("# Smol Talk with your Agent") # Title
307
+
308
+ with gr.Row():
309
+ with gr.Column(scale=3): # Main chat area
310
+ chatbot = gr.Chatbot(
311
+ label="Agent Interaction",
312
+ # Bubble full width can make text hard to read, try default
313
+ # bubble_full_width=False,
314
+ avatar_images=(
315
+ None, # User avatar
316
+ "https://huggingface.co/datasets/huggingface/brand-assets/resolve/main/hf-logo-round.png" # Agent avatar
317
+ ),
318
+ height=600
319
+ )
320
+ text_input = gr.Textbox(
321
+ lines=1,
322
+ label="Your Message to the Agent",
323
+ placeholder="Type your message and press Enter..."
324
+ )
325
+
326
+ with gr.Column(scale=1): # Sidebar for uploads and downloads
327
+ if self.file_upload_folder is not None:
328
+ gr.Markdown("### File Upload")
329
+ upload_file_component = gr.File(label="Upload a supporting file")
330
+ upload_status_display = gr.Textbox(label="Upload Status", interactive=False, visible=True, lines=2) # Make visible by default
331
+ upload_file_component.upload( # Use 'upload' event for gr.File
332
+ self.upload_file,
333
+ [upload_file_component, file_uploads_log],
334
+ [upload_status_display, file_uploads_log],
335
+ )
336
+
337
+ gr.Markdown("### Generated File")
338
+ # This button becomes visible if a file is created by the agent
339
+ download_btn = gr.Button("Download Generated File", visible=False)
340
+ # This gr.File component becomes visible and populated when the button above is clicked
341
+ file_output_display = gr.File(label="Downloadable Document", visible=False, interactive=False)
342
+
343
+ # --- Event Handling ---
344
+
345
+ # When user submits text_input:
346
+ # 1. log_user_message: prepares the prompt (text + file info), clears text_input.
347
+ # The output 'prepared_prompt' is then passed to interact_with_agent.
348
+ # 2. interact_with_agent: streams agent's responses to chatbot, updates download button.
349
+
350
+ # We need a state to hold the prepared prompt temporarily if log_user_message is separate
351
+ prepared_prompt_state = gr.State("")
352
+
353
  text_input.submit(
354
  self.log_user_message,
355
  [text_input, file_uploads_log],
356
+ [prepared_prompt_state, text_input] # prepared_prompt_state gets the full prompt, text_input is cleared
357
+ ).then(
358
+ self.interact_with_agent,
359
+ [prepared_prompt_state, chat_history_state, download_btn, file_output_display], # Pass current UI states
360
+ [chat_history_state, download_btn, file_output_display] # Update these UI states
361
+ )
362
+
363
+ # When download_btn is clicked:
364
+ download_btn.click(
365
+ self.prepare_and_show_download_file,
366
+ [], # No inputs needed from UI for this action
367
+ [file_output_display] # Update the file_output_display component
368
+ )
369
 
370
+ # Launch the Gradio app
371
+ # Set share=False if running locally or on Spaces where share=True might be an issue
372
+ demo.launch(debug=True, share=kwargs.get("share", False), **kwargs)
373
 
374
 
375
  __all__ = ["stream_to_gradio", "GradioUI"]