mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-10-13 04:01:35 +00:00
140 lines
5.2 KiB
Python
140 lines
5.2 KiB
Python
"""Execute Python code and return printed stdout."""
|
|
|
|
import asyncio
|
|
import sys
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
from ...utils.file_sandbox import create_file_sandbox_backend
|
|
from ...utils.file_sandbox_backend import FileSandboxBackend
|
|
from ..base_step import BaseStep
|
|
from ...components import R
|
|
|
|
DEFAULT_TIMEOUT = 60.0
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class _PythonResult:
|
|
stdout: str
|
|
stderr: str
|
|
returncode: int | None
|
|
timed_out: bool = False
|
|
|
|
|
|
@R.register("python_execute_step")
|
|
class PythonExecuteStep(BaseStep):
|
|
"""Run Python code in a subprocess and return stdout as the response answer."""
|
|
|
|
async def execute(self):
|
|
assert self.context is not None
|
|
|
|
code = self.context.get("code", "")
|
|
timeout, timeout_error = self._parse_timeout(
|
|
self.context.get("python_timeout", self.context.get("timeout", DEFAULT_TIMEOUT)),
|
|
)
|
|
if not isinstance(code, str) or not code.strip():
|
|
self.context.response.success = False
|
|
self.context.response.answer = "code is required"
|
|
return self.context.response
|
|
if timeout_error:
|
|
self.context.response.success = False
|
|
self.context.response.answer = timeout_error
|
|
return self.context.response
|
|
|
|
read_only_paths = self.context.get("read_only_paths", self.kwargs.get("read_only_paths"))
|
|
writable_paths = self.context.get("writable_paths", self.kwargs.get("writable_paths"))
|
|
memory_limit_bytes = self.context.get("memory_limit_bytes", self.kwargs.get("memory_limit_bytes"))
|
|
try:
|
|
backend = (
|
|
create_file_sandbox_backend(
|
|
cwd=self.workspace_path,
|
|
read_only_paths=[] if read_only_paths is None else read_only_paths,
|
|
writable_paths=[] if writable_paths is None else writable_paths,
|
|
memory_limit_bytes=memory_limit_bytes,
|
|
)
|
|
if read_only_paths is not None or writable_paths is not None
|
|
else None
|
|
)
|
|
except (TypeError, ValueError, RuntimeError) as exc:
|
|
self.context.response.success = False
|
|
self.context.response.answer = str(exc)
|
|
return self.context.response
|
|
|
|
try:
|
|
result = await self._run_python(code, timeout, backend)
|
|
except (OSError, RuntimeError) as exc:
|
|
self.context.response.success = False
|
|
self.context.response.answer = str(exc)
|
|
return self.context.response
|
|
if result.timed_out:
|
|
self.context.response.success = False
|
|
self.context.response.answer = f"Python execution timed out after {timeout:g}s"
|
|
self.context.response.metadata.update(
|
|
{
|
|
"returncode": result.returncode,
|
|
"stderr": result.stderr,
|
|
"python_timeout": timeout,
|
|
},
|
|
)
|
|
return self.context.response
|
|
|
|
stdout = result.stdout or ""
|
|
stderr = result.stderr or ""
|
|
self.context.response.success = result.returncode == 0
|
|
self.context.response.answer = stdout if stdout or result.returncode == 0 else stderr
|
|
self.context.response.metadata.update(
|
|
{
|
|
"returncode": result.returncode,
|
|
"stderr": stderr,
|
|
"python_timeout": timeout,
|
|
},
|
|
)
|
|
return self.context.response
|
|
|
|
async def _run_python(self, code: str, timeout: float, backend: FileSandboxBackend | None = None) -> _PythonResult:
|
|
if backend is not None:
|
|
result = await backend.exec_shell(
|
|
[sys.executable, "-c", code],
|
|
cwd=str(self.workspace_path),
|
|
timeout=timeout,
|
|
)
|
|
return _PythonResult(
|
|
stdout=result.stdout.decode(errors="replace"),
|
|
stderr=result.stderr.decode(errors="replace"),
|
|
returncode=result.exit_code,
|
|
timed_out=result.exit_code == -1 and result.stderr == b"timed out",
|
|
)
|
|
process = await asyncio.create_subprocess_exec(
|
|
sys.executable,
|
|
"-c",
|
|
code,
|
|
stdout=asyncio.subprocess.PIPE,
|
|
stderr=asyncio.subprocess.PIPE,
|
|
cwd=self.workspace_path,
|
|
)
|
|
try:
|
|
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=timeout)
|
|
return _PythonResult(
|
|
stdout=stdout.decode(),
|
|
stderr=stderr.decode(),
|
|
returncode=process.returncode,
|
|
)
|
|
except TimeoutError:
|
|
process.kill()
|
|
stdout, stderr = await process.communicate()
|
|
return _PythonResult(
|
|
stdout=stdout.decode(),
|
|
stderr=stderr.decode(),
|
|
returncode=process.returncode,
|
|
timed_out=True,
|
|
)
|
|
|
|
@staticmethod
|
|
def _parse_timeout(raw: Any) -> tuple[float, str]:
|
|
try:
|
|
timeout = float(raw)
|
|
except (TypeError, ValueError):
|
|
return DEFAULT_TIMEOUT, "python_timeout must be a positive number"
|
|
if timeout <= 0:
|
|
return DEFAULT_TIMEOUT, "python_timeout must be a positive number"
|
|
return timeout, ""
|