import os import io import sys import uuid import base64 import traceback import contextlib import tempfile import subprocess from typing import Dict, List, Any, Optional, Union import numpy as np import pandas as pd import matplotlib.pyplot as plt from PIL import Image from smolagents.tools import Tool class CodeExecutionTool(Tool): name = "code_execution" description = "Execute Python code with support for data analysis, plotting, and file operations. Returns text output and base64-encoded images for plots." inputs = { 'code': {'type': 'string', 'description': 'The Python code to execute'}, 'input_data': {'type': 'object', 'description': 'Optional input data for the code execution', 'nullable': True} } output_type = "object" def __init__(self, work_dir="code_execution"): super().__init__() self.work_dir = work_dir os.makedirs(work_dir, exist_ok=True) self._setup_matplotlib() def _setup_matplotlib(self): """Configure matplotlib for non-interactive backend""" plt.switch_backend('Agg') def _capture_output(self, code: str) -> Dict[str, Any]: """Execute code and capture output, including stdout, stderr, and plots""" # Create string buffers for stdout and stderr stdout_buffer = io.StringIO() stderr_buffer = io.StringIO() result = { 'output': '', 'error': '', 'images': [], 'dataframes': [] } # Create a temporary namespace for code execution namespace = { 'np': np, 'pd': pd, 'plt': plt, 'Image': Image, } try: # Execute in controlled environment with contextlib.redirect_stdout(stdout_buffer), \ contextlib.redirect_stderr(stderr_buffer): # Execute the code exec(code, namespace) # Capture any active plots if plt.get_fignums(): for fig_num in plt.get_fignums(): fig = plt.figure(fig_num) img_buffer = io.BytesIO() fig.savefig(img_buffer, format='png') img_buffer.seek(0) img_data = base64.b64encode(img_buffer.getvalue()).decode() result['images'].append(img_data) plt.close(fig) # Capture any DataFrames in the namespace for var_name, var_value in namespace.items(): if isinstance(var_value, pd.DataFrame): result['dataframes'].append({ 'name': var_name, 'data': var_value.to_dict(orient='records') }) # Get output from buffers result['output'] = stdout_buffer.getvalue() result['error'] = stderr_buffer.getvalue() except Exception as e: result['error'] = f"Execution error: {str(e)}\n{traceback.format_exc()}" finally: stdout_buffer.close() stderr_buffer.close() return result def forward(self, code: str, input_data: Optional[Dict] = None) -> Dict[str, Any]: if not code: return {"error": "Error: No code provided to execute."} # If input_data is provided, add it to the setup code setup_code = "" if input_data: for var_name, var_value in input_data.items(): if isinstance(var_value, (str, int, float, list, dict)): setup_code += f"{var_name} = {repr(var_value)}\n" # Combine setup code with user code full_code = setup_code + code try: # Execute the code and capture all output result = self._capture_output(full_code) return result except Exception as e: return { "error": f"Error executing code: {str(e)}", "output": "", "images": [], "dataframes": [] } def __del__(self): """Cleanup any temporary files""" try: if os.path.exists(self.work_dir): for root, dirs, files in os.walk(self.work_dir, topdown=False): for name in files: os.remove(os.path.join(root, name)) for name in dirs: os.rmdir(os.path.join(root, name)) os.rmdir(self.work_dir) except Exception: pass # Ignore cleanup errors