sql-debug-env / server /sql_environment.py
aakashdandekar's picture
Fix final sql_environment validation logic
f38af3b
Raw History Blame Contribute Delete
5.64 kB
"""
SQL Debug Environment — server-side environment logic.
Implements the OpenEnv Environment interface: reset(), step(), state property.
"""
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import random
from uuid import uuid4
from typing import Optional
from models import Action, Observation, State
from tasks import TASKS, TASK_GROUPS, get_task
from graders import grade_submission
MAX_STEPS = 5
class SQLDebugEnvironment:
"""
Real-world OpenEnv environment for SQL query debugging.
An agent is given a broken SQL query and must submit corrected versions
to earn rewards. The environment tracks attempts and computes shaped rewards.
"""
def __init__(self):
self._state: Optional[State] = None
self._current_task_id: Optional[str] = None
self._current_task: Optional[dict] = None
self._submissions: list = []
self._done: bool = False
self._last_reward: float = 0.0
self._last_result: str = ""
self._task_group: Optional[str] = None
def reset(
self,
task_group: Optional[str] = None,
task_id: Optional[str] = None,
seed: Optional[int] = None,
) -> Observation:
"""
Initialize a new episode.
Args:
task_group: One of 'easy_syntax_fix', 'medium_logic_fix', 'hard_optimization_fix'
task_id: Specific task id (overrides task_group)
seed: Random seed for reproducibility
"""
if seed is not None:
random.seed(seed)
if task_id and task_id in TASKS:
self._current_task_id = task_id
self._task_group = None
elif task_id and task_id in TASK_GROUPS:
self._current_task_id = random.choice(TASK_GROUPS[task_id])
self._task_group = task_id
elif task_group and task_group in TASK_GROUPS:
self._current_task_id = random.choice(TASK_GROUPS[task_group])
self._task_group = task_group
else:
self._current_task_id = random.choice(list(TASKS.keys()))
self._task_group = None
self._current_task = get_task(self._current_task_id)
self._submissions = []
self._done = False
self._last_reward = 0.0
self._last_result = ""
episode_id = str(uuid4())
self._state = State(
episode_id=episode_id,
step_count=0,
task_id=self._task_group if self._task_group else self._current_task_id,
max_steps=MAX_STEPS,
cumulative_reward=0.0,
)
return self._build_observation()
def step(self, action: Action) -> Observation:
"""
Execute one fix attempt.
Args:
action: Agent's submitted SQL query + optional explanation
Returns:
Observation with reward, done flag, and feedback
"""
if self._done:
return self._build_observation(
message="Episode already done. Call reset()."
)
if self._state is None:
raise RuntimeError("Environment not initialized. Call reset() first.")
submitted_query = action.query.strip()
self._submissions.append(submitted_query)
self._state.step_count += 1
score, feedback, details = grade_submission(
self._current_task_id, submitted_query
)
reward = self._compute_reward(score, self._state.step_count)
self._last_reward = reward
self._state.cumulative_reward += reward
self._last_result = feedback
if score == 1.0:
self._done = True
elif self._state.step_count >= MAX_STEPS:
self._done = True
return self._build_observation()
def state(self) -> State:
"""Return current environment state metadata."""
if self._state is None:
return State(episode_id="uninitialized", step_count=0)
return self._state
def _compute_reward(self, score: float, step_num: int) -> float:
"""
Shaped reward function with partial progress signals.
- score=1.0: full reward, bonus for solving early
- score=0.7: meaningful partial credit
- score=0.5: small partial credit
- score=0.3: tiny credit for at least running
- score=0.0: penalty for wasted attempt (after first)
"""
if score == 1.0:
early_bonus = max(0.0, (MAX_STEPS - step_num) * 0.05)
return round(1.0 + early_bonus, 4)
elif score >= 0.7:
return 0.5
elif score >= 0.5:
return 0.2
elif score >= 0.3:
return 0.1
else:
return -0.05 if step_num > 1 else 0.0
def _build_observation(self, message: str = "") -> Observation:
task = self._current_task
attempts_remaining = MAX_STEPS - (self._state.step_count if self._state else 0)
return Observation(
schema_sql=task["schema_sql"].strip(),
sample_data=task["sample_data"].strip(),
buggy_query=task["buggy_query"].strip(),
error_message=task["error_message"],
task_description=task["task_description"],
expected_columns=task["expected_columns"],
attempts_remaining=max(0, attempts_remaining),
last_attempt_result=self._last_result + (" " + message if message else ""),
reward=self._last_reward,
done=self._done,
success=self._done and self._last_reward >= 1.0,
)