| """ |
| difficulty_controller.py β Adaptive drift difficulty controller (v2). |
| |
| Tracks agent belief_accuracy per drift_type over a sliding window. |
| Escalates difficulty when the agent consistently performs well above threshold. |
| |
| Why this matters: |
| Prevents the agent from overfitting to 12 fixed scenarios. |
| The environment generates effectively infinite scenarios by combining drift |
| types and adjusting parameters based on agent competence. |
| |
| Escalation levels: |
| 0 β base scenarios unchanged |
| 1 β combine two drift types in same episode (secondary drift added 2 steps later) |
| 2 β noisy error messages (ambiguous 422 bodies β "validation error" without field name) |
| 3 β force silent semantic drift + noisy errors (hardest: no error signal at all) |
| """ |
| from __future__ import annotations |
|
|
| from typing import Dict, List |
| from collections import defaultdict, deque |
|
|
|
|
| ESCALATION_THRESHOLD = 0.80 |
| ESCALATION_WINDOW = 5 |
| EASY_DRIFT_TYPES = ["field_rename", "endpoint_version", "policy_change"] |
| HARD_DRIFT_TYPES = ["silent_semantic"] |
|
|
|
|
| class DriftDifficultyController: |
| """ |
| Tracks agent performance and adaptively escalates drift difficulty. |
| A single instance persists across all episodes for a given server process. |
| """ |
|
|
| def __init__(self): |
| |
| self._history: Dict[str, deque] = defaultdict( |
| lambda: deque(maxlen=ESCALATION_WINDOW) |
| ) |
| self._escalation_level: int = 0 |
|
|
| def record(self, drift_type: str, belief_accuracy: float) -> None: |
| """Call after each episode finishes to update the performance history.""" |
| self._history[drift_type].append(belief_accuracy) |
| self._maybe_escalate() |
|
|
| def _maybe_escalate(self) -> None: |
| """ |
| Escalate if agent averages above threshold on ALL easy drift types |
| for the last ESCALATION_WINDOW episodes. |
| """ |
| if self._escalation_level >= 3: |
| return |
| for dt in EASY_DRIFT_TYPES: |
| hist = list(self._history[dt]) |
| if len(hist) < ESCALATION_WINDOW: |
| return |
| avg = sum(hist) / len(hist) |
| if avg < ESCALATION_THRESHOLD: |
| return |
| self._escalation_level += 1 |
|
|
| def get_scenario_params(self, base_scenario: dict) -> dict: |
| """ |
| Modify a base scenario dict based on current escalation level. |
| |
| Level 0: return base scenario unchanged |
| Level 1: add secondary_drift_step 2 steps after primary (combines two drift types) |
| Level 2: set noisy_errors=True (ambiguous error messages) |
| Level 3: force drift_type to "silent_semantic" + noisy errors |
| """ |
| if self._escalation_level == 0: |
| return base_scenario |
|
|
| modified = dict(base_scenario) |
|
|
| if self._escalation_level >= 1: |
| |
| primary_step = base_scenario.get("drift_trigger_step", 3) |
| modified["secondary_drift_step"] = primary_step + 2 |
| modified["secondary_drift_type"] = "policy_change" |
| |
| if "initial_world" in modified: |
| modified["initial_world"] = dict(modified["initial_world"]) |
|
|
| if self._escalation_level >= 2: |
| |
| modified["noisy_errors"] = True |
| |
| if "mutated_world" in modified: |
| modified["mutated_world"] = dict(modified["mutated_world"]) |
| modified["mutated_world"]["noisy_errors"] = True |
|
|
| if self._escalation_level >= 3: |
| |
| modified["drift_type"] = "silent_semantic" |
| modified["noisy_errors"] = True |
| if "mutated_world" in modified: |
| modified["mutated_world"]["noisy_errors"] = True |
|
|
| return modified |
|
|
| def deescalate(self) -> None: |
| """Manually lower escalation level (for testing or reset).""" |
| if self._escalation_level > 0: |
| self._escalation_level -= 1 |
|
|
| def reset(self) -> None: |
| """Reset all state (for testing).""" |
| self._history.clear() |
| self._escalation_level = 0 |
|
|
| @property |
| def level(self) -> int: |
| return self._escalation_level |
|
|
| def get_history_summary(self) -> Dict[str, float]: |
| """Returns current average belief_accuracy per drift type (for logging).""" |
| return { |
| dt: (sum(hist) / len(hist) if hist else 0.0) |
| for dt, hist in self._history.items() |
| } |
|
|