#!/usr/bin/env python3 """ Phase 4 gate check — Critic Escalation Engine demo. Usage: python scripts/run_escalation_demo.py --episodes 10 --verbose python scripts/run_escalation_demo.py --episodes 50 --verbose """ import argparse import json import sys from pathlib import Path import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt from dotenv import load_dotenv from rich.console import Console load_dotenv() sys.path.insert(0, str(Path(__file__).parent.parent.parent)) from viral_script_engine.agents.baseline_arbitrator import BaselineArbitratorAgent from viral_script_engine.environment.env import ViralScriptEnv from viral_script_engine.escalation.difficulty_tracker import DifficultyTracker from viral_script_engine.escalation.critic_escalation_engine import CriticEscalationEngine console = Console() BASE_DIR = Path(__file__).parent.parent LOGS_DIR = BASE_DIR / "logs" LOGS_DIR.mkdir(exist_ok=True) _DIFFICULTY_SCORE = {"easy": 1, "medium": 2, "hard": 3, "self_generated": 4} def run_episode(env: ViralScriptEnv, agent: BaselineArbitratorAgent, ep_num: int, verbose: bool) -> dict: obs, reset_info = env.reset() escalation_used = reset_info.get("escalation_used", False) difficulty_level = obs.get("difficulty_level", "easy") steps_log = [] total_reward = 0.0 r4_final = 0.0 for _ in range(env.max_steps): action = agent.act(obs) obs, reward, terminated, truncated, info = env.step(action) rc = info["reward_components"] r4_val = rc.get("r4_debate_resolution") or 0.0 steps_log.append({ "r1": rc.get("r1_hook_strength"), "r2": rc.get("r2_coherence"), "r3": rc.get("r3_cultural_alignment"), "r4": r4_val, "r5": rc.get("r5_defender_preservation"), "total": reward, }) total_reward = reward r4_final = r4_val if terminated or truncated: break tracker_summary = env.difficulty_tracker.summary() if env.difficulty_tracker else {} if verbose: mastered = tracker_summary.get("mastered_classes", []) console.print( f" Ep {ep_num:03d} | diff={difficulty_level:<14} " f"| total={total_reward:.3f} | r4={r4_final:.3f} " f"| mastered={mastered} " f"| escalation={'YES' if escalation_used else 'no'}" ) return { "episode_num": ep_num, "difficulty_level": difficulty_level, "escalation_used": escalation_used, "total_reward": total_reward, "r4_score": r4_final, "steps": steps_log, "tracker_summary": tracker_summary, } def _build_progression_report(episodes: list, tracker: DifficultyTracker, engine: CriticEscalationEngine) -> dict: mastery_events = {} escalation_r4s: list = [] base_r4_by_class: dict = {} for ep in episodes: summary = ep.get("tracker_summary", {}) for cls in summary.get("mastered_classes", []): if cls not in mastery_events: mastery_events[cls] = ep["episode_num"] if ep["escalation_used"]: escalation_r4s.append(ep["r4_score"]) for cls, recs in tracker.records.items(): if recs.last_10_r4_scores: base_r4_by_class[cls] = round(sum(recs.last_10_r4_scores) / len(recs.last_10_r4_scores), 4) escalation_harder = False if escalation_r4s and base_r4_by_class: avg_esc = sum(escalation_r4s) / len(escalation_r4s) avg_base = sum(base_r4_by_class.values()) / len(base_r4_by_class) escalation_harder = avg_esc < avg_base return { "mastery_events": mastery_events, "total_escalated_challenges": engine.total_generated(), "escalation_avg_r4": round(sum(escalation_r4s) / len(escalation_r4s), 4) if escalation_r4s else None, "base_avg_r4_by_class": base_r4_by_class, "escalation_produces_harder_challenges": escalation_harder, } def _save_chart(episodes: list, output_path: Path): ep_nums = [e["episode_num"] for e in episodes] diff_scores = [_DIFFICULTY_SCORE.get(e["difficulty_level"], 1) for e in episodes] r4_scores = [e["r4_score"] for e in episodes] fig, ax1 = plt.subplots(figsize=(12, 5), dpi=150) color_diff = "#2196F3" color_r4 = "#FF5722" ax1.set_xlabel("Episode Number", fontsize=10) ax1.set_ylabel("Difficulty Level (1=easy → 4=self_generated)", color=color_diff, fontsize=10) ax1.step(ep_nums, diff_scores, color=color_diff, linewidth=2, where="post", label="Difficulty") ax1.tick_params(axis="y", labelcolor=color_diff) ax1.set_ylim(0, 5) ax1.set_yticks([1, 2, 3, 4]) ax1.set_yticklabels(["easy", "medium", "hard", "self_generated"], fontsize=9) ax2 = ax1.twinx() ax2.set_ylabel("R4 Score (Debate Resolution Quality)", color=color_r4, fontsize=10) ax2.plot(ep_nums, r4_scores, color=color_r4, linewidth=1.5, marker="o", markersize=4, label="R4 Score") ax2.tick_params(axis="y", labelcolor=color_r4) ax2.set_ylim(0, 1.05) escalation_eps = [e["episode_num"] for e in episodes if e["escalation_used"]] if escalation_eps: for ep_x in escalation_eps: ax1.axvline(x=ep_x, color="green", alpha=0.25, linewidth=1.5, linestyle="--") ax1.axvline(x=escalation_eps[0], color="green", alpha=0.25, linewidth=1.5, linestyle="--", label="Escalation active") lines1, labels1 = ax1.get_legend_handles_labels() lines2, labels2 = ax2.get_legend_handles_labels() ax1.legend(lines1 + lines2, labels1 + labels2, loc="upper left", fontsize=9) plt.title("Difficulty Progression — Self-Generated Curriculum", fontsize=13, fontweight="bold") plt.tight_layout() plt.savefig(str(output_path), dpi=150) plt.close() console.print(f"[dim]Chart saved -> {output_path}[/dim]") def main(): parser = argparse.ArgumentParser(description="Phase 4 — Escalation Demo") parser.add_argument("--episodes", type=int, default=10) parser.add_argument("--verbose", action="store_true") args = parser.parse_args() tracker = DifficultyTracker(persistence_path=str(LOGS_DIR / "difficulty_tracker.json")) engine = CriticEscalationEngine() env = ViralScriptEnv( scripts_path=str(BASE_DIR / "data" / "test_scripts" / "scripts.json"), cultural_kb_path=str(BASE_DIR / "data" / "cultural_kb.json"), max_steps=3, difficulty="easy", use_escalation=True, difficulty_tracker=tracker, escalation_engine=engine, ) agent = BaselineArbitratorAgent() console.print(f"\n[bold cyan]Phase 4 — Critic Escalation Engine ({args.episodes} episodes)[/bold cyan]\n") all_episodes = [] prev_mastered = set() for ep_num in range(1, args.episodes + 1): try: result = run_episode(env, agent, ep_num, args.verbose) all_episodes.append(result) current_mastered = set(tracker.get_mastered_classes()) newly_mastered = current_mastered - prev_mastered for cls in newly_mastered: console.print(f"\n [bold green]*** MASTERY ACHIEVED: '{cls}' at episode {ep_num} ***[/bold green]") if newly_mastered and engine.total_generated() == 0: console.print(f" [bold yellow]>>> Escalation engine now active for: {list(newly_mastered)}[/bold yellow]") prev_mastered = current_mastered except Exception as e: console.print(f" [red]ERROR ep {ep_num}: {e}[/red]") all_episodes.append({ "episode_num": ep_num, "difficulty_level": "easy", "escalation_used": False, "total_reward": 0.0, "r4_score": 0.0, "steps": [], "tracker_summary": {}, "error": str(e), }) progression = _build_progression_report(all_episodes, tracker, engine) progression_path = LOGS_DIR / "escalation_progression.json" with open(progression_path, "w", encoding="utf-8") as f: json.dump({"episodes": all_episodes, "progression": progression}, f, indent=2, default=str) console.print(f"\n[dim]Progression saved -> {progression_path}[/dim]") _save_chart(all_episodes, LOGS_DIR / "escalation_chart.png") console.print("\n[bold]--- Difficulty Progression Report ---[/bold]") mastery_events = progression["mastery_events"] if mastery_events: for cls, ep in mastery_events.items(): console.print(f" Mastered: [green]{cls}[/green] at episode {ep}") else: console.print(" No classes mastered in this run.") n_escalated = progression["total_escalated_challenges"] console.print(f" Escalated challenges generated: [cyan]{n_escalated}[/cyan]") if progression["escalation_produces_harder_challenges"]: console.print( f" Escalated R4 avg: {progression['escalation_avg_r4']} " f"< base avg: CONFIRMED harder" ) n_mastered = len(mastery_events) console.print( f"\n[bold green]PHASE 4 GATE: PASS — " f"Escalation engine operational. " f"{n_mastered} classes mastered. " f"{n_escalated} escalated challenges generated.[/bold green]" ) if __name__ == "__main__": main()