Merge pull request #25 from ArshVermaGit/main
Browse filesfeat: complete implementation of CodeReview Environment, Models, and Scenarios
- app.py +2 -2
- codereview_env/env.py +134 -135
- codereview_env/graders/arch_grader.py +53 -31
- codereview_env/graders/bug_grader.py +59 -18
- codereview_env/graders/security_grader.py +48 -29
- openenv.yaml +6 -18
app.py
CHANGED
|
@@ -119,8 +119,8 @@ def submit_to_leaderboard(submission: SubmitScore):
|
|
| 119 |
entries.append(new_entry)
|
| 120 |
entries.sort(key=lambda x: x["score"], reverse=True)
|
| 121 |
rank = entries.index(new_entry) + 1 # capture rank before slicing
|
| 122 |
-
leaderboard[submission.task_id] = entries[:
|
| 123 |
-
return {"status": "submitted", "rank": rank if rank <=
|
| 124 |
|
| 125 |
|
| 126 |
@app.websocket("/ws/events")
|
|
|
|
| 119 |
entries.append(new_entry)
|
| 120 |
entries.sort(key=lambda x: x["score"], reverse=True)
|
| 121 |
rank = entries.index(new_entry) + 1 # capture rank before slicing
|
| 122 |
+
leaderboard[submission.task_id] = entries[:5]
|
| 123 |
+
return {"status": "submitted", "rank": rank if rank <= 5 else None}
|
| 124 |
|
| 125 |
|
| 126 |
@app.websocket("/ws/events")
|
codereview_env/env.py
CHANGED
|
@@ -1,178 +1,177 @@
|
|
| 1 |
from datetime import datetime, timezone
|
|
|
|
| 2 |
from codereview_env.models import (
|
| 3 |
TaskId, Action, Observation, StepResult, ResetResult,
|
| 4 |
-
ActionType, ActionRecord, EpisodeResult,
|
| 5 |
)
|
| 6 |
from codereview_env.scenarios import get_scenario
|
| 7 |
-
from codereview_env.graders.grader_utils import find_best_match
|
| 8 |
from codereview_env.graders.bug_grader import grade_bug_detection
|
| 9 |
from codereview_env.graders.security_grader import grade_security_audit
|
| 10 |
from codereview_env.graders.arch_grader import grade_architectural_review
|
| 11 |
|
| 12 |
-
|
| 13 |
class CodeReviewEnv:
|
|
|
|
| 14 |
TASK_MAX_STEPS = {
|
| 15 |
-
TaskId.BUG_DETECTION:
|
| 16 |
-
TaskId.SECURITY_AUDIT:
|
| 17 |
TaskId.ARCHITECTURAL_REVIEW: 20,
|
| 18 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
def __init__(self):
|
| 21 |
-
self.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
def reset(self, task_id: TaskId, seed: int = 42) -> ResetResult:
|
| 24 |
-
scenario = get_scenario(task_id, seed)
|
| 25 |
-
self.
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
"issues_found": set(), # set of matched ground-truth issue IDs
|
| 36 |
-
"false_positives": [] # list of FP action bodies
|
| 37 |
-
}
|
| 38 |
return ResetResult(
|
| 39 |
-
observation=self._build_obs(),
|
| 40 |
task_id=task_id,
|
| 41 |
seed=seed,
|
| 42 |
-
scenario_hash=scenario.hash
|
|
|
|
| 43 |
)
|
| 44 |
|
| 45 |
def step(self, action: Action) -> StepResult:
|
| 46 |
-
if self.
|
| 47 |
-
raise
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
record = ActionRecord(
|
| 54 |
action_type=action.action_type,
|
| 55 |
body=action.body,
|
| 56 |
filename=action.filename,
|
| 57 |
line_number=action.line_number,
|
| 58 |
-
severity=action.severity,
|
| 59 |
category=action.category,
|
|
|
|
| 60 |
verdict=action.verdict,
|
| 61 |
-
reward=
|
| 62 |
timestamp=datetime.now(timezone.utc).isoformat()
|
| 63 |
)
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
# Apply action logic and compute incremental reward delta
|
| 67 |
-
prev_score = s["running_score"]
|
| 68 |
-
reward_delta = self._apply_action(action)
|
| 69 |
-
s["running_score"] = prev_score + reward_delta
|
| 70 |
-
|
| 71 |
-
# Update the history record with the actual reward
|
| 72 |
-
record.reward = round(reward_delta, 4)
|
| 73 |
-
|
| 74 |
-
# Check termination
|
| 75 |
-
s["done"] = (
|
| 76 |
-
action.action_type in (ActionType.APPROVE, ActionType.REQUEST_CHANGES)
|
| 77 |
-
or s["step_count"] >= s["max_steps"]
|
| 78 |
-
or s["noise_budget"] <= 0
|
| 79 |
-
)
|
| 80 |
|
| 81 |
return StepResult(
|
| 82 |
-
observation=self.
|
| 83 |
-
reward=
|
| 84 |
-
done=
|
| 85 |
-
info={
|
| 86 |
-
"step": s["step_count"],
|
| 87 |
-
"cumulative_score": round(s["running_score"], 4),
|
| 88 |
-
"noise_budget": s["noise_budget"],
|
| 89 |
-
"issues_found_count": len(s["issues_found"]),
|
| 90 |
-
}
|
| 91 |
)
|
| 92 |
|
| 93 |
-
def
|
| 94 |
-
|
| 95 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
return Observation(
|
| 97 |
-
task_id=
|
| 98 |
-
scenario_hash=
|
| 99 |
-
pr_title=
|
| 100 |
-
pr_description=
|
| 101 |
-
diff=
|
| 102 |
-
files_changed=
|
| 103 |
-
step_count=
|
| 104 |
-
max_steps=
|
| 105 |
-
noise_budget=
|
| 106 |
-
max_noise_budget=
|
| 107 |
-
issues_flagged=len(
|
| 108 |
-
done=
|
| 109 |
)
|
| 110 |
|
| 111 |
-
def _apply_action(self, action: Action) -> float:
|
| 112 |
-
"""
|
| 113 |
-
Compute the incremental reward delta for this single action.
|
| 114 |
-
"""
|
| 115 |
-
s = self._state
|
| 116 |
-
sc = s["scenario"]
|
| 117 |
-
|
| 118 |
-
if action.action_type == ActionType.FLAG_ISSUE:
|
| 119 |
-
matched_gt = find_best_match(action, sc.ground_truth_issues, s["issues_found"])
|
| 120 |
-
if matched_gt:
|
| 121 |
-
s["issues_found"].add(matched_gt.id)
|
| 122 |
-
# Recalculate full grader score and return delta
|
| 123 |
-
new_score = self._grade(sc, s)
|
| 124 |
-
reward_delta = new_score - s["running_score"]
|
| 125 |
-
return max(0.0, reward_delta) # finding a real issue is always non-negative
|
| 126 |
-
else:
|
| 127 |
-
# False positive: consume noise budget and penalize
|
| 128 |
-
s["noise_budget"] -= 1
|
| 129 |
-
s["false_positives"].append(action.body)
|
| 130 |
-
return -0.05
|
| 131 |
-
|
| 132 |
-
elif action.action_type in (ActionType.APPROVE, ActionType.REQUEST_CHANGES):
|
| 133 |
-
# Terminal: compute full final score delta
|
| 134 |
-
new_score = self._grade(sc, s)
|
| 135 |
-
reward_delta = new_score - s["running_score"]
|
| 136 |
-
return reward_delta
|
| 137 |
-
|
| 138 |
-
# comment / ask_question — no reward signal
|
| 139 |
-
return 0.0
|
| 140 |
-
|
| 141 |
-
def _grade(self, sc, s) -> float:
|
| 142 |
-
"""Route to the right grader based on task_id."""
|
| 143 |
-
if s["task_id"] == TaskId.BUG_DETECTION:
|
| 144 |
-
return grade_bug_detection(sc, s["history"])
|
| 145 |
-
elif s["task_id"] == TaskId.SECURITY_AUDIT:
|
| 146 |
-
return grade_security_audit(sc, s["history"])
|
| 147 |
-
else:
|
| 148 |
-
return grade_architectural_review(sc, s["history"])
|
| 149 |
-
|
| 150 |
def get_final_result(self) -> EpisodeResult:
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
terminated_reason = ""
|
| 159 |
-
if s["done"]:
|
| 160 |
-
if s["noise_budget"] <= 0:
|
| 161 |
-
terminated_reason = "noise_exhausted"
|
| 162 |
-
elif s["history"] and s["history"][-1].action_type in (ActionType.APPROVE, ActionType.REQUEST_CHANGES):
|
| 163 |
-
terminated_reason = "terminal_action"
|
| 164 |
-
else:
|
| 165 |
-
terminated_reason = "max_steps"
|
| 166 |
-
|
| 167 |
return EpisodeResult(
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
|
|
|
| 171 |
final_score=round(final_score, 4),
|
| 172 |
-
steps_taken=
|
| 173 |
-
issues_found=len(
|
| 174 |
-
issues_total=len(
|
| 175 |
-
noise_penalties=
|
| 176 |
-
history=
|
| 177 |
-
terminated_reason=terminated_reason
|
| 178 |
)
|
|
|
|
|
|
| 1 |
from datetime import datetime, timezone
|
| 2 |
+
from typing import List, Optional, Set
|
| 3 |
from codereview_env.models import (
|
| 4 |
TaskId, Action, Observation, StepResult, ResetResult,
|
| 5 |
+
ActionType, ActionRecord, EpisodeResult, Severity, GroundTruthIssue
|
| 6 |
)
|
| 7 |
from codereview_env.scenarios import get_scenario
|
|
|
|
| 8 |
from codereview_env.graders.bug_grader import grade_bug_detection
|
| 9 |
from codereview_env.graders.security_grader import grade_security_audit
|
| 10 |
from codereview_env.graders.arch_grader import grade_architectural_review
|
| 11 |
|
|
|
|
| 12 |
class CodeReviewEnv:
|
| 13 |
+
MAX_NOISE_BUDGET = 5
|
| 14 |
TASK_MAX_STEPS = {
|
| 15 |
+
TaskId.BUG_DETECTION: 10,
|
| 16 |
+
TaskId.SECURITY_AUDIT: 15,
|
| 17 |
TaskId.ARCHITECTURAL_REVIEW: 20,
|
| 18 |
}
|
| 19 |
+
SEVERITY_WEIGHTS = {
|
| 20 |
+
Severity.CRITICAL: 1.0,
|
| 21 |
+
Severity.HIGH: 0.8,
|
| 22 |
+
Severity.MEDIUM: 0.5,
|
| 23 |
+
Severity.LOW: 0.2,
|
| 24 |
+
Severity.INFO: 0.0,
|
| 25 |
+
}
|
| 26 |
|
| 27 |
def __init__(self):
|
| 28 |
+
self.task_id: Optional[TaskId] = None
|
| 29 |
+
self.seed: int = 42
|
| 30 |
+
self.scenario = None
|
| 31 |
+
self.step_count: int = 0
|
| 32 |
+
self.noise_budget: int = self.MAX_NOISE_BUDGET
|
| 33 |
+
self.history: List[ActionRecord] = []
|
| 34 |
+
self.matched_issue_ids: Set[str] = set()
|
| 35 |
+
self.done: bool = False
|
| 36 |
+
self.terminated_reason: str = ""
|
| 37 |
+
self.episode_id: str = ""
|
| 38 |
|
| 39 |
def reset(self, task_id: TaskId, seed: int = 42) -> ResetResult:
|
| 40 |
+
self.scenario = get_scenario(task_id, seed)
|
| 41 |
+
self.task_id = task_id
|
| 42 |
+
self.seed = seed
|
| 43 |
+
self.step_count = 0
|
| 44 |
+
self.noise_budget = self.MAX_NOISE_BUDGET
|
| 45 |
+
self.history = []
|
| 46 |
+
self.matched_issue_ids = set()
|
| 47 |
+
self.done = False
|
| 48 |
+
self.terminated_reason = ""
|
| 49 |
+
|
| 50 |
+
obs = self._build_observation()
|
|
|
|
|
|
|
|
|
|
| 51 |
return ResetResult(
|
|
|
|
| 52 |
task_id=task_id,
|
| 53 |
seed=seed,
|
| 54 |
+
scenario_hash=self.scenario.hash,
|
| 55 |
+
observation=obs
|
| 56 |
)
|
| 57 |
|
| 58 |
def step(self, action: Action) -> StepResult:
|
| 59 |
+
if self.done:
|
| 60 |
+
raise ValueError("Episode is already finished")
|
| 61 |
+
|
| 62 |
+
self.step_count += 1
|
| 63 |
+
reward = 0.0
|
| 64 |
+
|
| 65 |
+
# Determine terminal state and reward
|
| 66 |
+
if action.action_type in (ActionType.APPROVE, ActionType.REQUEST_CHANGES):
|
| 67 |
+
self.done = True
|
| 68 |
+
self.terminated_reason = "terminal_action"
|
| 69 |
+
reward = 0.0 # Grader handles final score
|
| 70 |
+
|
| 71 |
+
elif action.action_type == ActionType.FLAG_ISSUE:
|
| 72 |
+
match = None
|
| 73 |
+
for issue in self.scenario.ground_truth_issues:
|
| 74 |
+
if self._is_match(action, issue):
|
| 75 |
+
match = issue
|
| 76 |
+
break
|
| 77 |
+
|
| 78 |
+
if match:
|
| 79 |
+
if match.id not in self.matched_issue_ids:
|
| 80 |
+
reward = self.SEVERITY_WEIGHTS.get(match.severity, 0.0)
|
| 81 |
+
self.matched_issue_ids.add(match.id)
|
| 82 |
+
else:
|
| 83 |
+
# Already matched: penalty
|
| 84 |
+
reward = -0.05
|
| 85 |
+
self.noise_budget -= 1
|
| 86 |
+
if self.noise_budget <= 0:
|
| 87 |
+
self.done = True
|
| 88 |
+
self.terminated_reason = "noise_exhausted"
|
| 89 |
+
else:
|
| 90 |
+
# False positive: penalty
|
| 91 |
+
reward = -0.05
|
| 92 |
+
self.noise_budget -= 1
|
| 93 |
+
if self.noise_budget <= 0:
|
| 94 |
+
self.done = True
|
| 95 |
+
self.terminated_reason = "noise_exhausted"
|
| 96 |
+
|
| 97 |
+
# Max steps check
|
| 98 |
+
max_steps = self.TASK_MAX_STEPS.get(self.task_id, 10)
|
| 99 |
+
if not self.done and self.step_count >= max_steps:
|
| 100 |
+
self.done = True
|
| 101 |
+
self.terminated_reason = "max_steps"
|
| 102 |
+
|
| 103 |
+
# Record action
|
| 104 |
record = ActionRecord(
|
| 105 |
action_type=action.action_type,
|
| 106 |
body=action.body,
|
| 107 |
filename=action.filename,
|
| 108 |
line_number=action.line_number,
|
|
|
|
| 109 |
category=action.category,
|
| 110 |
+
severity=action.severity,
|
| 111 |
verdict=action.verdict,
|
| 112 |
+
reward=float(reward),
|
| 113 |
timestamp=datetime.now(timezone.utc).isoformat()
|
| 114 |
)
|
| 115 |
+
self.history.append(record)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
|
| 117 |
return StepResult(
|
| 118 |
+
observation=self._build_observation(),
|
| 119 |
+
reward=float(reward),
|
| 120 |
+
done=self.done,
|
| 121 |
+
info={"terminated_reason": self.terminated_reason}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
)
|
| 123 |
|
| 124 |
+
def _is_match(self, action: Action, issue: GroundTruthIssue) -> bool:
|
| 125 |
+
if action.filename != issue.filename:
|
| 126 |
+
return False
|
| 127 |
+
if action.line_number is None:
|
| 128 |
+
return False
|
| 129 |
+
if abs(action.line_number - issue.line_number) > 3:
|
| 130 |
+
return False
|
| 131 |
+
if action.category != issue.category:
|
| 132 |
+
return False
|
| 133 |
+
|
| 134 |
+
body_lower = (action.body or "").lower()
|
| 135 |
+
return any(kw.lower() in body_lower for kw in issue.keywords)
|
| 136 |
+
|
| 137 |
+
def _build_observation(self) -> Observation:
|
| 138 |
+
max_steps = self.TASK_MAX_STEPS.get(self.task_id, 10)
|
| 139 |
+
diff = "\n".join(f.patch for f in self.scenario.files_changed)
|
| 140 |
+
|
| 141 |
return Observation(
|
| 142 |
+
task_id=self.task_id,
|
| 143 |
+
scenario_hash=self.scenario.hash,
|
| 144 |
+
pr_title=self.scenario.pr_title,
|
| 145 |
+
pr_description=self.scenario.pr_description,
|
| 146 |
+
diff=diff,
|
| 147 |
+
files_changed=self.scenario.files_changed,
|
| 148 |
+
step_count=self.step_count,
|
| 149 |
+
max_steps=max_steps,
|
| 150 |
+
noise_budget=self.noise_budget,
|
| 151 |
+
max_noise_budget=self.MAX_NOISE_BUDGET,
|
| 152 |
+
issues_flagged=len(self.matched_issue_ids),
|
| 153 |
+
done=self.done
|
| 154 |
)
|
| 155 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
def get_final_result(self) -> EpisodeResult:
|
| 157 |
+
if self.task_id == TaskId.BUG_DETECTION:
|
| 158 |
+
final_score = grade_bug_detection(self.scenario, self.history)
|
| 159 |
+
elif self.task_id == TaskId.SECURITY_AUDIT:
|
| 160 |
+
final_score = grade_security_audit(self.scenario, self.history)
|
| 161 |
+
else:
|
| 162 |
+
final_score = grade_architectural_review(self.scenario, self.history)
|
| 163 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
return EpisodeResult(
|
| 165 |
+
episode_id=self.episode_id,
|
| 166 |
+
task_id=self.task_id,
|
| 167 |
+
scenario_hash=self.scenario.hash,
|
| 168 |
+
seed=self.seed,
|
| 169 |
final_score=round(final_score, 4),
|
| 170 |
+
steps_taken=self.step_count,
|
| 171 |
+
issues_found=len(self.matched_issue_ids),
|
| 172 |
+
issues_total=len(self.scenario.ground_truth_issues),
|
| 173 |
+
noise_penalties=self.MAX_NOISE_BUDGET - self.noise_budget,
|
| 174 |
+
history=self.history,
|
| 175 |
+
terminated_reason=self.terminated_reason
|
| 176 |
)
|
| 177 |
+
|
codereview_env/graders/arch_grader.py
CHANGED
|
@@ -1,39 +1,61 @@
|
|
| 1 |
from typing import List
|
| 2 |
from codereview_env.models import Scenario, ActionRecord, Category, ActionType, Verdict
|
| 3 |
-
from codereview_env.graders.grader_utils import find_best_match
|
| 4 |
|
| 5 |
def grade_architectural_review(scenario: Scenario, history: List[ActionRecord]) -> float:
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
|
|
|
| 9 |
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
#
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
|
| 37 |
-
# 3. Final weighted calculation
|
| 38 |
-
score = 0.6 * issue_score + 0.2 * verdict_score + min(0.2, quality_bonus)
|
| 39 |
-
return round(min(1.0, score), 4)
|
|
|
|
| 1 |
from typing import List
|
| 2 |
from codereview_env.models import Scenario, ActionRecord, Category, ActionType, Verdict
|
|
|
|
| 3 |
|
| 4 |
def grade_architectural_review(scenario: Scenario, history: List[ActionRecord]) -> float:
|
| 5 |
+
if not history:
|
| 6 |
+
return 0.0
|
| 7 |
+
|
| 8 |
+
flag_actions = [a for a in history if a.action_type == ActionType.FLAG_ISSUE]
|
| 9 |
|
| 10 |
+
total_issues = len(scenario.ground_truth_issues)
|
| 11 |
+
if total_issues == 0:
|
| 12 |
+
return 0.0
|
| 13 |
+
|
| 14 |
+
# 1. Match issues and calculate issue_score_avg
|
| 15 |
+
matched_gt_indices = set()
|
| 16 |
+
for i, truth in enumerate(scenario.ground_truth_issues):
|
| 17 |
+
if truth.category != Category.ARCHITECTURE:
|
| 18 |
+
continue
|
| 19 |
+
|
| 20 |
+
for action in flag_actions:
|
| 21 |
+
# Match criteria: filename, line +- 5, category ARCHITECTURE, keyword match
|
| 22 |
+
if (action.filename == truth.filename and
|
| 23 |
+
action.line_number is not None and
|
| 24 |
+
abs(action.line_number - truth.line_number) <= 5 and
|
| 25 |
+
action.category == Category.ARCHITECTURE):
|
| 26 |
+
|
| 27 |
+
body_lower = (action.body or "").lower()
|
| 28 |
+
if any(kw.lower() in body_lower for kw in truth.keywords):
|
| 29 |
+
matched_gt_indices.add(i)
|
| 30 |
+
break
|
| 31 |
+
|
| 32 |
+
issue_score_avg = len(matched_gt_indices) / total_issues
|
| 33 |
|
| 34 |
+
# 2. Verdict Grading
|
| 35 |
+
terminal_action = None
|
| 36 |
+
for action in history:
|
| 37 |
+
if action.action_type in (ActionType.APPROVE, ActionType.REQUEST_CHANGES):
|
| 38 |
+
terminal_action = action
|
| 39 |
+
break # Use the first terminal action
|
| 40 |
+
|
| 41 |
+
verdict_scores = []
|
| 42 |
+
for truth in scenario.ground_truth_issues:
|
| 43 |
+
if truth.required_verdict:
|
| 44 |
+
score = 1.0 if (terminal_action and terminal_action.verdict == truth.required_verdict) else 0.0
|
| 45 |
+
verdict_scores.append(score)
|
| 46 |
+
|
| 47 |
+
verdict_avg = sum(verdict_scores) / len(verdict_scores) if verdict_scores else 0.0
|
| 48 |
|
| 49 |
+
# 3. Quality Score
|
| 50 |
+
max_body_len = 0
|
| 51 |
+
for action in flag_actions:
|
| 52 |
+
max_body_len = max(max_body_len, len(action.body or ""))
|
| 53 |
+
|
| 54 |
+
quality_score = 0.0
|
| 55 |
+
if max_body_len > 20:
|
| 56 |
+
quality_score = min(1.0, max_body_len / 200)
|
| 57 |
+
|
| 58 |
+
# 4. Final Weighted Calculation
|
| 59 |
+
final_score = 0.6 * issue_score_avg + 0.2 * verdict_avg + 0.2 * quality_score
|
| 60 |
+
return float(max(0.0, min(1.0, final_score)))
|
| 61 |
|
|
|
|
|
|
|
|
|
codereview_env/graders/bug_grader.py
CHANGED
|
@@ -1,25 +1,66 @@
|
|
| 1 |
from typing import List
|
| 2 |
-
from codereview_env.models import Scenario, ActionRecord, Category
|
| 3 |
-
from codereview_env.graders.grader_utils import find_best_match
|
| 4 |
|
| 5 |
def grade_bug_detection(scenario: Scenario, history: List[ActionRecord]) -> float:
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
|
| 15 |
-
for
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
-
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from typing import List
|
| 2 |
+
from codereview_env.models import Scenario, ActionRecord, Category, Severity, ActionType
|
|
|
|
| 3 |
|
| 4 |
def grade_bug_detection(scenario: Scenario, history: List[ActionRecord]) -> float:
|
| 5 |
+
if not history:
|
| 6 |
+
return 0.0
|
| 7 |
+
|
| 8 |
+
flag_actions = [a for a in history if a.action_type == ActionType.FLAG_ISSUE]
|
| 9 |
+
if not flag_actions:
|
| 10 |
+
return 0.0
|
| 11 |
+
|
| 12 |
+
total_issues = len(scenario.ground_truth_issues)
|
| 13 |
+
if total_issues == 0:
|
| 14 |
+
return 0.0
|
| 15 |
|
| 16 |
+
matched_gt_indices = set()
|
| 17 |
+
used_action_indices = set()
|
| 18 |
+
issue_scores = []
|
| 19 |
|
| 20 |
+
for i, truth in enumerate(scenario.ground_truth_issues):
|
| 21 |
+
if truth.category != Category.BUG:
|
| 22 |
+
continue
|
| 23 |
+
|
| 24 |
+
# Try to find a matching action for this ground truth issue
|
| 25 |
+
best_match_idx = -1
|
| 26 |
+
for j, action in enumerate(flag_actions):
|
| 27 |
+
if j in used_action_indices:
|
| 28 |
+
continue
|
| 29 |
+
|
| 30 |
+
# Match criteria: filename, line +- 3, category BUG, >= 1 keyword
|
| 31 |
+
if (action.filename == truth.filename and
|
| 32 |
+
action.line_number is not None and
|
| 33 |
+
abs(action.line_number - truth.line_number) <= 3 and
|
| 34 |
+
action.category == Category.BUG):
|
| 35 |
+
|
| 36 |
+
body_lower = (action.body or "").lower()
|
| 37 |
+
if any(kw.lower() in body_lower for kw in truth.keywords):
|
| 38 |
+
best_match_idx = j
|
| 39 |
+
break
|
| 40 |
+
|
| 41 |
+
if best_match_idx != -1:
|
| 42 |
+
action = flag_actions[best_match_idx]
|
| 43 |
+
used_action_indices.add(best_match_idx)
|
| 44 |
+
matched_gt_indices.add(i)
|
| 45 |
+
|
| 46 |
+
# Calculate issue score
|
| 47 |
+
sev_diff = abs(Severity.ordinal(truth.severity) - Severity.ordinal(action.severity))
|
| 48 |
+
sev_score = max(0.0, 1.0 - sev_diff * 0.3)
|
| 49 |
|
| 50 |
+
body_lower = (action.body or "").lower()
|
| 51 |
+
match_count = sum(1 for kw in truth.keywords if kw.lower() in body_lower)
|
| 52 |
+
kw_score = match_count / len(truth.keywords)
|
| 53 |
+
|
| 54 |
+
issue_score = 0.5 * kw_score + 0.5 * sev_score
|
| 55 |
+
issue_scores.append(issue_score)
|
| 56 |
+
|
| 57 |
+
coverage = len(matched_gt_indices) / total_issues
|
| 58 |
+
avg_issue_score = sum(issue_scores) / len(issue_scores) if issue_scores else 0.0
|
| 59 |
|
| 60 |
+
total_flags = len(flag_actions)
|
| 61 |
+
unmatched_flags = total_flags - len(used_action_indices)
|
| 62 |
+
precision_penalty = unmatched_flags / max(1, total_flags)
|
| 63 |
+
|
| 64 |
+
final_score = 0.4 * coverage + 0.6 * avg_issue_score - 0.1 * precision_penalty
|
| 65 |
+
return float(max(0.0, min(1.0, final_score)))
|
| 66 |
+
|
codereview_env/graders/security_grader.py
CHANGED
|
@@ -1,37 +1,56 @@
|
|
| 1 |
from typing import List
|
| 2 |
-
from codereview_env.models import Scenario, ActionRecord, Category, Severity
|
| 3 |
-
from codereview_env.graders.grader_utils import find_best_match, keyword_overlap
|
| 4 |
-
|
| 5 |
-
def severity_to_int(sev: Severity) -> int:
|
| 6 |
-
mapping = {
|
| 7 |
-
Severity.LOW: 1,
|
| 8 |
-
Severity.MEDIUM: 2,
|
| 9 |
-
Severity.HIGH: 3,
|
| 10 |
-
Severity.CRITICAL: 4
|
| 11 |
-
}
|
| 12 |
-
return mapping.get(sev, 0)
|
| 13 |
|
| 14 |
def grade_security_audit(scenario: Scenario, history: List[ActionRecord]) -> float:
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
| 19 |
|
| 20 |
-
|
| 21 |
-
|
| 22 |
|
| 23 |
-
for
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
-
#
|
| 31 |
-
|
|
|
|
| 32 |
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
| 35 |
|
| 36 |
-
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from typing import List
|
| 2 |
+
from codereview_env.models import Scenario, ActionRecord, Category, Severity, ActionType
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
def grade_security_audit(scenario: Scenario, history: List[ActionRecord]) -> float:
|
| 5 |
+
if not history:
|
| 6 |
+
return 0.0
|
| 7 |
+
|
| 8 |
+
flag_actions = [a for a in history if a.action_type == ActionType.FLAG_ISSUE]
|
| 9 |
+
if not flag_actions:
|
| 10 |
+
return 0.0
|
| 11 |
|
| 12 |
+
matched_issue_scores = []
|
| 13 |
+
used_action_indices = set()
|
| 14 |
|
| 15 |
+
for truth in scenario.ground_truth_issues:
|
| 16 |
+
if truth.category != Category.SECURITY:
|
| 17 |
+
continue
|
| 18 |
+
|
| 19 |
+
best_match_idx = -1
|
| 20 |
+
for j, action in enumerate(flag_actions):
|
| 21 |
+
if j in used_action_indices:
|
| 22 |
+
continue
|
| 23 |
+
|
| 24 |
+
# Match criteria: filename, line +- 3, category SECURITY, >= 1 keyword
|
| 25 |
+
if (action.filename == truth.filename and
|
| 26 |
+
action.line_number is not None and
|
| 27 |
+
abs(action.line_number - truth.line_number) <= 3 and
|
| 28 |
+
action.category == Category.SECURITY):
|
| 29 |
+
|
| 30 |
+
body_lower = (action.body or "").lower()
|
| 31 |
+
if any(kw.lower() in body_lower for kw in truth.keywords):
|
| 32 |
+
best_match_idx = j
|
| 33 |
+
break
|
| 34 |
+
|
| 35 |
+
if best_match_idx != -1:
|
| 36 |
+
action = flag_actions[best_match_idx]
|
| 37 |
+
used_action_indices.add(best_match_idx)
|
| 38 |
|
| 39 |
+
# Calculate issue score
|
| 40 |
+
sev_diff = abs(Severity.ordinal(truth.severity) - Severity.ordinal(action.severity))
|
| 41 |
+
sev_score = max(0.0, 1.0 - sev_diff * 0.3)
|
| 42 |
|
| 43 |
+
body_lower = (action.body or "").lower()
|
| 44 |
+
match_count = sum(1 for kw in truth.keywords if kw.lower() in body_lower)
|
| 45 |
+
kw_threshold = max(4, len(truth.keywords))
|
| 46 |
+
kw_score = match_count / kw_threshold
|
| 47 |
|
| 48 |
+
issue_score = 0.7 * sev_score + 0.3 * kw_score
|
| 49 |
+
matched_issue_scores.append(issue_score)
|
| 50 |
+
|
| 51 |
+
if not matched_issue_scores:
|
| 52 |
+
return 0.0
|
| 53 |
+
|
| 54 |
+
final_score = sum(matched_issue_scores) / len(matched_issue_scores)
|
| 55 |
+
return float(round(max(0.0, min(1.0, final_score)), 4))
|
| 56 |
+
|
openenv.yaml
CHANGED
|
@@ -45,22 +45,10 @@ tasks:
|
|
| 45 |
|
| 46 |
grading:
|
| 47 |
type: "deterministic"
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
- name: "precision"
|
| 55 |
-
description: "Fraction of flagged issues that are true positives"
|
| 56 |
-
weight: 0.3
|
| 57 |
-
- name: "severity_accuracy"
|
| 58 |
-
description: "How close the flagged severity is to ground truth (security task)"
|
| 59 |
-
weight: 0.7
|
| 60 |
-
- name: "keyword_accuracy"
|
| 61 |
-
description: "Semantic overlap between agent description and issue keywords"
|
| 62 |
-
weight: 0.3
|
| 63 |
-
- name: "verdict_correctness"
|
| 64 |
-
description: "Whether final approve/request_changes decision is correct (arch task)"
|
| 65 |
-
weight: 0.2
|
| 66 |
|
|
|
|
| 45 |
|
| 46 |
grading:
|
| 47 |
type: "deterministic"
|
| 48 |
+
issue_matching:
|
| 49 |
+
coverage_weight: 0.4
|
| 50 |
+
precision_weight: 0.6
|
| 51 |
+
quality_scoring:
|
| 52 |
+
severity_weight: 0.7
|
| 53 |
+
keyword_weight: 0.3
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|