Source code for agency.tools.interpreter

"""A tool for running python code in a REPL.

Taken from https://github.com/langchain-ai/langchain-experimental/blob/main/libs/experimental/langchain_experimental/utilities/python.py
"""

import ast
import re
import sys
import functools
import logging
import multiprocessing
from contextlib import redirect_stdout
from io import StringIO
from typing import Any, Dict, Optional, Type


from langchain_core.callbacks.manager import (
    AsyncCallbackManagerForToolRun,
    CallbackManagerForToolRun,
)
from langchain_core.runnables.config import run_in_executor
from langchain_core.tools import BaseTool
from pydantic import BaseModel, Field, model_validator


logger = logging.getLogger(__name__)


[docs] @functools.lru_cache(maxsize=None) def warn_once() -> None: """Warn once about the dangers of PythonREPL.""" logger.warning("Python REPL can execute arbitrary code. Use with caution.")
[docs] class PythonREPL(BaseModel): """Simulates a standalone Python REPL.""" globals: Optional[Dict] = Field(default_factory=dict, alias="_globals") # type: ignore[arg-type] locals: Optional[Dict] = Field(default_factory=dict, alias="_locals") # type: ignore[arg-type]
[docs] @staticmethod def sanitize_input(query: str) -> str: """Sanitize input to the python REPL. Remove whitespace, backtick & python (if llm mistakes python console as terminal) Args: query: The query to sanitize Returns: str: The sanitized query """ query = re.sub(r"^(\s|`)*(?i:python)?\s*", "", query) query = re.sub(r"(\s|`)*$", "", query) return query
[docs] @classmethod def worker( cls, command: str, globals: Optional[Dict], locals: Optional[Dict], queue: multiprocessing.Queue, ) -> None: old_stdout = sys.stdout sys.stdout = mystdout = StringIO() try: cleaned_command = cls.sanitize_input(command) exec(cleaned_command, globals, locals) sys.stdout = old_stdout queue.put(mystdout.getvalue()) except Exception as e: sys.stdout = old_stdout queue.put(repr(e))
[docs] def run(self, command: str, timeout: Optional[int] = None) -> str: """Run command with own globals/locals and returns anything printed. Args: command: Python code to execute timeout: Timeout in seconds (optional) Returns: Output from the command execution or error message """ # Warn against dangers of PythonREPL warn_once() queue: multiprocessing.Queue = multiprocessing.Queue() # Only use multiprocessing if we are enforcing a timeout if timeout is not None: # create a Process p = multiprocessing.Process( target=self.worker, args=(command, self.globals, self.locals, queue) ) # start it p.start() # wait for the process to finish or kill it after timeout seconds p.join(timeout) if p.is_alive(): p.terminate() return "Execution timed out" else: self.worker(command, self.globals, self.locals, queue) # get the result from the worker function return queue.get()
def _get_default_python_repl() -> PythonREPL: return PythonREPL(_globals=globals(), _locals=None)
[docs] def sanitize_input(query: str) -> str: """Sanitize input to the python REPL. Remove whitespace, backtick & python (if llm mistakes python console as terminal) Args: query: The query to sanitize Returns: str: The sanitized query """ # Removes `, whitespace & python from start query = re.sub(r"^(\s|`)*(?i:python)?\s*", "", query) # Removes whitespace & ` from end query = re.sub(r"(\s|`)*$", "", query) return query
[docs] class PythonREPLTool(BaseTool): """Tool for running python code in a REPL.""" name: str = "Python_REPL" description: str = ( "A Python shell. Use this to execute python commands. " "Input should be a valid python command. " "If you want to see the output of a value, you should print it out " "with `print(...)`." ) python_repl: PythonREPL = Field(default_factory=_get_default_python_repl) sanitize_input: bool = True def _run( self, query: str, run_manager: Optional[CallbackManagerForToolRun] = None, ) -> Any: """Execute Python code in the REPL. Args: query: Python code to execute run_manager: Optional callback manager Returns: Output from code execution """ if self.sanitize_input: query = sanitize_input(query) return self.python_repl.run(query) async def _arun( self, query: str, run_manager: Optional[AsyncCallbackManagerForToolRun] = None, ) -> Any: """Use the tool asynchronously.""" if self.sanitize_input: query = sanitize_input(query) return await run_in_executor(None, self.run, query)
[docs] class PythonInputs(BaseModel): """Python inputs.""" query: str = Field(description="code snippet to run")
[docs] class PythonAstREPLTool(BaseTool): """Tool for running python code in a REPL.""" name: str = "python_repl_ast" description: str = ( "A Python shell. Use this to execute python commands. " "Input should be a valid python command. " "When using this tool, sometimes output is abbreviated - " "make sure it does not look abbreviated before using it in your answer." ) globals: Optional[Dict] = Field(default_factory=dict) # type: ignore[arg-type] locals: Optional[Dict] = Field(default_factory=dict) # type: ignore[arg-type] sanitize_input: bool = True args_schema: Type[BaseModel] = PythonInputs
[docs] @model_validator(mode="before") @classmethod def validate_python_version(cls, values: Dict) -> Any: """Validate that Python version is 3.9 or higher. Args: values: Model values to validate Returns: Validated values Raises: ValueError: If Python version is too old """ if sys.version_info < (3, 9): raise ValueError( "This tool relies on Python 3.9 or higher " "(as it uses new functionality in the `ast` module, " f"you have Python version: {sys.version}" ) return values
def _run( self, query: str, run_manager: Optional[CallbackManagerForToolRun] = None, ) -> str: """Execute Python code using AST parsing for safer evaluation. Args: query: Python code to execute run_manager: Optional callback manager Returns: Output from code execution or error message """ try: if self.sanitize_input: query = sanitize_input(query) tree = ast.parse(query) module = ast.Module(tree.body[:-1], type_ignores=[]) exec(ast.unparse(module), self.globals, self.locals) # type: ignore module_end = ast.Module(tree.body[-1:], type_ignores=[]) module_end_str = ast.unparse(module_end) # type: ignore io_buffer = StringIO() try: with redirect_stdout(io_buffer): ret = eval(module_end_str, self.globals, self.locals) if ret is None: return io_buffer.getvalue() else: return ret except Exception: with redirect_stdout(io_buffer): exec(module_end_str, self.globals, self.locals) return io_buffer.getvalue() except Exception as e: return "{}: {}".format(type(e).__name__, str(e)) async def _arun( self, query: str, run_manager: Optional[AsyncCallbackManagerForToolRun] = None, ) -> Any: """Execute Python code asynchronously using AST parsing. Args: query: Python code to execute run_manager: Optional async callback manager Returns: Output from code execution """ return await run_in_executor(None, self._run, query)