""" Evaluation script for the trained moderation agent. Tests escalation behavior, fairness, and edge cases. """ import numpy as np import json from sb3_contrib import MaskablePPO from src.env.discord_env import DiscordEnv from src.env.wrappers import MetricsTrackingWrapper from sb3_contrib.common.wrappers import ActionMasker import gymnasium as gym ACTION_NAMES = ["ALLOW", "WARN", "DELETE", "TIMEOUT", "BAN"] def mask_fn(env: gym.Env) -> np.ndarray: return env.action_masks() def evaluate_on_episodes(model_path: str, n_episodes: int = 100): """Run deterministic evaluation on real data episodes.""" env = DiscordEnv() env = MetricsTrackingWrapper(env) env = ActionMasker(env, mask_fn) model = MaskablePPO.load(model_path) results = [] action_totals = {i: 0 for i in range(5)} total_fp = 0 total_fn = 0 total_steps = 0 for ep in range(n_episodes): obs, info = env.reset() done = False while not done: mask = env.action_masks() action, _ = model.predict(obs, deterministic=True, action_masks=mask) obs, reward, terminated, truncated, info = env.step(action) done = terminated or truncated if "episode_metrics" in info: m = info["episode_metrics"] results.append(m) for a, count in m["action_distribution"].items(): action_totals[int(a)] += count total_fp += m["false_positives"] total_fn += m["false_negatives"] total_steps += m["length"] print(f"\n{'='*60}") print(f"EVALUATION RESULTS ({n_episodes} episodes, {total_steps} steps)") print(f"{'='*60}") print(f"\nAction Distribution:") for a in range(5): pct = action_totals[a] / max(total_steps, 1) * 100 bar = "ā–ˆ" * int(pct / 2) print(f" {ACTION_NAMES[a]:8s}: {action_totals[a]:5d} ({pct:5.1f}%) {bar}") avg_reward = np.mean([r["cumulative_reward"] for r in results]) print(f"\nAvg Episode Reward: {avg_reward:.3f}") print(f" FP Rate: {total_fp / max(total_steps, 1):.4f}") print(f" FN Rate: {total_fn / max(total_steps, 1):.4f}") # Fairness check print(f"\nšŸŒ Language Fairness Check:") lang_bans = {} lang_totals = {} for r in results: for lang, actions in r.get("language_actions", {}).items(): lang_totals[lang] = lang_totals.get(lang, 0) + sum(actions.values()) lang_bans[lang] = lang_bans.get(lang, 0) + actions.get(4, 0) if lang_totals: overall_ban_rate = sum(lang_bans.values()) / max(sum(lang_totals.values()), 1) print(f" Overall BAN rate: {overall_ban_rate:.4f}") for lang in sorted(lang_totals.keys()): ban_rate = lang_bans.get(lang, 0) / max(lang_totals[lang], 1) ratio = ban_rate / max(overall_ban_rate, 1e-8) flag = "DISPARITY" if ratio > 1.5 and ban_rate > 0 else "" print(f" {lang:6s}: BAN rate={ban_rate:.4f} (ratio={ratio:.2f}) {flag}") def test_escalation_scenarios(model_path: str): """ Test specific behavioral scenarios to verify escalation logic. Uses synthetic observations to control exact inputs. """ model = MaskablePPO.load(model_path) print(f"\n{'='*60}") print(f"ESCALATION SCENARIO TESTS") print(f"{'='*60}") # Scenario 1: Gradual escalation with repeat offenses print(f"\n--- Scenario 1: Repeat Mild Offender ---") embedding = np.random.randn(384).astype(np.float32) * 0.1 warns = 0.0 timeouts = 0.0 total = 0.0 for step, tox in enumerate([0.1, 0.4, 0.45, 0.5, 0.55, 0.6, 0.7, 0.8, 0.9]): obs = { "message_embedding": embedding, "toxicity_score": np.array([tox], dtype=np.float32), "user_history": np.array([ min(warns / 5.0, 1.0), min(timeouts / 3.0, 1.0), min(total / 10.0, 1.0), ], dtype=np.float32), "server_heat": np.array([0.2, 0.1], dtype=np.float32), "language_id": np.array([1,0,0,0,0,0,0,0,0,0,0,0,0], dtype=np.float32), # English } mask = np.ones(5, dtype=bool) if tox < 0.20: mask[3:] = False elif tox < 0.35: mask[4] = False elif tox >= 0.90: mask[0] = False action, _ = model.predict(obs, deterministic=True, action_masks=mask) action = int(action) if action == 1: warns += 1; total += 1 elif action == 2: total += 1 elif action == 3: timeouts += 1; total += 1 elif action == 4: total += 1 print(f" Step {step+1}: tox={tox:.2f} → {ACTION_NAMES[action]} " f"(w={warns:.0f}, t={timeouts:.0f}, inf={total:.0f})") # Scenario 2: Rehabilitation print(f"\n--- Scenario 2: Rehabilitation (toxic then clean) ---") warns = 2.0 timeouts = 1.0 total = 3.0 for step, tox in enumerate([0.05, 0.08, 0.03, 0.10, 0.05]): obs = { "message_embedding": embedding, "toxicity_score": np.array([tox], dtype=np.float32), "user_history": np.array([ min(warns / 5.0, 1.0), min(timeouts / 3.0, 1.0), min(total / 10.0, 1.0), ], dtype=np.float32), "server_heat": np.array([0.1, 0.05], dtype=np.float32), "language_id": np.array([1,0,0,0,0,0,0,0,0,0,0,0,0], dtype=np.float32), # English } mask = np.ones(5, dtype=bool) mask[3:] = False # Low tox action, _ = model.predict(obs, deterministic=True, action_masks=mask) print(f" Step {step+1}: tox={tox:.2f} → {ACTION_NAMES[int(action)]}") if __name__ == "__main__": import sys model_path = sys.argv[1] if len(sys.argv) > 1 else "data/models/best/best_model.zip" evaluate_on_episodes(model_path, n_episodes=100) test_escalation_scenarios(model_path)