Spaces:
Sleeping
Sleeping
| """Async customer support environment conforming to the OpenEnv contract.""" | |
| from __future__ import annotations | |
| from pathlib import Path | |
| from typing import Any, Literal | |
| from pydantic import ValidationError | |
| from env.billing import BillingRecord, BillingSystem, FailedPayment, Invoice | |
| from env.crm import CRMSystem | |
| from env.customers import CustomerQueueManager | |
| from env.errors import EnvironmentDoneError, EnvironmentNotResetError | |
| from env.incident_history import IncidentHistoryStore | |
| from env.knowledge_base import KBArticle, PersistentKnowledgeBase | |
| from env.policy_engine import PolicyChange as EnginePolicyChange | |
| from env.policy_engine import PolicyEngine | |
| from env.runbooks import RunbookEngine | |
| from env.state import INCIDENT_PHASE_VALID_ACTIONS, PHASE_VALID_ACTIONS, IncidentState, InternalState | |
| from env.stakeholders import StakeholderManager | |
| from env.world import WorldState | |
| from graders.grader import DeterministicGrader | |
| from graders.investigation_grader import ChangeAdvisoryBoard | |
| from models.action import ACTION_CLASSES | |
| from models.action import ( | |
| Action, | |
| ActionAdapter, | |
| ApplyFixAction, | |
| CheckBillingAction, | |
| CheckMonitoringAction, | |
| CheckPolicyAction, | |
| ClassifyAction, | |
| EscalateAction, | |
| FetchLogsAction, | |
| FetchUserDataAction, | |
| FollowRunbookStepAction, | |
| NotifyStakeholdersAction, | |
| ProbeServiceAction, | |
| QueryIncidentHistoryAction, | |
| QueryKBAction, | |
| RequestInfoAction, | |
| RollbackFixAction, | |
| RespondAction, | |
| ResolveAction, | |
| RouteAction, | |
| UpdateKBAction, | |
| VerifyFixAction, | |
| WritePostmortemAction, | |
| ) | |
| from models.incident import IncidentScenario | |
| from models.step_result import StepResult | |
| from tasks.incident_bank import IncidentBank | |
| from tasks.ticket_bank import TicketBank | |
| _TicketAction = ( | |
| ClassifyAction | |
| | RouteAction | |
| | RespondAction | |
| | EscalateAction | |
| | ResolveAction | |
| | RequestInfoAction | |
| ) | |
| _IncidentPhase = Literal["triage", "investigation", "response", "resolution"] | |
| # Quality multiplier applied to keyword scores when the agent skips | |
| # request_info on a partial-info ticket. Creates a speed-vs-quality | |
| # trade-off: skipping saves a step (less SLA risk) but caps quality. | |
| _INFO_SKIP_QUALITY_FACTOR = 0.6 | |
| class _IncidentRuntime: | |
| """Incident mode runtime state for phase-3 action dispatch.""" | |
| __slots__ = ( | |
| "seed", | |
| "incident", | |
| "world", | |
| "crm", | |
| "billing", | |
| "policy_engine", | |
| "history_store", | |
| "runbook_engine", | |
| "stakeholder_mgr", | |
| "customer_queue_mgr", | |
| "persistent_kb", | |
| "knowledge_base", | |
| "incident_phase", | |
| "steps_taken", | |
| "max_steps", | |
| "done", | |
| "known_facts", | |
| "active_policies", | |
| "tool_results", | |
| "active_alerts", | |
| "suggested_runbook", | |
| "actions_log", | |
| "cumulative_reward", | |
| "last_action_json", | |
| "last_reward_breakdown", | |
| "severity", | |
| ) | |
| def __init__( | |
| self, | |
| *, | |
| seed: int, | |
| incident: IncidentScenario, | |
| world: WorldState, | |
| crm: CRMSystem, | |
| billing: BillingSystem, | |
| policy_engine: PolicyEngine, | |
| history_store: IncidentHistoryStore, | |
| runbook_engine: RunbookEngine, | |
| stakeholder_mgr: StakeholderManager, | |
| customer_queue_mgr: CustomerQueueManager, | |
| persistent_kb: PersistentKnowledgeBase, | |
| ) -> None: | |
| self.seed = seed | |
| self.incident = incident | |
| self.world = world | |
| self.crm = crm | |
| self.billing = billing | |
| self.policy_engine = policy_engine | |
| self.history_store = history_store | |
| self.runbook_engine = runbook_engine | |
| self.stakeholder_mgr = stakeholder_mgr | |
| self.customer_queue_mgr = customer_queue_mgr | |
| self.persistent_kb = persistent_kb | |
| self.knowledge_base = persistent_kb.reset_for_episode(incident) | |
| self.incident_phase: _IncidentPhase = "triage" | |
| self.steps_taken = 0 | |
| self.max_steps = incident.max_steps | |
| self.done = False | |
| self.known_facts: dict[str, Any] = {} | |
| self.active_policies: dict[str, Any] = {} | |
| self.tool_results: dict[str, Any] | None = None | |
| self.active_alerts: list[str] = [] | |
| self.suggested_runbook = _suggested_runbook(runbook_engine, incident) | |
| self.actions_log = [] | |
| self.cumulative_reward = 0.0 | |
| self.last_action_json: str | None = None | |
| self.last_reward_breakdown: dict[str, float] = {} | |
| self.severity: str | None = None | |
| def available_actions(self) -> list[str]: | |
| return sorted(INCIDENT_PHASE_VALID_ACTIONS[self.incident_phase]) | |
| def record_action(self, action_type: str, feedback: str, reward: float) -> None: | |
| from models.observation import ActionRecord | |
| self.actions_log.append( | |
| ActionRecord( | |
| step=self.steps_taken, | |
| action_taken=action_type, | |
| env_feedback=feedback, | |
| reward_earned=round(reward, 4), | |
| ) | |
| ) | |
| self.steps_taken += 1 | |
| self.cumulative_reward += reward | |
| if self.steps_taken >= self.max_steps: | |
| self.done = True | |
| def to_observation(self): | |
| from models.observation import Observation | |
| ticket = self.world.support_queue[0] if self.world.support_queue else None | |
| ticket_text = ( | |
| getattr(ticket, "body", None) or getattr(ticket, "ticket_text", None) or "" | |
| ) if ticket is not None else self.incident.description | |
| return Observation( | |
| ticket_id=ticket.ticket_id if ticket is not None else self.incident.incident_id, | |
| ticket_text=ticket_text, | |
| customer_sentiment="frustrated", | |
| customer_tier="enterprise", | |
| customer_value="high", | |
| category_hint=None, | |
| constraints=[], | |
| phase="responding" if self.incident_phase in ("response", "resolution") else "classified", | |
| available_actions=self.available_actions, | |
| current_step=self.steps_taken, | |
| max_steps=self.max_steps, | |
| sla_steps_remaining=max(0, self.max_steps - self.steps_taken), | |
| history=list(self.actions_log), | |
| max_total_reward=self.incident.max_total_reward, | |
| incident_id=self.incident.incident_id, | |
| incident_title=self.incident.title, | |
| mode="incident", | |
| system_status=self.world.service_mesh.get_health_summary(), | |
| active_alerts=list(self.active_alerts), | |
| tool_results=self.tool_results, | |
| known_facts=dict(self.known_facts), | |
| active_policies=dict(self.active_policies), | |
| stakeholder_patience=self.stakeholder_mgr.get_patience_levels(), | |
| pending_customer_tickets=len(self.world.support_queue), | |
| incident_phase=self.incident_phase, | |
| suggested_runbook=self.suggested_runbook, | |
| total_incident_cost=self.world.total_downtime_cost, | |
| ) | |
| def to_info(self) -> dict[str, Any]: | |
| return { | |
| "mode": "incident", | |
| "incident_id": self.incident.incident_id, | |
| "incident_phase": self.incident_phase, | |
| "steps_taken": self.steps_taken, | |
| "max_steps": self.max_steps, | |
| "cumulative_reward": round(self.cumulative_reward, 4), | |
| "known_facts": dict(self.known_facts), | |
| "active_policies": dict(self.active_policies), | |
| "total_incident_cost": self.world.total_downtime_cost, | |
| "reward_breakdown": dict(self.last_reward_breakdown), | |
| } | |
| class CustomerSupportEnv: | |
| """Production-grade async environment for customer support triage. | |
| Usage:: | |
| env = CustomerSupportEnv() | |
| res = await env.reset(seed=0, difficulty="easy") | |
| while not res.done: | |
| res = await env.step(agent.act(res.observation)) | |
| await env.close() | |
| """ | |
| def __init__( | |
| self, | |
| ticket_bank: TicketBank | None = None, | |
| incident_bank: IncidentBank | None = None, | |
| ) -> None: | |
| self._bank = ticket_bank or TicketBank() | |
| self._incident_bank = incident_bank or IncidentBank() | |
| self._state: InternalState | None = None | |
| self._incident_state: IncidentState | None = None | |
| self._mode: Literal["ticket", "incident"] = "ticket" | |
| self._grader = DeterministicGrader() | |
| self._cab = ChangeAdvisoryBoard() | |
| # ================================================================== | |
| # Public async API | |
| # ================================================================== | |
| async def reset( | |
| self, | |
| seed: int = 0, | |
| difficulty: str | None = None, | |
| mode: Literal["ticket", "incident"] = "ticket", | |
| ) -> StepResult: | |
| """Start a new episode in ticket or incident mode.""" | |
| self._mode = mode | |
| self._incident_state = None | |
| self._state = None | |
| if mode == "incident": | |
| return self._reset_incident(seed=seed, difficulty=difficulty) | |
| ticket = self._bank.get_ticket(seed=seed, difficulty=difficulty) | |
| self._state = InternalState(ticket) | |
| return StepResult( | |
| observation=self._state.to_observation(), | |
| reward=0.0, | |
| done=False, | |
| info=self._state.to_info(), | |
| ) | |
| def _reset_incident(self, seed: int, difficulty: str | None) -> StepResult: | |
| incident = self._incident_bank.get_incident(seed=seed, difficulty=difficulty) | |
| world = WorldState(seed=seed, incident=incident) | |
| crm = CRMSystem(incident.affected_customer_profiles) | |
| billing = BillingSystem(_build_billing_records(incident)) | |
| history_store = IncidentHistoryStore.from_json( | |
| Path(__file__).resolve().parents[1] / "tasks" / "history_incidents.json" | |
| ) | |
| runbook_engine = RunbookEngine.from_json( | |
| Path(__file__).resolve().parents[1] / "tasks" / "runbooks.json" | |
| ) | |
| stakeholder_mgr = StakeholderManager() | |
| customer_queue_mgr = CustomerQueueManager(crm=crm) | |
| persistent_kb = PersistentKnowledgeBase(base_articles=_base_kb_articles()) | |
| policy_engine = PolicyEngine( | |
| initial_policies=dict(incident.initial_policies), | |
| drift_schedule=_convert_policy_schedule(incident), | |
| ) | |
| knowledge_base = persistent_kb.reset_for_episode(incident) | |
| self._incident_state = IncidentState( | |
| incident=incident, | |
| world=world, | |
| crm=crm, | |
| billing=billing, | |
| policy_engine=policy_engine, | |
| history_store=history_store, | |
| runbook_engine=runbook_engine, | |
| stakeholder_mgr=stakeholder_mgr, | |
| customer_queue_mgr=customer_queue_mgr, | |
| persistent_kb=persistent_kb, | |
| knowledge_base=knowledge_base, | |
| suggested_runbook=_suggested_runbook(runbook_engine, incident), | |
| ) | |
| return StepResult( | |
| observation=self._incident_state.to_observation(), | |
| reward=0.0, | |
| done=False, | |
| info=self._incident_state.to_info(), | |
| ) | |
| async def step(self, action: dict[str, Any] | Action) -> StepResult: # type: ignore[type-arg] | |
| """Apply *action*, return ``(observation, reward, done, info)``.""" | |
| if self._mode == "incident": | |
| return self._step_incident(action) | |
| state = self._require_active_state_ticket() | |
| # --- parse ------------------------------------------------------- | |
| parsed = self._safe_parse(action) | |
| if parsed is None: | |
| return self._penalty(state, "invalid_parse", "Action failed schema validation.") | |
| # --- repeat detection -------------------------------------------- | |
| action_json = parsed.model_dump_json(exclude_none=True) | |
| repeat_pen = -0.05 if action_json == state.last_action_json else 0.0 | |
| state.last_action_json = action_json | |
| # --- phase gate -------------------------------------------------- | |
| action_type: str = parsed.action_type # type: ignore[union-attr] | |
| valid = PHASE_VALID_ACTIONS[state.phase] | |
| if action_type not in valid: | |
| return self._penalty( | |
| state, | |
| action_type, | |
| f"Action '{action_type}' not valid in phase '{state.phase}'. " | |
| f"Valid: {sorted(valid)}", | |
| ) | |
| # --- dispatch & reward ------------------------------------------- | |
| reward, feedback, breakdown = self._dispatch_ticket(state, parsed) | |
| reward += repeat_pen | |
| # --- SLA penalty ------------------------------------------------- | |
| sla_pen = self._grader.sla_penalty(state.steps_taken, state.sla_steps) | |
| reward += sla_pen | |
| parts = [feedback] | |
| if repeat_pen < 0: | |
| parts.append(f"Repeat penalty: {repeat_pen:+.2f}.") | |
| if sla_pen < 0: | |
| state.urgency_penalty_accrued += abs(sla_pen) | |
| over = state.steps_taken - state.sla_steps + 1 | |
| parts.append(f"SLA exceeded ({over} over): {sla_pen:+.2f}.") | |
| feedback = " ".join(parts) | |
| # --- clamp ------------------------------------------------------- | |
| reward = max(-0.25, min(reward, 0.30)) | |
| # --- reward breakdown -------------------------------------------- | |
| breakdown["repeat_penalty"] = repeat_pen | |
| breakdown["sla_penalty"] = sla_pen | |
| breakdown["total"] = round(reward, 4) | |
| state.last_reward_breakdown = breakdown | |
| state.record_action(action_type, feedback, reward) | |
| if action_type == "resolve": | |
| state.phase = "resolved" | |
| state.done = True | |
| self._finalize_escalation_score(state) | |
| return StepResult( | |
| observation=state.to_observation(), | |
| reward=round(reward, 4), | |
| done=state.done, | |
| info=state.to_info(), | |
| ) | |
| async def state(self) -> StepResult | None: | |
| """Return the current observation without advancing the episode.""" | |
| if self._mode == "incident": | |
| if self._incident_state is None: | |
| return None | |
| return StepResult( | |
| observation=self._incident_state.to_observation(), | |
| reward=0.0, | |
| done=self._incident_state.episode_done, | |
| info=self._incident_state.to_info(), | |
| ) | |
| if self._state is None: | |
| return None | |
| return StepResult( | |
| observation=self._state.to_observation(), | |
| reward=0.0, | |
| done=self._state.done, | |
| info=self._state.to_info(), | |
| ) | |
| async def close(self) -> None: | |
| """Release resources and reset internal state.""" | |
| self._state = None | |
| self._incident_state = None | |
| self._mode = "ticket" | |
| # ================================================================== | |
| # Private helpers | |
| # ================================================================== | |
| def _require_active_state_ticket(self) -> InternalState: | |
| if self._state is None: | |
| raise EnvironmentNotResetError("Call reset() before step().") | |
| if self._state.done: | |
| raise EnvironmentDoneError("Episode ended. Call reset() for a new one.") | |
| return self._state | |
| def _safe_parse(self, action: dict[str, Any] | Action) -> Action | None: # type: ignore[type-arg] | |
| if isinstance(action, dict): | |
| try: | |
| return ActionAdapter.validate_python(action) # type: ignore[return-value] | |
| except (ValidationError, ValueError): | |
| return None | |
| if isinstance(action, ACTION_CLASSES): | |
| return action | |
| return None | |
| def _penalty(self, state: InternalState, label: str, feedback: str) -> StepResult: | |
| reward = -0.05 | |
| state.last_reward_breakdown = {"penalty": reward, "total": reward} | |
| state.record_action(label, feedback, reward) | |
| return StepResult( | |
| observation=state.to_observation(), | |
| reward=reward, | |
| done=state.done, | |
| info=state.to_info(), | |
| ) | |
| def _finalize_escalation_score(state: InternalState) -> None: | |
| """Set escalation_score when it was never explicitly determined.""" | |
| if state.escalation_score is not None: | |
| return | |
| if state.ticket.requires_escalation: | |
| state.escalation_score = 0.0 # missed required escalation | |
| else: | |
| state.escalation_score = 1.0 # correctly never escalated | |
| # ------------------------------------------------------------------ | |
| # Action dispatch | |
| # ------------------------------------------------------------------ | |
| _DispatchResult = tuple[float, str, dict[str, float]] | |
| def _dispatch_ticket(self, state: InternalState, action: Action) -> _DispatchResult: | |
| if isinstance(action, ClassifyAction): | |
| return self._on_classify(state, action) | |
| if isinstance(action, RouteAction): | |
| return self._on_route(state, action) | |
| if isinstance(action, RespondAction): | |
| return self._on_respond(state, action) | |
| if isinstance(action, EscalateAction): | |
| return self._on_escalate(state, action) | |
| if isinstance(action, ResolveAction): | |
| return self._on_resolve(state, action) | |
| if isinstance(action, RequestInfoAction): | |
| return self._on_request_info(state, action) | |
| return -0.05, "Unrecognised action type.", {"penalty": -0.05} | |
| def _step_incident(self, action: dict[str, Any] | Action) -> StepResult: # type: ignore[type-arg] | |
| runtime = self._require_active_incident_state() | |
| parsed = self._safe_parse(action) | |
| if parsed is None: | |
| return self._incident_penalty(runtime, "invalid_parse", "Action failed schema validation.") | |
| action_json = parsed.model_dump_json(exclude_none=True) | |
| repeat_pen = -0.05 if action_json == runtime.last_action_json else 0.0 | |
| runtime.last_action_json = action_json | |
| action_type: str = parsed.action_type # type: ignore[union-attr] | |
| if action_type not in runtime.available_actions: | |
| return self._incident_penalty( | |
| runtime, | |
| action_type, | |
| f"Action '{action_type}' not valid in incident phase '{runtime.incident_phase}'.", | |
| ) | |
| reward, feedback, breakdown = self._dispatch_incident(runtime, parsed) | |
| reward += repeat_pen | |
| reward = max(-0.25, min(reward, 0.30)) | |
| breakdown["repeat_penalty"] = repeat_pen | |
| breakdown["total"] = round(reward, 4) | |
| runtime.last_reward_breakdown = breakdown | |
| runtime.record_action(action_type, feedback, reward) | |
| self._tick_incident_runtime(runtime) | |
| return StepResult( | |
| observation=runtime.to_observation(), | |
| reward=round(reward, 4), | |
| done=runtime.episode_done, | |
| info=runtime.to_info(), | |
| ) | |
| def _require_active_incident_state(self) -> IncidentState: | |
| if self._incident_state is None: | |
| raise EnvironmentNotResetError("Call reset(mode='incident') before step().") | |
| if self._incident_state.episode_done: | |
| raise EnvironmentDoneError("Episode ended. Call reset() for a new one.") | |
| return self._incident_state | |
| def _incident_penalty( | |
| self, runtime: IncidentState, action_label: str, feedback: str | |
| ) -> StepResult: | |
| reward = -0.05 | |
| runtime.last_reward_breakdown = {"penalty": reward, "total": reward} | |
| runtime.record_action(action_label, feedback, reward) | |
| self._tick_incident_runtime(runtime) | |
| return StepResult( | |
| observation=runtime.to_observation(), | |
| reward=reward, | |
| done=runtime.episode_done, | |
| info=runtime.to_info(), | |
| ) | |
| def _tick_incident_runtime(self, runtime: IncidentState) -> None: | |
| world_events = runtime.world.tick() | |
| runtime.policy_engine.apply_scheduled_drifts(runtime.steps_taken) | |
| new_tickets = runtime.customer_queue_mgr.generate_tickets( | |
| runtime.world, runtime.steps_taken | |
| ) | |
| runtime.world.support_queue.extend(new_tickets) | |
| runtime.customer_queue_mgr.update_frustration(runtime.steps_taken) | |
| runtime.stakeholder_mgr.tick() | |
| runtime.active_alerts = [ | |
| event.message for event in world_events if event.event_type == "alert_generated" | |
| ] | |
| runtime.apply_severity_auto_escalation() | |
| runtime.maybe_update_reclassification() | |
| chaos = runtime.chaos_injector.maybe_inject( | |
| runtime.world, runtime.steps_taken, runtime.incident.difficulty | |
| ) | |
| if chaos is not None: | |
| runtime.active_alerts.append(chaos.alert_text) | |
| runtime.known_facts["chaos_event"] = { | |
| "step": chaos.step, | |
| "service": chaos.new_service, | |
| "reason": chaos.reason, | |
| } | |
| runtime.advance_phase() | |
| if runtime.steps_taken >= runtime.max_steps or runtime.all_objectives_complete(): | |
| runtime.episode_done = True | |
| def _dispatch_incident(self, runtime: IncidentState, action: Action) -> _DispatchResult: | |
| if isinstance(action, ClassifyAction): | |
| return self._on_incident_classify(runtime, action) | |
| if isinstance(action, RouteAction): | |
| return self._on_incident_route(runtime, action) | |
| if isinstance(action, RespondAction): | |
| return self._on_incident_respond(runtime, action) | |
| if isinstance(action, EscalateAction): | |
| return self._on_incident_escalate(runtime, action) | |
| if isinstance(action, ResolveAction): | |
| return self._on_incident_resolve(runtime, action) | |
| if isinstance(action, RequestInfoAction): | |
| return self._on_incident_request_info(runtime, action) | |
| if isinstance(action, CheckMonitoringAction): | |
| return self._on_check_monitoring(runtime, action) | |
| if isinstance(action, ProbeServiceAction): | |
| return self._on_probe_service(runtime, action) | |
| if isinstance(action, FetchLogsAction): | |
| return self._on_fetch_logs(runtime, action) | |
| if isinstance(action, FetchUserDataAction): | |
| return self._on_fetch_user_data(runtime, action) | |
| if isinstance(action, CheckBillingAction): | |
| return self._on_check_billing(runtime, action) | |
| if isinstance(action, QueryKBAction): | |
| return self._on_query_kb(runtime, action) | |
| if isinstance(action, CheckPolicyAction): | |
| return self._on_check_policy(runtime, action) | |
| if isinstance(action, QueryIncidentHistoryAction): | |
| return self._on_query_incident_history(runtime, action) | |
| if isinstance(action, ApplyFixAction): | |
| return self._on_apply_fix(runtime, action) | |
| if isinstance(action, VerifyFixAction): | |
| return self._on_verify_fix(runtime, action) | |
| if isinstance(action, RollbackFixAction): | |
| return self._on_rollback_fix(runtime, action) | |
| if isinstance(action, NotifyStakeholdersAction): | |
| return self._on_notify_stakeholders(runtime, action) | |
| if isinstance(action, FollowRunbookStepAction): | |
| return self._on_follow_runbook_step(runtime, action) | |
| if isinstance(action, WritePostmortemAction): | |
| return self._on_write_postmortem(runtime, action) | |
| if isinstance(action, UpdateKBAction): | |
| return self._on_update_kb(runtime, action) | |
| return -0.05, "Unrecognised incident action type.", {"penalty": -0.05} | |
| def _on_incident_classify( | |
| self, runtime: IncidentState, action: ClassifyAction | |
| ) -> _DispatchResult: | |
| severity = _severity_for_difficulty(runtime.incident.difficulty) | |
| correct = action.priority == severity | |
| runtime.current_severity = action.priority # type: ignore[assignment] | |
| runtime.triage_complete = True | |
| reward = 0.1 if correct else 0.02 | |
| return reward, "Incident severity classified.", {"classification": reward} | |
| def _on_incident_route(self, runtime: IncidentState, action: RouteAction) -> _DispatchResult: | |
| runtime.investigation_complete = True | |
| reward = 0.08 if action.department == "technical" else 0.02 | |
| return reward, "Incident routed to response team.", {"routing": reward} | |
| def _on_incident_respond( | |
| self, runtime: IncidentState, action: RespondAction | |
| ) -> _DispatchResult: | |
| reward = 0.05 if action.tone == "empathetic" else 0.01 | |
| return reward, "Customer communication sent.", {"respond": reward} | |
| def _on_incident_escalate( | |
| self, runtime: IncidentState, action: EscalateAction | |
| ) -> _DispatchResult: | |
| if not runtime.resource_budget.consume("escalation"): | |
| return -0.05, "Escalation budget exhausted.", {"escalate": -0.05} | |
| runtime.has_escalated = True | |
| runtime.add_audit_entry( | |
| action_type="escalate", | |
| target=action.target_team, | |
| policy_checked="escalation" in runtime.policies_checked, | |
| compliant=True, | |
| ) | |
| reward = 0.06 if action.target_team in ("engineering", "management") else 0.03 | |
| return reward, "Incident escalated.", {"escalate": reward} | |
| def _on_incident_resolve(self, runtime: IncidentState, action: ResolveAction) -> _DispatchResult: | |
| runtime.response_complete = True | |
| runtime.fix_verified = True | |
| if runtime.world.support_queue: | |
| resolved = runtime.world.support_queue.pop(0) | |
| runtime.tickets_resolved.append(resolved.ticket_id) | |
| runtime.add_audit_entry( | |
| action_type="resolve", | |
| target="ticket", | |
| policy_checked=True, | |
| compliant=True, | |
| ) | |
| return 0.12, "Incident marked resolved.", {"resolve": 0.12} | |
| def _on_incident_request_info( | |
| self, runtime: IncidentState, action: RequestInfoAction | |
| ) -> _DispatchResult: | |
| return 0.03, "Additional customer information requested.", {"request_info": 0.03} | |
| def _on_check_monitoring( | |
| self, runtime: IncidentState, action: CheckMonitoringAction | |
| ) -> _DispatchResult: | |
| snapshot = runtime.world.service_mesh.get_monitoring_data(action.service_name) | |
| data = snapshot.model_dump() | |
| runtime.tool_results = {"check_monitoring": data} | |
| runtime.known_facts["system_status"] = runtime.world.service_mesh.get_health_summary() | |
| runtime.evidence_chain.add_evidence( | |
| step=runtime.steps_taken, | |
| source="check_monitoring", | |
| finding=str(data), | |
| conclusion="monitoring reviewed", | |
| service=action.service_name, | |
| ) | |
| if action.service_name is None: | |
| runtime.triage_complete = True | |
| return 0.02, "Monitoring data retrieved.", {"check_monitoring": 0.02} | |
| def _on_probe_service( | |
| self, runtime: IncidentState, action: ProbeServiceAction | |
| ) -> _DispatchResult: | |
| result = runtime.world.service_mesh.probe_service(action.service_name, action.check_type) | |
| data = result.model_dump() | |
| runtime.tool_results = {"probe_service": data} | |
| runtime.known_facts[f"probe:{action.service_name}"] = data | |
| runtime.evidence_chain.add_evidence( | |
| step=runtime.steps_taken, | |
| source="probe_service", | |
| finding=str(data), | |
| conclusion=f"probe completed for {action.service_name}", | |
| service=action.service_name, | |
| ) | |
| return 0.03, "Service probe completed.", {"probe_service": 0.03} | |
| def _on_fetch_logs(self, runtime: IncidentState, action: FetchLogsAction) -> _DispatchResult: | |
| state = runtime.world.service_mesh.services[action.service_name] | |
| logs = [ | |
| f"{action.service_name} status={state.health}", | |
| f"{action.service_name} error_rate={state.error_rate:.2f}", | |
| f"time_range={action.time_range}", | |
| ] | |
| runtime.tool_results = {"fetch_logs": {"service": action.service_name, "entries": logs}} | |
| runtime.known_facts[f"logs:{action.service_name}"] = logs | |
| runtime.logs_checked_for.add(action.service_name) | |
| runtime.evidence_chain.add_evidence( | |
| step=runtime.steps_taken, | |
| source="fetch_logs", | |
| finding="; ".join(logs), | |
| conclusion=f"log context gathered for {action.service_name}", | |
| service=action.service_name, | |
| ) | |
| return 0.02, "Logs retrieved.", {"fetch_logs": 0.02} | |
| def _on_fetch_user_data( | |
| self, runtime: IncidentState, action: FetchUserDataAction | |
| ) -> _DispatchResult: | |
| data = runtime.crm.fetch_user_data(action.customer_id).model_dump() | |
| runtime.tool_results = {"fetch_user_data": data} | |
| runtime.known_facts[f"customer:{action.customer_id}"] = data | |
| return 0.02, "CRM data fetched.", {"fetch_user_data": 0.02} | |
| def _on_check_billing( | |
| self, runtime: IncidentState, action: CheckBillingAction | |
| ) -> _DispatchResult: | |
| data = runtime.billing.check_billing(action.customer_id).model_dump() | |
| runtime.tool_results = {"check_billing": data} | |
| runtime.known_facts[f"billing:{action.customer_id}"] = data | |
| return 0.02, "Billing data fetched.", {"check_billing": 0.02} | |
| def _on_query_kb(self, runtime: IncidentState, action: QueryKBAction) -> _DispatchResult: | |
| result = runtime.knowledge_base.query(action.query).model_dump() | |
| runtime.tool_results = {"query_kb": result} | |
| runtime.known_facts[f"kb:{action.query}"] = result | |
| runtime.kb_queried = True | |
| return 0.02, "Knowledge base queried.", {"query_kb": 0.02} | |
| def _on_check_policy( | |
| self, runtime: IncidentState, action: CheckPolicyAction | |
| ) -> _DispatchResult: | |
| response = runtime.policy_engine.check_policy(action.policy_type).model_dump() | |
| runtime.active_policies[action.policy_type] = response | |
| runtime.policies_checked.add(action.policy_type) | |
| runtime.tool_results = {"check_policy": response} | |
| return 0.02, "Policy checked.", {"check_policy": 0.02} | |
| def _on_query_incident_history( | |
| self, runtime: IncidentState, action: QueryIncidentHistoryAction | |
| ) -> _DispatchResult: | |
| result = runtime.history_store.query(action.query, action.service_filter).model_dump() | |
| runtime.tool_results = {"query_incident_history": result} | |
| runtime.known_facts[f"history:{action.query}"] = result | |
| return 0.03, "Incident history queried.", {"query_incident_history": 0.03} | |
| def _on_apply_fix(self, runtime: IncidentState, action: ApplyFixAction) -> _DispatchResult: | |
| if not runtime.resource_budget.consume("fix_attempt"): | |
| return -0.05, "Fix attempts exhausted", {"apply_fix": -0.05} | |
| approval = self._cab.review_fix( | |
| fix=action, | |
| evidence_chain=runtime.evidence_chain, | |
| escalated=runtime.has_escalated, | |
| ) | |
| if not approval.approved: | |
| runtime.resource_budget.remaining_fix_attempts += 1 | |
| runtime.tool_results = { | |
| "apply_fix": {"cab_rejected": True, "reason": approval.reason} | |
| } | |
| runtime.add_audit_entry( | |
| action_type="apply_fix", | |
| target=action.service_name, | |
| policy_checked="escalation" in runtime.policies_checked, | |
| compliant=False, | |
| ) | |
| return approval.penalty, approval.reason, {"cab_rejected": 1.0} | |
| result = runtime.world.service_mesh.apply_fix(action.service_name, action.fix_type) | |
| runtime.tool_results = {"apply_fix": result.model_dump()} | |
| runtime.fix_applied = True | |
| runtime.mark_fix_applied(action, result.success) | |
| if result.success: | |
| runtime.response_complete = True | |
| return 0.15, "Fix applied successfully!", {"fix_correct": 1.0} | |
| blast = runtime.world.service_mesh.apply_wrong_fix(action.service_name, action.fix_type) | |
| runtime.tool_results["apply_fix"]["blast_radius"] = blast.model_dump() | |
| return blast.penalty, blast.description, {"blast_radius": 1.0} | |
| def _on_verify_fix(self, runtime: IncidentState, action: VerifyFixAction) -> _DispatchResult: | |
| health = runtime.world.service_mesh.get_health_summary().get(action.service_name, "unknown") | |
| runtime.tool_results = {"verify_fix": {"service_name": action.service_name, "health": health}} | |
| runtime.fix_verified = health == "healthy" | |
| if runtime.fix_verified: | |
| runtime.response_complete = True | |
| reward = 0.05 if health == "healthy" else -0.02 | |
| return reward, "Fix verification completed.", {"verify_fix": reward} | |
| def _on_rollback_fix( | |
| self, runtime: IncidentState, action: RollbackFixAction | |
| ) -> _DispatchResult: | |
| service = runtime.world.service_mesh.services[action.service_name] | |
| if service.fix_applied and not service.fix_correct: | |
| service.health = "degraded" | |
| runtime.tool_results = {"rollback_fix": {"service_name": action.service_name, "rolled_back": True}} | |
| return 0.02, "Rolled back a bad fix.", {"rollback_fix": 0.02} | |
| if service.fix_correct: | |
| service.health = "degraded" | |
| runtime.tool_results = {"rollback_fix": {"service_name": action.service_name, "rolled_back": True}} | |
| return -0.03, "Rolled back a correct fix.", {"rollback_fix": -0.03} | |
| runtime.tool_results = {"rollback_fix": {"service_name": action.service_name, "rolled_back": False}} | |
| return -0.01, "No fix to roll back.", {"rollback_fix": -0.01} | |
| def _on_notify_stakeholders( | |
| self, runtime: IncidentState, action: NotifyStakeholdersAction | |
| ) -> _DispatchResult: | |
| if not runtime.resource_budget.consume("notification"): | |
| return -0.05, "Stakeholder notification budget exhausted.", {"notify_stakeholders": -0.05} | |
| targets = ( | |
| ["vp_engineering", "legal", "support_lead"] | |
| if action.stakeholder == "all" | |
| else [action.stakeholder] | |
| ) | |
| results = [runtime.stakeholder_mgr.notify(target, action.message).model_dump() for target in targets] | |
| runtime.tool_results = {"notify_stakeholders": results} | |
| runtime.add_audit_entry( | |
| action_type="notify_stakeholders", | |
| target=action.stakeholder, | |
| policy_checked="communication" in runtime.policies_checked, | |
| compliant=True, | |
| ) | |
| return 0.03, "Stakeholders notified.", {"notify_stakeholders": 0.03} | |
| def _on_follow_runbook_step( | |
| self, runtime: IncidentState, action: FollowRunbookStepAction | |
| ) -> _DispatchResult: | |
| step = runtime.runbook_engine.follow_runbook_step(action.runbook_id, action.step_index) | |
| is_correct = runtime.runbook_engine.is_correct_for_incident(action.runbook_id) | |
| runtime.tool_results = {"follow_runbook_step": step.model_dump()} | |
| reward = 0.03 if is_correct else -0.08 | |
| return reward, "Runbook step executed.", {"follow_runbook_step": reward} | |
| def _on_write_postmortem( | |
| self, runtime: IncidentState, action: WritePostmortemAction | |
| ) -> _DispatchResult: | |
| has_root_cause = bool(action.root_cause_description.strip()) | |
| has_remediation = len(action.remediation_steps) > 0 | |
| has_prevention = len(action.prevention_measures) > 0 | |
| quality = 0.03 + (0.01 if has_root_cause else 0.0) + (0.01 if has_remediation else 0.0) + (0.01 if has_prevention else 0.0) | |
| runtime.tool_results = {"write_postmortem": {"quality": quality}} | |
| runtime.postmortem_written = True | |
| return quality, "Postmortem recorded.", {"write_postmortem": quality} | |
| def _on_update_kb(self, runtime: IncidentState, action: UpdateKBAction) -> _DispatchResult: | |
| result = runtime.knowledge_base.update_article(action.article_title, action.content) | |
| runtime.persistent_kb.record_update( | |
| title=action.article_title, | |
| content=action.content, | |
| accepted_for_persistence=result.accepted_for_persistence, | |
| ) | |
| runtime.tool_results = {"update_kb": result.model_dump()} | |
| runtime.kb_updated = True | |
| reward = 0.05 if result.accepted_for_persistence else 0.01 | |
| return reward, result.message, {"update_kb": reward} | |
| # ------------------------------------------------------------------ | |
| # Action handlers | |
| # ------------------------------------------------------------------ | |
| def _on_classify( | |
| self, state: InternalState, action: ClassifyAction | |
| ) -> _DispatchResult: | |
| ticket = state.ticket | |
| cat_ok = action.category == ticket.gold_category | |
| pri_ok = action.priority == ticket.gold_priority | |
| state.classification_correct = cat_ok and pri_ok | |
| state.phase = "classified" | |
| if cat_ok and pri_ok: | |
| base, feedback = 0.10, "Correct classification." | |
| elif cat_ok: | |
| base = 0.06 | |
| feedback = f"Category correct; expected priority '{ticket.gold_priority}'." | |
| elif pri_ok: | |
| base = 0.04 | |
| feedback = f"Priority correct; expected category '{ticket.gold_category}'." | |
| else: | |
| base = 0.01 | |
| feedback = ( | |
| f"Incorrect. Expected '{ticket.gold_category}' / '{ticket.gold_priority}'." | |
| ) | |
| urgency_bonus = 0.0 | |
| if pri_ok and ticket.gold_priority in ("critical", "high"): | |
| urgency_bonus = 0.10 | |
| state.urgency_handled = True | |
| feedback += " Urgency correctly identified (+0.10)." | |
| reward = base + urgency_bonus | |
| breakdown = {"classification": base, "urgency_bonus": urgency_bonus} | |
| return reward, feedback, breakdown | |
| def _on_route( | |
| self, state: InternalState, action: RouteAction | |
| ) -> _DispatchResult: | |
| correct = action.department == state.ticket.gold_department | |
| state.routing_correct = correct | |
| state.phase = "routed" | |
| if correct: | |
| return 0.10, "Routed to the correct department.", {"routing": 0.10} | |
| return 0.01, f"Incorrect routing; expected '{state.ticket.gold_department}'.", {"routing": 0.01} | |
| def _on_respond( | |
| self, state: InternalState, action: RespondAction | |
| ) -> _DispatchResult: | |
| ticket = state.ticket | |
| state.phase = "responding" | |
| quality, forbidden_pen = self._grader.weighted_keyword_score( | |
| action.response_text, ticket.response_spec | |
| ) | |
| info_factor = 1.0 | |
| if ticket.partial_info and not state.info_requested: | |
| info_factor = _INFO_SKIP_QUALITY_FACTOR | |
| quality *= info_factor | |
| base_reward = round(quality * 0.20, 4) | |
| forbidden_pen = round(forbidden_pen * ticket.penalty_multiplier, 4) | |
| tone_pen = self._eval_tone_constraint(state, action.tone) | |
| tone_pen = round(tone_pen * ticket.penalty_multiplier, 4) | |
| reward = base_reward + forbidden_pen + tone_pen | |
| state.response_quality_score = quality | |
| parts = [f"Response quality: {quality:.0%}."] | |
| if info_factor < 1.0: | |
| parts.append( | |
| f"Incomplete information (factor {info_factor:.1f}): " | |
| "customer clarification was not gathered." | |
| ) | |
| if forbidden_pen < 0: | |
| parts.append(f"Forbidden pattern penalty: {forbidden_pen:+.3f}.") | |
| if tone_pen < 0: | |
| parts.append("Tone constraint violated.") | |
| breakdown: dict[str, float] = { | |
| "response_quality": base_reward, | |
| "incomplete_info_factor": info_factor, | |
| "forbidden_penalty": forbidden_pen, | |
| "tone_penalty": tone_pen, | |
| } | |
| return round(reward, 4), " ".join(parts), breakdown | |
| def _on_escalate( | |
| self, state: InternalState, action: EscalateAction | |
| ) -> _DispatchResult: | |
| ticket = state.ticket | |
| state.phase = "escalated" | |
| if not ticket.requires_escalation: | |
| pen = round(-0.10 * ticket.penalty_multiplier, 4) | |
| state.escalation_score = 0.0 | |
| return pen, "Unnecessary escalation; ticket does not require it.", {"escalation": pen} | |
| team_ok = ( | |
| ticket.escalation_target is not None | |
| and action.target_team == ticket.escalation_target | |
| ) | |
| if team_ok: | |
| state.escalation_score = 1.0 | |
| return 0.15, "Correctly escalated to the right team.", {"escalation": 0.15} | |
| state.escalation_score = 0.3 | |
| return 0.05, ( | |
| f"Escalation needed but target should be '{ticket.escalation_target}'." | |
| ), {"escalation": 0.05} | |
| def _on_resolve( | |
| self, state: InternalState, action: ResolveAction | |
| ) -> _DispatchResult: | |
| ticket = state.ticket | |
| quality, forbidden_pen = self._grader.weighted_keyword_score( | |
| action.resolution_summary, ticket.resolution_spec | |
| ) | |
| info_factor = 1.0 | |
| if ticket.partial_info and not state.info_requested: | |
| info_factor = _INFO_SKIP_QUALITY_FACTOR | |
| quality *= info_factor | |
| comp_score = self._grader.compensation_accuracy( | |
| action.offered_compensation, ticket.compensation_range | |
| ) | |
| constraint_pen = self._eval_resolve_constraints(state, action) | |
| quality_r = round(quality * 0.15, 4) | |
| comp_r = round(comp_score * 0.05, 4) | |
| base_r = 0.05 | |
| raw = quality_r + comp_r + base_r | |
| forbidden_scaled = round(forbidden_pen * ticket.penalty_multiplier, 4) | |
| constraint_scaled = round(constraint_pen * ticket.penalty_multiplier, 4) | |
| reward = round(max(0.0, min(raw + forbidden_scaled + constraint_scaled, 0.25)), 4) | |
| state.resolution_quality_score = quality | |
| parts = [f"Resolution quality: {quality:.0%}."] | |
| if info_factor < 1.0: | |
| parts.append( | |
| f"Incomplete information (factor {info_factor:.1f}): " | |
| "customer clarification was not gathered." | |
| ) | |
| if forbidden_pen < 0: | |
| parts.append(f"Forbidden pattern penalty: {forbidden_pen:+.3f}.") | |
| if constraint_pen < 0: | |
| parts.append(f"Constraint penalty: {constraint_pen:+.3f}.") | |
| if comp_score < 1.0 and ticket.compensation_range is not None: | |
| parts.append("Compensation outside optimal range.") | |
| breakdown: dict[str, float] = { | |
| "resolution_quality": quality_r, | |
| "incomplete_info_factor": info_factor, | |
| "compensation": comp_r, | |
| "base": base_r, | |
| "forbidden_penalty": forbidden_scaled, | |
| "constraint_penalty": constraint_scaled, | |
| } | |
| return reward, " ".join(parts), breakdown | |
| def _on_request_info( | |
| self, state: InternalState, action: RequestInfoAction | |
| ) -> _DispatchResult: | |
| ticket = state.ticket | |
| if ticket.partial_info and not state.info_requested: | |
| state.info_requested = True | |
| reveal = ticket.info_reveals or "No additional details available." | |
| return 0.05, f"Customer clarification: {reveal}", {"info_bonus": 0.05} | |
| pen = round(-0.03 * ticket.penalty_multiplier, 4) | |
| if state.info_requested: | |
| return pen, "No new information. Customer already provided clarification.", {"info_penalty": pen} | |
| return pen, "Information requested. No additional details available (simulated).", {"info_penalty": pen} | |
| # ------------------------------------------------------------------ | |
| # Constraint helpers | |
| # ------------------------------------------------------------------ | |
| def _eval_tone_constraint(state: InternalState, tone: str) -> float: | |
| """Return raw penalty (caller applies business-impact multiplier).""" | |
| for c in state.ticket.constraints: | |
| cl = c.lower() | |
| if "empathetic" in cl and tone != "empathetic": | |
| state.constraints_violated.append(c) | |
| return -0.05 | |
| if "formal" in cl and tone != "formal": | |
| state.constraints_violated.append(c) | |
| return -0.05 | |
| return 0.0 | |
| def _eval_resolve_constraints( | |
| self, state: InternalState, action: ResolveAction | |
| ) -> float: | |
| """Return raw penalty (caller applies business-impact multiplier).""" | |
| ticket = state.ticket | |
| penalty = 0.0 | |
| for constraint in ticket.constraints: | |
| cl = constraint.lower() | |
| if "refund" in cl and action.offered_compensation is not None: | |
| if self._grader.check_refund_constraint( | |
| constraint, action.offered_compensation | |
| ): | |
| state.constraints_violated.append(constraint) | |
| penalty -= 0.05 | |
| if "escalat" in cl and ticket.requires_escalation: | |
| if state.phase != "escalated": | |
| already = any( | |
| "escalat" in v.lower() for v in state.constraints_violated | |
| ) | |
| if not already: | |
| state.constraints_violated.append(constraint) | |
| penalty -= 0.05 | |
| return penalty | |
| def _severity_for_difficulty(difficulty: str) -> str: | |
| if difficulty == "easy": | |
| return "medium" | |
| if difficulty == "medium": | |
| return "high" | |
| return "critical" | |
| def _build_billing_records(incident: IncidentScenario) -> dict[str, BillingRecord]: | |
| records: dict[str, BillingRecord] = {} | |
| for customer in incident.affected_customer_profiles: | |
| balance = 500.0 if customer.tier == "enterprise" else 150.0 | |
| status: Literal["current", "overdue", "failed", "disputed"] = "failed" | |
| records[customer.customer_id] = BillingRecord( | |
| customer_id=customer.customer_id, | |
| current_balance=balance, | |
| payment_status=status, | |
| pending_invoices=[ | |
| Invoice( | |
| invoice_id=f"INV-{customer.customer_id}", | |
| amount=balance, | |
| due_step=2, | |
| status="open", | |
| ) | |
| ], | |
| failed_payments=[ | |
| FailedPayment( | |
| payment_id=f"PAY-{customer.customer_id}", | |
| amount=balance, | |
| reason="incident_related_failure", | |
| ) | |
| ], | |
| total_lifetime_value=10000.0 if customer.tier == "enterprise" else 3000.0, | |
| ) | |
| return records | |
| def _convert_policy_schedule(incident: IncidentScenario) -> list[EnginePolicyChange]: | |
| changes: list[EnginePolicyChange] = [] | |
| for item in incident.policy_drift_schedule: | |
| policy_type = "refund" if item.key == "refund_cap" else ( | |
| "escalation" if item.key == "escalation_required" else "communication" | |
| ) | |
| key_name = "max_refund" if policy_type == "refund" else ( | |
| "required" if policy_type == "escalation" else item.key | |
| ) | |
| changes.append( | |
| EnginePolicyChange( | |
| trigger_step=item.step, | |
| policy_type=policy_type, | |
| old_value={}, | |
| new_value={key_name: item.value}, | |
| reason=f"Scheduled drift for {item.key}", | |
| ) | |
| ) | |
| return changes | |
| def _base_kb_articles() -> list[KBArticle]: | |
| return [ | |
| KBArticle( | |
| article_id="KB-BASE-001", | |
| title="Database OOM Recovery", | |
| content="verify root cause and apply fix safely", | |
| solution_steps=["verify", "apply memory fix"], | |
| tags=["database", "oom"], | |
| last_updated="2026-04-20", | |
| is_accurate=True, | |
| ) | |
| ] | |
| def _suggested_runbook(runbook_engine: RunbookEngine, incident: IncidentScenario) -> dict[str, Any] | None: | |
| incident_type = "payment_500" | |
| first = incident.root_causes[0] if incident.root_causes else None | |
| if first is not None: | |
| if first.service == "database" and first.failure_mode == "oom": | |
| incident_type = "database_oom" | |
| elif first.service == "auth" and first.failure_mode == "rate_limiting": | |
| incident_type = "auth_rate_limiting" | |
| suggestion = runbook_engine.suggest_runbook(incident_type) | |
| if suggestion is None: | |
| return None | |
| return suggestion.model_dump() | |