feat: add code execution verifier and rewards

pull/808/head
RileyChen23 2026-07-12 17:29:07 +08:00
parent 512eed0b65
commit 8e06fb6d52
4 changed files with 441 additions and 0 deletions

22
code_agent/__init__.py Normal file
View File

@ -0,0 +1,22 @@
"""Utilities for execution-feedback code generation experiments."""
from .reward import CodeReward, score_verification
from .verifier import (
CodeTask,
TestCase,
VerificationResult,
VerificationStatus,
extract_python_code,
verify_code,
)
__all__ = [
"CodeReward",
"CodeTask",
"TestCase",
"VerificationResult",
"VerificationStatus",
"extract_python_code",
"score_verification",
"verify_code",
]

36
code_agent/reward.py Normal file
View File

@ -0,0 +1,36 @@
"""Verifiable rewards derived from deterministic code execution."""
from __future__ import annotations
from dataclasses import dataclass
from .verifier import VerificationResult, VerificationStatus
@dataclass(frozen=True)
class CodeReward:
total: float
correctness: float
execution: float
format: float
reason: str
def score_verification(result: VerificationResult) -> CodeReward:
"""Map verifier outcomes to a transparent reward in [-1, 1]."""
status = result.status
if status == VerificationStatus.PASSED:
return CodeReward(1.0, 0.8, 0.1, 0.1, "all tests passed")
if status == VerificationStatus.WRONG_ANSWER:
correctness = 0.8 * result.pass_rate
total = -0.2 + correctness + 0.1 + 0.1
return CodeReward(total, correctness, 0.1, 0.1, "partial test credit")
if status == VerificationStatus.RUNTIME_ERROR:
return CodeReward(-0.4, 0.0, -0.5, 0.1, "runtime error")
if status == VerificationStatus.TIMEOUT:
return CodeReward(-0.6, 0.0, -0.7, 0.1, "execution timeout")
if status == VerificationStatus.SYNTAX_ERROR:
return CodeReward(-0.8, 0.0, -0.8, 0.0, "syntax error")
if status in {VerificationStatus.NO_CODE, VerificationStatus.POLICY_VIOLATION}:
return CodeReward(-1.0, 0.0, 0.0, -1.0, status.value)
return CodeReward(-1.0, 0.0, -1.0, 0.0, "verifier failure")

326
code_agent/verifier.py Normal file
View File

@ -0,0 +1,326 @@
"""Deterministic verifier for function-style Python code generation tasks.
The local subprocess backend is intended for controlled experiments. It uses
AST policy checks, a temporary working directory, an isolated Python process,
and a hard timeout, but it is not a production security boundary. Run code
from untrusted users inside a dedicated container or micro-VM.
"""
from __future__ import annotations
import ast
import json
import math
import os
import re
import subprocess
import sys
import tempfile
import textwrap
import time
from dataclasses import asdict, dataclass, field
from enum import Enum
from pathlib import Path
from typing import Any, Iterable
class VerificationStatus(str, Enum):
PASSED = "passed"
NO_CODE = "no_code"
SYNTAX_ERROR = "syntax_error"
POLICY_VIOLATION = "policy_violation"
RUNTIME_ERROR = "runtime_error"
TIMEOUT = "timeout"
WRONG_ANSWER = "wrong_answer"
INTERNAL_ERROR = "internal_error"
@dataclass(frozen=True)
class TestCase:
args: tuple[Any, ...] = ()
kwargs: dict[str, Any] = field(default_factory=dict)
expected: Any = None
def to_json(self) -> dict[str, Any]:
return {"args": list(self.args), "kwargs": self.kwargs, "expected": self.expected}
@dataclass(frozen=True)
class CodeTask:
task_id: str
prompt: str
entry_point: str
tests: tuple[TestCase, ...]
def __post_init__(self) -> None:
if not self.entry_point.isidentifier():
raise ValueError(f"entry_point must be a Python identifier: {self.entry_point!r}")
if not self.tests:
raise ValueError("a code task must contain at least one test")
# Fail early instead of discovering non-serializable tests in a worker.
json.dumps([case.to_json() for case in self.tests], ensure_ascii=False)
@dataclass(frozen=True)
class VerificationResult:
status: VerificationStatus
passed_tests: int
total_tests: int
duration_ms: float
feedback: str
stdout: str = ""
stderr: str = ""
@property
def pass_rate(self) -> float:
return self.passed_tests / self.total_tests if self.total_tests else 0.0
def to_dict(self) -> dict[str, Any]:
payload = asdict(self)
payload["status"] = self.status.value
payload["pass_rate"] = self.pass_rate
return payload
_FENCED_CODE = re.compile(r"```(?:python|py)?\s*\n(.*?)```", re.IGNORECASE | re.DOTALL)
_THINK_BLOCK = re.compile(r"<think>.*?</think>", re.IGNORECASE | re.DOTALL)
_ALLOWED_IMPORT_ROOTS = {
"bisect",
"collections",
"functools",
"heapq",
"itertools",
"math",
"operator",
"re",
"string",
"typing",
}
_BLOCKED_CALLS = {"breakpoint", "compile", "eval", "exec", "input", "open", "__import__"}
_BLOCKED_NAMES = {"os", "pathlib", "requests", "shutil", "socket", "subprocess", "sys"}
def extract_python_code(response: str) -> str:
"""Extract the first fenced Python block, falling back to plain text."""
cleaned = _THINK_BLOCK.sub("", response or "").strip()
matches = _FENCED_CODE.findall(cleaned)
if matches:
return textwrap.dedent(matches[0]).strip()
return textwrap.dedent(cleaned).strip()
def _policy_errors(tree: ast.AST) -> list[str]:
errors: list[str] = []
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for alias in node.names:
root = alias.name.split(".", 1)[0]
if root not in _ALLOWED_IMPORT_ROOTS:
errors.append(f"import {alias.name!r} is not allowed")
elif isinstance(node, ast.ImportFrom):
root = (node.module or "").split(".", 1)[0]
if root not in _ALLOWED_IMPORT_ROOTS:
errors.append(f"import from {node.module!r} is not allowed")
elif isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
if node.func.id in _BLOCKED_CALLS:
errors.append(f"call to {node.func.id!r} is not allowed")
elif isinstance(node, ast.Name) and node.id in _BLOCKED_NAMES:
errors.append(f"name {node.id!r} is not allowed")
elif isinstance(node, ast.Attribute) and node.attr.startswith("__"):
errors.append(f"dunder attribute {node.attr!r} is not allowed")
return sorted(set(errors))
def _limit_resources(timeout_seconds: float, memory_mb: int):
if os.name == "nt":
return None
def apply_limits() -> None:
import resource
cpu_seconds = max(1, math.ceil(timeout_seconds))
memory_bytes = memory_mb * 1024 * 1024
resource.setrlimit(resource.RLIMIT_CPU, (cpu_seconds, cpu_seconds))
resource.setrlimit(resource.RLIMIT_AS, (memory_bytes, memory_bytes))
resource.setrlimit(resource.RLIMIT_FSIZE, (1024 * 1024, 1024 * 1024))
return apply_limits
_RUNNER_SOURCE = r'''
import importlib.util
import json
import math
import traceback
def equivalent(actual, expected):
if isinstance(actual, float) or isinstance(expected, float):
try:
return math.isclose(float(actual), float(expected), rel_tol=1e-7, abs_tol=1e-9)
except (TypeError, ValueError):
return False
if isinstance(actual, tuple):
actual = list(actual)
if isinstance(expected, tuple):
expected = list(expected)
return actual == expected
def main():
spec = importlib.util.spec_from_file_location("candidate_solution", "solution.py")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
with open("cases.json", "r", encoding="utf-8") as handle:
payload = json.load(handle)
function = getattr(module, payload["entry_point"])
passed = 0
failure = None
for index, case in enumerate(payload["tests"]):
actual = function(*case["args"], **case["kwargs"])
if equivalent(actual, case["expected"]):
passed += 1
elif failure is None:
failure = {
"index": index,
"expected": repr(case["expected"]),
"actual": repr(actual),
}
print("__MINIMIND_RESULT__=" + json.dumps({"passed": passed, "failure": failure}))
if __name__ == "__main__":
try:
main()
except Exception:
print("__MINIMIND_EXCEPTION__=" + traceback.format_exc())
raise
'''
def _clip_output(value: str, limit: int = 4000) -> str:
return value if len(value) <= limit else value[-limit:]
def verify_code(
response_or_code: str,
task: CodeTask,
*,
timeout_seconds: float = 2.0,
memory_mb: int = 256,
) -> VerificationResult:
"""Run a candidate function against deterministic JSON-serializable tests."""
started = time.perf_counter()
code = extract_python_code(response_or_code)
if not code:
return VerificationResult(
VerificationStatus.NO_CODE, 0, len(task.tests), 0.0, "No Python code was found."
)
try:
tree = ast.parse(code)
except SyntaxError as exc:
duration = (time.perf_counter() - started) * 1000
return VerificationResult(
VerificationStatus.SYNTAX_ERROR,
0,
len(task.tests),
duration,
f"SyntaxError on line {exc.lineno}: {exc.msg}",
)
errors = _policy_errors(tree)
if errors:
duration = (time.perf_counter() - started) * 1000
return VerificationResult(
VerificationStatus.POLICY_VIOLATION,
0,
len(task.tests),
duration,
"; ".join(errors),
)
payload = {"entry_point": task.entry_point, "tests": [case.to_json() for case in task.tests]}
env = {"PYTHONHASHSEED": "0", "PYTHONIOENCODING": "utf-8"}
creationflags = subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0
try:
with tempfile.TemporaryDirectory(prefix="minimind-code-") as temp_dir:
root = Path(temp_dir)
(root / "solution.py").write_text(code, encoding="utf-8")
(root / "cases.json").write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")
(root / "runner.py").write_text(_RUNNER_SOURCE, encoding="utf-8")
completed = subprocess.run(
[sys.executable, "-I", "-S", "runner.py"],
cwd=root,
env=env,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=timeout_seconds,
check=False,
creationflags=creationflags,
preexec_fn=_limit_resources(timeout_seconds, memory_mb),
)
except subprocess.TimeoutExpired as exc:
duration = (time.perf_counter() - started) * 1000
return VerificationResult(
VerificationStatus.TIMEOUT,
0,
len(task.tests),
duration,
f"Execution exceeded the {timeout_seconds:.2f}s timeout.",
_clip_output(exc.stdout or ""),
_clip_output(exc.stderr or ""),
)
except Exception as exc:
duration = (time.perf_counter() - started) * 1000
return VerificationResult(
VerificationStatus.INTERNAL_ERROR,
0,
len(task.tests),
duration,
f"Verifier failure: {type(exc).__name__}: {exc}",
)
duration = (time.perf_counter() - started) * 1000
stdout, stderr = _clip_output(completed.stdout), _clip_output(completed.stderr)
marker = "__MINIMIND_RESULT__="
result_line = next((line for line in reversed(stdout.splitlines()) if line.startswith(marker)), None)
if completed.returncode != 0 or result_line is None:
exception_line = next(
(line for line in reversed(stdout.splitlines()) if line.startswith("__MINIMIND_EXCEPTION__=")),
None,
)
feedback = exception_line.split("=", 1)[1] if exception_line else (stderr.strip() or "Candidate raised an exception.")
return VerificationResult(
VerificationStatus.RUNTIME_ERROR, 0, len(task.tests), duration, feedback, stdout, stderr
)
try:
worker_result = json.loads(result_line[len(marker):])
passed = int(worker_result["passed"])
except (KeyError, TypeError, ValueError, json.JSONDecodeError) as exc:
return VerificationResult(
VerificationStatus.INTERNAL_ERROR,
0,
len(task.tests),
duration,
f"Invalid verifier output: {exc}",
stdout,
stderr,
)
failure = worker_result.get("failure")
if passed == len(task.tests):
status = VerificationStatus.PASSED
feedback = f"Passed all {passed}/{len(task.tests)} tests."
else:
status = VerificationStatus.WRONG_ANSWER
feedback = (
f"Passed {passed}/{len(task.tests)} tests. First failure at test {failure['index']}: "
f"expected {failure['expected']}, got {failure['actual']}."
)
return VerificationResult(status, passed, len(task.tests), duration, feedback, stdout, stderr)

View File

@ -0,0 +1,57 @@
from code_agent import (
CodeTask,
TestCase as CodeTestCase,
VerificationStatus,
extract_python_code,
score_verification,
verify_code,
)
TASK = CodeTask(
task_id="add-two",
prompt="Implement add(a, b).",
entry_point="add",
tests=(
CodeTestCase(args=(1, 2), expected=3),
CodeTestCase(args=(5, 0), expected=5),
),
)
def test_extracts_fenced_code_after_thinking():
response = "<think>reasoning</think>\n```python\ndef add(a, b):\n return a + b\n```"
assert extract_python_code(response).startswith("def add")
def test_verifier_accepts_correct_solution():
result = verify_code("def add(a, b):\n return a + b", TASK)
assert result.status == VerificationStatus.PASSED
assert result.pass_rate == 1.0
assert score_verification(result).total == 1.0
def test_verifier_reports_partial_credit_and_feedback():
result = verify_code("def add(a, b):\n return a - b", TASK)
assert result.status == VerificationStatus.WRONG_ANSWER
assert result.passed_tests == 1
assert "expected 3, got -1" in result.feedback
assert score_verification(result).total == 0.4
def test_verifier_reports_syntax_error():
result = verify_code("def add(a, b)\n return a + b", TASK)
assert result.status == VerificationStatus.SYNTAX_ERROR
assert score_verification(result).total == -0.8
def test_verifier_blocks_unsafe_import():
result = verify_code("import os\ndef add(a, b):\n return a + b", TASK)
assert result.status == VerificationStatus.POLICY_VIOLATION
assert "import 'os' is not allowed" in result.feedback
def test_verifier_times_out_infinite_loop():
result = verify_code("def add(a, b):\n while True:\n pass", TASK, timeout_seconds=0.2)
assert result.status == VerificationStatus.TIMEOUT
assert score_verification(result).total == -0.6