ai-code-review-env / env /environment.py
YAXH64
fix: docker build issue
a2ae9c3
Raw
History Blame Contribute Delete
6.98 kB
from typing import Dict, Any
from env.models import Observation, Action, EpisodeState
from env.tasks import TASKS
from env.grader import grade_identify, grade_fix
def _clamp(reward: float) -> float:
"""Clamp reward strictly to [0.0, 1.0] per OpenEnv spec."""
return round(max(0.0, min(reward, 1.0)), 4)
class CodeReviewEnvironment:
"""
OpenEnv-compliant code review environment.
Agent reads buggy code, identifies issues, and suggests fixes.
3 tasks: easy (syntax) β†’ medium (logic) β†’ hard (performance)
All rewards are clamped to [0.0, 1.0] before being returned.
"""
def __init__(self):
self._tasks = TASKS
self._current_task_index = 0
self._current_step = 0
self._history = []
self._phase = "identify" # "identify" β†’ "fix"
self._identify_reward = 0.0
self._total_reward = 0.0
self._done = False
# ------------------------------------------------------------------
# OpenEnv required: reset()
# ------------------------------------------------------------------
def reset(self) -> Observation:
self._current_task_index = 0
self._current_step = 0
self._history = []
self._phase = "identify"
self._identify_reward = 0.0
self._total_reward = 0.0
self._done = False
return self._make_observation()
# ------------------------------------------------------------------
# OpenEnv required: step(action)
# ------------------------------------------------------------------
def step(self, action: Action) -> Dict[str, Any]:
if self._done:
return {
"observation": self._make_observation(),
"reward": 0.0,
"done": True,
"info": {"error": "Episode already done. Call reset()."},
}
task = self._current_task()
reward = 0.0
info = {}
self._current_step += 1
# ── IDENTIFY phase ──────────────────────────────────────────────
if action.action_type == "identify":
if self._phase == "fix":
# Repeated identify after fix phase β€” penalise (clamped to 0.0)
reward = 0.0
info["warning"] = "Already in fix phase. Skipping repeated identify."
else:
reward = grade_identify(task["identify_keywords"], action.content)
self._identify_reward = reward
self._phase = "fix"
info["phase_transition"] = "identify β†’ fix"
info["identify_score"] = reward
# ── FIX phase ───────────────────────────────────────────────────
elif action.action_type == "fix":
if self._phase == "identify":
# Jumped straight to fix without identifying β€” partial credit only
fix_score = grade_fix(task["fixed_code"], action.content)
reward = fix_score * 0.5 # halved because no identify step
info["warning"] = "Skipped identify phase. Partial fix credit."
else:
fix_score = grade_fix(task["fixed_code"], action.content)
# Identify bonus for continuous signal β€” final reward clamped to 1.0
bonus = 0.1 if self._identify_reward >= 0.4 else 0.0
reward = fix_score + bonus
info["fix_score"] = fix_score
info["identify_bonus"] = bonus
# Clamp before recording and returning
reward = _clamp(reward)
self._total_reward += reward
# Move to next task or end episode
done = self._advance_task()
self._phase = "identify"
self._identify_reward = 0.0
obs = self._make_observation()
self._history.append(
f"[task={task['id']}] action={action.action_type} reward={reward:.2f}"
)
info["task_completed"] = task["id"]
info["task_difficulty"] = task["difficulty"]
return {
"observation": obs,
"reward": reward,
"done": done,
"info": info,
}
else:
# Unknown action type β€” penalise, clamped to 0.0
reward = 0.0
info["error"] = f"Unknown action_type '{action.action_type}'. Use 'identify' or 'fix'."
reward = _clamp(reward)
self._total_reward += reward
self._history.append(
f"[task={task['id']}] action={action.action_type} reward={reward:.2f}"
)
return {
"observation": self._make_observation(),
"reward": reward,
"done": self._done,
"info": info,
}
# ------------------------------------------------------------------
# OpenEnv required: state (property)
# ------------------------------------------------------------------
@property
def state(self) -> EpisodeState:
task = self._current_task()
return EpisodeState(
current_task_id=task["id"],
current_task_difficulty=task["difficulty"],
current_task_category=task["category"],
phase=self._phase,
step=self._current_step,
total_reward=round(self._total_reward, 4),
done=self._done,
history=list(self._history),
)
# ------------------------------------------------------------------
# Internal helpers
# ------------------------------------------------------------------
def _current_task(self) -> dict:
# Clamp to last task when episode is done to avoid index error
idx = min(self._current_task_index, len(self._tasks) - 1)
return self._tasks[idx]
def _make_observation(self) -> Observation:
task = self._current_task()
task_prompt = (
f"[{task['difficulty'].upper()} | {task['category']}] {task['title']}\n"
f"{task['description']}\n\n"
f"Phase: {self._phase.upper()}\n"
f"{'Identify the bug.' if self._phase == 'identify' else 'Fix the code.'}"
)
return Observation(
code=task["code"],
task=task_prompt,
history=list(self._history),
task_id=task["id"],
language=task["language"],
difficulty=task["difficulty"],
category=task["category"],
)
def _advance_task(self) -> bool:
"""Move to next task. Returns True if episode is done."""
self._current_task_index += 1
if self._current_task_index >= len(self._tasks):
self._done = True
return True
return False