Leonardo commited on
Commit
435980d
·
verified ·
1 Parent(s): e2e1581

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +302 -401
app.py CHANGED
@@ -1,11 +1,11 @@
1
  #!/usr/bin/env python
2
  # coding=utf-8
3
  # Copyright 2024 The Footscray Coding Collective. All rights reserved.
 
4
  import os
5
  import re
6
  import shutil
7
- import datetime
8
- from typing import Optional, List, Dict, Tuple, Generator, Union
9
 
10
  from dotenv import load_dotenv
11
  from huggingface_hub import login
@@ -38,48 +38,53 @@ from smolagents.agent_types import AgentText, AgentImage, AgentAudio
38
  from smolagents.gradio_ui import pull_messages_from_step, handle_agent_output_types
39
 
40
  # ------------------------ Configuration and Setup ------------------------
 
41
  AUTHORIZED_IMPORTS = [
42
- "requests",
43
- "zipfile",
44
- "pandas",
45
- "numpy",
46
- "sympy",
47
- "json",
48
- "bs4",
49
- "pubchempy",
50
  "yaml",
51
- "xml",
52
- "yahoo_finance",
53
- "Bio",
54
- "sklearn",
55
- "scipy",
56
- "pydub",
57
- "PIL",
58
- "chess",
59
- "PyPDF2",
60
- "pptx",
61
- "torch",
62
- "datetime",
63
- "fractions",
64
- "csv",
65
- "cleantext",
66
- "os",
67
- "re",
68
- "collections",
69
- "math",
70
- "random",
71
- "io",
72
- "urllib.parse",
73
- "typing",
74
- "concurrent.futures",
75
- "time",
76
- "tempfile",
77
- "matplotlib",
78
- "seaborn",
79
- "lxml",
80
- "selenium",
81
- "sqlite3",
82
- "schedule",
 
 
 
 
83
  ]
84
 
85
  USER_AGENT = (
@@ -98,6 +103,7 @@ BROWSER_CONFIG = {
98
 
99
  CUSTOM_ROLE_CONVERSIONS = {"tool-call": "assistant", "tool-response": "user"}
100
 
 
101
  ALLOWED_FILE_TYPES = [
102
  "application/pdf",
103
  "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
@@ -113,31 +119,14 @@ ALLOWED_FILE_TYPES = [
113
  "audio/wav",
114
  "audio/ogg",
115
  ]
116
- ALLOWED_EXTENSIONS = [
117
- ".pdf",
118
- ".docx",
119
- ".txt",
120
- ".md",
121
- ".json",
122
- ".png",
123
- ".webp",
124
- ".jpeg",
125
- ".jpg",
126
- ".gif",
127
- ".mp4",
128
- ".mpeg",
129
- ".wav",
130
- ".ogg",
131
- ]
132
 
133
 
134
  def setup_environment():
135
- """Initialize environment variables and authenticate with Hugging Face Hub."""
136
  load_dotenv(override=True)
137
- hf_token = os.getenv("HF_TOKEN")
138
- if hf_token:
139
- login(hf_token)
140
- print(f"HF_TOKEN (last 10 characters): {hf_token[-10:]}")
141
  else:
142
  print("HF_TOKEN not found in environment variables.")
143
 
@@ -147,44 +136,36 @@ class ModelManager:
147
  """Manages model loading and initialization."""
148
 
149
  @staticmethod
150
- def load_model(
151
- chosen_inference: str, model_id: str, key_manager: Optional[object] = None
152
- ) -> Union[HfApiModel, LiteLLMModel, OpenAIServerModel, TransformersModel]:
153
- """Load the specified model with appropriate configuration.
154
-
155
- Args:
156
- chosen_inference: The type of inference to use (e.g., "hf_api", "openai").
157
- model_id: The ID of the model to load.
158
- key_manager: Key manager for API keys (required for OpenAI).
159
-
160
- Returns:
161
- An instance of the specified model class.
162
-
163
- Raises:
164
- ValueError: If an invalid inference type is specified or if the key manager
165
- is missing for OpenAI models.
166
- Exception: If the model fails to load.
167
- """
168
  try:
169
  if chosen_inference == "hf_api":
170
  return HfApiModel(model_id=model_id)
171
- if chosen_inference == "hf_api_provider":
 
172
  return HfApiModel(provider="together")
173
- if chosen_inference == "litellm":
 
174
  return LiteLLMModel(model_id=model_id)
175
- if chosen_inference == "openai":
 
176
  if not key_manager:
177
  raise ValueError("Key manager required for OpenAI model")
 
178
  return OpenAIServerModel(
179
  model_id=model_id, api_key=key_manager.get_key("openai_api_key")
180
  )
181
- if chosen_inference == "transformers":
 
182
  return TransformersModel(
183
  model_id="HuggingFaceTB/SmolLM2-1.7B-Instruct",
184
  device_map="auto",
185
  max_new_tokens=1000,
186
  )
187
- raise ValueError(f"Invalid inference type: {chosen_inference}")
 
 
 
188
  except Exception as e:
189
  print(f"✗ Couldn't load model: {e}")
190
  raise
@@ -194,17 +175,8 @@ class ToolRegistry:
194
  """Manages tool initialization and organization."""
195
 
196
  @staticmethod
197
- def load_web_tools(model, browser, text_limit: int = 20000) -> List[Tool]:
198
- """Initialize and return web-related tools.
199
-
200
- Args:
201
- model: The language model to use.
202
- browser: The web browser instance.
203
- text_limit: The maximum text length for the text inspector tool.
204
-
205
- Returns:
206
- A list of web-related tools.
207
- """
208
  return [
209
  GoogleSearchTool(provider="serper"),
210
  VisitTool(browser),
@@ -217,21 +189,20 @@ class ToolRegistry:
217
  ]
218
 
219
  @staticmethod
220
- def load_document_tools() -> List[Tool]:
221
- """Initialize and return document processing tools.
222
-
223
  Returns:
224
- List of document tools.
225
  """
226
- return [FrontmatterGeneratorTool(), TextCleanerTool()]
 
 
 
227
 
228
  @staticmethod
229
- def load_image_generation_tools() -> Optional[Tool]:
230
- """Initialize and return image generation tools.
231
-
232
- Returns:
233
- The image generation tool or None if initialization fails.
234
- """
235
  try:
236
  return Tool.from_space(
237
  space_id="xkerser/FLUX.1-dev",
@@ -240,56 +211,45 @@ class ToolRegistry:
240
  )
241
  except Exception as e:
242
  print(f"✗ Couldn't initialize image generation tool: {e}")
243
- return None
244
 
245
 
246
  # ------------------------ Agent Creation and Execution ------------------------
247
- def create_agent() -> CodeAgent:
248
- """Creates a fresh agent instance with configured tools.
249
-
250
  Returns:
251
- CodeAgent: Configured agent ready for use.
252
-
253
  Raises:
254
- ValueError: If tool validation fails.
255
- RuntimeError: If agent creation fails.
256
  """
257
  try:
258
- # Model Configuration
259
- model_id = "openrouter/deepseek/deepseek-chat-v3-0324:free" # Define Model
260
  # Initialize model
261
  model = LiteLLMModel(
262
  custom_role_conversions=CUSTOM_ROLE_CONVERSIONS,
263
- model_id=model_id,
264
  )
265
 
266
  # Initialize tools
267
  text_limit = 30000
268
  browser = SimpleTextBrowser(**BROWSER_CONFIG)
269
 
270
- # Create tool instances with proper error handling
271
  web_tools = ToolRegistry.load_web_tools(model, browser, text_limit)
272
- doc_tools = [] # Initialize as empty list
273
- image_generator = None # Initialize as None
274
-
275
- try:
276
- doc_tools = ToolRegistry.load_document_tools()
277
- except Exception as e:
278
- print(f"Warning: Error loading document tools: {str(e)}")
279
- print("Attempting to continue with available tools...")
280
- doc_tools = []
281
-
282
  image_generator = ToolRegistry.load_image_generation_tools()
283
 
284
- # Combine available tools (filter out None values)
285
- all_tools = [visualizer] + web_tools + doc_tools
286
- if image_generator:
287
- all_tools.append(image_generator)
288
 
289
- # Log available tools
290
- print(f"Loaded {len(all_tools)} tools successfully")
291
  for tool in all_tools:
292
- print(f"- {tool.name}: {tool.description[:50]}...")
 
 
 
 
293
 
294
  return CodeAgent(
295
  model=model,
@@ -299,54 +259,46 @@ def create_agent() -> CodeAgent:
299
  additional_authorized_imports=AUTHORIZED_IMPORTS,
300
  planning_interval=4,
301
  )
302
- except Exception as e:
303
  print(f"Failed to create agent: {e}")
304
  raise RuntimeError(f"Agent creation failed: {e}")
305
 
306
 
307
  def stream_to_gradio(
308
- agent: CodeAgent,
309
  task: str,
310
  reset_agent_memory: bool = False,
311
  additional_args: Optional[dict] = None,
312
- ) -> Generator[gr.ChatMessage, None, None]:
313
  """Runs an agent with the given task and streams messages as Gradio ChatMessages."""
314
- try:
315
- for step_log in agent.run(
316
- task, stream=True, reset=reset_agent_memory, additional_args=additional_args
317
- ):
318
- for message in pull_messages_from_step(step_log):
319
- yield message
320
-
321
- # Process final answer with comprehensive media output
322
- final_answer = step_log # Last log is the run's final_answer
323
- final_answer = handle_agent_output_types(final_answer)
324
-
325
- # Output handling based on type
326
- if isinstance(final_answer, AgentText):
327
- yield gr.ChatMessage(
328
- role="assistant",
329
- content=f"Final answer:\n{final_answer.to_string()}\n",
330
- )
331
- elif isinstance(final_answer, AgentImage):
332
- yield gr.ChatMessage(
333
- role="assistant",
334
- content={"image": final_answer.to_string(), "type": "file"},
335
- )
336
- elif isinstance(final_answer, AgentAudio):
337
- yield gr.ChatMessage(
338
- role="assistant",
339
- content={"audio": final_answer.to_string(), "type": "file"},
340
- )
341
- else:
342
- yield gr.ChatMessage(
343
- role="assistant", content=f"Final answer: {str(final_answer)}"
344
- )
345
- except Exception as e:
346
- error_message = f"Error occurred during processing: {str(e)}\n\nPlease try again with a different query or check your inputs."
347
  yield gr.ChatMessage(
348
  role="assistant",
349
- content=error_message,
 
 
 
 
350
  )
351
 
352
 
@@ -354,170 +306,132 @@ def stream_to_gradio(
354
  class GradioUI:
355
  """A one-line interface to launch your agent in Gradio."""
356
 
357
- def __init__(self, file_upload_folder: Optional[str] = None):
358
  """Initialize the Gradio UI with optional file upload functionality."""
359
  self.file_upload_folder = file_upload_folder
360
- self.allowed_extensions = ALLOWED_EXTENSIONS
361
 
362
- if self.file_upload_folder:
363
- os.makedirs(self.file_upload_folder, exist_ok=True)
 
364
 
365
- def interact_with_agent(
366
- self,
367
- prompt: str,
368
- messages: List[Dict],
369
- session_state: Dict,
370
- uploaded_files: List[str],
371
- ) -> List[Dict]:
372
  """Main interaction handler with the agent."""
373
 
 
374
  if "agent" not in session_state:
375
- try:
376
- session_state["agent"] = create_agent()
377
- session_state["creation_time"] = datetime.datetime.now()
378
- session_state["request_count"] = 0
379
- except Exception as e:
380
- error_message = f"Error initializing agent: {str(e)}\n\nPlease refresh the page and try again."
381
- messages.append(
382
- gr.ChatMessage(
383
- role="assistant",
384
- content=error_message,
385
- )
386
- )
387
- yield messages
388
- return
389
 
390
- session_state["request_count"] += 1
391
-
392
- messages.append(gr.ChatMessage(role="user", content=prompt))
393
- yield messages
394
-
395
- file_message = ""
396
  try:
397
- if uploaded_files:
398
- file_info = {}
399
- for file_path in uploaded_files:
400
- ext = os.path.splitext(file_path)[1].lower()
401
- if ext in [".jpg", ".jpeg", ".png", ".gif", ".webp"]:
402
- category = "images"
403
- elif ext in [".mp3", ".wav", ".ogg"]:
404
- category = "audio"
405
- else:
406
- category = "documents"
407
-
408
- if category not in file_info:
409
- file_info[category] = []
410
- file_info[category].append(os.path.basename(file_path))
411
-
412
- file_message = "\nYou have been provided with these files:\n"
413
- for category, files in file_info.items():
414
- file_message += f"- {category.capitalize()}: {', '.join(files)}\n"
415
-
416
- prompt_with_files = prompt + file_message
417
- else:
418
- prompt_with_files = prompt
419
- except Exception as e:
420
- prompt_with_files = prompt
421
- print(
422
- f"WARNING: Error processing files: {e}. Continuing without file info."
423
- )
424
 
425
- try:
426
- reset_needed = session_state["request_count"] > 15
427
 
428
  for msg in stream_to_gradio(
429
- session_state["agent"],
430
- task=prompt_with_files,
431
- reset_agent_memory=reset_needed,
432
  ):
433
  messages.append(msg)
434
- yield messages
435
-
436
- if reset_needed:
437
- session_state["request_count"] = 1
438
 
439
  except Exception as e:
440
- error_message = f"Error processing your request: {str(e)}\n\nPlease try again with a different query."
441
- messages.append(
442
- gr.ChatMessage(
443
- role="assistant",
444
- content=error_message,
445
- )
446
- )
447
- yield messages
448
-
449
- def log_user_message(self, text_input: str) -> Tuple[str, gr.Textbox, gr.Button]:
450
- """Process user message log files."""
451
- return (
452
- text_input,
453
- gr.Textbox(value="", interactive=False, placeholder="Processing..."),
454
- gr.Button(interactive=False),
455
- )
456
-
457
- def upload_file(self, files: List[str]) -> Tuple[str, List[str]]:
458
- """Handle file uploads with validation, security, and clear feedback.
459
-
460
- Args:
461
- files: List of file paths to upload
462
 
463
- Returns:
464
- Tuple of (status message, list of uploaded file paths)
465
- """
466
- if not files:
467
- return "No file uploaded", []
468
-
469
- uploaded_files = []
470
- error_message = None
471
-
472
- for file_path in files:
473
- try:
474
- file_extension = os.path.splitext(file_path)[1].lower()
475
- if file_extension not in self.allowed_extensions:
476
- error_message = (
477
- f"❌ File type '{file_extension}' is not allowed. "
478
- f"Supported types: {', '.join(ALLOWED_EXTENSIONS)}"
479
- )
480
- return error_message, []
481
 
482
- file_size_mb = os.path.getsize(file_path) / (1024 * 1024)
483
- max_file_size_mb = 50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
484
 
485
- if file_size_mb > max_file_size_mb:
486
- error_message = f"❌ File size ({file_size_mb:.1f} MB) exceeds {max_file_size_mb} MB limit."
487
- return error_message, []
488
 
489
- sanitized_name = re.sub(r"[^\w\-.]", "", os.path.basename(file_path))
490
- dest_path = os.path.join(self.file_upload_folder, sanitized_name)
491
- shutil.copy(file_path, dest_path)
492
- uploaded_files.append(dest_path)
493
- print(f"Uploaded {file_path} to {dest_path}")
494
 
495
- except Exception as e:
496
- error_message = f"❌ Upload error: {str(e)}"
497
- return error_message, []
498
 
499
- if error_message:
500
- return error_message, []
501
 
502
  return (
503
- f"✓ Files uploaded successfully: {', '.join([os.path.basename(f) for f in uploaded_files])}",
504
- uploaded_files,
 
 
 
 
 
505
  )
506
 
507
- def detect_device(self, request: gr.Request) -> str:
508
  """Detect whether the user is on mobile or desktop device."""
509
  if not request:
510
- return "Desktop"
511
 
 
512
  is_mobile_header = request.headers.get("sec-ch-ua-mobile")
513
  if is_mobile_header:
514
  return "Mobile" if "?1" in is_mobile_header else "Desktop"
515
 
 
516
  user_agent = request.headers.get("user-agent", "").lower()
517
  mobile_keywords = ["android", "iphone", "ipad", "mobile", "phone"]
 
518
  if any(keyword in user_agent for keyword in mobile_keywords):
519
  return "Mobile"
520
 
 
521
  platform = request.headers.get("sec-ch-ua-platform", "").lower()
522
  if platform:
523
  if platform in ['"android"', '"ios"']:
@@ -525,6 +439,7 @@ class GradioUI:
525
  if platform in ['"windows"', '"macos"', '"linux"']:
526
  return "Desktop"
527
 
 
528
  return "Desktop"
529
 
530
  def launch(self, **kwargs):
@@ -534,118 +449,101 @@ class GradioUI:
534
  @gr.render()
535
  def layout(request: gr.Request):
536
  device = self.detect_device(request)
537
- print(f"Device detected: {device}")
 
538
  if device == "Desktop":
539
  return self._create_desktop_layout()
540
  return self._create_mobile_layout()
541
 
542
- demo.queue(max_size=20).launch(debug=True, **kwargs)
 
 
543
 
544
  def _create_desktop_layout(self):
545
- """Create the desktop layout with sidebar and enhanced styling."""
546
- with gr.Column(visible=False) as sidebar_demo:
547
  with gr.Sidebar():
548
  gr.Markdown(
549
- """# 🔍 OpenDeepResearch
550
- ### Smolagents + Document Tools
551
- """
552
  )
553
  with gr.Group():
554
- gr.Markdown("What can I help you with today?", container=True)
555
  text_input = gr.Textbox(
556
- lines=4,
557
  label="Your request",
558
  container=False,
559
- placeholder="Enter your question or task here...",
560
- show_label=False,
 
 
 
 
 
 
 
 
 
 
 
 
 
561
  )
562
 
563
- with gr.Row():
564
- clear_btn = gr.Button("Clear", variant="secondary")
565
- launch_research_btn = gr.Button("Run", variant="primary")
566
-
567
- if self.file_upload_folder:
568
- with gr.Group():
569
- gr.Markdown("📎 Upload Documents")
570
- file_upload = gr.File(
571
- label="Upload files for analysis",
572
- file_types=self.allowed_extensions,
573
- file_count="multiple",
574
- )
575
-
576
- upload_status = gr.Textbox(
577
- label="Upload Status", interactive=False, visible=False
578
- )
579
- uploaded_files_state = gr.State([])
580
-
581
- gr.HTML("<br><hr><h4><center>Powered by:</center></h4>")
582
  with gr.Row():
583
  gr.HTML(
584
  """
585
- <div style="display: flex; align-items: center; justify-content: center; gap: 8px; font-family: system-ui, -apple-system, sans-serif;">
586
- <img src="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/smolagents/mascot_smol.png"
587
- style="width: 32px; height: 32px; object-fit: contain;" alt="logo">
588
- <a target="_blank" href="https://github.com/huggingface/smolagents">
589
- <b>huggingface/smolagents</b>
590
- </a>
591
- </div>
592
- """
593
  )
594
 
595
- session_state = gr.State({})
 
596
  stored_messages = gr.State([])
 
 
597
 
598
  chatbot = gr.Chatbot(
599
- label="OpenDeepResearch Assistant",
600
  type="messages",
601
  avatar_images=(
602
  None,
603
  "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/smolagents/mascot_smol.png",
604
  ),
605
- resizeable=True,
606
- show_copy_button=True,
607
  scale=1,
608
  elem_id="my-chatbot",
609
- height=700,
610
- )
611
-
612
- clear_btn.click(
613
- lambda: ([], [], {"agent": session_state.get("agent")}, []),
614
- outputs=[chatbot, stored_messages, session_state, uploaded_files_state],
615
- )
616
-
617
- file_upload.change(
618
- self.upload_file,
619
- inputs=[file_upload],
620
- outputs=[upload_status, uploaded_files_state],
621
  )
622
 
623
  self._connect_event_handlers(
624
  text_input,
625
  launch_research_btn,
 
626
  stored_messages,
627
  chatbot,
628
  session_state,
629
- uploaded_files_state,
630
  )
631
 
632
  return sidebar_demo
633
 
634
  def _create_mobile_layout(self):
635
  """Create the mobile layout (simpler without sidebar)."""
636
- with gr.Column(visible=False) as simple_demo:
637
- gr.Markdown("# OpenDeepResearch - free the AI agents!")
 
638
  session_state = gr.State({})
639
  stored_messages = gr.State([])
640
- file_upload = gr.File(
641
- label="Upload files for analysis",
642
- file_types=self.allowed_extensions,
643
- file_count="multiple",
644
- )
645
- uploaded_files_state = gr.State([])
646
 
647
  chatbot = gr.Chatbot(
648
- label="OpenDeepResearch",
649
  type="messages",
650
  avatar_images=(
651
  None,
@@ -655,14 +553,16 @@ class GradioUI:
655
  scale=1,
656
  )
657
 
658
- if self.file_upload_folder:
 
 
659
  upload_status = gr.Textbox(
660
  label="Upload Status", interactive=False, visible=False
661
  )
662
- file_upload.change(
663
  self.upload_file,
664
- [file_upload],
665
- [upload_status, uploaded_files_state],
666
  )
667
 
668
  text_input = gr.Textbox(
@@ -675,78 +575,79 @@ class GradioUI:
675
  self._connect_event_handlers(
676
  text_input,
677
  launch_research_btn,
 
678
  stored_messages,
679
  chatbot,
680
  session_state,
681
- uploaded_files_state,
682
  )
683
 
684
  return simple_demo
685
 
686
  def _connect_event_handlers(
687
  self,
688
- text_input: gr.Textbox,
689
- launch_btn: gr.Button,
690
- stored_messages: gr.State,
691
- chatbot: gr.Chatbot,
692
- session_state: gr.State,
693
- uploaded_files_state: gr.State,
694
- ) -> None:
695
- """Connect the event handlers for input elements with proper flow control."""
696
-
697
- # Text input submission handler
698
  text_input.submit(
699
  self.log_user_message,
700
- inputs=[text_input],
701
- outputs=[text_input, text_input, launch_btn],
702
  ).then(
703
  self.interact_with_agent,
704
- inputs=[text_input, stored_messages, session_state, uploaded_files_state],
705
- outputs=[chatbot],
706
  ).then(
707
  lambda: (
708
  gr.Textbox(
709
- value="", interactive=True, placeholder="Enter your prompt here..."
 
710
  ),
711
  gr.Button(interactive=True),
712
  ),
713
- outputs=[text_input, launch_btn],
 
714
  )
715
 
716
- # Button click handler (same flow)
717
- launch_btn.click(
718
  self.log_user_message,
719
- inputs=[text_input],
720
- outputs=[text_input, text_input, launch_btn],
721
  ).then(
722
  self.interact_with_agent,
723
- inputs=[text_input, stored_messages, session_state, uploaded_files_state],
724
- outputs=[chatbot],
725
  ).then(
726
  lambda: (
727
  gr.Textbox(
728
- value="", interactive=True, placeholder="Enter your prompt here..."
 
729
  ),
730
  gr.Button(interactive=True),
731
  ),
732
- outputs=[text_input, launch_btn],
 
733
  )
734
 
735
 
736
  # ------------------------ Execution ------------------------
737
  def main():
738
  """Main entry point for the application."""
 
739
  setup_environment()
740
- os.makedirs(f"./{BROWSER_CONFIG['downloads_folder']}", exist_ok=True)
741
 
742
- # Set up file upload directory
743
- upload_folder = os.path.join(os.getcwd(), "uploads")
744
- os.makedirs(upload_folder, exist_ok=True)
745
 
746
- # Initialize and launch the UI
747
- ui = GradioUI(file_upload_folder=upload_folder)
748
- ui.launch(share=True)
749
 
750
 
751
  if __name__ == "__main__":
752
- main()
 
1
  #!/usr/bin/env python
2
  # coding=utf-8
3
  # Copyright 2024 The Footscray Coding Collective. All rights reserved.
4
+ import mimetypes
5
  import os
6
  import re
7
  import shutil
8
+ from typing import Optional
 
9
 
10
  from dotenv import load_dotenv
11
  from huggingface_hub import login
 
38
  from smolagents.gradio_ui import pull_messages_from_step, handle_agent_output_types
39
 
40
  # ------------------------ Configuration and Setup ------------------------
41
+ # Constants and configurations
42
  AUTHORIZED_IMPORTS = [
43
+ "requests", # Web requests (fetching data from the internet)
44
+ "zipfile", # Working with ZIP archives
45
+ "pandas", # Data manipulation and analysis (DataFrames)
46
+ "numpy", # Numerical computing (arrays, linear algebra)
47
+ "sympy", # Symbolic mathematics (algebra, calculus)
48
+ "json", # JSON data serialization/deserialization
49
+ "bs4", # Beautiful Soup for HTML/XML parsing
50
+ "pubchempy", # Accessing PubChem chemical database
51
  "yaml",
52
+ "xml", # XML processing
53
+ "yahoo_finance", # Fetching stock data
54
+ "Bio", # Bioinformatics tools (e.g., sequence analysis)
55
+ "sklearn", # Scikit-learn for machine learning
56
+ "scipy", # Scientific computing (stats, optimization)
57
+ "pydub", # Audio manipulation
58
+ "PIL", # Pillow for image processing
59
+ "chess", # Chess-related functionality
60
+ "PyPDF2", # PDF manipulation
61
+ "pptx", # PowerPoint file manipulation
62
+ "torch", # PyTorch for neural networks
63
+ "datetime", # Date and time handling
64
+ "fractions", # Rational number arithmetic
65
+ "csv", # CSV file reading/writing
66
+ "cleantext", # Text cleaning and normalization
67
+ "os", # Operating system interaction (file system, etc.) VERY IMPORTANT
68
+ "re", # Regular expressions for text processing
69
+ "collections", # Useful data structures (e.g., defaultdict, Counter)
70
+ "math", # Basic mathematical functions
71
+ "random", # Random number generation
72
+ "io", # Input/output streams
73
+ "urllib.parse", # URL parsing and manipulation (safe URL handling)
74
+ "typing", # Support for type hints (improve code clarity)
75
+ "concurrent.futures", # For parallel execution
76
+ "time", # Measuring time
77
+ "tempfile", # Creating temporary files and directories
78
+ # Data Visualization (if needed) - Consider security implications carefully
79
+ "matplotlib", # Plotting library (basic charts)
80
+ "seaborn", # Statistical data visualization (more advanced)
81
+ # Web Scraping (more specific/controlled) - Consider ethical implications
82
+ "lxml", # Faster XML/HTML processing (alternative to bs4)
83
+ "selenium", # Automated browser control (for dynamic websites)
84
+ # Database interaction (if needed) - Handle credentials securely!
85
+ "sqlite3", # SQLite database access
86
+ # Task scheduling
87
+ "schedule", # Allow the agent to schedule tasks
88
  ]
89
 
90
  USER_AGENT = (
 
103
 
104
  CUSTOM_ROLE_CONVERSIONS = {"tool-call": "assistant", "tool-response": "user"}
105
 
106
+
107
  ALLOWED_FILE_TYPES = [
108
  "application/pdf",
109
  "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
 
119
  "audio/wav",
120
  "audio/ogg",
121
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
 
123
 
124
  def setup_environment():
125
+ """Initialize environment variables and authentication."""
126
  load_dotenv(override=True)
127
+ if os.getenv("HF_TOKEN"): # Check if token is actually set
128
+ login(os.getenv("HF_TOKEN"))
129
+ print("HF_TOKEN (last 10 characters):", os.getenv("HF_TOKEN")[-10:])
 
130
  else:
131
  print("HF_TOKEN not found in environment variables.")
132
 
 
136
  """Manages model loading and initialization."""
137
 
138
  @staticmethod
139
+ def load_model(chosen_inference: str, model_id: str, key_manager=None):
140
+ """Load the specified model with appropriate configuration."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  try:
142
  if chosen_inference == "hf_api":
143
  return HfApiModel(model_id=model_id)
144
+
145
+ elif chosen_inference == "hf_api_provider":
146
  return HfApiModel(provider="together")
147
+
148
+ elif chosen_inference == "litellm":
149
  return LiteLLMModel(model_id=model_id)
150
+
151
+ elif chosen_inference == "openai":
152
  if not key_manager:
153
  raise ValueError("Key manager required for OpenAI model")
154
+
155
  return OpenAIServerModel(
156
  model_id=model_id, api_key=key_manager.get_key("openai_api_key")
157
  )
158
+
159
+ elif chosen_inference == "transformers":
160
  return TransformersModel(
161
  model_id="HuggingFaceTB/SmolLM2-1.7B-Instruct",
162
  device_map="auto",
163
  max_new_tokens=1000,
164
  )
165
+
166
+ else:
167
+ raise ValueError(f"Invalid inference type: {chosen_inference}")
168
+
169
  except Exception as e:
170
  print(f"✗ Couldn't load model: {e}")
171
  raise
 
175
  """Manages tool initialization and organization."""
176
 
177
  @staticmethod
178
+ def load_web_tools(model, browser, text_limit=20000):
179
+ """Initialize and return web-related tools."""
 
 
 
 
 
 
 
 
 
180
  return [
181
  GoogleSearchTool(provider="serper"),
182
  VisitTool(browser),
 
189
  ]
190
 
191
  @staticmethod
192
+ def load_document_tools():
193
+ """
194
+ Initialize and return document processing, i.e. sanitisation and indexing, tools.
195
  Returns:
196
+ List of document tools
197
  """
198
+ return [
199
+ FrontmatterGeneratorTool(),
200
+ TextCleanerTool(),
201
+ ]
202
 
203
  @staticmethod
204
+ def load_image_generation_tools():
205
+ """Initialize and return image generation tools."""
 
 
 
 
206
  try:
207
  return Tool.from_space(
208
  space_id="xkerser/FLUX.1-dev",
 
211
  )
212
  except Exception as e:
213
  print(f"✗ Couldn't initialize image generation tool: {e}")
214
+ raise
215
 
216
 
217
  # ------------------------ Agent Creation and Execution ------------------------
218
+ def create_agent():
219
+ """
220
+ Creates a fresh agent instance with properly configured tools.
221
  Returns:
222
+ CodeAgent: Configured agent ready for use
 
223
  Raises:
224
+ ValueError: If tool validation fails
225
+ RuntimeError: If agent creation fails
226
  """
227
  try:
 
 
228
  # Initialize model
229
  model = LiteLLMModel(
230
  custom_role_conversions=CUSTOM_ROLE_CONVERSIONS,
231
+ model_id="openrouter/deepseek/deepseek-chat-v3-0324:free",
232
  )
233
 
234
  # Initialize tools
235
  text_limit = 30000
236
  browser = SimpleTextBrowser(**BROWSER_CONFIG)
237
 
238
+ # Collect all tools in a single list
239
  web_tools = ToolRegistry.load_web_tools(model, browser, text_limit)
240
+ doc_tools = ToolRegistry.load_document_tools() # New document tools
 
 
 
 
 
 
 
 
 
241
  image_generator = ToolRegistry.load_image_generation_tools()
242
 
243
+ # Combine all tools into a single list
244
+ all_tools = [visualizer] + web_tools + doc_tools + [image_generator]
 
 
245
 
246
+ # Validate tools before creating agent
 
247
  for tool in all_tools:
248
+ if not isinstance(tool, Tool):
249
+ raise ValueError(
250
+ f"Invalid tool type: {type(tool)}. "
251
+ f"All tools must be instances of Tool class."
252
+ )
253
 
254
  return CodeAgent(
255
  model=model,
 
259
  additional_authorized_imports=AUTHORIZED_IMPORTS,
260
  planning_interval=4,
261
  )
262
+ except (ValueError, RuntimeError) as e:
263
  print(f"Failed to create agent: {e}")
264
  raise RuntimeError(f"Agent creation failed: {e}")
265
 
266
 
267
  def stream_to_gradio(
268
+ agent,
269
  task: str,
270
  reset_agent_memory: bool = False,
271
  additional_args: Optional[dict] = None,
272
+ ):
273
  """Runs an agent with the given task and streams messages as Gradio ChatMessages."""
274
+ for step_log in agent.run(
275
+ task, stream=True, reset=reset_agent_memory, additional_args=additional_args
276
+ ):
277
+ for message in pull_messages_from_step(step_log):
278
+ yield message
279
+
280
+ # Process final answer : Use a more comprehensive media output
281
+ final_answer = step_log # Last log is the run's final_answer
282
+ final_answer = handle_agent_output_types(final_answer)
283
+
284
+ if isinstance(final_answer, AgentText):
285
+ yield gr.ChatMessage(
286
+ role="assistant",
287
+ content=f"**Final answer:**\n{final_answer.to_string()}\n",
288
+ )
289
+ elif isinstance(final_answer, AgentImage):
290
+ yield gr.ChatMessage(
291
+ role="assistant",
292
+ content={"image": final_answer.to_string(), "type": "file"},
293
+ ) # Send as Gradio-compatible file object:
294
+ elif isinstance(final_answer, AgentAudio):
 
 
 
 
 
 
 
 
 
 
 
 
295
  yield gr.ChatMessage(
296
  role="assistant",
297
+ content={"audio": final_answer.to_string(), "type": "file"},
298
+ ) # Send as Gradio-compatible file object
299
+ else:
300
+ yield gr.ChatMessage(
301
+ role="assistant", content=f"**Final answer:** {str(final_answer)}"
302
  )
303
 
304
 
 
306
  class GradioUI:
307
  """A one-line interface to launch your agent in Gradio."""
308
 
309
+ def __init__(self, file_upload_folder: str | None = None):
310
  """Initialize the Gradio UI with optional file upload functionality."""
311
  self.file_upload_folder = file_upload_folder
 
312
 
313
+ if self.file_upload_folder is not None:
314
+ if not os.path.exists(file_upload_folder):
315
+ os.mkdir(file_upload_folder)
316
 
317
+ def interact_with_agent(self, prompt, messages, session_state):
 
 
 
 
 
 
318
  """Main interaction handler with the agent."""
319
 
320
+ # Get or create session-specific agent
321
  if "agent" not in session_state:
322
+ session_state["agent"] = create_agent()
 
 
 
 
 
 
 
 
 
 
 
 
 
323
 
324
+ # Adding monitoring
 
 
 
 
 
325
  try:
326
+ # Log the existence of agent memory
327
+ has_memory = hasattr(session_state["agent"], "memory")
328
+ print(f"Agent has memory: {has_memory}")
329
+ if has_memory:
330
+ print(f"Memory type: {type(session_state['agent'].memory)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
 
332
+ messages.append(gr.ChatMessage(role="user", content=prompt))
333
+ yield messages
334
 
335
  for msg in stream_to_gradio(
336
+ session_state["agent"], task=prompt, reset_agent_memory=False
 
 
337
  ):
338
  messages.append(msg)
339
+ yield messages # Yield messages after each step
340
+ yield messages # Yield messages one last time
 
 
341
 
342
  except Exception as e:
343
+ print(f"Error in interaction: {str(e)}")
344
+ raise
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
345
 
346
+ def upload_file(
347
+ self,
348
+ file,
349
+ file_uploads_log,
350
+ ):
351
+ """Handle file uploads with proper validation and security."""
352
+ if file is None:
353
+ return gr.Textbox("No file uploaded", visible=True), file_uploads_log
 
 
 
 
 
 
 
 
 
 
354
 
355
+ try:
356
+ mime_type, _ = mimetypes.guess_type(file.name)
357
+ except Exception as e:
358
+ return gr.Textbox(f"Error: {e}", visible=True), file_uploads_log
359
+
360
+ if mime_type not in ALLOWED_FILE_TYPES:
361
+ return gr.Textbox("File type disallowed", visible=True), file_uploads_log
362
+
363
+ # Sanitize file name
364
+ original_name = os.path.basename(file.name)
365
+ sanitized_name = re.sub(
366
+ r"[^\w\-.]", "_", original_name
367
+ ) # Replace invalid chars with underscores
368
+
369
+ # Ensure the extension correlates to the mime type
370
+ type_to_ext = {}
371
+ for ext, t in mimetypes.types_map.items():
372
+ if t not in type_to_ext:
373
+ type_to_ext[t] = ext
374
+
375
+ # Build sanitized filename with proper extension
376
+ name_parts = sanitized_name.split(".")[:-1]
377
+ extension = type_to_ext.get(mime_type, "")
378
+ sanitized_name = "".join(name_parts) + extension
379
+
380
+ # Limit File Size, and Throw Error
381
+ max_file_size_mb = 50 # Define the limit
382
+ file_size_mb = os.path.getsize(file.name) / (1024 * 1024) # Size in MB
383
+
384
+ if file_size_mb > max_file_size_mb:
385
+ return (
386
+ gr.Textbox(
387
+ f"File size exceeds {max_file_size_mb} MB limit.", visible=True
388
+ ),
389
+ file_uploads_log,
390
+ )
391
 
392
+ # Save the uploaded file to the specified folder
393
+ file_path = os.path.join(self.file_upload_folder, sanitized_name)
394
+ shutil.copy(file.name, file_path)
395
 
396
+ return gr.Textbox(
397
+ f"File uploaded: {file_path}", visible=True
398
+ ), file_uploads_log + [file_path]
 
 
399
 
400
+ def log_user_message(self, text_input, file_uploads_log):
401
+ """Process user message and handle file references."""
402
+ message = text_input
403
 
404
+ if len(file_uploads_log) > 0:
405
+ message += f"\nYou have been provided with these files, which might be helpful or not: {file_uploads_log}" # Added file list
406
 
407
  return (
408
+ message,
409
+ gr.Textbox(
410
+ value="",
411
+ interactive=False,
412
+ placeholder="Processing...", # Changed placeholder.
413
+ ),
414
+ gr.Button(interactive=False),
415
  )
416
 
417
+ def detect_device(self, request: gr.Request):
418
  """Detect whether the user is on mobile or desktop device."""
419
  if not request:
420
+ return "Unknown device" # Handle case where request is none.
421
 
422
+ # Method 1: Check sec-ch-ua-mobile header
423
  is_mobile_header = request.headers.get("sec-ch-ua-mobile")
424
  if is_mobile_header:
425
  return "Mobile" if "?1" in is_mobile_header else "Desktop"
426
 
427
+ # Method 2: Check user-agent string
428
  user_agent = request.headers.get("user-agent", "").lower()
429
  mobile_keywords = ["android", "iphone", "ipad", "mobile", "phone"]
430
+
431
  if any(keyword in user_agent for keyword in mobile_keywords):
432
  return "Mobile"
433
 
434
+ # Method 3: Check platform
435
  platform = request.headers.get("sec-ch-ua-platform", "").lower()
436
  if platform:
437
  if platform in ['"android"', '"ios"']:
 
439
  if platform in ['"windows"', '"macos"', '"linux"']:
440
  return "Desktop"
441
 
442
+ # Default case if no clear indicators
443
  return "Desktop"
444
 
445
  def launch(self, **kwargs):
 
449
  @gr.render()
450
  def layout(request: gr.Request):
451
  device = self.detect_device(request)
452
+ print(f"device - {device}")
453
+ # Render layout with sidebar
454
  if device == "Desktop":
455
  return self._create_desktop_layout()
456
  return self._create_mobile_layout()
457
 
458
+ demo.queue(max_size=20).launch(
459
+ debug=True, **kwargs
460
+ ) # Add queue with reasonable size
461
 
462
  def _create_desktop_layout(self):
463
+ """Create the desktop layout with sidebar."""
464
+ with gr.Blocks(fill_height=True) as sidebar_demo:
465
  with gr.Sidebar():
466
  gr.Markdown(
467
+ """#OpenDeepResearch - 3theSmolagents!
468
+ Model_id: google/gemini-2.0-flash-001"""
 
469
  )
470
  with gr.Group():
471
+ gr.Markdown("**What's on your mind mate?**", container=True)
472
  text_input = gr.Textbox(
473
+ lines=3,
474
  label="Your request",
475
  container=False,
476
+ placeholder="Enter your prompt here and press Shift+Enter or press the button",
477
+ )
478
+ launch_research_btn = gr.Button("Run", variant="primary")
479
+
480
+ # If an upload folder is provided, enable the upload feature
481
+ if self.file_upload_folder is not None:
482
+ upload_file = gr.File(label="Upload a file")
483
+ upload_status = gr.Textbox(
484
+ label="Upload Status", interactive=False, visible=False
485
+ )
486
+ file_uploads_log = gr.State([])
487
+ upload_file.change(
488
+ self.upload_file,
489
+ [upload_file, file_uploads_log],
490
+ [upload_status, file_uploads_log],
491
  )
492
 
493
+ gr.HTML("<br><br><h4><center>Powered by:</center></h4>")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
494
  with gr.Row():
495
  gr.HTML(
496
  """
497
+ <div style="display: flex; align-items: center; gap: 8px; font-family: system-ui, -apple-system, sans-serif;">
498
+ <img src="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/smolagents/mascot_smol.png"
499
+ style="width: 32px; height: 32px; object-fit: contain;" alt="logo">
500
+ <a target="_blank" href="https://github.com/huggingface/smolagents">
501
+ <b>huggingface/smolagents</b>
502
+ </a>
503
+ </div>
504
+ """
505
  )
506
 
507
+ # Add session state to store session-specific data
508
+ session_state = gr.State({}) # Initialize empty state for each session
509
  stored_messages = gr.State([])
510
+ if "file_uploads_log" not in locals():
511
+ file_uploads_log = gr.State([])
512
 
513
  chatbot = gr.Chatbot(
514
+ label="open-Deep-Research",
515
  type="messages",
516
  avatar_images=(
517
  None,
518
  "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/smolagents/mascot_smol.png",
519
  ),
520
+ resizeable=False,
 
521
  scale=1,
522
  elem_id="my-chatbot",
 
 
 
 
 
 
 
 
 
 
 
 
523
  )
524
 
525
  self._connect_event_handlers(
526
  text_input,
527
  launch_research_btn,
528
+ file_uploads_log,
529
  stored_messages,
530
  chatbot,
531
  session_state,
 
532
  )
533
 
534
  return sidebar_demo
535
 
536
  def _create_mobile_layout(self):
537
  """Create the mobile layout (simpler without sidebar)."""
538
+ with gr.Blocks(fill_height=True) as simple_demo:
539
+ gr.Markdown("""#OpenDeepResearch - free the AI agents!""")
540
+ # Add session state to store session-specific data
541
  session_state = gr.State({})
542
  stored_messages = gr.State([])
543
+ file_uploads_log = gr.State([])
 
 
 
 
 
544
 
545
  chatbot = gr.Chatbot(
546
+ label="open-Deep-Research",
547
  type="messages",
548
  avatar_images=(
549
  None,
 
553
  scale=1,
554
  )
555
 
556
+ # If an upload folder is provided, enable the upload feature
557
+ if self.file_upload_folder is not None:
558
+ upload_file = gr.File(label="Upload a file")
559
  upload_status = gr.Textbox(
560
  label="Upload Status", interactive=False, visible=False
561
  )
562
+ upload_file.change(
563
  self.upload_file,
564
+ [upload_file, file_uploads_log],
565
+ [upload_status, file_uploads_log],
566
  )
567
 
568
  text_input = gr.Textbox(
 
575
  self._connect_event_handlers(
576
  text_input,
577
  launch_research_btn,
578
+ file_uploads_log,
579
  stored_messages,
580
  chatbot,
581
  session_state,
 
582
  )
583
 
584
  return simple_demo
585
 
586
  def _connect_event_handlers(
587
  self,
588
+ text_input,
589
+ launch_research_btn,
590
+ file_uploads_log,
591
+ stored_messages,
592
+ chatbot,
593
+ session_state,
594
+ ):
595
+ """Connect the event handlers for input elements."""
596
+ # Connect text input submit event
 
597
  text_input.submit(
598
  self.log_user_message,
599
+ [text_input, file_uploads_log],
600
+ [stored_messages, text_input, launch_research_btn],
601
  ).then(
602
  self.interact_with_agent,
603
+ [stored_messages, chatbot, session_state],
604
+ [chatbot],
605
  ).then(
606
  lambda: (
607
  gr.Textbox(
608
+ interactive=True,
609
+ placeholder="Enter your prompt here and press the button",
610
  ),
611
  gr.Button(interactive=True),
612
  ),
613
+ None,
614
+ [text_input, launch_research_btn],
615
  )
616
 
617
+ # Connect button click event
618
+ launch_research_btn.click(
619
  self.log_user_message,
620
+ [text_input, file_uploads_log],
621
+ [stored_messages, text_input, launch_research_btn],
622
  ).then(
623
  self.interact_with_agent,
624
+ [stored_messages, chatbot, session_state],
625
+ [chatbot],
626
  ).then(
627
  lambda: (
628
  gr.Textbox(
629
+ interactive=True,
630
+ placeholder="Enter your prompt here and press the button",
631
  ),
632
  gr.Button(interactive=True),
633
  ),
634
+ None,
635
+ [text_input, launch_research_btn],
636
  )
637
 
638
 
639
  # ------------------------ Execution ------------------------
640
  def main():
641
  """Main entry point for the application."""
642
+ # Initialize environment
643
  setup_environment()
 
644
 
645
+ # Ensure downloads folder exists
646
+ os.makedirs(f"./{BROWSER_CONFIG['downloads_folder']}", exist_ok=True)
 
647
 
648
+ # Launch UI
649
+ GradioUI(file_upload_folder="uploaded_files").launch()
 
650
 
651
 
652
  if __name__ == "__main__":
653
+ main()