""" task3_hard.py ───────────── Task 3 (Hard) — Full Triage Episode The agent manages a 30-step episode. Each step it observes new sensor readings and chooses one of 4 actions. It must balance: - Detecting true anomaly events (sensitivity) - Avoiding false alerts when the patient is stable (specificity) - Using proportionate alert levels (soft before escalate) Grading uses a composite F1-style score: precision = TP / (TP + FP) — of all alerts, how many were warranted? recall = TP / (TP + FN) — of all anomaly steps, how many were caught? f1 = 2 * P * R / (P + R) level_bonus = partial credit for using appropriate alert levels final_score = 0.70 * f1 + 0.30 * level_bonus ∈ [0.0, 1.0] """ from __future__ import annotations from typing import List, Dict, Any import sys, os sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) from env.cognitive_env import CogTraceEnv from env.patient_simulator import PatientConfig, make_hard_patient TASK_ID = "task3_hard" TASK_NAME = "Full Triage Episode" DIFFICULTY = "hard" DESCRIPTION = ( "Manage a 30-step patient monitoring episode. " "Each step, observe behavioral signals and choose an action: " "0=do_nothing, 1=soft_alert, 2=medium_alert, 3=escalate. " "Scored on F1 (sensitivity + specificity) and alert proportionality." ) # Mapping: anomaly active → ideal minimum action level _IDEAL_LEVEL: Dict[bool, int] = {True: 2, False: 0} def _f1_score(tp: int, fp: int, fn: int) -> float: precision = tp / (tp + fp) if (tp + fp) > 0 else 0.0 recall = tp / (tp + fn) if (tp + fn) > 0 else 0.0 if precision + recall == 0: return 0.0 return 2 * precision * recall / (precision + recall) def _level_bonus(trajectory: List[Dict[str, Any]]) -> float: """ Score how well the agent matched alert level to anomaly severity. During anomaly: soft alert scores 0.5, medium 0.8, escalate 1.0 Outside anomaly: do_nothing 1.0, soft 0.5, medium 0.2, escalate 0.0 """ scores = [] for step in trajectory: a = step["action"] anomaly = step["anomaly_active"] if anomaly: level_score = {0: 0.0, 1: 0.50, 2: 0.80, 3: 1.00}[a] else: level_score = {0: 1.00, 1: 0.50, 2: 0.20, 3: 0.00}[a] scores.append(level_score) return sum(scores) / len(scores) if scores else 0.0 def run_episode(env: CogTraceEnv, agent_fn) -> Dict[str, Any]: """ Run a 30-step episode. agent_fn : callable(observation_dict, step: int, history: list) -> int Receives current observation, step index, and list of past actions. Must return action 0–3. """ obs = env.reset() trajectory = [] action_history = [] tp = fp = fn = tn = 0 for step in range(env.config.episode_length): action = int(agent_fn(obs.model_dump(), step, list(action_history))) action = max(0, min(3, action)) next_obs, reward, done, info = env.step(action) alerted = (action > 0) anomaly = info.anomaly_active if alerted and anomaly: tp += 1 elif alerted and not anomaly: fp += 1 elif not alerted and anomaly: fn += 1 else: tn += 1 trajectory.append({ "step": step, "action": action, "anomaly_active": anomaly, "reward": reward.value, "true_stage": info.true_stage, }) action_history.append(action) obs = next_obs if done: break f1 = _f1_score(tp, fp, fn) lb = _level_bonus(trajectory) final_score = round(0.70 * f1 + 0.30 * lb, 4) return { "tp": tp, "fp": fp, "fn": fn, "tn": tn, "f1_score": round(f1, 4), "level_bonus": round(lb, 4), "score": final_score, "trajectory": trajectory, } def grade(trajectory: List[Dict[str, Any]]) -> float: """ Grade a list of episode result dicts. Each dict must contain "score" in [0.0, 1.0]. Returns mean score. """ if not trajectory: return 0.0 return round(sum(r["score"] for r in trajectory) / len(trajectory), 4) def build_env(seed: int = 0) -> CogTraceEnv: sim = make_hard_patient(seed) return CogTraceEnv(config=sim.config) if __name__ == "__main__": import random results = [] for seed in range(10): env = build_env(seed) result = run_episode( env, agent_fn=lambda obs, step, hist: random.randint(0, 3) ) results.append(result) final = grade(results) print(f"[Task 3] Random baseline score: {final:.4f}") print("Expected ~0.25–0.35 (random F1 on imbalanced classes)")