- Add code-interpreter, kokoro-tts, remotion-best-practices, research-to-paper-slides, summarize, tavily-tool to source repo - skill-review: add main/xiaoming agent mapping in handler.ts + SKILL.md - tts-voice: handler.ts updates from agent workspace Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
242 lines
7.9 KiB
Python
242 lines
7.9 KiB
Python
#!/usr/bin/env python3
|
|
import argparse
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
import pathlib
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
from typing import Optional
|
|
|
|
WORKSPACE = pathlib.Path('/home/selig/.openclaw/workspace').resolve()
|
|
RUNS_DIR = WORKSPACE / '.tmp' / 'code-interpreter-runs'
|
|
MAX_PREVIEW = 12000
|
|
ARTIFACT_SCAN_LIMIT = 100
|
|
PACKAGE_PROBES = ['pandas', 'numpy', 'matplotlib']
|
|
PYTHON_BIN = str(WORKSPACE / '.venv-code-interpreter' / 'bin' / 'python')
|
|
|
|
|
|
def current_python_paths(run_dir_path: pathlib.Path) -> str:
|
|
"""Build PYTHONPATH: run_dir (for ci_helpers) only.
|
|
Venv site-packages are already on sys.path when using PYTHON_BIN."""
|
|
return str(run_dir_path)
|
|
|
|
|
|
def read_code(args: argparse.Namespace) -> str:
|
|
sources = [bool(args.code), bool(args.file), bool(args.stdin)]
|
|
if sum(sources) != 1:
|
|
raise SystemExit('Provide exactly one of --code, --file, or --stdin')
|
|
if args.code:
|
|
return args.code
|
|
if args.file:
|
|
return pathlib.Path(args.file).read_text(encoding='utf-8')
|
|
return sys.stdin.read()
|
|
|
|
|
|
def ensure_within_workspace(path_str: Optional[str], must_exist: bool = True) -> pathlib.Path:
|
|
if not path_str:
|
|
return WORKSPACE
|
|
p = pathlib.Path(path_str).expanduser().resolve()
|
|
if p != WORKSPACE and WORKSPACE not in p.parents:
|
|
raise SystemExit(f'Path must stay inside workspace: {WORKSPACE}')
|
|
if must_exist and (not p.exists() or not p.is_dir()):
|
|
raise SystemExit(f'Path not found or not a directory: {p}')
|
|
return p
|
|
|
|
|
|
def ensure_output_path(path_str: Optional[str]) -> Optional[pathlib.Path]:
|
|
if not path_str:
|
|
return None
|
|
p = pathlib.Path(path_str).expanduser().resolve()
|
|
p.parent.mkdir(parents=True, exist_ok=True)
|
|
return p
|
|
|
|
|
|
def write_text(path_str: Optional[str], text: str) -> None:
|
|
p = ensure_output_path(path_str)
|
|
if not p:
|
|
return
|
|
p.write_text(text, encoding='utf-8')
|
|
|
|
|
|
def truncate(text: str) -> str:
|
|
if len(text) <= MAX_PREVIEW:
|
|
return text
|
|
extra = len(text) - MAX_PREVIEW
|
|
return text[:MAX_PREVIEW] + f'\n...[truncated {extra} chars]'
|
|
|
|
|
|
def package_status() -> dict:
|
|
out: dict[str, bool] = {}
|
|
for name in PACKAGE_PROBES:
|
|
proc = subprocess.run(
|
|
[PYTHON_BIN, '-c', f"import importlib.util; print('1' if importlib.util.find_spec('{name}') else '0')"],
|
|
capture_output=True,
|
|
text=True,
|
|
encoding='utf-8',
|
|
errors='replace',
|
|
)
|
|
out[name] = proc.stdout.strip() == '1'
|
|
return out
|
|
|
|
|
|
def rel_to(path: pathlib.Path, base: pathlib.Path) -> str:
|
|
try:
|
|
return str(path.relative_to(base))
|
|
except Exception:
|
|
return str(path)
|
|
|
|
|
|
def scan_artifacts(base_dir: pathlib.Path, root_label: str) -> list[dict]:
|
|
if not base_dir.exists():
|
|
return []
|
|
items: list[dict] = []
|
|
for p in sorted(base_dir.rglob('*')):
|
|
if len(items) >= ARTIFACT_SCAN_LIMIT:
|
|
break
|
|
if p.is_file():
|
|
try:
|
|
size = p.stat().st_size
|
|
except Exception:
|
|
size = None
|
|
items.append({
|
|
'root': root_label,
|
|
'path': str(p),
|
|
'relative': rel_to(p, base_dir),
|
|
'bytes': size,
|
|
})
|
|
return items
|
|
|
|
|
|
def write_helper(run_dir_path: pathlib.Path, artifact_dir: pathlib.Path) -> None:
|
|
helper = run_dir_path / 'ci_helpers.py'
|
|
helper.write_text(
|
|
"""
|
|
from pathlib import Path
|
|
import json
|
|
import os
|
|
|
|
WORKSPACE = Path(os.environ['OPENCLAW_WORKSPACE'])
|
|
RUN_DIR = Path(os.environ['CODE_INTERPRETER_RUN_DIR'])
|
|
ARTIFACT_DIR = Path(os.environ['CODE_INTERPRETER_ARTIFACT_DIR'])
|
|
|
|
|
|
def save_text(name: str, text: str) -> str:
|
|
path = ARTIFACT_DIR / name
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(text, encoding='utf-8')
|
|
return str(path)
|
|
|
|
|
|
def save_json(name: str, data) -> str:
|
|
path = ARTIFACT_DIR / name
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding='utf-8')
|
|
return str(path)
|
|
""".lstrip(),
|
|
encoding='utf-8',
|
|
)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description='Local Python runner for OpenClaw code-interpreter skill')
|
|
parser.add_argument('--code', help='Python code to execute')
|
|
parser.add_argument('--file', help='Path to a Python file to execute')
|
|
parser.add_argument('--stdin', action='store_true', help='Read Python code from stdin')
|
|
parser.add_argument('--cwd', help='Working directory inside workspace')
|
|
parser.add_argument('--artifact-dir', help='Artifact directory inside workspace to keep outputs')
|
|
parser.add_argument('--timeout', type=int, default=20, help='Timeout seconds (default: 20)')
|
|
parser.add_argument('--stdout-file', help='Optional file path to save full stdout')
|
|
parser.add_argument('--stderr-file', help='Optional file path to save full stderr')
|
|
parser.add_argument('--keep-run-dir', action='store_true', help='Keep generated temp run directory even on success')
|
|
args = parser.parse_args()
|
|
|
|
code = read_code(args)
|
|
cwd = ensure_within_workspace(args.cwd)
|
|
RUNS_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
run_dir_path = pathlib.Path(tempfile.mkdtemp(prefix='run-', dir=str(RUNS_DIR))).resolve()
|
|
artifact_dir = ensure_within_workspace(args.artifact_dir, must_exist=False) if args.artifact_dir else (run_dir_path / 'artifacts')
|
|
artifact_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
script_path = run_dir_path / 'main.py'
|
|
script_path.write_text(code, encoding='utf-8')
|
|
write_helper(run_dir_path, artifact_dir)
|
|
|
|
env = {
|
|
'PATH': os.environ.get('PATH', '/usr/bin:/bin'),
|
|
'HOME': str(run_dir_path),
|
|
'PYTHONPATH': current_python_paths(run_dir_path),
|
|
'PYTHONIOENCODING': 'utf-8',
|
|
'PYTHONUNBUFFERED': '1',
|
|
'OPENCLAW_WORKSPACE': str(WORKSPACE),
|
|
'CODE_INTERPRETER_RUN_DIR': str(run_dir_path),
|
|
'CODE_INTERPRETER_ARTIFACT_DIR': str(artifact_dir),
|
|
'MPLBACKEND': 'Agg',
|
|
}
|
|
|
|
started = time.time()
|
|
timed_out = False
|
|
exit_code = None
|
|
stdout = ''
|
|
stderr = ''
|
|
|
|
try:
|
|
proc = subprocess.run(
|
|
[PYTHON_BIN, '-B', str(script_path)],
|
|
cwd=str(cwd),
|
|
env=env,
|
|
capture_output=True,
|
|
text=True,
|
|
encoding='utf-8',
|
|
errors='replace',
|
|
timeout=max(1, args.timeout),
|
|
)
|
|
exit_code = proc.returncode
|
|
stdout = proc.stdout
|
|
stderr = proc.stderr
|
|
except subprocess.TimeoutExpired as exc:
|
|
timed_out = True
|
|
exit_code = 124
|
|
raw_out = exc.stdout or ''
|
|
raw_err = exc.stderr or ''
|
|
stdout = raw_out if isinstance(raw_out, str) else raw_out.decode('utf-8', errors='replace')
|
|
stderr = (raw_err if isinstance(raw_err, str) else raw_err.decode('utf-8', errors='replace')) + f'\nExecution timed out after {args.timeout}s.'
|
|
|
|
duration = round(time.time() - started, 3)
|
|
|
|
write_text(args.stdout_file, stdout)
|
|
write_text(args.stderr_file, stderr)
|
|
|
|
artifacts = scan_artifacts(artifact_dir, 'artifactDir')
|
|
if artifact_dir != run_dir_path:
|
|
artifacts.extend(scan_artifacts(run_dir_path / 'artifacts', 'runArtifacts'))
|
|
|
|
result = {
|
|
'ok': (exit_code == 0 and not timed_out),
|
|
'exitCode': exit_code,
|
|
'timeout': timed_out,
|
|
'durationSec': duration,
|
|
'cwd': str(cwd),
|
|
'runDir': str(run_dir_path),
|
|
'artifactDir': str(artifact_dir),
|
|
'packageStatus': package_status(),
|
|
'artifacts': artifacts,
|
|
'stdout': truncate(stdout),
|
|
'stderr': truncate(stderr),
|
|
}
|
|
|
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
|
|
if not args.keep_run_dir and result['ok'] and artifact_dir != run_dir_path:
|
|
shutil.rmtree(run_dir_path, ignore_errors=True)
|
|
|
|
return 0 if result['ok'] else 1
|
|
|
|
|
|
if __name__ == '__main__':
|
|
raise SystemExit(main())
|