| |
| |
| |
| |
| |
|
|
| """Local Python Executor (enhanced). |
| |
| This module provides a safer wrapper around smolagents.LocalPythonExecutor |
| with improved exception handling and a few helpful tools registered with |
| the executor to make debugging executed code easier. |
| |
| Key improvements: |
| - Register a few helper utilities via send_tools so user code can use |
| them for reporting (e.g. `format_exc`). |
| - More robust extraction of stdout/stderr/exit codes from the executor |
| result object, tolerant to different versions of smolagents. |
| - Detailed stderr on unexpected exceptions including full traceback. |
| - Structured logging for operational visibility. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import json |
| import logging |
| import traceback |
|
|
| from openenv.core.env_server.types import CodeExecResult |
| from smolagents import LocalPythonExecutor |
|
|
| logger = logging.getLogger(__name__) |
| logger.addHandler(logging.NullHandler()) |
|
|
|
|
| class PyExecutor: |
| """Wrapper around smolagents LocalPythonExecutor. |
| |
| The wrapper registers a few non-privileged helper tools to the |
| LocalPythonExecutor that can be used by the executed code to |
| format exceptions and to safely stringify results for improved |
| error reporting. |
| """ |
|
|
| def __init__(self, additional_imports: list[str] | None = None): |
| if additional_imports is None: |
| additional_imports = [] |
|
|
| self._executor = LocalPythonExecutor( |
| additional_authorized_imports=additional_imports |
| ) |
|
|
| |
| |
| tools = { |
| |
| |
| "format_exc": traceback.format_exc, |
| |
| "safe_json_dumps": lambda obj: json.dumps(obj, default=lambda o: repr(o)), |
| } |
|
|
| |
| |
| |
| try: |
| self._executor.send_tools(tools) |
| except Exception: |
| |
| |
| logger.debug( |
| "LocalPythonExecutor.send_tools failed; continuing without extra tools", |
| exc_info=True, |
| ) |
|
|
| def run(self, code: str) -> CodeExecResult: |
| """Execute Python code and return a CodeExecResult. |
| |
| This method is intentionally defensive: it attempts to extract |
| meaningful stdout/stderr/exit_code information from a variety of |
| possible return shapes that different versions of smolagents |
| may provide. |
| """ |
| try: |
| exec_result = self._executor(code) |
|
|
| |
| stdout_parts: list[str] = [] |
| stderr_parts: list[str] = [] |
| exit_code = 0 |
|
|
| |
| try: |
| logs = getattr(exec_result, "logs", None) |
| if logs: |
| stdout_parts.append(str(logs)) |
| except Exception: |
| logger.debug("Failed to read exec_result.logs", exc_info=True) |
|
|
| |
| try: |
| if hasattr(exec_result, "output"): |
| out_val = exec_result.output |
| |
| if out_val is not None: |
| |
| try: |
| stdout_parts.append(json.dumps(out_val)) |
| except Exception: |
| stdout_parts.append(repr(out_val)) |
| except Exception: |
| logger.debug("Failed to read exec_result.output", exc_info=True) |
|
|
| |
| try: |
| err = getattr(exec_result, "error", None) |
| if err: |
| stderr_parts.append(str(err)) |
| except Exception: |
| logger.debug("Failed to read exec_result.error", exc_info=True) |
|
|
| try: |
| ex = getattr(exec_result, "exception", None) |
| if ex: |
| stderr_parts.append(str(ex)) |
| except Exception: |
| logger.debug("Failed to read exec_result.exception", exc_info=True) |
|
|
| |
| try: |
| if hasattr(exec_result, "exit_code"): |
| exit_code = ( |
| int(exec_result.exit_code) |
| if exec_result.exit_code is not None |
| else 0 |
| ) |
| elif hasattr(exec_result, "success"): |
| |
| exit_code = 0 if exec_result.success else 1 |
| else: |
| |
| exit_code = 1 if stderr_parts else 0 |
| except Exception: |
| logger.debug("Failed to determine exec_result exit code", exc_info=True) |
| exit_code = 1 if stderr_parts else 0 |
|
|
| |
| stdout = "\n".join(part for part in stdout_parts if part is not None) |
| stderr = "\n".join(part for part in stderr_parts if part is not None) |
|
|
| return CodeExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code) |
|
|
| except Exception as e: |
| |
| |
| tb = traceback.format_exc() |
| logger.exception("LocalPythonExecutor raised an exception during run") |
| return CodeExecResult(stdout="", stderr=tb, exit_code=1) |
|
|