Spaces:
Runtime error
Runtime error
| """ | |
| 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." | |
| ) | |
| 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, | |
| } | |