Initial commit
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
"""Python code execution tool."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from opc.layer4_tools.execution_context import (
|
||||
build_subprocess_env,
|
||||
resolve_python_executable,
|
||||
resolve_task_execution_context,
|
||||
wrap_command_for_context,
|
||||
)
|
||||
from opc.layer4_tools.output_budget import clip_text, persist_tool_result
|
||||
from opc.layer4_tools.registry import ToolDefinition
|
||||
|
||||
|
||||
async def python_exec(
|
||||
code: str,
|
||||
timeout: int = 60,
|
||||
task: Any | None = None,
|
||||
on_progress: Any = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Execute Python code in a subprocess and capture output."""
|
||||
_ = on_progress
|
||||
context = resolve_task_execution_context(task)
|
||||
workspace_root = str(context.get("workspace_root", "") or "").strip()
|
||||
temp_dir = workspace_root if workspace_root and Path(workspace_root).exists() else None
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False, dir=temp_dir) as f:
|
||||
f.write(code)
|
||||
tmp_path = f.name
|
||||
|
||||
proc: asyncio.subprocess.Process | None = None
|
||||
try:
|
||||
executable = resolve_python_executable(context)
|
||||
cwd = workspace_root or os.getcwd()
|
||||
env = build_subprocess_env(context)
|
||||
wrapped_args, sandbox_meta = wrap_command_for_context(
|
||||
[executable, tmp_path],
|
||||
cwd=str(Path(cwd).resolve()),
|
||||
context=context,
|
||||
)
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
*wrapped_args,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
cwd=str(Path(cwd).resolve()),
|
||||
env=env,
|
||||
)
|
||||
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
|
||||
stdout_text = stdout.decode("utf-8", errors="replace")
|
||||
stderr_text = stderr.decode("utf-8", errors="replace")
|
||||
stdout_clip = clip_text(stdout_text, limit=30000, marker="python stdout truncated")
|
||||
stderr_clip = clip_text(stderr_text, limit=10000, marker="python stderr truncated")
|
||||
stdout_persisted = (
|
||||
persist_tool_result(stdout_text, tool_name="python_exec_stdout", task=task, extension="txt")
|
||||
if stdout_clip.truncated else {}
|
||||
)
|
||||
stderr_persisted = (
|
||||
persist_tool_result(stderr_text, tool_name="python_exec_stderr", task=task, extension="txt")
|
||||
if stderr_clip.truncated else {}
|
||||
)
|
||||
return {
|
||||
"stdout": stdout_clip.text,
|
||||
"stderr": stderr_clip.text,
|
||||
"stdout_truncated": stdout_clip.truncated,
|
||||
"stdout_omitted_chars": stdout_clip.omitted_chars,
|
||||
"stderr_truncated": stderr_clip.truncated,
|
||||
"stderr_omitted_chars": stderr_clip.omitted_chars,
|
||||
"full_stdout_path": stdout_persisted.get("full_output_path", ""),
|
||||
"full_stderr_path": stderr_persisted.get("full_output_path", ""),
|
||||
"exit_code": proc.returncode,
|
||||
"sandbox": sandbox_meta,
|
||||
"execution_context": {
|
||||
"workspace_root": workspace_root,
|
||||
"venv_path": str(context.get("venv_path", "") or ""),
|
||||
"python_executable": executable,
|
||||
"preparation_error": str(context.get("preparation_error", "") or ""),
|
||||
"sandbox": dict(context.get("sandbox", {}) or {}),
|
||||
},
|
||||
}
|
||||
except asyncio.TimeoutError:
|
||||
if proc is not None:
|
||||
proc.kill()
|
||||
return {
|
||||
"error": f"Execution timed out after {timeout}s",
|
||||
"exit_code": -1,
|
||||
"execution_context": {
|
||||
"workspace_root": workspace_root,
|
||||
"venv_path": str(context.get("venv_path", "") or ""),
|
||||
"python_executable": resolve_python_executable(context),
|
||||
"preparation_error": str(context.get("preparation_error", "") or ""),
|
||||
},
|
||||
}
|
||||
except RuntimeError as exc:
|
||||
return {
|
||||
"error": str(exc),
|
||||
"exit_code": -1,
|
||||
"execution_context": {
|
||||
"workspace_root": workspace_root,
|
||||
"venv_path": str(context.get("venv_path", "") or ""),
|
||||
"python_executable": resolve_python_executable(context),
|
||||
"preparation_error": str(context.get("preparation_error", "") or ""),
|
||||
"sandbox": dict(context.get("sandbox", {}) or {}),
|
||||
},
|
||||
}
|
||||
finally:
|
||||
Path(tmp_path).unlink(missing_ok=True)
|
||||
|
||||
|
||||
def create_python_tool() -> ToolDefinition:
|
||||
return ToolDefinition(
|
||||
name="python_exec",
|
||||
description="Execute Python code and return stdout/stderr. Use for data processing, calculations, or testing.",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {"type": "string", "description": "Python code to execute"},
|
||||
"timeout": {"type": "integer", "description": "Timeout in seconds", "default": 60},
|
||||
},
|
||||
"required": ["code"],
|
||||
},
|
||||
func=python_exec,
|
||||
category="compute",
|
||||
self_bounded_output=True,
|
||||
max_result_chars=80_000,
|
||||
)
|
||||
Reference in New Issue
Block a user