Nymbo commited on
Commit
159b3ad
·
verified ·
1 Parent(s): 1a73c50

Python Executor tool now can create and output files

Browse files
Files changed (1) hide show
  1. app.py +147 -16
app.py CHANGED
@@ -689,16 +689,98 @@ def Search_DuckDuckGo( # <-- MCP tool #2 (DDG Search)
689
  # Code Execution: Python (MCP tool #3)
690
  # ======================================
691
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
692
  def Execute_Python(code: Annotated[str, "Python source code to run; stdout is captured and returned."]) -> str:
693
  """
694
- Execute arbitrary Python code and return captured stdout or an error message.
 
 
 
695
 
696
  Args:
697
  code (str): Python source code to run; stdout is captured and returned.
698
 
699
  Returns:
700
- str: Combined stdout produced by the code, or the exception text if
701
- execution failed.
702
  """
703
  _log_call_start("Execute_Python", code=_truncate_for_log(code or "", 300))
704
  if code is None:
@@ -706,15 +788,63 @@ def Execute_Python(code: Annotated[str, "Python source code to run; stdout is ca
706
  _log_call_end("Execute_Python", result)
707
  return result
708
 
709
- old_stdout = sys.stdout
710
- redirected_output = sys.stdout = StringIO()
711
- try:
712
- exec(code)
713
- result = redirected_output.getvalue()
714
- except Exception as e:
715
- result = str(e)
716
- finally:
717
- sys.stdout = old_stdout
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
718
  _log_call_end("Execute_Python", _truncate_for_log(result))
719
  return result
720
 
@@ -1245,14 +1375,15 @@ code_interface = gr.Interface(
1245
  outputs=gr.Textbox(label="Output"),
1246
  title="Python Code Executor",
1247
  description=(
1248
- "<div style=\"text-align:center\">Execute Python code and see the output.</div>"
1249
  ),
1250
  api_description=(
1251
- "Execute arbitrary Python code and return captured stdout or an error message. "
1252
  "Supports any valid Python code including imports, variables, functions, loops, and calculations. "
1253
- "Examples: 'print(2+2)', 'import math; print(math.sqrt(16))', 'for i in range(3): print(i)'. "
 
1254
  "Parameters: code (str - Python source code to execute). "
1255
- "Returns: Combined stdout output or exception text if execution fails."
1256
  ),
1257
  flagging_mode="never",
1258
  )
 
689
  # Code Execution: Python (MCP tool #3)
690
  # ======================================
691
 
692
+ import tempfile
693
+ import base64
694
+ from pathlib import Path
695
+
696
+ def _detect_created_files(working_dir: str, before_files: set) -> list[str]:
697
+ """
698
+ Detect files created during code execution.
699
+ Returns list of newly created file paths.
700
+ """
701
+ try:
702
+ current_files = set()
703
+ for file_path in Path(working_dir).rglob("*"):
704
+ if file_path.is_file():
705
+ current_files.add(str(file_path))
706
+
707
+ new_files = current_files - before_files
708
+ return list(new_files)
709
+ except Exception:
710
+ return []
711
+
712
+
713
+ def _generate_file_url(file_path: str) -> dict:
714
+ """
715
+ Generate a data URL for small files or file info for larger files.
716
+ Returns dict with file info and download URL.
717
+ """
718
+ try:
719
+ path = Path(file_path)
720
+ file_size = path.stat().st_size
721
+
722
+ # For files under 1MB, create data URL
723
+ if file_size < 1024 * 1024: # 1MB limit
724
+ with open(file_path, 'rb') as f:
725
+ file_data = f.read()
726
+
727
+ # Determine MIME type based on extension
728
+ mime_types = {
729
+ '.csv': 'text/csv',
730
+ '.txt': 'text/plain',
731
+ '.json': 'application/json',
732
+ '.png': 'image/png',
733
+ '.jpg': 'image/jpeg',
734
+ '.jpeg': 'image/jpeg',
735
+ '.gif': 'image/gif',
736
+ '.pdf': 'application/pdf',
737
+ '.html': 'text/html',
738
+ '.xml': 'text/xml',
739
+ '.svg': 'image/svg+xml'
740
+ }
741
+
742
+ mime_type = mime_types.get(path.suffix.lower(), 'application/octet-stream')
743
+ encoded_data = base64.b64encode(file_data).decode('utf-8')
744
+ data_url = f"data:{mime_type};base64,{encoded_data}"
745
+
746
+ return {
747
+ 'name': path.name,
748
+ 'size': file_size,
749
+ 'type': mime_type,
750
+ 'url': data_url,
751
+ 'downloadable': True
752
+ }
753
+ else:
754
+ # For larger files, just return file info
755
+ return {
756
+ 'name': path.name,
757
+ 'size': file_size,
758
+ 'type': 'file',
759
+ 'url': None,
760
+ 'downloadable': False,
761
+ 'note': f'File too large ({file_size} bytes) for data URL'
762
+ }
763
+ except Exception as e:
764
+ return {
765
+ 'name': Path(file_path).name,
766
+ 'error': str(e),
767
+ 'downloadable': False
768
+ }
769
+
770
+
771
  def Execute_Python(code: Annotated[str, "Python source code to run; stdout is captured and returned."]) -> str:
772
  """
773
+ Execute arbitrary Python code and return captured stdout plus any created files.
774
+
775
+ Supports creating downloadable artifacts like CSV files, images, etc. Files created
776
+ during execution will be detected and made available as data URLs for download.
777
 
778
  Args:
779
  code (str): Python source code to run; stdout is captured and returned.
780
 
781
  Returns:
782
+ str: Combined stdout produced by the code, plus information about any files
783
+ created during execution with download links for small files.
784
  """
785
  _log_call_start("Execute_Python", code=_truncate_for_log(code or "", 300))
786
  if code is None:
 
788
  _log_call_end("Execute_Python", result)
789
  return result
790
 
791
+ # Create a temporary working directory
792
+ with tempfile.TemporaryDirectory() as temp_dir:
793
+ # Change to temp directory and capture existing files
794
+ original_cwd = os.getcwd()
795
+ os.chdir(temp_dir)
796
+
797
+ try:
798
+ # Get initial file list
799
+ before_files = set()
800
+ for file_path in Path(temp_dir).rglob("*"):
801
+ if file_path.is_file():
802
+ before_files.add(str(file_path))
803
+
804
+ # Execute code with stdout capture
805
+ old_stdout = sys.stdout
806
+ redirected_output = sys.stdout = StringIO()
807
+
808
+ try:
809
+ exec(code)
810
+ stdout_result = redirected_output.getvalue()
811
+ except Exception as e:
812
+ stdout_result = f"Error: {str(e)}"
813
+ finally:
814
+ sys.stdout = old_stdout
815
+
816
+ # Detect any files created during execution
817
+ created_files = _detect_created_files(temp_dir, before_files)
818
+
819
+ # Build result with stdout and file information
820
+ result_parts = []
821
+
822
+ if stdout_result.strip():
823
+ result_parts.append("=== Output ===")
824
+ result_parts.append(stdout_result.strip())
825
+
826
+ if created_files:
827
+ result_parts.append("\n=== Created Files ===")
828
+ for file_path in created_files:
829
+ file_info = _generate_file_url(file_path)
830
+
831
+ if file_info.get('downloadable', False):
832
+ result_parts.append(f"📁 {file_info['name']} ({file_info['size']} bytes)")
833
+ result_parts.append(f" Type: {file_info['type']}")
834
+ result_parts.append(f" Download: {file_info['url']}")
835
+ elif file_info.get('error'):
836
+ result_parts.append(f"❌ {file_info['name']} (error: {file_info['error']})")
837
+ else:
838
+ result_parts.append(f"📄 {file_info['name']} ({file_info.get('size', 'unknown')} bytes)")
839
+ if 'note' in file_info:
840
+ result_parts.append(f" Note: {file_info['note']}")
841
+
842
+ result = "\n".join(result_parts) if result_parts else "No output or files generated."
843
+
844
+ finally:
845
+ # Restore original working directory
846
+ os.chdir(original_cwd)
847
+
848
  _log_call_end("Execute_Python", _truncate_for_log(result))
849
  return result
850
 
 
1375
  outputs=gr.Textbox(label="Output"),
1376
  title="Python Code Executor",
1377
  description=(
1378
+ "<div style=\"text-align:center\">Execute Python code and create downloadable files. Supports CSV exports, image generation, and more.</div>"
1379
  ),
1380
  api_description=(
1381
+ "Execute arbitrary Python code and return captured stdout plus any created files with download URLs. "
1382
  "Supports any valid Python code including imports, variables, functions, loops, and calculations. "
1383
+ "Files created during execution (CSV, PNG, TXT, etc.) are automatically detected and made available as data URLs for download. "
1384
+ "Examples: 'print(2+2)', 'import pandas as pd; df.to_csv(\"data.csv\")', 'import matplotlib.pyplot as plt; plt.savefig(\"plot.png\")'. "
1385
  "Parameters: code (str - Python source code to execute). "
1386
+ "Returns: Combined stdout output and file information with download links for created artifacts."
1387
  ),
1388
  flagging_mode="never",
1389
  )