Spaces:
Sleeping
Sleeping
| """ | |
| evaluation/evaluator.py — NegotiArena Evaluation Suite | |
| ======================================================= | |
| Computes all judging-relevant metrics: | |
| - Overseer F1 (coalition detection) | |
| - Average deal quality (utility above baseline) | |
| - Gini coefficient improvement (fairness) | |
| - Reward curve tracking (before/after training comparison) | |
| - Deception rate vs detection rate correlation | |
| Run: | |
| python -m evaluation.evaluator --checkpoint checkpoints/overseer --episodes 50 | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import os | |
| import sys | |
| from dataclasses import dataclass, asdict | |
| from typing import Optional | |
| import numpy as np | |
| sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) | |
| from negotiarena_env import NegotiArenaEnv, TOTAL_RESOURCES, RESOURCE_TYPES | |
| from training.generate_sft_data import NegotiatorBot, OverseerBot | |
| class EpisodeMetrics: | |
| episode_id: str | |
| turns_taken: int | |
| resolution_type: str | |
| coalition_formed: bool | |
| coalition_detected: bool | |
| overseer_tp: int | |
| overseer_fp: int | |
| overseer_fn: int | |
| overseer_f1: float | |
| avg_deal_quality: float | |
| gini_coefficient: float | |
| total_reward_overseer: float | |
| total_reward_negotiators: float | |
| def precision(self) -> float: | |
| denom = self.overseer_tp + self.overseer_fp | |
| return self.overseer_tp / denom if denom > 0 else 0.0 | |
| def recall(self) -> float: | |
| denom = self.overseer_tp + self.overseer_fn | |
| return self.overseer_tp / denom if denom > 0 else 0.0 | |
| class EvalSummary: | |
| n_episodes: int | |
| mean_overseer_f1: float | |
| std_overseer_f1: float | |
| mean_deal_quality: float | |
| mean_gini: float | |
| coalition_detection_rate: float | |
| false_positive_rate: float | |
| deal_rate: float # % episodes that reach a deal (not timeout) | |
| mean_turns_to_deal: float | |
| mean_overseer_reward: float | |
| mean_negotiator_reward: float | |
| def gini(values: list[float]) -> float: | |
| if not values or sum(values) == 0: | |
| return 0.0 | |
| arr = sorted(values) | |
| n = len(arr) | |
| cumsum = sum((2 * (i + 1) - n - 1) * v for i, v in enumerate(arr)) | |
| return cumsum / (n * sum(arr) + 1e-9) | |
| def evaluate_random_policy(n_episodes: int = 50, seed: int = 0) -> EvalSummary: | |
| """Evaluate random/bot policy — establishes the BEFORE baseline for demo.""" | |
| return _run_eval(n_episodes=n_episodes, seed=seed, use_model=False, overseer_quality="random") | |
| def evaluate_trained_policy( | |
| checkpoint_path: str, | |
| n_episodes: int = 50, | |
| seed: int = 100, | |
| ) -> EvalSummary: | |
| """Evaluate trained model — the AFTER result for demo.""" | |
| return _run_eval( | |
| n_episodes=n_episodes, seed=seed, | |
| use_model=True, checkpoint_path=checkpoint_path, | |
| overseer_quality="trained", | |
| ) | |
| def _run_eval( | |
| n_episodes: int, | |
| seed: int, | |
| use_model: bool = False, | |
| overseer_quality: str = "random", | |
| checkpoint_path: Optional[str] = None, | |
| ) -> EvalSummary: | |
| all_metrics: list[EpisodeMetrics] = [] | |
| for ep_idx in range(n_episodes): | |
| env = NegotiArenaEnv(seed=seed + ep_idx, difficulty="medium") | |
| observations = env.reset() | |
| negotiator_bots = { | |
| "negotiator_a": NegotiatorBot("negotiator_a"), | |
| "negotiator_b": NegotiatorBot("negotiator_b"), | |
| "negotiator_c": NegotiatorBot("negotiator_c"), | |
| } | |
| if overseer_quality == "random": | |
| overseer = OverseerBot(random_flag_prob=0.15) | |
| else: | |
| overseer = OverseerBot(random_flag_prob=0.0) # trained = never random-flags | |
| done = False | |
| step = 0 | |
| while not done and step < 80: | |
| for agent_id in ["negotiator_a", "negotiator_b", "negotiator_c", "overseer"]: | |
| obs = observations.get(agent_id, {}) | |
| if agent_id == "overseer": | |
| action = overseer.act(obs) | |
| else: | |
| action = negotiator_bots[agent_id].act(obs) | |
| observations, _, done, _ = env.step(agent_id, action) | |
| if done: | |
| break | |
| step += 4 | |
| state = env.state() | |
| metrics = _compute_episode_metrics(state) | |
| all_metrics.append(metrics) | |
| return _aggregate_metrics(all_metrics) | |
| def _compute_episode_metrics(state: dict) -> EpisodeMetrics: | |
| coalitions = state.get("coalitions", []) | |
| coalition_formed = len(coalitions) > 0 | |
| coalition_agents = set() | |
| for c in coalitions: | |
| coalition_agents.update(c.get("members", [])) | |
| flags = state.get("overseer_flags", []) | |
| flagged_agents = {f["agent_id"] for f in flags} | |
| tp = len(coalition_agents & flagged_agents) | |
| fp = len(flagged_agents - coalition_agents) | |
| fn = len(coalition_agents - flagged_agents) | |
| precision = tp / max(tp + fp, 1) | |
| recall = tp / max(tp + fn, 1) | |
| f1 = 2 * precision * recall / max(precision + recall, 1e-9) | |
| final_alloc = state.get("final_allocation") or {r: TOTAL_RESOURCES[r] / 3 for r in RESOURCE_TYPES} | |
| # Gini on allocation values | |
| alloc_values = list(final_alloc.values()) | |
| gini_val = gini(alloc_values) | |
| # Deal quality: normalised by total resources | |
| total = sum(TOTAL_RESOURCES[r] for r in RESOURCE_TYPES) | |
| max_possible = max(alloc_values) / total if alloc_values else 0 | |
| return EpisodeMetrics( | |
| episode_id=state.get("episode_id", "?"), | |
| turns_taken=state.get("turn", 0), | |
| resolution_type=state.get("resolution_type", "timeout"), | |
| coalition_formed=coalition_formed, | |
| coalition_detected=any(c.get("detected") for c in coalitions), | |
| overseer_tp=tp, | |
| overseer_fp=fp, | |
| overseer_fn=fn, | |
| overseer_f1=f1, | |
| avg_deal_quality=max_possible, | |
| gini_coefficient=gini_val, | |
| total_reward_overseer=0.0, | |
| total_reward_negotiators=0.0, | |
| ) | |
| def _aggregate_metrics(metrics: list[EpisodeMetrics]) -> EvalSummary: | |
| f1_scores = [m.overseer_f1 for m in metrics] | |
| deal_q = [m.avg_deal_quality for m in metrics] | |
| gini_vals = [m.gini_coefficient for m in metrics] | |
| deals = [m for m in metrics if m.resolution_type == "deal"] | |
| fp_rates = [m.overseer_fp / max(m.overseer_tp + m.overseer_fp, 1) for m in metrics] | |
| detection_rates = [1.0 if m.coalition_detected else 0.0 for m in metrics if m.coalition_formed] | |
| return EvalSummary( | |
| n_episodes=len(metrics), | |
| mean_overseer_f1=float(np.mean(f1_scores)), | |
| std_overseer_f1=float(np.std(f1_scores)), | |
| mean_deal_quality=float(np.mean(deal_q)), | |
| mean_gini=float(np.mean(gini_vals)), | |
| coalition_detection_rate=float(np.mean(detection_rates)) if detection_rates else 0.0, | |
| false_positive_rate=float(np.mean(fp_rates)), | |
| deal_rate=len(deals) / max(len(metrics), 1), | |
| mean_turns_to_deal=float(np.mean([m.turns_taken for m in deals])) if deals else 20.0, | |
| mean_overseer_reward=float(np.mean([m.total_reward_overseer for m in metrics])), | |
| mean_negotiator_reward=float(np.mean([m.total_reward_negotiators for m in metrics])), | |
| ) | |
| def print_comparison(before: EvalSummary, after: EvalSummary) -> None: | |
| """Print before/after comparison table — perfect for demo and HF blog.""" | |
| print("\n" + "=" * 60) | |
| print(" NegotiArena BEFORE vs AFTER Training") | |
| print("=" * 60) | |
| rows = [ | |
| ("Overseer F1 (detection)", f"{before.mean_overseer_f1:.3f}", f"{after.mean_overseer_f1:.3f}"), | |
| ("Coalition Detection Rate", f"{before.coalition_detection_rate:.1%}", f"{after.coalition_detection_rate:.1%}"), | |
| ("False Positive Rate", f"{before.false_positive_rate:.1%}", f"{after.false_positive_rate:.1%}"), | |
| ("Avg Deal Quality", f"{before.mean_deal_quality:.3f}", f"{after.mean_deal_quality:.3f}"), | |
| ("Fairness (Gini)", f"{before.mean_gini:.3f}", f"{after.mean_gini:.3f}"), | |
| ("Deal Rate", f"{before.deal_rate:.1%}", f"{after.deal_rate:.1%}"), | |
| ] | |
| print(f" {'Metric':<30} {'BEFORE':>10} {'AFTER':>10} {'Δ':>8}") | |
| print("-" * 60) | |
| for name, b, a in rows: | |
| try: | |
| delta = float(a.rstrip("%")) - float(b.rstrip("%")) | |
| sign = "+" if delta > 0 else "" | |
| print(f" {name:<30} {b:>10} {a:>10} {sign}{delta:.3f}") | |
| except ValueError: | |
| print(f" {name:<30} {b:>10} {a:>10}") | |
| print("=" * 60) | |
| def main(): | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--checkpoint", type=str, default=None) | |
| parser.add_argument("--episodes", type=int, default=50) | |
| parser.add_argument("--output", type=str, default="eval_results.json") | |
| args = parser.parse_args() | |
| print("Running BEFORE (random policy) evaluation...") | |
| before = evaluate_random_policy(n_episodes=args.episodes) | |
| if args.checkpoint: | |
| print("Running AFTER (trained model) evaluation...") | |
| after = evaluate_trained_policy(args.checkpoint, n_episodes=args.episodes) | |
| print_comparison(before, after) | |
| results = { | |
| "before": asdict(before), | |
| "after": asdict(after), | |
| } | |
| else: | |
| print("No checkpoint provided. Showing baseline metrics only.") | |
| print(json.dumps(asdict(before), indent=2)) | |
| results = {"before": asdict(before)} | |
| with open(args.output, "w") as f: | |
| json.dump(results, f, indent=2) | |
| print(f"\n✅ Eval results saved to {args.output}") | |
| if __name__ == "__main__": | |
| main() |