Python Executor tool now can create and output files
Browse files
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
|
|
|
|
|
|
|
|
|
|
| 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,
|
| 701 |
-
execution
|
| 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 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 1249 |
),
|
| 1250 |
api_description=(
|
| 1251 |
-
"Execute arbitrary Python code and return captured stdout
|
| 1252 |
"Supports any valid Python code including imports, variables, functions, loops, and calculations. "
|
| 1253 |
-
"
|
|
|
|
| 1254 |
"Parameters: code (str - Python source code to execute). "
|
| 1255 |
-
"Returns: Combined stdout output
|
| 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 |
)
|