AlbertoFor's picture
Edit tools
c657a71
raw
history blame
3.06 kB
from langchain_core.tools.base import BaseTool, ToolException
from typing import Optional
import subprocess
import tempfile
import os
from pydantic import PrivateAttr
class PythonExecutionTool(BaseTool):
"""
A LangChain “tool” that takes a string of Python code,
writes it to a temporary .py file, executes it in a fresh
Python subprocess, captures stdout/stderr, and returns the result.
"""
name : str = "python_execution"
description : str = (
"Executes a string of Python code in an isolated subprocess. "
"Returns stdout on success, or stderr (with exit code) on failure."
)
_python_executable: str = PrivateAttr()
_timeout: int = PrivateAttr()
_temp_dir: str = PrivateAttr()
def __init__(
self,
python_executable: str = "python",
timeout: int = 5,
*,
temp_dir: Optional[str] = None
):
"""
:param python_executable: Path to the Python interpreter to invoke.
:param timeout: Maximum seconds to allow the code to run.
:param temp_dir: Optional directory in which to create the temp file.
"""
super().__init__()
self._python_executable = python_executable
self._timeout = timeout
self._temp_dir = temp_dir
def _run(self, code: str) -> str:
"""
Synchronously execute the provided Python code.
:param code: The complete Python source to run.
:return: Captured stdout if exit code is 0; otherwise stderr + exit code.
:raises ToolException: On internal error (e.g. unable to write temp file).
"""
# 1. Write code to a temporary file on disk to avoid shell-quoting issues.
try:
with tempfile.NamedTemporaryFile(
suffix=".py", delete=False, dir=self._temp_dir, mode="w", encoding="utf-8"
) as tmp:
tmp.write(code)
tmp_path = tmp.name
except Exception as e:
raise ToolException(f"Failed to write temp file: {e!r}")
# 2. Invoke a fresh Python process on that file, capturing stdout & stderr.
try:
result = subprocess.run(
[self._python_executable, "-u", tmp_path],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=self._timeout,
)
except subprocess.TimeoutExpired:
return f"⚠️ Execution timed out after {self._timeout} seconds."
except Exception as e:
raise ToolException(f"Failed to launch subprocess: {e!r}")
finally:
# 3. Clean up the temp file no matter what
try:
os.remove(tmp_path)
except OSError:
pass
# 4. Process the result
if result.returncode != 0:
return (
f"❌ Process exited with code {result.returncode}.\n"
f"stderr:\n{result.stderr}"
)
return result.stdout