v2-AI-ethic-HOT-explanatory / core /shell_executor.py
linxinhua's picture
Deploy: chapters-as-corpus restructure + analyze-input/direct-teaching skills + unified scratch (6 fields) + explicit BASE_DIR shell hint
82cc20c verified
Raw
History Blame Contribute Delete
4.33 kB
"""Local shell executor: run shell_call commands and return shell_call_output."""
import re
import subprocess
import yaml
from pathlib import Path
from core.config_loader import BASE_DIR, PATHS, IS_WINDOWS
SHELL_TIMEOUT = 30
# Match `type/cat foo.md` (single-line), used to short-circuit pure file reads
# back to Python — saves a cmd.exe spawn (~50-200ms on Windows).
_FILE_READ_RE = re.compile(
r"^(?:type|cat)\s+[\"']?([^\"|']+\.md)[\"']?\s*$",
re.IGNORECASE,
)
# {path_fragment: skill_name} — both absolute and relative forms are listed
# because the model may emit either (relative is typical: `cat skills/foo/...`).
SKILL_PATHS = {
p: name
for name, rel_path in PATHS.get("skills", {}).items()
for p in (
rel_path.replace("\\", "/"),
str(BASE_DIR / rel_path).replace("\\", "/"),
)
}
def parse_skill_description(skill_md_path):
"""Parse description from SKILL.md YAML frontmatter."""
content = skill_md_path.read_text(encoding="utf-8")
parts = content.split("---", 2)
if len(parts) >= 3:
frontmatter = yaml.safe_load(parts[1])
if isinstance(frontmatter, dict) and frontmatter.get("description"):
return frontmatter["description"]
return None
def build_tools():
"""Build the tools list from config skill paths."""
skills = []
for name, rel_path in PATHS["skills"].items():
skill_dir = BASE_DIR / rel_path
skill_md = skill_dir / "SKILL.md"
description = f"Skill: {name}"
if skill_md.exists():
description = parse_skill_description(skill_md) or description
skills.append({
"name": name,
"description": description,
"path": str(skill_dir),
})
return [{
"type": "shell",
"environment": {"type": "local", "skills": skills},
}]
def detect_skills(commands):
"""Detect which skill(s) a set of commands reference, based on path fragments."""
triggered = set()
for cmd in commands:
normalized_cmd = cmd.replace("\\", "/")
for path_fragment, skill_name in SKILL_PATHS.items():
if path_fragment in normalized_cmd:
triggered.add(skill_name)
return sorted(triggered)
def execute_shell_call(shell_call):
"""Execute a shell_call locally. Returns (shell_call_output_dict, list_of_skill_names)."""
commands = getattr(shell_call.action, "commands", [])
results = []
for cmd in commands:
# Short-circuit `type/cat *.md` → read the file directly, skipping the
# cmd.exe / sh spawn (saves ~50-200ms per read on Windows).
m = _FILE_READ_RE.match(cmd.strip())
if m:
rel = m.group(1).replace("\\", "/")
path = (BASE_DIR / rel).resolve()
try:
results.append({
"stdout": path.read_text(encoding="utf-8", errors="replace"),
"stderr": "",
"outcome": {"type": "exit", "exit_code": 0},
})
except OSError as e:
results.append({
"stdout": "",
"stderr": f"{type(e).__name__}: {e}",
"outcome": {"type": "exit", "exit_code": 1},
})
continue
try:
proc = subprocess.run(
cmd,
shell=True,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=SHELL_TIMEOUT,
cwd=str(BASE_DIR),
)
results.append({
# OpenAI rejects null stdout/stderr — coerce to empty string.
"stdout": proc.stdout or "",
"stderr": proc.stderr or "",
"outcome": {"type": "exit", "exit_code": proc.returncode},
})
except subprocess.TimeoutExpired:
results.append({
"stdout": "",
"stderr": f"Command timed out after {SHELL_TIMEOUT} seconds.",
"outcome": {"type": "timeout"},
})
skills_used = detect_skills(commands)
return {
"type": "shell_call_output",
"call_id": shell_call.call_id,
"output": results,
}, skills_used