Spaces:
Runtime error
feat: add real code execution — run_code, run_tests, submit_fix tools
Browse filesThis is what makes CodeReviewEnv different from every other submission:
agents execute real Python code, see real test failures, and submit
fixes that are verified by actual test execution.
New tools:
run_code — execute Python code, see stdout/stderr/errors (like a REPL)
run_tests — run test cases against buggy code, see which fail
submit_fix — submit corrected code; tests verify the fix works
(60% test-based + 40% review-based reward)
Also:
- server/code_executor.py: sandboxed Python executor
- 12 snippet test suites (binary_search, fibonacci, merge_sort, etc.)
- Gradio UI for interactive testing
- 8 tools total (was 5)
- 32 tests, 18/18 validation checks passing
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- server/code_executor.py +221 -0
- server/code_review_environment.py +189 -22
- tests/test_code_review_env.py +2 -2
- validate.py +2 -2
|
@@ -0,0 +1,221 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Code Executor — Sandboxed Python execution for CodeReviewEnv.
|
| 3 |
+
|
| 4 |
+
Runs Python code + test cases in an isolated namespace.
|
| 5 |
+
Returns execution results, stdout, stderr, and test pass/fail.
|
| 6 |
+
|
| 7 |
+
This makes CodeReviewEnv a REAL tool server: agents run code,
|
| 8 |
+
see actual failures, and submit fixes that are verified by execution.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import io
|
| 12 |
+
import sys
|
| 13 |
+
import traceback
|
| 14 |
+
from typing import Any, Dict, List, Optional, Tuple
|
| 15 |
+
|
| 16 |
+
# Maximum execution time (seconds) and output length
|
| 17 |
+
EXEC_TIMEOUT = 5
|
| 18 |
+
MAX_OUTPUT_LEN = 4096
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def execute_code(code: str, test_code: str = "") -> Dict[str, Any]:
|
| 22 |
+
"""Execute Python code + test cases in an isolated namespace.
|
| 23 |
+
|
| 24 |
+
Args:
|
| 25 |
+
code: The source code to execute
|
| 26 |
+
test_code: Test assertions to run after the code
|
| 27 |
+
|
| 28 |
+
Returns:
|
| 29 |
+
{
|
| 30 |
+
"success": bool,
|
| 31 |
+
"stdout": str,
|
| 32 |
+
"stderr": str,
|
| 33 |
+
"error": str or None,
|
| 34 |
+
"tests_passed": int,
|
| 35 |
+
"tests_failed": int,
|
| 36 |
+
"test_results": [{"name": str, "passed": bool, "error": str}],
|
| 37 |
+
}
|
| 38 |
+
"""
|
| 39 |
+
namespace: Dict[str, Any] = {}
|
| 40 |
+
stdout_capture = io.StringIO()
|
| 41 |
+
stderr_capture = io.StringIO()
|
| 42 |
+
|
| 43 |
+
result = {
|
| 44 |
+
"success": False,
|
| 45 |
+
"stdout": "",
|
| 46 |
+
"stderr": "",
|
| 47 |
+
"error": None,
|
| 48 |
+
"tests_passed": 0,
|
| 49 |
+
"tests_failed": 0,
|
| 50 |
+
"test_results": [],
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
# Step 1: Execute the main code
|
| 54 |
+
old_stdout, old_stderr = sys.stdout, sys.stderr
|
| 55 |
+
try:
|
| 56 |
+
sys.stdout = stdout_capture
|
| 57 |
+
sys.stderr = stderr_capture
|
| 58 |
+
exec(code, namespace)
|
| 59 |
+
result["success"] = True
|
| 60 |
+
except Exception as e:
|
| 61 |
+
result["error"] = f"{type(e).__name__}: {e}"
|
| 62 |
+
result["success"] = False
|
| 63 |
+
finally:
|
| 64 |
+
sys.stdout = old_stdout
|
| 65 |
+
sys.stderr = old_stderr
|
| 66 |
+
result["stdout"] = stdout_capture.getvalue()[:MAX_OUTPUT_LEN]
|
| 67 |
+
result["stderr"] = stderr_capture.getvalue()[:MAX_OUTPUT_LEN]
|
| 68 |
+
|
| 69 |
+
# Step 2: Run test cases
|
| 70 |
+
if test_code and result["success"]:
|
| 71 |
+
test_results = _run_tests(test_code, namespace)
|
| 72 |
+
result["test_results"] = test_results
|
| 73 |
+
result["tests_passed"] = sum(1 for t in test_results if t["passed"])
|
| 74 |
+
result["tests_failed"] = sum(1 for t in test_results if not t["passed"])
|
| 75 |
+
|
| 76 |
+
return result
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def _run_tests(test_code: str, namespace: Dict) -> List[Dict[str, Any]]:
|
| 80 |
+
"""Run individual test assertions and collect results."""
|
| 81 |
+
results = []
|
| 82 |
+
|
| 83 |
+
# Split test code into individual assertions
|
| 84 |
+
lines = test_code.strip().split('\n')
|
| 85 |
+
test_blocks = []
|
| 86 |
+
current_block = []
|
| 87 |
+
|
| 88 |
+
for line in lines:
|
| 89 |
+
stripped = line.strip()
|
| 90 |
+
if stripped.startswith('# test:') or stripped.startswith('# Test:'):
|
| 91 |
+
if current_block:
|
| 92 |
+
test_blocks.append(('\n'.join(current_block), current_block[0].strip()))
|
| 93 |
+
current_block = []
|
| 94 |
+
current_block.append(line)
|
| 95 |
+
|
| 96 |
+
if current_block:
|
| 97 |
+
test_blocks.append(('\n'.join(current_block), current_block[0].strip()))
|
| 98 |
+
|
| 99 |
+
# If no labeled blocks, treat each assert as a test
|
| 100 |
+
if len(test_blocks) <= 1:
|
| 101 |
+
test_blocks = []
|
| 102 |
+
for line in lines:
|
| 103 |
+
stripped = line.strip()
|
| 104 |
+
if stripped and not stripped.startswith('#'):
|
| 105 |
+
test_blocks.append((line, stripped[:60]))
|
| 106 |
+
|
| 107 |
+
for code_block, name in test_blocks:
|
| 108 |
+
try:
|
| 109 |
+
exec(code_block, namespace)
|
| 110 |
+
results.append({"name": name, "passed": True, "error": None})
|
| 111 |
+
except AssertionError as e:
|
| 112 |
+
results.append({"name": name, "passed": False, "error": f"AssertionError: {e}"})
|
| 113 |
+
except Exception as e:
|
| 114 |
+
results.append({"name": name, "passed": False, "error": f"{type(e).__name__}: {e}"})
|
| 115 |
+
|
| 116 |
+
return results
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def apply_fix_and_test(
|
| 120 |
+
original_code: str,
|
| 121 |
+
fix_code: str,
|
| 122 |
+
test_code: str,
|
| 123 |
+
) -> Dict[str, Any]:
|
| 124 |
+
"""Apply agent's fix to the code and run tests.
|
| 125 |
+
|
| 126 |
+
The fix_code can be either:
|
| 127 |
+
1. Complete replacement code
|
| 128 |
+
2. A patch description (we try to apply it)
|
| 129 |
+
|
| 130 |
+
Returns execution results with test pass/fail.
|
| 131 |
+
"""
|
| 132 |
+
# Try the fix as complete replacement first
|
| 133 |
+
result = execute_code(fix_code, test_code)
|
| 134 |
+
if result["tests_passed"] > 0:
|
| 135 |
+
return result
|
| 136 |
+
|
| 137 |
+
# If that didn't work, try prepending original code context
|
| 138 |
+
combined = original_code + "\n\n" + fix_code
|
| 139 |
+
result = execute_code(combined, test_code)
|
| 140 |
+
return result
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
# ─── Test Case Templates ─────────────────────────────────────────────────────
|
| 144 |
+
|
| 145 |
+
# These map snippet names to test cases that validate correctness.
|
| 146 |
+
# Tests are written against the ORIGINAL (clean) code — they should
|
| 147 |
+
# PASS on clean code and FAIL on buggy code.
|
| 148 |
+
|
| 149 |
+
SNIPPET_TESTS: Dict[str, str] = {
|
| 150 |
+
"binary_search": """\
|
| 151 |
+
assert binary_search([1,2,3,4,5], 3) == 2
|
| 152 |
+
assert binary_search([1,2,3,4,5], 1) == 0
|
| 153 |
+
assert binary_search([1,2,3,4,5], 5) == 4
|
| 154 |
+
assert binary_search([1,2,3,4,5], 6) == -1
|
| 155 |
+
assert binary_search([], 1) == -1
|
| 156 |
+
assert binary_search([1], 1) == 0
|
| 157 |
+
""",
|
| 158 |
+
"fibonacci": """\
|
| 159 |
+
assert fibonacci(0) == 0
|
| 160 |
+
assert fibonacci(1) == 1
|
| 161 |
+
assert fibonacci(2) == 1
|
| 162 |
+
assert fibonacci(5) == 5
|
| 163 |
+
assert fibonacci(10) == 55
|
| 164 |
+
""",
|
| 165 |
+
"max_subarray": """\
|
| 166 |
+
assert max_subarray([1, -2, 3, 4, -1]) == 7
|
| 167 |
+
assert max_subarray([-1, -2, -3]) == -1
|
| 168 |
+
assert max_subarray([5]) == 5
|
| 169 |
+
assert max_subarray([]) == 0
|
| 170 |
+
""",
|
| 171 |
+
"is_palindrome": """\
|
| 172 |
+
assert is_palindrome("racecar") == True
|
| 173 |
+
assert is_palindrome("hello") == False
|
| 174 |
+
assert is_palindrome("A man a plan a canal Panama") == True
|
| 175 |
+
assert is_palindrome("") == True
|
| 176 |
+
""",
|
| 177 |
+
"merge_sort": """\
|
| 178 |
+
assert merge_sort([3,1,4,1,5]) == [1,1,3,4,5]
|
| 179 |
+
assert merge_sort([]) == []
|
| 180 |
+
assert merge_sort([1]) == [1]
|
| 181 |
+
assert merge_sort([5,4,3,2,1]) == [1,2,3,4,5]
|
| 182 |
+
""",
|
| 183 |
+
"flatten_dict": """\
|
| 184 |
+
assert flatten_dict({"a": 1, "b": {"c": 2}}) == {"a": 1, "b.c": 2}
|
| 185 |
+
assert flatten_dict({}) == {}
|
| 186 |
+
assert flatten_dict({"x": {"y": {"z": 1}}}) == {"x.y.z": 1}
|
| 187 |
+
""",
|
| 188 |
+
"validate_email": """\
|
| 189 |
+
assert validate_email("user@example.com") == True
|
| 190 |
+
assert validate_email("bad") == False
|
| 191 |
+
assert validate_email("@example.com") == False
|
| 192 |
+
assert validate_email("user@.com") == False
|
| 193 |
+
assert validate_email("") == False
|
| 194 |
+
""",
|
| 195 |
+
"group_by": """\
|
| 196 |
+
result = group_by([1,2,3,4,5], lambda x: x % 2)
|
| 197 |
+
assert result[0] == [2, 4]
|
| 198 |
+
assert result[1] == [1, 3, 5]
|
| 199 |
+
""",
|
| 200 |
+
"matrix_multiply": """\
|
| 201 |
+
assert matrix_multiply([[1,2],[3,4]], [[5,6],[7,8]]) == [[19,22],[43,50]]
|
| 202 |
+
assert matrix_multiply([[1]], [[2]]) == [[2]]
|
| 203 |
+
""",
|
| 204 |
+
"csv_parser": """\
|
| 205 |
+
assert parse_csv("a,b,c") == [["a", "b", "c"]]
|
| 206 |
+
assert parse_csv("1,2\\n3,4") == [["1", "2"], ["3", "4"]]
|
| 207 |
+
""",
|
| 208 |
+
"topological_sort": """\
|
| 209 |
+
result = topological_sort({"a": ["b"], "b": ["c"], "c": []})
|
| 210 |
+
assert result.index("a") < result.index("b")
|
| 211 |
+
assert result.index("b") < result.index("c")
|
| 212 |
+
""",
|
| 213 |
+
"lru_cache": """\
|
| 214 |
+
cache = LRUCache(2)
|
| 215 |
+
cache.put("a", 1)
|
| 216 |
+
cache.put("b", 2)
|
| 217 |
+
assert cache.get("a") == 1
|
| 218 |
+
cache.put("c", 3)
|
| 219 |
+
assert cache.get("b") == -1
|
| 220 |
+
""",
|
| 221 |
+
}
|
|
@@ -1,16 +1,20 @@
|
|
| 1 |
"""
|
| 2 |
-
CodeReviewEnvironment — MCP tool-calling RL environment
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
"""
|
| 15 |
|
| 16 |
from typing import Any, Dict, List, Optional
|
|
@@ -22,42 +26,62 @@ from openenv.core.env_server.types import EnvironmentMetadata
|
|
| 22 |
from models import CodeReviewAction, CodeReviewObservation, CodeReviewState
|
| 23 |
from snippet_bank import generate_episode, BugRecord
|
| 24 |
from reward import compute_reward
|
|
|
|
| 25 |
|
| 26 |
MAX_STEPS = 10
|
| 27 |
LINE_TOLERANCE = 3
|
| 28 |
|
| 29 |
-
# Tool definitions — what agents discover via ListToolsAction
|
| 30 |
TOOLS = [
|
| 31 |
{
|
| 32 |
"name": "get_code",
|
| 33 |
-
"description": "Get the buggy source code to review. Returns
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
"parameters": {},
|
| 35 |
},
|
| 36 |
{
|
| 37 |
"name": "analyze_code",
|
| 38 |
-
"description": "
|
| 39 |
"parameters": {},
|
| 40 |
},
|
| 41 |
{
|
| 42 |
"name": "check_line",
|
| 43 |
-
"description": "Check if a specific line
|
| 44 |
"parameters": {
|
| 45 |
"line": {"type": "integer", "description": "Line number to check (1-indexed)"},
|
| 46 |
},
|
| 47 |
},
|
| 48 |
{
|
| 49 |
"name": "get_hint",
|
| 50 |
-
"description": "Get a progressive hint about the bugs.
|
| 51 |
"parameters": {},
|
| 52 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
{
|
| 54 |
"name": "submit_review",
|
| 55 |
-
"description": "Submit
|
| 56 |
"parameters": {
|
| 57 |
-
"issues": {"type": "array", "items": {"type": "string"}, "description": "
|
| 58 |
-
"flagged_lines": {"type": "array", "items": {"type": "integer"}, "description": "
|
| 59 |
-
"suggestion": {"type": "string", "description": "Suggested fix
|
| 60 |
-
"comment": {"type": "string", "description": "
|
| 61 |
},
|
| 62 |
},
|
| 63 |
]
|
|
@@ -236,9 +260,12 @@ class CodeReviewEnvironment(
|
|
| 236 |
|
| 237 |
handlers = {
|
| 238 |
"get_code": self._tool_get_code,
|
|
|
|
|
|
|
| 239 |
"analyze_code": self._tool_analyze_code,
|
| 240 |
"check_line": self._tool_check_line,
|
| 241 |
"get_hint": self._tool_get_hint,
|
|
|
|
| 242 |
"submit_review": self._tool_submit_review,
|
| 243 |
}
|
| 244 |
|
|
@@ -394,6 +421,146 @@ class CodeReviewEnvironment(
|
|
| 394 |
reward=0.0,
|
| 395 |
)
|
| 396 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 397 |
def _tool_submit_review(self, args: Dict) -> CodeReviewObservation:
|
| 398 |
"""Tool: submit_review — full 5-signal grading. Ends episode."""
|
| 399 |
issues = args.get("issues", [])
|
|
|
|
| 1 |
"""
|
| 2 |
+
CodeReviewEnvironment — MCP tool-calling RL environment with REAL code execution.
|
| 3 |
+
|
| 4 |
+
Like repl_env: agents run real code, see real failures, and submit fixes
|
| 5 |
+
that are verified by actual test execution. Not a keyword matcher.
|
| 6 |
+
|
| 7 |
+
Tools:
|
| 8 |
+
get_code — retrieve the buggy source code
|
| 9 |
+
run_code — EXECUTE the code and see output/errors (real Python executor)
|
| 10 |
+
run_tests — run test cases against the code (see which tests fail)
|
| 11 |
+
analyze_code — structural analysis
|
| 12 |
+
check_line — check if a line is near a bug (immediate feedback)
|
| 13 |
+
get_hint — progressive hints (costs efficiency)
|
| 14 |
+
submit_fix — submit fixed code; tests are re-run to verify the fix works
|
| 15 |
+
submit_review — submit text review (alternative to submit_fix)
|
| 16 |
+
|
| 17 |
+
MDP: up to 10 tool calls per episode.
|
| 18 |
"""
|
| 19 |
|
| 20 |
from typing import Any, Dict, List, Optional
|
|
|
|
| 26 |
from models import CodeReviewAction, CodeReviewObservation, CodeReviewState
|
| 27 |
from snippet_bank import generate_episode, BugRecord
|
| 28 |
from reward import compute_reward
|
| 29 |
+
from server.code_executor import execute_code, apply_fix_and_test, SNIPPET_TESTS
|
| 30 |
|
| 31 |
MAX_STEPS = 10
|
| 32 |
LINE_TOLERANCE = 3
|
| 33 |
|
|
|
|
| 34 |
TOOLS = [
|
| 35 |
{
|
| 36 |
"name": "get_code",
|
| 37 |
+
"description": "Get the buggy source code to review. Returns code with line numbers, language, and difficulty.",
|
| 38 |
+
"parameters": {},
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"name": "run_code",
|
| 42 |
+
"description": "Execute the current code and see stdout/stderr/errors. Like a real REPL — see what actually happens when you run it.",
|
| 43 |
+
"parameters": {
|
| 44 |
+
"code": {"type": "string", "description": "Python code to execute (optional — defaults to the buggy snippet)"},
|
| 45 |
+
},
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
"name": "run_tests",
|
| 49 |
+
"description": "Run test cases against the current buggy code. See which tests pass and which fail. The failures reveal the bugs.",
|
| 50 |
"parameters": {},
|
| 51 |
},
|
| 52 |
{
|
| 53 |
"name": "analyze_code",
|
| 54 |
+
"description": "Structural analysis: line count, functions, conditionals, complexity hints.",
|
| 55 |
"parameters": {},
|
| 56 |
},
|
| 57 |
{
|
| 58 |
"name": "check_line",
|
| 59 |
+
"description": "Check if a specific line is near a known bug. Immediate reward: +0.15 hit, -0.05 miss.",
|
| 60 |
"parameters": {
|
| 61 |
"line": {"type": "integer", "description": "Line number to check (1-indexed)"},
|
| 62 |
},
|
| 63 |
},
|
| 64 |
{
|
| 65 |
"name": "get_hint",
|
| 66 |
+
"description": "Get a progressive hint about the bugs. Costs -0.05 efficiency per hint.",
|
| 67 |
"parameters": {},
|
| 68 |
},
|
| 69 |
+
{
|
| 70 |
+
"name": "submit_fix",
|
| 71 |
+
"description": "Submit fixed Python code. The environment runs tests against your fix. If tests pass, you get high reward. This is the BEST way to end an episode.",
|
| 72 |
+
"parameters": {
|
| 73 |
+
"fixed_code": {"type": "string", "description": "The corrected source code"},
|
| 74 |
+
"comment": {"type": "string", "description": "What you fixed and why"},
|
| 75 |
+
},
|
| 76 |
+
},
|
| 77 |
{
|
| 78 |
"name": "submit_review",
|
| 79 |
+
"description": "Submit a text-based code review (alternative to submit_fix). Lower reward ceiling than submit_fix.",
|
| 80 |
"parameters": {
|
| 81 |
+
"issues": {"type": "array", "items": {"type": "string"}, "description": "Bug descriptions found"},
|
| 82 |
+
"flagged_lines": {"type": "array", "items": {"type": "integer"}, "description": "Buggy line numbers"},
|
| 83 |
+
"suggestion": {"type": "string", "description": "Suggested fix description"},
|
| 84 |
+
"comment": {"type": "string", "description": "Review comment"},
|
| 85 |
},
|
| 86 |
},
|
| 87 |
]
|
|
|
|
| 260 |
|
| 261 |
handlers = {
|
| 262 |
"get_code": self._tool_get_code,
|
| 263 |
+
"run_code": self._tool_run_code,
|
| 264 |
+
"run_tests": self._tool_run_tests,
|
| 265 |
"analyze_code": self._tool_analyze_code,
|
| 266 |
"check_line": self._tool_check_line,
|
| 267 |
"get_hint": self._tool_get_hint,
|
| 268 |
+
"submit_fix": self._tool_submit_fix,
|
| 269 |
"submit_review": self._tool_submit_review,
|
| 270 |
}
|
| 271 |
|
|
|
|
| 421 |
reward=0.0,
|
| 422 |
)
|
| 423 |
|
| 424 |
+
def _tool_run_code(self, args: Dict) -> CodeReviewObservation:
|
| 425 |
+
"""Tool: run_code — EXECUTE Python code and see real output/errors."""
|
| 426 |
+
if self._language != "python":
|
| 427 |
+
return CodeReviewObservation(
|
| 428 |
+
success=True,
|
| 429 |
+
tool_result={"error": f"Execution only supported for Python, got {self._language}",
|
| 430 |
+
"language": self._language},
|
| 431 |
+
metadata={"episode_id": self._episode_id, "step": self._step_count},
|
| 432 |
+
done=False, reward=0.0,
|
| 433 |
+
)
|
| 434 |
+
|
| 435 |
+
code_to_run = args.get("code", self._buggy_code)
|
| 436 |
+
exec_result = execute_code(code_to_run)
|
| 437 |
+
self._record("run_code", 0.0)
|
| 438 |
+
|
| 439 |
+
return CodeReviewObservation(
|
| 440 |
+
success=True,
|
| 441 |
+
tool_result={
|
| 442 |
+
"executed": True,
|
| 443 |
+
"success": exec_result["success"],
|
| 444 |
+
"stdout": exec_result["stdout"],
|
| 445 |
+
"stderr": exec_result["stderr"],
|
| 446 |
+
"error": exec_result["error"],
|
| 447 |
+
},
|
| 448 |
+
metadata={"episode_id": self._episode_id, "step": self._step_count},
|
| 449 |
+
done=False, reward=0.0,
|
| 450 |
+
)
|
| 451 |
+
|
| 452 |
+
def _tool_run_tests(self, args: Dict) -> CodeReviewObservation:
|
| 453 |
+
"""Tool: run_tests — run test cases against the buggy code. See real failures."""
|
| 454 |
+
if self._language != "python":
|
| 455 |
+
return CodeReviewObservation(
|
| 456 |
+
success=True,
|
| 457 |
+
tool_result={"error": f"Tests only available for Python, got {self._language}",
|
| 458 |
+
"tests_available": False},
|
| 459 |
+
metadata={"episode_id": self._episode_id, "step": self._step_count},
|
| 460 |
+
done=False, reward=0.0,
|
| 461 |
+
)
|
| 462 |
+
|
| 463 |
+
test_code = SNIPPET_TESTS.get(self._snippet_name, "")
|
| 464 |
+
if not test_code:
|
| 465 |
+
return CodeReviewObservation(
|
| 466 |
+
success=True,
|
| 467 |
+
tool_result={"tests_available": False, "message": "No test cases for this snippet."},
|
| 468 |
+
metadata={"episode_id": self._episode_id, "step": self._step_count},
|
| 469 |
+
done=False, reward=0.0,
|
| 470 |
+
)
|
| 471 |
+
|
| 472 |
+
exec_result = execute_code(self._buggy_code, test_code)
|
| 473 |
+
self._record("run_tests", 0.0)
|
| 474 |
+
|
| 475 |
+
return CodeReviewObservation(
|
| 476 |
+
success=True,
|
| 477 |
+
tool_result={
|
| 478 |
+
"tests_available": True,
|
| 479 |
+
"code_executed": exec_result["success"],
|
| 480 |
+
"code_error": exec_result["error"],
|
| 481 |
+
"tests_passed": exec_result["tests_passed"],
|
| 482 |
+
"tests_failed": exec_result["tests_failed"],
|
| 483 |
+
"test_results": exec_result["test_results"],
|
| 484 |
+
"total_tests": exec_result["tests_passed"] + exec_result["tests_failed"],
|
| 485 |
+
},
|
| 486 |
+
metadata={"episode_id": self._episode_id, "step": self._step_count},
|
| 487 |
+
done=False, reward=0.0,
|
| 488 |
+
)
|
| 489 |
+
|
| 490 |
+
def _tool_submit_fix(self, args: Dict) -> CodeReviewObservation:
|
| 491 |
+
"""Tool: submit_fix — submit corrected code, verified by test execution.
|
| 492 |
+
|
| 493 |
+
This is the highest-reward path: if the fix passes all tests,
|
| 494 |
+
the agent gets near-perfect score.
|
| 495 |
+
"""
|
| 496 |
+
fixed_code = args.get("fixed_code", "")
|
| 497 |
+
comment = args.get("comment", "")
|
| 498 |
+
|
| 499 |
+
if not fixed_code:
|
| 500 |
+
return CodeReviewObservation(
|
| 501 |
+
success=False,
|
| 502 |
+
error_message="fixed_code is required",
|
| 503 |
+
metadata={"episode_id": self._episode_id, "step": self._step_count},
|
| 504 |
+
done=False, reward=0.0,
|
| 505 |
+
)
|
| 506 |
+
|
| 507 |
+
# Run tests against the fix
|
| 508 |
+
test_code = SNIPPET_TESTS.get(self._snippet_name, "")
|
| 509 |
+
if test_code and self._language == "python":
|
| 510 |
+
exec_result = apply_fix_and_test(self._buggy_code, fixed_code, test_code)
|
| 511 |
+
total_tests = exec_result["tests_passed"] + exec_result["tests_failed"]
|
| 512 |
+
fix_pass_rate = exec_result["tests_passed"] / total_tests if total_tests > 0 else 0.0
|
| 513 |
+
else:
|
| 514 |
+
exec_result = {"tests_passed": 0, "tests_failed": 0, "test_results": []}
|
| 515 |
+
fix_pass_rate = 0.0
|
| 516 |
+
|
| 517 |
+
# Compute reward: test-based (0.60) + review-based (0.40)
|
| 518 |
+
# Test pass rate is the primary signal — this is what makes us different
|
| 519 |
+
test_reward = fix_pass_rate * 0.60
|
| 520 |
+
|
| 521 |
+
# Also compute text-based reward for the comment
|
| 522 |
+
_, text_breakdown = compute_reward(
|
| 523 |
+
issues=[f"Fixed: {comment}"] if comment else [],
|
| 524 |
+
flagged_lines=self._flagged_lines,
|
| 525 |
+
suggestion=fixed_code[:200],
|
| 526 |
+
comment=comment,
|
| 527 |
+
gold_bugs=self._gold_bugs,
|
| 528 |
+
step_count=self._step_count,
|
| 529 |
+
hint_count=self._hint_count,
|
| 530 |
+
difficulty=self._difficulty,
|
| 531 |
+
)
|
| 532 |
+
text_reward = text_breakdown.get("weighted_total", 0.0) * 0.40
|
| 533 |
+
|
| 534 |
+
total_reward = min(1.0, test_reward + text_reward)
|
| 535 |
+
|
| 536 |
+
self._total_reward += total_reward
|
| 537 |
+
self._done = True
|
| 538 |
+
self._record("submit_fix", total_reward)
|
| 539 |
+
|
| 540 |
+
breakdown = {
|
| 541 |
+
"test_pass_rate": round(fix_pass_rate, 4),
|
| 542 |
+
"test_reward": round(test_reward, 4),
|
| 543 |
+
"text_reward": round(text_reward, 4),
|
| 544 |
+
"tests_passed": exec_result["tests_passed"],
|
| 545 |
+
"tests_failed": exec_result["tests_failed"],
|
| 546 |
+
"total_reward": round(total_reward, 4),
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
+
return CodeReviewObservation(
|
| 550 |
+
success=True,
|
| 551 |
+
tool_result={
|
| 552 |
+
"fix_accepted": fix_pass_rate > 0.5,
|
| 553 |
+
"test_results": exec_result["test_results"],
|
| 554 |
+
"tests_passed": exec_result["tests_passed"],
|
| 555 |
+
"tests_failed": exec_result["tests_failed"],
|
| 556 |
+
"reward": total_reward,
|
| 557 |
+
"breakdown": breakdown,
|
| 558 |
+
},
|
| 559 |
+
metadata={"episode_id": self._episode_id, "step": self._step_count, "breakdown": breakdown},
|
| 560 |
+
done=True,
|
| 561 |
+
reward=total_reward,
|
| 562 |
+
)
|
| 563 |
+
|
| 564 |
def _tool_submit_review(self, args: Dict) -> CodeReviewObservation:
|
| 565 |
"""Tool: submit_review — full 5-signal grading. Ends episode."""
|
| 566 |
issues = args.get("issues", [])
|
|
@@ -30,7 +30,7 @@ class TestCoreInterface:
|
|
| 30 |
assert isinstance(obs, CodeReviewObservation)
|
| 31 |
assert obs.done is False
|
| 32 |
assert obs.tools_list is not None
|
| 33 |
-
assert len(obs.tools_list) ==
|
| 34 |
|
| 35 |
def test_list_tools_action(self):
|
| 36 |
"""ListToolsAction returns available tools."""
|
|
@@ -40,7 +40,7 @@ class TestCoreInterface:
|
|
| 40 |
assert obs.success is True
|
| 41 |
assert obs.tools_list is not None
|
| 42 |
tool_names = {t["name"] for t in obs.tools_list}
|
| 43 |
-
assert tool_names == {"get_code", "analyze_code", "check_line", "get_hint", "submit_review"}
|
| 44 |
|
| 45 |
def test_tool_call_get_code(self):
|
| 46 |
"""ToolCallAction with get_code returns source code."""
|
|
|
|
| 30 |
assert isinstance(obs, CodeReviewObservation)
|
| 31 |
assert obs.done is False
|
| 32 |
assert obs.tools_list is not None
|
| 33 |
+
assert len(obs.tools_list) == 8
|
| 34 |
|
| 35 |
def test_list_tools_action(self):
|
| 36 |
"""ListToolsAction returns available tools."""
|
|
|
|
| 40 |
assert obs.success is True
|
| 41 |
assert obs.tools_list is not None
|
| 42 |
tool_names = {t["name"] for t in obs.tools_list}
|
| 43 |
+
assert tool_names == {"get_code", "run_code", "run_tests", "analyze_code", "check_line", "get_hint", "submit_fix", "submit_review"}
|
| 44 |
|
| 45 |
def test_tool_call_get_code(self):
|
| 46 |
"""ToolCallAction with get_code returns source code."""
|
|
@@ -40,7 +40,7 @@ def validate():
|
|
| 40 |
obs = env.reset(seed=42, difficulty=difficulty)
|
| 41 |
results.append(check(
|
| 42 |
f"reset() returns tools_list ({difficulty})",
|
| 43 |
-
isinstance(obs, CodeReviewObservation) and obs.tools_list is not None and len(obs.tools_list) ==
|
| 44 |
))
|
| 45 |
except Exception as e:
|
| 46 |
results.append(check(f"reset() ({difficulty})", False, str(e)))
|
|
@@ -53,7 +53,7 @@ def validate():
|
|
| 53 |
tool_names = {t["name"] for t in obs.tools_list}
|
| 54 |
results.append(check(
|
| 55 |
"ListToolsAction returns 5 tools",
|
| 56 |
-
tool_names == {"get_code", "analyze_code", "check_line", "get_hint", "submit_review"},
|
| 57 |
))
|
| 58 |
except Exception as e:
|
| 59 |
results.append(check("ListToolsAction", False, str(e)))
|
|
|
|
| 40 |
obs = env.reset(seed=42, difficulty=difficulty)
|
| 41 |
results.append(check(
|
| 42 |
f"reset() returns tools_list ({difficulty})",
|
| 43 |
+
isinstance(obs, CodeReviewObservation) and obs.tools_list is not None and len(obs.tools_list) == 8,
|
| 44 |
))
|
| 45 |
except Exception as e:
|
| 46 |
results.append(check(f"reset() ({difficulty})", False, str(e)))
|
|
|
|
| 53 |
tool_names = {t["name"] for t in obs.tools_list}
|
| 54 |
results.append(check(
|
| 55 |
"ListToolsAction returns 5 tools",
|
| 56 |
+
tool_names == {"get_code", "run_code", "run_tests", "analyze_code", "check_line", "get_hint", "submit_fix", "submit_review"},
|
| 57 |
))
|
| 58 |
except Exception as e:
|
| 59 |
results.append(check("ListToolsAction", False, str(e)))
|