Rushhaabhhh commited on
Commit
7e9c2fa
·
verified ·
1 Parent(s): 56ac1f3

Fixed range values and formatting

Browse files
Files changed (5) hide show
  1. inference.py +26 -70
  2. openenv.yaml +1 -1
  3. src/env.py +10 -9
  4. src/grader.py +1 -5
  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
- HF_TOKEN: str = os.environ["HF_TOKEN"]
 
 
 
 
 
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
- # ---- Easy (1 comment each, matches bug[0]) ----
37
- "easy_001_off_by_one": [
38
- "off-by-one error: loop iterates past valid range, causing IndexError out of range.",
39
- ],
40
- "easy_002_null_dereference": [
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
- # ---- Hard (1 comment each, matches bug[0] only harder to fully detect) ----
74
- "hard_001_race_condition": [
75
- "race condition: counter read non-atomically lock acquired after read, critical section unprotected.",
76
- ],
77
- "hard_002_sort_comparator": [
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
- "off-by-one IndexError. null NoneType dereference. ZeroDivisionError division by zero. "
95
- "hardcoded plaintext credential. identity comparison 'is not' instead of equality '!='.",
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
- print(f"[END] success={success_str} steps={steps} rewards={rewards_str}", flush=True)
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
- log_step(step_num, comment, reward_val, done, step_error)
 
 
 
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: [-0.5, 1.0]
 
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 # reward split evenly across all bugs in a scenario
15
- _FALSE_POS = 0.02 # small reward for comments on clean/duplicate (avoids 0.0)
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 = round(max(0.02, min(0.98, reward_val)), 4)
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 # any comment on a clean PR is hallucination
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 = round(max(0.02, min(0.98, result["score"])), 4)
143
  result["score"] = self._score
144
  decision_reward = _DECISION_CORRECT if result["decision_correct"] else _DECISION_WRONG
145
- clipped_reward = round(max(0.02, min(0.98, decision_reward)), 4)
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 [0, 1] = bug_detection * 0.7 + decision * 0.3,
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: str # "comment" | "approve" | "request_changes"
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 = {}