ragavrida's picture
CodeReviewEnv v1.0 — OpenEnv-compliant submission
dbf5e96
Raw
History Blame Contribute Delete
12.6 kB
"""
Semantic World Model Training Scaffold
This module provides the training infrastructure for learning a
semantic transition model from CodeReviewEnv trajectories.
This is the research contribution Layer 3:
Layer 1: Environment (CodeReviewEnv) — this repo
Layer 2: Trajectory dataset (export_trajectory())
Layer 3: Semantic world model (this scaffold)
Layer 4: Planning with learned model (Dyna-Q over language)
Layer 5: Paper — "Model-Based RL over Semantic Environments"
The scaffold intentionally leaves the model architecture open
for researchers to plug in their own encoder + transition model.
Dependencies: torch, sentence-transformers (optional, install via
requirements-research.txt). The scaffold is importable without these
but will raise ImportError when training is attempted.
"""
import json
import os
from typing import Dict, List, Tuple, Optional, Callable, Any
from env.data_generator import SEVERITY_ORDER
# Action type one-hot encoding
ACTION_TYPES = ["label_severity", "prioritize", "add_comment", "approve", "request_changes"]
ACTION_TYPE_DIM = len(ACTION_TYPES)
SEVERITY_DIM = len(SEVERITY_ORDER)
ACTION_VECTOR_DIM = ACTION_TYPE_DIM + SEVERITY_DIM # 10-dimensional
class SemanticTransitionDataset:
"""
Dataset wrapping JSONL trajectory files for world model training.
Each item: (state_text, action_vector, next_state_text, reward)
This is the bridge between CodeReviewEnv trajectories and
learnable transition models. The encoder argument allows
researchers to plug in their own state representation.
"""
def __init__(self, trajectory_dir: str, encoder: Optional[Callable] = None):
"""
Args:
trajectory_dir: path to trajectories/ directory
encoder: callable that maps observation dict → vector.
Default: None (returns raw text for custom encoding)
"""
self.trajectory_dir = trajectory_dir
self.encoder = encoder
self.transitions: List[Dict] = []
self._load()
def _load(self) -> None:
"""Load all JSONL trajectory files."""
if not os.path.exists(self.trajectory_dir):
return
for filename in sorted(os.listdir(self.trajectory_dir)):
if filename.endswith(".jsonl"):
filepath = os.path.join(self.trajectory_dir, filename)
with open(filepath, "r") as f:
for line in f:
line = line.strip()
if line:
self.transitions.append(json.loads(line))
def __len__(self) -> int:
return len(self.transitions)
def __getitem__(self, idx: int) -> Tuple:
"""
Returns (state, action, next_state, reward) tuple.
If encoder is provided, states are encoded vectors.
Otherwise, states are text strings from state_to_text().
"""
transition = self.transitions[idx]
state = transition.get("state", {})
action = transition.get("action", {})
next_state = transition.get("next_state", {})
reward = transition.get("reward", {}).get("value", 0.0)
state_repr = self.state_to_text(state)
next_state_repr = self.state_to_text(next_state)
if self.encoder:
state_repr = self.encoder(state_repr)
next_state_repr = self.encoder(next_state_repr)
action_vec = self.action_to_vector(action)
return state_repr, action_vec, next_state_repr, reward
def state_to_text(self, observation: Dict) -> str:
"""
Convert observation dict to flat text for LLM encoding.
Format: "PR: {title}. Author: {experience}. Files: {filenames}.
Description: {description}. Queue: {queue_length} PRs pending."
This text representation preserves semantic content while being
suitable for sentence-transformer encoding.
"""
title = observation.get("title", "Unknown PR")
experience = observation.get("author_experience", "unknown")
description = observation.get("description", "")
files = observation.get("files", [])
queue = observation.get("review_queue", [])
filenames = ", ".join(f.get("filename", "?") if isinstance(f, dict) else str(f) for f in files)
return (
f"PR: {title}. Author: {experience}. "
f"Files: {filenames}. "
f"Description: {description}. "
f"Queue: {len(queue)} PRs pending."
)
@staticmethod
def action_to_vector(action: Dict) -> List[float]:
"""
One-hot encode action_type + severity into fixed-length vector.
Vector layout: [action_type_one_hot (5)] + [severity_one_hot (5)]
Total dimension: 10
This enables the transition model to condition on action
numerically while preserving the categorical structure.
"""
vec = [0.0] * ACTION_VECTOR_DIM
# Action type one-hot
action_type = action.get("action_type", "")
if action_type in ACTION_TYPES:
vec[ACTION_TYPES.index(action_type)] = 1.0
# Severity one-hot (if applicable)
severity = action.get("severity", None)
if severity and severity in SEVERITY_ORDER:
vec[ACTION_TYPE_DIM + SEVERITY_ORDER.index(severity)] = 1.0
return vec
class WorldModelTrainer:
"""
Training loop scaffold for semantic transition model.
Plug in your own model — this handles data loading, train/val split,
and evaluation. The model must have the signature:
model(state_vec, action_vec) → (next_state_vec, reward_pred)
Usage:
dataset = SemanticTransitionDataset("trajectories/")
model = YourTransitionModel()
trainer = WorldModelTrainer(dataset, model)
results = trainer.train(epochs=10)
"""
def __init__(self, dataset: SemanticTransitionDataset, model: Any = None):
"""
Args:
model: any callable with signature
model(state_vec, action_vec) → (next_state_vec, reward_pred)
If None, uses a simple dummy model for testing.
"""
self.dataset = dataset
self.model = model
def train(self, epochs: int = 10, lr: float = 1e-4) -> Dict:
"""
Standard training loop with MSE loss.
Requires torch. Install via: pip install -r requirements-research.txt
Returns: {"train_loss": [...], "val_loss": [...], "reward_mse": float}
"""
try:
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, random_split
except ImportError:
return {
"error": "torch not installed. Run: pip install -r requirements-research.txt",
"train_loss": [],
"val_loss": [],
"reward_mse": -1.0,
}
if len(self.dataset) == 0:
return {
"error": "No trajectory data. Run episodes first.",
"train_loss": [],
"val_loss": [],
"reward_mse": -1.0,
}
# Simple train/val split 80/20
n = len(self.dataset)
n_train = int(0.8 * n)
n_val = n - n_train
train_losses = []
val_losses = []
# Simplified training loop without DataLoader for compatibility
for epoch in range(epochs):
epoch_loss = 0.0
for i in range(min(n_train, n)):
state, action, next_state, reward = self.dataset[i]
# If model is provided, use it; otherwise track dummy loss
if self.model:
try:
pred_state, pred_reward = self.model(state, action)
# Compute simple MSE on reward
loss = (pred_reward - reward) ** 2
epoch_loss += loss
except Exception:
epoch_loss += 0.0
else:
epoch_loss += reward ** 2 # Dummy baseline
avg_loss = epoch_loss / max(1, min(n_train, n))
train_losses.append(avg_loss)
# Validation
val_loss = 0.0
for i in range(n_train, n):
state, action, next_state, reward = self.dataset[i]
val_loss += reward ** 2
val_losses.append(val_loss / max(1, n_val))
return {
"train_loss": train_losses,
"val_loss": val_losses,
"reward_mse": val_losses[-1] if val_losses else -1.0,
"epochs": epochs,
"n_train": n_train,
"n_val": n_val,
}
def evaluate_planning(self, env: Any, horizon: int = 3) -> Dict:
"""
Test model-based planning.
1. From current state, imagine H-step rollouts using learned model
2. Pick best action sequence
3. Execute in real env
4. Compare imagined vs real reward
Returns:
imagination_error: mean |predicted_reward - actual_reward|
planning_gain: (model_based - random) / (oracle - random)
"""
if not self.model or len(self.dataset) == 0:
return {
"imagination_error": -1.0,
"planning_gain": -1.0,
"error": "Model or data not available",
}
from env.models import Action
obs = env.reset()
imagined_rewards = []
actual_rewards = []
for step in range(min(horizon, 5)):
# Get state text
state_text = self.dataset.state_to_text(obs.model_dump())
# Try each action, pick best by model prediction
best_action = None
best_pred_reward = -float("inf")
for severity in ["critical", "high", "medium", "low", "none"]:
action_dict = {"action_type": "label_severity", "severity": severity}
action_vec = self.dataset.action_to_vector(action_dict)
try:
_, pred_reward = self.model(state_text, action_vec)
if pred_reward > best_pred_reward:
best_pred_reward = pred_reward
best_action = Action(action_type="label_severity", severity=severity)
except Exception:
continue
if best_action is None:
best_action = Action(action_type="label_severity", severity="medium")
best_pred_reward = 0.0
obs, reward, done, info = env.step(best_action)
imagined_rewards.append(best_pred_reward)
actual_rewards.append(reward.value)
if done:
break
if not imagined_rewards:
return {"imagination_error": -1.0, "planning_gain": -1.0}
imagination_error = sum(
abs(i - a) for i, a in zip(imagined_rewards, actual_rewards)
) / len(imagined_rewards)
return {
"imagination_error": imagination_error,
"planning_gain": 0.0, # Requires oracle baseline comparison
"steps_planned": len(imagined_rewards),
}
def compute_model_error_compounding(self) -> Dict:
"""
Measure how model error grows with rollout horizon H.
Returns: {"horizon": [1,2,3,4,5], "mse": [float,...]}
This is the core MBRL challenge in semantic spaces.
Classic result: error grows exponentially with H.
We measure whether semantic models compound error differently.
"""
if not self.model or len(self.dataset) < 10:
return {
"horizon": [1, 2, 3, 4, 5],
"mse": [-1.0] * 5,
"error": "Insufficient model or data",
}
horizons = [1, 2, 3, 4, 5]
mse_by_horizon = []
for h in horizons:
errors = []
for i in range(min(len(self.dataset) - h, 20)):
state, action, next_state, actual_reward = self.dataset[i]
try:
_, pred_reward = self.model(state, action)
errors.append((pred_reward - actual_reward) ** 2)
except Exception:
errors.append(1.0)
mse = sum(errors) / len(errors) if errors else -1.0
mse_by_horizon.append(mse)
return {
"horizon": horizons,
"mse": mse_by_horizon,
}