#!/usr/bin/env python3 """ Evaluate the trained Arbitrator model on the same 20-episode schedule as the baseline. Saves results to logs/trained_results.json, then generates training_vs_baseline.png. Usage: python training/eval_trained_model.py --model outputs/checkpoints/final_model """ import argparse import json import sys from pathlib import Path from dotenv import load_dotenv load_dotenv() sys.path.insert(0, str(Path(__file__).parent.parent.parent)) BASE_DIR = Path(__file__).parent.parent LOGS_DIR = BASE_DIR / "logs" LOGS_DIR.mkdir(exist_ok=True) _SCHEDULE = ( [(i, "easy") for i in range(1, 9)] + [(i, "medium") for i in range(9, 17)] + [(i, "hard") for i in range(17, 21)] ) def _make_env(difficulty: str): from viral_script_engine.environment.env import ViralScriptEnv return ViralScriptEnv( scripts_path=str(BASE_DIR / "data" / "test_scripts" / "scripts.json"), cultural_kb_path=str(BASE_DIR / "data" / "cultural_kb.json"), max_steps=5, difficulty=difficulty, ) def _load_trained_agent(model_path: str): """ Load a fine-tuned model and return a callable agent. Uses unsloth FastLanguageModel if available; falls back to a HuggingFace pipeline. """ model_path = Path(model_path) if not model_path.exists(): raise FileNotFoundError(f"Trained model not found: {model_path}") try: from unsloth import FastLanguageModel model, tokenizer = FastLanguageModel.from_pretrained( str(model_path), max_seq_length=2048, dtype=None, load_in_4bit=True ) FastLanguageModel.for_inference(model) return _HFAgent(model, tokenizer) except ImportError: pass try: from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline tokenizer = AutoTokenizer.from_pretrained(str(model_path)) model = AutoModelForCausalLM.from_pretrained(str(model_path)) pipe = pipeline("text-generation", model=model, tokenizer=tokenizer) return _PipelineAgent(pipe) except Exception as e: raise RuntimeError(f"Could not load trained model: {e}") class _HFAgent: def __init__(self, model, tokenizer): self.model = model self.tokenizer = tokenizer def act(self, observation: dict) -> dict: from viral_script_engine.training.rollout_function import ( _format_observation_prompt, _extract_json_action, _model_generate, ) prompt = _format_observation_prompt(observation, observation.get("step_num", 1), 5) raw = _model_generate(self.model, self.tokenizer, prompt, max_new_tokens=256) return _extract_json_action(raw) class _PipelineAgent: def __init__(self, pipe): self.pipe = pipe def act(self, observation: dict) -> dict: import json from viral_script_engine.training.rollout_function import ( _format_observation_prompt, _extract_json_action, ) prompt = _format_observation_prompt(observation, observation.get("step_num", 1), 5) out = self.pipe(prompt, max_new_tokens=256, return_full_text=False) raw = out[0]["generated_text"] if out else "" return _extract_json_action(raw) def run_episode(ep_num: int, difficulty: str, agent) -> dict: env = _make_env(difficulty) obs, _ = env.reset() episode_id = obs["episode_id"] state = env.state() original_script = state.get("original_script", "") steps_log = [] total_reward = 0.0 for _ in range(env.max_steps): action = agent.act(obs) obs, reward, terminated, truncated, info = env.step(action) rc = info["reward_components"] anti_log = info.get("anti_gaming_log", {}) steps_log.append({ "r1": rc.get("r1_hook_strength"), "r2": rc.get("r2_coherence"), "r3": rc.get("r3_cultural_alignment"), "r4": rc.get("r4_debate_resolution"), "r5": rc.get("r5_defender_preservation"), "total": reward, "anti_gaming_triggered": anti_log.get("triggered", False), "penalty": anti_log.get("penalty_applied", 0.0), }) total_reward = reward if terminated or truncated: break final_state = env.state() return { "episode_num": ep_num, "episode_id": episode_id, "difficulty": difficulty, "steps": steps_log, "total_reward": total_reward, "anti_gaming_logs": final_state.get("anti_gaming_logs", []), "original_script": original_script, "final_script": final_state.get("current_script", ""), } def main(): parser = argparse.ArgumentParser(description="Evaluate trained Arbitrator model") parser.add_argument("--model", required=True, help="Path to trained model directory") parser.add_argument("--output", default="logs/trained_results.json", help="Output JSON path") args = parser.parse_args() print(f"Loading trained model from: {args.model}") agent = _load_trained_agent(args.model) all_episodes = [] print("Running 20 evaluation episodes (same schedule as baseline)...") for ep_num, difficulty in _SCHEDULE: print(f" Episode {ep_num:02d}/20 ({difficulty})...") try: result = run_episode(ep_num, difficulty, agent) all_episodes.append(result) print(f" -> total_reward={result['total_reward']:.3f} steps={len(result['steps'])}") except Exception as e: print(f" ERROR episode {ep_num}: {e}") all_episodes.append({ "episode_num": ep_num, "difficulty": difficulty, "steps": [], "total_reward": 0.0, "anti_gaming_logs": [], "original_script": "", "final_script": "", "error": str(e), }) output_path = BASE_DIR / args.output output_path.parent.mkdir(parents=True, exist_ok=True) with open(output_path, "w", encoding="utf-8") as f: json.dump(all_episodes, f, indent=2, default=str) print(f"\nSaved -> {output_path}") from viral_script_engine.training.reward_curves import plot_training_curves baseline_path = str(LOGS_DIR / "baseline_results.json") plot_training_curves( baseline_log_path=baseline_path, training_log_path=str(output_path), output_path=str(LOGS_DIR / "training_vs_baseline.png"), ) if __name__ == "__main__": main()