Spaces:
Running
Running
| """ | |
| CodeReviewEnv β OpenEnv-compliant environment for AI agent code review training. | |
| Implements: step() / reset() / state() | |
| """ | |
| from typing import Any, Dict, Optional, Tuple | |
| from models import Action, Observation, Reward, EnvironmentState | |
| from tasks import get_task, get_all_tasks | |
| from reward import calculate_reward | |
| class CodeReviewEnv: | |
| """ | |
| An OpenEnv environment that simulates a human code reviewer's workflow. | |
| The agent receives a code diff and must identify bugs, security issues, | |
| and style problems by submitting CodeComment actions with severity ratings. | |
| At the end of each episode the agent issues a final verdict. | |
| """ | |
| def __init__(self, task_id: str = "easy"): | |
| self.task_id = task_id | |
| self._task: Dict[str, Any] = {} | |
| self._step_number: int = 0 | |
| self._done: bool = False | |
| self._total_reward: float = 0.0 | |
| self._episode_history: list = [] | |
| self._current_observation: Optional[Observation] = None | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # OpenEnv Core API | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def reset(self, task_id: Optional[str] = None) -> Observation: | |
| """Reset the environment to a clean initial state. Returns first observation.""" | |
| if task_id: | |
| self.task_id = task_id | |
| self._task = get_task(self.task_id) | |
| if not self._task: | |
| raise ValueError(f"Unknown task_id: '{self.task_id}'. " | |
| f"Valid options: {[t['id'] for t in get_all_tasks()]}") | |
| self._step_number = 0 | |
| self._done = False | |
| self._total_reward = 0.0 | |
| self._episode_history = [] | |
| self._current_observation = Observation( | |
| diff=self._task["diff"], | |
| file_name=self._task["file_name"], | |
| pr_title=self._task["pr_title"], | |
| pr_description=self._task["pr_description"], | |
| step_number=self._step_number, | |
| max_steps=self._task["max_steps"], | |
| task_id=self.task_id, | |
| task_description=self._task["description"], | |
| ) | |
| return self._current_observation | |
| def step(self, action: Action) -> Tuple[Observation, float, bool, Dict[str, Any]]: | |
| """ | |
| Process one agent action and return (observation, reward, done, info). | |
| Args: | |
| action: An Action object with comments and a verdict. | |
| Returns: | |
| observation: Updated observation (same diff, updated step counter). | |
| reward: Float reward signal for this step. | |
| done: True when episode is complete. | |
| info: Dict with reward breakdown and debug info. | |
| """ | |
| if self._done: | |
| raise RuntimeError( | |
| "Episode is finished. Call reset() to start a new episode." | |
| ) | |
| if not self._task: | |
| raise RuntimeError("Environment not initialised. Call reset() first.") | |
| self._step_number += 1 | |
| # Calculate reward for this action | |
| reward_obj: Reward = calculate_reward( | |
| action=action, | |
| known_issues=self._task["known_issues"], | |
| required_verdict=self._task["required_verdict"], | |
| step_number=self._step_number, | |
| ) | |
| self._total_reward += reward_obj.value | |
| # Record step in history | |
| step_record = { | |
| "step": self._step_number, | |
| "action": action.model_dump(), | |
| "reward": reward_obj.value, | |
| "reward_breakdown": reward_obj.breakdown, | |
| "reward_message": reward_obj.message, | |
| "issues_found_this_step": reward_obj.issues_found, | |
| "false_positives_this_step": reward_obj.false_positives, | |
| } | |
| self._episode_history.append(step_record) | |
| # Determine if episode is done | |
| max_steps = self._task["max_steps"] | |
| verdict_issued = action.verdict in ("approve", "request_changes") | |
| self._done = verdict_issued or (self._step_number >= max_steps) | |
| # Build next observation | |
| self._current_observation = Observation( | |
| diff=self._task["diff"], | |
| file_name=self._task["file_name"], | |
| pr_title=self._task["pr_title"], | |
| pr_description=self._task["pr_description"], | |
| step_number=self._step_number, | |
| max_steps=max_steps, | |
| task_id=self.task_id, | |
| task_description=self._task["description"], | |
| ) | |
| info = { | |
| "reward_breakdown": reward_obj.breakdown, | |
| "reward_message": reward_obj.message, | |
| "issues_found": reward_obj.issues_found, | |
| "issues_missed": reward_obj.issues_missed, | |
| "false_positives": reward_obj.false_positives, | |
| "total_reward_so_far": round(self._total_reward, 4), | |
| "steps_remaining": max(0, max_steps - self._step_number), | |
| } | |
| return self._current_observation, reward_obj.value, self._done, info | |
| def state(self) -> EnvironmentState: | |
| """Return the full current internal state of the environment.""" | |
| return EnvironmentState( | |
| task_id=self.task_id, | |
| step_number=self._step_number, | |
| max_steps=self._task.get("max_steps", 0) if self._task else 0, | |
| done=self._done, | |
| total_reward=round(self._total_reward, 4), | |
| current_diff=self._task.get("diff", "") if self._task else "", | |
| known_issue_count=len(self._task.get("known_issues", [])) if self._task else 0, | |
| agent_comment_count=sum( | |
| len(s.get("action", {}).get("comments", [])) | |
| for s in self._episode_history | |
| ), | |
| episode_history=self._episode_history, | |
| ) | |