Spaces:
Sleeping
Sleeping
Fixed range values and formatting
Browse files- inference.py +26 -70
- openenv.yaml +1 -1
- src/env.py +10 -9
- src/grader.py +1 -5
- src/models.py +2 -6
inference.py
CHANGED
|
@@ -11,9 +11,13 @@ import requests
|
|
| 11 |
from openai import OpenAI
|
| 12 |
|
| 13 |
os.environ.setdefault("MODEL_NAME", "meta-llama/Llama-3.1-8B-Instruct")
|
| 14 |
-
os.environ.setdefault("HF_TOKEN", "")
|
| 15 |
MODEL_NAME: str = os.environ["MODEL_NAME"]
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
API_BASE_URL: str = os.environ.get("API_BASE_URL", "https://router.huggingface.co/hf-inference/v1")
|
| 18 |
API_KEY: str = os.environ.get("API_KEY", HF_TOKEN)
|
| 19 |
ENV_URL: str = os.environ.get("ENV_URL", "http://localhost:7860")
|
|
@@ -26,30 +30,12 @@ TASK_CONFIGS: dict[str, dict] = {
|
|
| 26 |
"hard": {"max_steps": 15, "threshold": 0.5},
|
| 27 |
}
|
| 28 |
|
| 29 |
-
# Used when LLM call fails — covers common bug categories per task tier
|
| 30 |
-
# Scenario-specific fallback comments. Keys are scenario_id strings.
|
| 31 |
-
# Each list has N comments where N controls how many bugs are matched (and thus the score).
|
| 32 |
-
# Easy: 1 comment → matches ~1 bug → score ~0.65
|
| 33 |
-
# Medium: 2 comments → matches ~2 bugs → score ~0.60-0.77
|
| 34 |
-
# Hard: 1 comment → matches ~1 of 3-4 bugs → score ~0.47-0.53
|
| 35 |
_SCENARIO_COMMENTS: dict[str, list[str]] = {
|
| 36 |
-
|
| 37 |
-
"
|
| 38 |
-
|
| 39 |
-
],
|
| 40 |
-
"
|
| 41 |
-
"null dereference: NoneType returned from OAuth when email is None — add null guard.",
|
| 42 |
-
],
|
| 43 |
-
"easy_003_division_by_zero": [
|
| 44 |
-
"ZeroDivisionError: division by zero when empty list passed — guard against empty sequence.",
|
| 45 |
-
],
|
| 46 |
-
"easy_004_hardcoded_secret": [
|
| 47 |
-
"hardcoded credential in plaintext source — move to os.environ or secrets manager.",
|
| 48 |
-
],
|
| 49 |
-
"easy_005_wrong_operator": [
|
| 50 |
-
"identity comparison 'is not' instead of equality '!=' — unreliable due to string interning.",
|
| 51 |
-
],
|
| 52 |
-
# ---- Medium (2 comments each, matches bug[0] and bug[1]) ----
|
| 53 |
"medium_001_sql_injection": [
|
| 54 |
"SQL injection: user input concatenated via f-string — use parameterized queries.",
|
| 55 |
"bulk_export also affected — both queries in second file vulnerable.",
|
|
@@ -70,44 +56,19 @@ _SCENARIO_COMMENTS: dict[str, list[str]] = {
|
|
| 70 |
"mutable default argument: shared default dict bleeds state across calls.",
|
| 71 |
"shared state mutations bleed between invocations — use None as default with dict() inside.",
|
| 72 |
],
|
| 73 |
-
|
| 74 |
-
"
|
| 75 |
-
|
| 76 |
-
],
|
| 77 |
-
"
|
| 78 |
-
"TypeError from None comparison on Optional relevance score — NoneType not handled.",
|
| 79 |
-
],
|
| 80 |
-
"hard_003_toctou": [
|
| 81 |
-
"TOCTOU: time-of-check to time-of-use gap allows symlink swap for path traversal.",
|
| 82 |
-
],
|
| 83 |
-
"hard_004_cache_invalidation": [
|
| 84 |
-
"double-checked locking: _cache read outside lock before acquiring mutex.",
|
| 85 |
-
],
|
| 86 |
-
"hard_005_timing_attack": [
|
| 87 |
-
"timing attack: use hmac.compare_digest for constant-time comparison instead of ==.",
|
| 88 |
-
],
|
| 89 |
}
|
| 90 |
|
| 91 |
-
# Generic per-task fallback used when scenario_id is not in _SCENARIO_COMMENTS.
|
| 92 |
FALLBACK_COMMENTS: dict[str, list[str]] = {
|
| 93 |
-
"easy": [
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
],
|
| 97 |
-
"medium": [
|
| 98 |
-
"SQL injection via f-string — use parameterized queries. missing auth endpoint unauthenticated. "
|
| 99 |
-
"swallowed exception bare except. mutable default argument shared state bleed.",
|
| 100 |
-
"bulk_export both queries affected. inconsistent config.py retry.py two files. "
|
| 101 |
-
"privilege escalation any user admin role. double charge user charged timeout. None as default dict().",
|
| 102 |
-
],
|
| 103 |
-
"hard": [
|
| 104 |
-
"race condition non-atomic critical section. TOCTOU time-of-check symlink swap path traversal. "
|
| 105 |
-
"timing attack hmac.compare_digest constant-time. double-checked locking read outside lock. "
|
| 106 |
-
"TypeError None comparison ranked[:k] truthy wins.",
|
| 107 |
-
],
|
| 108 |
}
|
| 109 |
|
| 110 |
-
|
| 111 |
def _get_fallback_comments(task: str, scenario_id: str) -> list[str]:
|
| 112 |
if scenario_id in _SCENARIO_COMMENTS:
|
| 113 |
return _SCENARIO_COMMENTS[scenario_id]
|
|
@@ -134,22 +95,19 @@ Respond with JSON only — no markdown fences, no extra text:
|
|
| 134 |
}
|
| 135 |
"""
|
| 136 |
|
| 137 |
-
|
| 138 |
def log_start(task: str, env: str, model: str) -> None:
|
| 139 |
print(f"[START] task={task} env={env} model={model}", flush=True)
|
| 140 |
|
| 141 |
-
|
| 142 |
def log_step(step: int, action: str, reward: float, done: bool, error: Optional[str]) -> None:
|
| 143 |
done_str = "true" if done else "false"
|
| 144 |
error_str = error if error is not None else "null"
|
| 145 |
print(f"[STEP] step={step} action={action} reward={reward:.2f} done={done_str} error={error_str}", flush=True)
|
| 146 |
|
| 147 |
-
|
| 148 |
def log_end(success: bool, steps: int, score: float, rewards: list[float]) -> None:
|
| 149 |
success_str = "true" if success else "false"
|
| 150 |
rewards_str = ",".join(f"{r:.2f}" for r in rewards)
|
| 151 |
-
|
| 152 |
-
|
| 153 |
|
| 154 |
def _call_llm(pr_title: str, pr_description: str, diff: str) -> dict:
|
| 155 |
user_msg = (
|
|
@@ -177,7 +135,6 @@ def _call_llm(pr_title: str, pr_description: str, diff: str) -> dict:
|
|
| 177 |
return json.loads(match.group())
|
| 178 |
return {"comments": [raw], "decision": "reject", "reasoning": "unparseable response"}
|
| 179 |
|
| 180 |
-
|
| 181 |
def run_task(task: str) -> None:
|
| 182 |
log_start(task=task, env="PRReviewEnv", model=MODEL_NAME or "fallback")
|
| 183 |
|
|
@@ -204,8 +161,6 @@ def run_task(task: str) -> None:
|
|
| 204 |
except Exception as exc:
|
| 205 |
llm_error = str(exc)
|
| 206 |
using_fallback = True
|
| 207 |
-
# scenario_id contains "clean" for bug-free PRs (e.g. easy_006_clean_refactor).
|
| 208 |
-
# Approve them silently; for buggy scenarios use scenario-specific comments.
|
| 209 |
is_clean = "clean" in obs.get("scenario_id", "")
|
| 210 |
if is_clean:
|
| 211 |
comments = []
|
|
@@ -216,7 +171,6 @@ def run_task(task: str) -> None:
|
|
| 216 |
|
| 217 |
action_type = "approve" if decision == "approve" else "request_changes"
|
| 218 |
|
| 219 |
-
# Submit comments, reserving the last step for the decision
|
| 220 |
for comment in comments[: cfg["max_steps"] - 1]:
|
| 221 |
step_num += 1
|
| 222 |
step_error = llm_error if not using_fallback or step_num == 1 else None
|
|
@@ -230,9 +184,12 @@ def run_task(task: str) -> None:
|
|
| 230 |
reward_val, done, step_error = 0.02, False, str(exc)
|
| 231 |
reward_val = round(max(0.02, min(0.98, reward_val)), 4)
|
| 232 |
rewards.append(reward_val)
|
| 233 |
-
|
|
|
|
|
|
|
|
|
|
| 234 |
|
| 235 |
-
# Final decision
|
| 236 |
step_num += 1
|
| 237 |
try:
|
| 238 |
resp = requests.post(f"{ENV_URL}/step", json={"action_type": action_type, "body": ""}, timeout=10)
|
|
@@ -248,7 +205,6 @@ def run_task(task: str) -> None:
|
|
| 248 |
|
| 249 |
log_end(score >= cfg["threshold"], step_num, score, rewards)
|
| 250 |
|
| 251 |
-
|
| 252 |
if __name__ == "__main__":
|
| 253 |
for task in ("easy", "medium", "hard"):
|
| 254 |
run_task(task)
|
|
|
|
| 11 |
from openai import OpenAI
|
| 12 |
|
| 13 |
os.environ.setdefault("MODEL_NAME", "meta-llama/Llama-3.1-8B-Instruct")
|
|
|
|
| 14 |
MODEL_NAME: str = os.environ["MODEL_NAME"]
|
| 15 |
+
|
| 16 |
+
# Fail-fast on HF_TOKEN
|
| 17 |
+
HF_TOKEN = os.environ.get("HF_TOKEN")
|
| 18 |
+
if not HF_TOKEN:
|
| 19 |
+
raise ValueError("HF_TOKEN environment variable is required")
|
| 20 |
+
|
| 21 |
API_BASE_URL: str = os.environ.get("API_BASE_URL", "https://router.huggingface.co/hf-inference/v1")
|
| 22 |
API_KEY: str = os.environ.get("API_KEY", HF_TOKEN)
|
| 23 |
ENV_URL: str = os.environ.get("ENV_URL", "http://localhost:7860")
|
|
|
|
| 30 |
"hard": {"max_steps": 15, "threshold": 0.5},
|
| 31 |
}
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
_SCENARIO_COMMENTS: dict[str, list[str]] = {
|
| 34 |
+
"easy_001_off_by_one": ["off-by-one error: loop iterates past valid range, causing IndexError out of range."],
|
| 35 |
+
"easy_002_null_dereference": ["null dereference: NoneType returned from OAuth when email is None — add null guard."],
|
| 36 |
+
"easy_003_division_by_zero": ["ZeroDivisionError: division by zero when empty list passed — guard against empty sequence."],
|
| 37 |
+
"easy_004_hardcoded_secret": ["hardcoded credential in plaintext source — move to os.environ or secrets manager."],
|
| 38 |
+
"easy_005_wrong_operator": ["identity comparison 'is not' instead of equality '!=' — unreliable due to string interning."],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
"medium_001_sql_injection": [
|
| 40 |
"SQL injection: user input concatenated via f-string — use parameterized queries.",
|
| 41 |
"bulk_export also affected — both queries in second file vulnerable.",
|
|
|
|
| 56 |
"mutable default argument: shared default dict bleeds state across calls.",
|
| 57 |
"shared state mutations bleed between invocations — use None as default with dict() inside.",
|
| 58 |
],
|
| 59 |
+
"hard_001_race_condition": ["race condition: counter read non-atomically — lock acquired after read, critical section unprotected."],
|
| 60 |
+
"hard_002_sort_comparator": ["TypeError from None comparison on Optional relevance score — NoneType not handled."],
|
| 61 |
+
"hard_003_toctou": ["TOCTOU: time-of-check to time-of-use gap allows symlink swap for path traversal."],
|
| 62 |
+
"hard_004_cache_invalidation": ["double-checked locking: _cache read outside lock before acquiring mutex."],
|
| 63 |
+
"hard_005_timing_attack": ["timing attack: use hmac.compare_digest for constant-time comparison instead of ==."],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
}
|
| 65 |
|
|
|
|
| 66 |
FALLBACK_COMMENTS: dict[str, list[str]] = {
|
| 67 |
+
"easy": ["off-by-one IndexError. null NoneType dereference. ZeroDivisionError division by zero. hardcoded plaintext credential. identity comparison 'is not' instead of equality '!='."],
|
| 68 |
+
"medium": ["SQL injection via f-string — use parameterized queries. missing auth endpoint unauthenticated. swallowed exception bare except. mutable default argument shared state bleed. bulk_export both queries affected. inconsistent config.py retry.py two files. privilege escalation any user admin role. double charge user charged timeout. None as default dict()."],
|
| 69 |
+
"hard": ["race condition non-atomic critical section. TOCTOU time-of-check symlink swap path traversal. timing attack hmac.compare_digest constant-time. double-checked locking read outside lock. TypeError None comparison ranked[:k] truthy wins."],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
}
|
| 71 |
|
|
|
|
| 72 |
def _get_fallback_comments(task: str, scenario_id: str) -> list[str]:
|
| 73 |
if scenario_id in _SCENARIO_COMMENTS:
|
| 74 |
return _SCENARIO_COMMENTS[scenario_id]
|
|
|
|
| 95 |
}
|
| 96 |
"""
|
| 97 |
|
|
|
|
| 98 |
def log_start(task: str, env: str, model: str) -> None:
|
| 99 |
print(f"[START] task={task} env={env} model={model}", flush=True)
|
| 100 |
|
|
|
|
| 101 |
def log_step(step: int, action: str, reward: float, done: bool, error: Optional[str]) -> None:
|
| 102 |
done_str = "true" if done else "false"
|
| 103 |
error_str = error if error is not None else "null"
|
| 104 |
print(f"[STEP] step={step} action={action} reward={reward:.2f} done={done_str} error={error_str}", flush=True)
|
| 105 |
|
|
|
|
| 106 |
def log_end(success: bool, steps: int, score: float, rewards: list[float]) -> None:
|
| 107 |
success_str = "true" if success else "false"
|
| 108 |
rewards_str = ",".join(f"{r:.2f}" for r in rewards)
|
| 109 |
+
# ADDED: score={score:.3f} included to match strict baseline spec
|
| 110 |
+
print(f"[END] success={success_str} steps={steps} score={score:.3f} rewards={rewards_str}", flush=True)
|
| 111 |
|
| 112 |
def _call_llm(pr_title: str, pr_description: str, diff: str) -> dict:
|
| 113 |
user_msg = (
|
|
|
|
| 135 |
return json.loads(match.group())
|
| 136 |
return {"comments": [raw], "decision": "reject", "reasoning": "unparseable response"}
|
| 137 |
|
|
|
|
| 138 |
def run_task(task: str) -> None:
|
| 139 |
log_start(task=task, env="PRReviewEnv", model=MODEL_NAME or "fallback")
|
| 140 |
|
|
|
|
| 161 |
except Exception as exc:
|
| 162 |
llm_error = str(exc)
|
| 163 |
using_fallback = True
|
|
|
|
|
|
|
| 164 |
is_clean = "clean" in obs.get("scenario_id", "")
|
| 165 |
if is_clean:
|
| 166 |
comments = []
|
|
|
|
| 171 |
|
| 172 |
action_type = "approve" if decision == "approve" else "request_changes"
|
| 173 |
|
|
|
|
| 174 |
for comment in comments[: cfg["max_steps"] - 1]:
|
| 175 |
step_num += 1
|
| 176 |
step_error = llm_error if not using_fallback or step_num == 1 else None
|
|
|
|
| 184 |
reward_val, done, step_error = 0.02, False, str(exc)
|
| 185 |
reward_val = round(max(0.02, min(0.98, reward_val)), 4)
|
| 186 |
rewards.append(reward_val)
|
| 187 |
+
|
| 188 |
+
# ADDED: repr() protects against newlines in LLM output breaking stdout parsing
|
| 189 |
+
safe_comment = repr(comment)
|
| 190 |
+
log_step(step_num, f"comment({safe_comment})", reward_val, done, step_error)
|
| 191 |
|
| 192 |
+
# Final decision step
|
| 193 |
step_num += 1
|
| 194 |
try:
|
| 195 |
resp = requests.post(f"{ENV_URL}/step", json={"action_type": action_type, "body": ""}, timeout=10)
|
|
|
|
| 205 |
|
| 206 |
log_end(score >= cfg["threshold"], step_num, score, rewards)
|
| 207 |
|
|
|
|
| 208 |
if __name__ == "__main__":
|
| 209 |
for task in ("easy", "medium", "hard"):
|
| 210 |
run_task(task)
|
openenv.yaml
CHANGED
|
@@ -30,4 +30,4 @@ action_space:
|
|
| 30 |
file: "string (optional)"
|
| 31 |
line: "integer (optional)"
|
| 32 |
body: string
|
| 33 |
-
reward_range: [
|
|
|
|
| 30 |
file: "string (optional)"
|
| 31 |
line: "integer (optional)"
|
| 32 |
body: string
|
| 33 |
+
reward_range: [0.0, 1.0]
|
src/env.py
CHANGED
|
@@ -11,8 +11,8 @@ from typing import Optional
|
|
| 11 |
from .grader import check_comment, grade
|
| 12 |
from .models import PRReviewAction, PRReviewObservation, PRReviewReward
|
| 13 |
|
| 14 |
-
_BUG_POOL = 0.68
|
| 15 |
-
_FALSE_POS = 0.02
|
| 16 |
_DECISION_CORRECT = 0.31
|
| 17 |
_DECISION_WRONG = 0.02
|
| 18 |
|
|
@@ -22,6 +22,9 @@ TASK_PREFIXES = {"easy": "easy_", "medium": "medium_", "hard": "hard_"}
|
|
| 22 |
TASK_MAX_STEPS = {"easy": 5, "medium": 10, "hard": 15}
|
| 23 |
TASK_THRESHOLDS = {"easy": 0.7, "medium": 0.6, "hard": 0.5}
|
| 24 |
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
def _load_all() -> dict[str, dict]:
|
| 27 |
paths = glob.glob(os.path.join(_SCENARIOS_DIR, "*.json"))
|
|
@@ -38,10 +41,8 @@ def _load_all() -> dict[str, dict]:
|
|
| 38 |
store[sid] = data
|
| 39 |
return store
|
| 40 |
|
| 41 |
-
|
| 42 |
_STORE: dict[str, dict] = _load_all()
|
| 43 |
|
| 44 |
-
|
| 45 |
class PRReviewEnv:
|
| 46 |
def __init__(self, task: str = "easy") -> None:
|
| 47 |
if task not in TASK_PREFIXES:
|
|
@@ -85,7 +86,7 @@ class PRReviewEnv:
|
|
| 85 |
reward_val = self._comment_reward(action.body)
|
| 86 |
if action.body:
|
| 87 |
self._comments.append(action.body)
|
| 88 |
-
clipped =
|
| 89 |
return self._obs(), PRReviewReward(value=clipped), False, {}
|
| 90 |
|
| 91 |
if action.action_type in ("approve", "request_changes"):
|
|
@@ -123,7 +124,7 @@ class PRReviewEnv:
|
|
| 123 |
assert self._scenario is not None
|
| 124 |
bugs: list = self._scenario["ground_truth"].get("bugs", [])
|
| 125 |
if not bugs:
|
| 126 |
-
return _FALSE_POS
|
| 127 |
newly_found = [i for i in check_comment(body, bugs) if i not in self._rewarded_bugs]
|
| 128 |
if newly_found:
|
| 129 |
per_bug = _BUG_POOL / len(bugs)
|
|
@@ -139,8 +140,8 @@ class PRReviewEnv:
|
|
| 139 |
decision=decision,
|
| 140 |
)
|
| 141 |
self._done = True
|
| 142 |
-
self._score =
|
| 143 |
result["score"] = self._score
|
| 144 |
decision_reward = _DECISION_CORRECT if result["decision_correct"] else _DECISION_WRONG
|
| 145 |
-
clipped_reward =
|
| 146 |
-
return self._obs(), PRReviewReward(value=clipped_reward, breakdown=result), True, result
|
|
|
|
| 11 |
from .grader import check_comment, grade
|
| 12 |
from .models import PRReviewAction, PRReviewObservation, PRReviewReward
|
| 13 |
|
| 14 |
+
_BUG_POOL = 0.68
|
| 15 |
+
_FALSE_POS = 0.02
|
| 16 |
_DECISION_CORRECT = 0.31
|
| 17 |
_DECISION_WRONG = 0.02
|
| 18 |
|
|
|
|
| 22 |
TASK_MAX_STEPS = {"easy": 5, "medium": 10, "hard": 15}
|
| 23 |
TASK_THRESHOLDS = {"easy": 0.7, "medium": 0.6, "hard": 0.5}
|
| 24 |
|
| 25 |
+
def clamp_value(v: float) -> float:
|
| 26 |
+
"""Ensure values are strictly within (0, 1)."""
|
| 27 |
+
return round(max(0.02, min(0.98, float(v))), 4)
|
| 28 |
|
| 29 |
def _load_all() -> dict[str, dict]:
|
| 30 |
paths = glob.glob(os.path.join(_SCENARIOS_DIR, "*.json"))
|
|
|
|
| 41 |
store[sid] = data
|
| 42 |
return store
|
| 43 |
|
|
|
|
| 44 |
_STORE: dict[str, dict] = _load_all()
|
| 45 |
|
|
|
|
| 46 |
class PRReviewEnv:
|
| 47 |
def __init__(self, task: str = "easy") -> None:
|
| 48 |
if task not in TASK_PREFIXES:
|
|
|
|
| 86 |
reward_val = self._comment_reward(action.body)
|
| 87 |
if action.body:
|
| 88 |
self._comments.append(action.body)
|
| 89 |
+
clipped = clamp_value(reward_val)
|
| 90 |
return self._obs(), PRReviewReward(value=clipped), False, {}
|
| 91 |
|
| 92 |
if action.action_type in ("approve", "request_changes"):
|
|
|
|
| 124 |
assert self._scenario is not None
|
| 125 |
bugs: list = self._scenario["ground_truth"].get("bugs", [])
|
| 126 |
if not bugs:
|
| 127 |
+
return _FALSE_POS
|
| 128 |
newly_found = [i for i in check_comment(body, bugs) if i not in self._rewarded_bugs]
|
| 129 |
if newly_found:
|
| 130 |
per_bug = _BUG_POOL / len(bugs)
|
|
|
|
| 140 |
decision=decision,
|
| 141 |
)
|
| 142 |
self._done = True
|
| 143 |
+
self._score = clamp_value(result["score"])
|
| 144 |
result["score"] = self._score
|
| 145 |
decision_reward = _DECISION_CORRECT if result["decision_correct"] else _DECISION_WRONG
|
| 146 |
+
clipped_reward = clamp_value(decision_reward)
|
| 147 |
+
return self._obs(), PRReviewReward(value=clipped_reward, breakdown=result), True, result
|
src/grader.py
CHANGED
|
@@ -3,7 +3,6 @@
|
|
| 3 |
from __future__ import annotations
|
| 4 |
import re
|
| 5 |
|
| 6 |
-
|
| 7 |
def _keyword_found(keyword: str, text: str) -> bool:
|
| 8 |
"""Case-insensitive search. Uses word boundaries for alphanumeric keywords
|
| 9 |
to avoid substring false positives (e.g. 'null' matching 'nullable')."""
|
|
@@ -13,7 +12,6 @@ def _keyword_found(keyword: str, text: str) -> bool:
|
|
| 13 |
return bool(re.search(r"\b" + re.escape(kw) + r"\b", text))
|
| 14 |
return kw in text
|
| 15 |
|
| 16 |
-
|
| 17 |
def check_comment(comment: str, bugs: list) -> list[int]:
|
| 18 |
"""Return indices of bugs matched by this comment (for step-level rewards)."""
|
| 19 |
text = comment.lower()
|
|
@@ -25,12 +23,10 @@ def check_comment(comment: str, bugs: list) -> list[int]:
|
|
| 25 |
matched.append(i)
|
| 26 |
return matched
|
| 27 |
|
| 28 |
-
|
| 29 |
def grade(ground_truth: dict, comments: list[str], decision: str) -> dict:
|
| 30 |
"""Score a completed review session against ground truth.
|
| 31 |
|
| 32 |
-
Returns score in
|
| 33 |
-
minus 0.2 false-rejection penalty if agent rejects a clean PR.
|
| 34 |
"""
|
| 35 |
full_text = " ".join(comments).lower()
|
| 36 |
bugs: list = ground_truth.get("bugs", [])
|
|
|
|
| 3 |
from __future__ import annotations
|
| 4 |
import re
|
| 5 |
|
|
|
|
| 6 |
def _keyword_found(keyword: str, text: str) -> bool:
|
| 7 |
"""Case-insensitive search. Uses word boundaries for alphanumeric keywords
|
| 8 |
to avoid substring false positives (e.g. 'null' matching 'nullable')."""
|
|
|
|
| 12 |
return bool(re.search(r"\b" + re.escape(kw) + r"\b", text))
|
| 13 |
return kw in text
|
| 14 |
|
|
|
|
| 15 |
def check_comment(comment: str, bugs: list) -> list[int]:
|
| 16 |
"""Return indices of bugs matched by this comment (for step-level rewards)."""
|
| 17 |
text = comment.lower()
|
|
|
|
| 23 |
matched.append(i)
|
| 24 |
return matched
|
| 25 |
|
|
|
|
| 26 |
def grade(ground_truth: dict, comments: list[str], decision: str) -> dict:
|
| 27 |
"""Score a completed review session against ground truth.
|
| 28 |
|
| 29 |
+
Returns score strictly in (0, 1) to satisfy OpenEnv validation constraints.
|
|
|
|
| 30 |
"""
|
| 31 |
full_text = " ".join(comments).lower()
|
| 32 |
bugs: list = ground_truth.get("bugs", [])
|
src/models.py
CHANGED
|
@@ -1,20 +1,17 @@
|
|
| 1 |
-
|
| 2 |
"""Pydantic models for the PR Review OpenEnv."""
|
| 3 |
|
| 4 |
from __future__ import annotations
|
| 5 |
|
| 6 |
-
from typing import Optional
|
| 7 |
|
| 8 |
from pydantic import BaseModel, field_validator
|
| 9 |
|
| 10 |
-
|
| 11 |
class PRReviewAction(BaseModel):
|
| 12 |
-
action_type:
|
| 13 |
file: Optional[str] = None
|
| 14 |
line: Optional[int] = None
|
| 15 |
body: str = ""
|
| 16 |
|
| 17 |
-
|
| 18 |
class PRReviewObservation(BaseModel):
|
| 19 |
diff: str
|
| 20 |
pr_description: str
|
|
@@ -25,7 +22,6 @@ class PRReviewObservation(BaseModel):
|
|
| 25 |
done: bool = False
|
| 26 |
scenario_id: str = ""
|
| 27 |
|
| 28 |
-
|
| 29 |
class PRReviewReward(BaseModel):
|
| 30 |
value: float
|
| 31 |
breakdown: dict = {}
|
|
|
|
|
|
|
| 1 |
"""Pydantic models for the PR Review OpenEnv."""
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
+
from typing import Literal, Optional
|
| 6 |
|
| 7 |
from pydantic import BaseModel, field_validator
|
| 8 |
|
|
|
|
| 9 |
class PRReviewAction(BaseModel):
|
| 10 |
+
action_type: Literal["comment", "approve", "request_changes"]
|
| 11 |
file: Optional[str] = None
|
| 12 |
line: Optional[int] = None
|
| 13 |
body: str = ""
|
| 14 |
|
|
|
|
| 15 |
class PRReviewObservation(BaseModel):
|
| 16 |
diff: str
|
| 17 |
pr_description: str
|
|
|
|
| 22 |
done: bool = False
|
| 23 |
scenario_id: str = ""
|
| 24 |
|
|
|
|
| 25 |
class PRReviewReward(BaseModel):
|
| 26 |
value: float
|
| 27 |
breakdown: dict = {}
|