Team Ai
Apppublic

openenv/coding_env

sourceHugging Faceupdated 3mo agoView on Hugging Face
21likes
python_executor.py158 linesDownload Raw Back to server
1# Copyright (c) Meta Platforms, Inc. and affiliates.2# All rights reserved.3#4# This source code is licensed under the BSD-style license found in the5# LICENSE file in the root directory of this source tree.6 7"""Local Python Executor (enhanced).8 9This module provides a safer wrapper around smolagents.LocalPythonExecutor10with improved exception handling and a few helpful tools registered with11the executor to make debugging executed code easier.12 13Key improvements:14- Register a few helper utilities via send_tools so user code can use15  them for reporting (e.g. `format_exc`).16- More robust extraction of stdout/stderr/exit codes from the executor17  result object, tolerant to different versions of smolagents.18- Detailed stderr on unexpected exceptions including full traceback.19- Structured logging for operational visibility.20"""21 22from __future__ import annotations23 24import json25import logging26import traceback27 28from openenv.core.env_server.types import CodeExecResult29from smolagents import LocalPythonExecutor30 31logger = logging.getLogger(__name__)32logger.addHandler(logging.NullHandler())33 34 35class PyExecutor:36    """Wrapper around smolagents LocalPythonExecutor.37 38    The wrapper registers a few non-privileged helper tools to the39    LocalPythonExecutor that can be used by the executed code to40    format exceptions and to safely stringify results for improved41    error reporting.42    """43 44    def __init__(self, additional_imports: list[str] | None = None):45        if additional_imports is None:46            additional_imports = []47 48        self._executor = LocalPythonExecutor(49            additional_authorized_imports=additional_imports50        )51 52        # Register helpful utilities exposed to the execution environment.53        # These are intentionally small, read-only helpers.54        tools = {55            # Provide a small helper to format the current exception in the56            # executed context. This is a *string formatting* helper only.57            "format_exc": traceback.format_exc,58            # Safe JSON dumps with a fallback for non-serializable objects.59            "safe_json_dumps": lambda obj: json.dumps(obj, default=lambda o: repr(o)),60        }61 62        # `send_tools` is the public API on LocalPythonExecutor to make63        # helper callables available to the sandboxed runtime. We don't64        # provide any builtins that could change the environment.65        try:66            self._executor.send_tools(tools)67        except Exception:68            # If the LocalPythonExecutor implementation doesn't support69            # send_tools or fails, log and continue — the executor is still usable.70            logger.debug(71                "LocalPythonExecutor.send_tools failed; continuing without extra tools",72                exc_info=True,73            )74 75    def run(self, code: str) -> CodeExecResult:76        """Execute Python code and return a CodeExecResult.77 78        This method is intentionally defensive: it attempts to extract79        meaningful stdout/stderr/exit_code information from a variety of80        possible return shapes that different versions of smolagents81        may provide.82        """83        try:84            exec_result = self._executor(code)85 86            # Default values87            stdout_parts: list[str] = []88            stderr_parts: list[str] = []89            exit_code = 090 91            # Extract logs/prints92            try:93                logs = getattr(exec_result, "logs", None)94                if logs:95                    stdout_parts.append(str(logs))96            except Exception:97                logger.debug("Failed to read exec_result.logs", exc_info=True)98 99            # Extract the result / output value100            try:101                if hasattr(exec_result, "output"):102                    out_val = exec_result.output103                    # If the output is not None, stringify it in a safe way104                    if out_val is not None:105                        # Prefer JSON if possible, otherwise repr106                        try:107                            stdout_parts.append(json.dumps(out_val))108                        except Exception:109                            stdout_parts.append(repr(out_val))110            except Exception:111                logger.debug("Failed to read exec_result.output", exc_info=True)112 113            # Some runtime implementations may put errors on `error` or `exception`114            try:115                err = getattr(exec_result, "error", None)116                if err:117                    stderr_parts.append(str(err))118            except Exception:119                logger.debug("Failed to read exec_result.error", exc_info=True)120 121            try:122                ex = getattr(exec_result, "exception", None)123                if ex:124                    stderr_parts.append(str(ex))125            except Exception:126                logger.debug("Failed to read exec_result.exception", exc_info=True)127 128            # Determine exit code if provided129            try:130                if hasattr(exec_result, "exit_code"):131                    exit_code = (132                        int(exec_result.exit_code)133                        if exec_result.exit_code is not None134                        else 0135                    )136                elif hasattr(exec_result, "success"):137                    # Some versions use `success` boolean138                    exit_code = 0 if exec_result.success else 1139                else:140                    # Fallback: if there were any stderr parts, treat as non-zero141                    exit_code = 1 if stderr_parts else 0142            except Exception:143                logger.debug("Failed to determine exec_result exit code", exc_info=True)144                exit_code = 1 if stderr_parts else 0145 146            # Compose the final stdout/stderr strings147            stdout = "\n".join(part for part in stdout_parts if part is not None)148            stderr = "\n".join(part for part in stderr_parts if part is not None)149 150            return CodeExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)151 152        except Exception as e:153            # Any unexpected exception from the LocalPythonExecutor is154            # returned with a full traceback to make debugging easier.155            tb = traceback.format_exc()156            logger.exception("LocalPythonExecutor raised an exception during run")157            return CodeExecResult(stdout="", stderr=tb, exit_code=1)158