zcbot/tools/run_python.py

86 lines
3.0 KiB
Python

"""run_python: 在 subprocess 里执行 Python 代码 (Hybrid 范式的关键)。
JSON tool call 处理离散操作 (read/edit/shell),run_python 处理连续逻辑
(数据计算、批处理、生成 .pptx/.docx)。让模型自己挑工具。
阶段 1 (本地): subprocess + 工作目录限制 + 敏感环境变量过滤
阶段 2 (后期): Docker / E2B 替换执行后端
"""
from __future__ import annotations
import os
import subprocess
import sys
import tempfile
from pathlib import Path
from .base import Tool
_SENSITIVE_PATTERNS = ("API_KEY", "TOKEN", "SECRET", "PASSWORD", "PRIVATE_KEY")
class RunPythonTool(Tool):
name = "run_python"
description = (
"Execute Python code in a subprocess. Returns stdout/stderr/exit_code.\n"
"Use for: data analysis, batch file ops, document generation (.pptx/.docx), "
"matplotlib charts, or any task where Python is more natural than chaining tools.\n"
"Working directory is the agent's base dir. Files you create persist there.\n"
"Available libs (install with shell pip if missing): "
"pandas, numpy, matplotlib, python-pptx, python-docx, requests, pypdf."
)
parameters = {
"type": "object",
"properties": {
"code": {
"type": "string",
"description": "Python source. Anything written to stdout is returned.",
},
"timeout": {"type": "integer", "default": 120, "description": "Seconds before kill"},
},
"required": ["code"],
}
def execute(self, code: str, timeout: int = 120) -> str:
# 写到临时文件,避免 -c 转义问题
with tempfile.NamedTemporaryFile(
suffix=".py", mode="w", delete=False, encoding="utf-8"
) as f:
f.write(code)
script_path = f.name
try:
env = os.environ.copy()
for k in list(env):
u = k.upper()
if any(p in u for p in _SENSITIVE_PATTERNS):
del env[k]
env["PYTHONIOENCODING"] = "utf-8"
env["PYTHONPATH"] = str(self.base_dir) + os.pathsep + env.get("PYTHONPATH", "")
result = subprocess.run(
[sys.executable, script_path],
cwd=str(self.base_dir),
capture_output=True,
timeout=timeout,
text=True,
encoding="utf-8",
errors="replace",
env=env,
)
except subprocess.TimeoutExpired:
return f"[Error] python script timed out after {timeout}s"
finally:
try:
Path(script_path).unlink()
except OSError:
pass
parts = []
if result.stdout:
parts.append(f"[stdout]\n{result.stdout.rstrip()}")
if result.stderr:
parts.append(f"[stderr]\n{result.stderr.rstrip()}")
parts.append(f"[exit {result.returncode}]")
return "\n".join(parts)