Spaces:
Sleeping
Sleeping
| """Internal mutable state for a single episode.""" | |
| from __future__ import annotations | |
| from typing import Any, Literal | |
| from env.customers import CustomerQueueManager | |
| from env.incident_history import IncidentHistoryStore | |
| from env.knowledge_base import KnowledgeBase, PersistentKnowledgeBase | |
| from env.policy_engine import PolicyEngine | |
| from env.runbooks import RunbookEngine | |
| from env.stakeholders import StakeholderManager | |
| from env.world import WorldState | |
| from graders.investigation_grader import ACTION_COSTS, EvidenceChain, SeverityReEvaluation | |
| from models.action import ApplyFixAction | |
| from models.incident import IncidentScenario | |
| from models.observation import ActionRecord, Observation, Phase | |
| from models.ticket import TicketData | |
| PHASE_VALID_ACTIONS: dict[Phase, frozenset[str]] = { | |
| "unclassified": frozenset(["classify"]), | |
| "classified": frozenset(["route", "escalate"]), | |
| "routed": frozenset(["respond", "escalate", "resolve", "request_info"]), | |
| "responding": frozenset(["respond", "escalate", "resolve", "request_info"]), | |
| "escalated": frozenset(["resolve"]), | |
| "resolved": frozenset(), | |
| } | |
| MAX_STEPS: dict[str, int] = { | |
| "easy": 8, | |
| "medium": 9, | |
| "hard": 10, | |
| } | |
| IncidentPhase = Literal["triage", "investigation", "response", "resolution"] | |
| IncidentSeverity = Literal["medium", "high", "critical", "P0"] | |
| INCIDENT_PHASE_VALID_ACTIONS: dict[IncidentPhase, frozenset[str]] = { | |
| "triage": frozenset( | |
| ["classify", "check_monitoring", "query_kb", "query_incident_history", "follow_runbook_step"] | |
| ), | |
| "investigation": frozenset( | |
| [ | |
| "check_monitoring", | |
| "probe_service", | |
| "fetch_logs", | |
| "fetch_user_data", | |
| "check_billing", | |
| "query_kb", | |
| "check_policy", | |
| "query_incident_history", | |
| "follow_runbook_step", | |
| "classify", | |
| "route", | |
| ] | |
| ), | |
| "response": frozenset( | |
| [ | |
| "apply_fix", | |
| "rollback_fix", | |
| "respond", | |
| "escalate", | |
| "request_info", | |
| "notify_stakeholders", | |
| "check_policy", | |
| "fetch_user_data", | |
| "check_billing", | |
| "query_kb", | |
| "follow_runbook_step", | |
| ] | |
| ), | |
| "resolution": frozenset( | |
| [ | |
| "verify_fix", | |
| "resolve", | |
| "respond", | |
| "write_postmortem", | |
| "update_kb", | |
| "notify_stakeholders", | |
| ] | |
| ), | |
| } | |
| SEVERITY_ESCALATION: dict[int, IncidentSeverity] = { | |
| 10: "high", | |
| 25: "critical", | |
| 40: "P0", | |
| } | |
| def compute_max_total_reward(ticket: TicketData) -> float: | |
| """Achievable maximum reward for an optimal agent on this ticket. | |
| Accounts for action rewards *and* any unavoidable SLA penalties incurred | |
| when the minimum optimal path exceeds the ticket's SLA deadline. | |
| """ | |
| total = 0.10 # classify base | |
| if ticket.gold_priority in ("critical", "high"): | |
| total += 0.10 # urgency bonus | |
| total += 0.10 # route | |
| min_steps = 3 # classify + route + resolve (always required) | |
| if ticket.difficulty in ("medium", "hard"): | |
| total += 0.20 # respond | |
| min_steps += 1 | |
| if ticket.partial_info: | |
| total += 0.05 # request_info bonus | |
| min_steps += 1 | |
| if ticket.requires_escalation: | |
| total += 0.15 # escalate | |
| min_steps += 1 | |
| total += 0.25 # resolve | |
| sla = ticket.effective_sla_steps | |
| for step_idx in range(min_steps): | |
| if step_idx >= sla: | |
| total -= 0.02 * (step_idx - sla + 1) | |
| return round(total, 4) | |
| class InternalState: | |
| """Tracks episode progress, phase transitions, and cumulative quality scores.""" | |
| __slots__ = ( | |
| "ticket", | |
| "phase", | |
| "steps_taken", | |
| "max_steps", | |
| "max_total_reward", | |
| "actions_log", | |
| "cumulative_reward", | |
| "classification_correct", | |
| "routing_correct", | |
| "urgency_handled", | |
| "response_quality_score", | |
| "resolution_quality_score", | |
| "escalation_score", | |
| "constraints_violated", | |
| "done", | |
| "last_action_json", | |
| # v2 additions | |
| "sla_steps", | |
| "urgency_penalty_accrued", | |
| "info_requested", | |
| # v3 — interpretability | |
| "last_reward_breakdown", | |
| ) | |
| def __init__(self, ticket: TicketData) -> None: | |
| self.ticket = ticket | |
| self.phase: Phase = "unclassified" | |
| self.steps_taken: int = 0 | |
| self.max_steps: int = MAX_STEPS[ticket.difficulty] | |
| self.max_total_reward: float = compute_max_total_reward(ticket) | |
| self.actions_log: list[ActionRecord] = [] | |
| self.cumulative_reward: float = 0.0 | |
| self.classification_correct: bool | None = None | |
| self.routing_correct: bool | None = None | |
| self.urgency_handled: bool = False | |
| self.response_quality_score: float | None = None | |
| self.resolution_quality_score: float | None = None | |
| self.escalation_score: float | None = None | |
| self.constraints_violated: list[str] = [] | |
| self.done: bool = False | |
| self.last_action_json: str | None = None | |
| # v2 | |
| self.sla_steps: int = ticket.effective_sla_steps | |
| self.urgency_penalty_accrued: float = 0.0 | |
| self.info_requested: bool = False | |
| # v3 — interpretability | |
| self.last_reward_breakdown: dict[str, float] = {} | |
| # ---- helpers -------------------------------------------------------- | |
| def available_actions(self) -> list[str]: | |
| actions = PHASE_VALID_ACTIONS[self.phase] | |
| if self.info_requested: | |
| actions = actions - frozenset(["request_info"]) | |
| return sorted(actions) | |
| def record_action( | |
| self, action_summary: str, feedback: str, reward: float | |
| ) -> None: | |
| self.actions_log.append( | |
| ActionRecord( | |
| step=self.steps_taken, | |
| action_taken=action_summary, | |
| env_feedback=feedback, | |
| reward_earned=round(reward, 4), | |
| ) | |
| ) | |
| self.cumulative_reward += reward | |
| self.steps_taken += 1 | |
| if self.steps_taken >= self.max_steps: | |
| self.done = True | |
| def to_observation(self) -> Observation: | |
| return Observation( | |
| ticket_id=self.ticket.ticket_id, | |
| ticket_text=self.ticket.ticket_text, | |
| customer_sentiment=self.ticket.customer_sentiment, | |
| customer_tier=self.ticket.customer_tier, | |
| category_hint=self.ticket.category_hint, | |
| history=list(self.actions_log), | |
| pending_tickets=0, | |
| current_step=self.steps_taken, | |
| max_steps=self.max_steps, | |
| constraints=list(self.ticket.constraints), | |
| available_actions=self.available_actions, | |
| phase=self.phase, | |
| sla_steps_remaining=max(0, self.sla_steps - self.steps_taken), | |
| customer_value=self.ticket.customer_value, | |
| max_total_reward=self.max_total_reward, | |
| ) | |
| def to_info(self) -> dict[str, Any]: | |
| mtr = self.max_total_reward | |
| info: dict[str, Any] = { | |
| "phase": self.phase, | |
| "steps_taken": self.steps_taken, | |
| "max_steps": self.max_steps, | |
| "cumulative_reward": round(self.cumulative_reward, 4), | |
| "max_total_reward": mtr, | |
| "normalized_score": round( | |
| min(max(self.cumulative_reward / mtr, 0.0), 1.0), 4 | |
| ), | |
| "classification_correct": self.classification_correct, | |
| "routing_correct": self.routing_correct, | |
| "urgency_handled": self.urgency_handled, | |
| "response_quality_score": self.response_quality_score, | |
| "resolution_quality_score": self.resolution_quality_score, | |
| "escalation_score": self.escalation_score, | |
| "constraints_violated": list(self.constraints_violated), | |
| "difficulty": self.ticket.difficulty, | |
| "sla_steps": self.sla_steps, | |
| "sla_overage": max(0, self.steps_taken - self.sla_steps), | |
| "urgency_penalty_accrued": round(self.urgency_penalty_accrued, 4), | |
| "customer_value": self.ticket.customer_value, | |
| "reward_breakdown": dict(self.last_reward_breakdown), | |
| } | |
| if self.done: | |
| info["final_score_breakdown"] = self._compute_final_breakdown() | |
| return info | |
| def _compute_final_breakdown(self) -> dict[str, float]: | |
| """Episode-level weighted breakdown mirroring ``grade_episode``.""" | |
| cls_s = 1.0 if self.classification_correct else 0.0 | |
| rte_s = 1.0 if self.routing_correct else 0.0 | |
| rsp_s = self.response_quality_score if self.response_quality_score is not None else 0.0 | |
| res_s = self.resolution_quality_score if self.resolution_quality_score is not None else 0.0 | |
| esc_s = max(0.0, self.escalation_score) if self.escalation_score is not None else 0.0 | |
| urg_s = 1.0 if self.urgency_handled else 0.0 | |
| eff_s = max(0.0, 1.0 - self.steps_taken / self.max_steps) if self.max_steps > 0 else 0.0 | |
| sla_overage = max(0, self.steps_taken - self.sla_steps) | |
| sla_s = max(0.0, 1.0 - sla_overage * 0.2) | |
| constraint_pen = len(self.constraints_violated) * 0.05 | |
| raw = ( | |
| 0.15 * cls_s | |
| + 0.10 * rte_s | |
| + 0.20 * rsp_s | |
| + 0.20 * res_s | |
| + 0.10 * esc_s | |
| + 0.10 * urg_s | |
| + 0.05 * eff_s | |
| + 0.10 * sla_s | |
| ) | |
| return { | |
| "classification": round(0.15 * cls_s, 4), | |
| "routing": round(0.10 * rte_s, 4), | |
| "response_quality": round(0.20 * rsp_s, 4), | |
| "resolution_quality": round(0.20 * res_s, 4), | |
| "escalation": round(0.10 * esc_s, 4), | |
| "urgency": round(0.10 * urg_s, 4), | |
| "efficiency": round(0.05 * eff_s, 4), | |
| "sla_compliance": round(0.10 * sla_s, 4), | |
| "constraint_penalty": round(-constraint_pen, 4), | |
| "total": round(max(0.0, min(raw - constraint_pen, 1.0)), 4), | |
| } | |
| class ResourceBudget: | |
| """Finite resources available during an incident episode.""" | |
| __slots__ = ( | |
| "max_fix_attempts", | |
| "max_escalations", | |
| "max_stakeholder_notifications", | |
| "remaining_fix_attempts", | |
| "remaining_escalations", | |
| "remaining_notifications", | |
| ) | |
| def __init__( | |
| self, | |
| max_fix_attempts: int = 3, | |
| max_escalations: int = 2, | |
| max_stakeholder_notifications: int = 5, | |
| ) -> None: | |
| self.max_fix_attempts = max_fix_attempts | |
| self.max_escalations = max_escalations | |
| self.max_stakeholder_notifications = max_stakeholder_notifications | |
| self.remaining_fix_attempts = max_fix_attempts | |
| self.remaining_escalations = max_escalations | |
| self.remaining_notifications = max_stakeholder_notifications | |
| def consume(self, resource: str) -> bool: | |
| """Consume one unit of a named resource if available.""" | |
| if resource == "fix_attempt": | |
| if self.remaining_fix_attempts <= 0: | |
| return False | |
| self.remaining_fix_attempts -= 1 | |
| return True | |
| if resource == "escalation": | |
| if self.remaining_escalations <= 0: | |
| return False | |
| self.remaining_escalations -= 1 | |
| return True | |
| if resource == "notification": | |
| if self.remaining_notifications <= 0: | |
| return False | |
| self.remaining_notifications -= 1 | |
| return True | |
| return False | |
| class AuditEntry: | |
| """Compliance audit row for one action.""" | |
| __slots__ = ( | |
| "step", | |
| "timestamp_simulated", | |
| "action_type", | |
| "target", | |
| "rationale_required", | |
| "policy_checked", | |
| "compliant", | |
| ) | |
| def __init__( | |
| self, | |
| *, | |
| step: int, | |
| timestamp_simulated: str, | |
| action_type: str, | |
| target: str, | |
| rationale_required: bool, | |
| policy_checked: bool, | |
| compliant: bool, | |
| ) -> None: | |
| self.step = step | |
| self.timestamp_simulated = timestamp_simulated | |
| self.action_type = action_type | |
| self.target = target | |
| self.rationale_required = rationale_required | |
| self.policy_checked = policy_checked | |
| self.compliant = compliant | |
| class AuditTrail: | |
| """Compliance audit trail across incident actions.""" | |
| __slots__ = ("entries",) | |
| def __init__(self) -> None: | |
| self.entries: list[AuditEntry] = [] | |
| def append(self, entry: AuditEntry) -> None: | |
| """Append a new audit entry.""" | |
| self.entries.append(entry) | |
| def grade_compliance(self) -> float: | |
| """Return required-policy-check compliance ratio.""" | |
| requiring = [ | |
| entry for entry in self.entries if entry.action_type in _POLICY_SENSITIVE_ACTIONS | |
| ] | |
| if not requiring: | |
| return 1.0 | |
| compliant = sum(1 for entry in requiring if entry.policy_checked and entry.compliant) | |
| return round(compliant / len(requiring), 4) | |
| class ChaosEvent: | |
| """Mid-episode injected failure event.""" | |
| __slots__ = ("step", "new_service", "reason", "alert_text") | |
| def __init__(self, step: int, new_service: str, reason: str, alert_text: str) -> None: | |
| self.step = step | |
| self.new_service = new_service | |
| self.reason = reason | |
| self.alert_text = alert_text | |
| class ChaosInjector: | |
| """Inject deterministic new failures during response phase.""" | |
| CHAOS_TRIGGERS: dict[str, dict[str, object]] = { | |
| "hard": { | |
| "trigger_step": 35, | |
| "probability": 0.5, | |
| "new_failure": { | |
| "service": "notifications", | |
| "mode": "queue_overflow", | |
| "reason": "Backpressure from payment retry storm", | |
| }, | |
| }, | |
| "nightmare": { | |
| "trigger_step": 25, | |
| "probability": 1.0, | |
| "new_failure": { | |
| "service": "analytics", | |
| "mode": "batch_job_runaway", | |
| "reason": "Error logging spike triggered batch reprocessing", | |
| }, | |
| }, | |
| } | |
| def maybe_inject(self, world: WorldState, step: int, difficulty: str) -> ChaosEvent | None: | |
| """Inject deterministic chaos event based on seed and step.""" | |
| config = self.CHAOS_TRIGGERS.get(difficulty) | |
| if config is None: | |
| return None | |
| trigger_step = int(config["trigger_step"]) | |
| probability = float(config["probability"]) | |
| if step < trigger_step: | |
| return None | |
| if not self._should_trigger(world.seed, step, probability): | |
| return None | |
| failure = config["new_failure"] | |
| service = str(failure["service"]) | |
| mode = str(failure["mode"]) | |
| reason = str(failure["reason"]) | |
| world.service_mesh.inject_failure(service, mode) | |
| return ChaosEvent( | |
| step=step, | |
| new_service=service, | |
| reason=reason, | |
| alert_text=f"NEW ALERT: {service} showing errors", | |
| ) | |
| def _should_trigger(seed: int, step: int, probability: float) -> bool: | |
| threshold = int(probability * 100) | |
| value = (seed * 31 + step * 17) % 100 | |
| return value < threshold | |
| class IncidentState: | |
| """Tracks incident lifecycle progression and integrated world state.""" | |
| __slots__ = ( | |
| "incident", | |
| "world", | |
| "incident_phase", | |
| "triage_complete", | |
| "investigation_complete", | |
| "response_complete", | |
| "episode_done", | |
| "root_cause_identified", | |
| "fix_applied", | |
| "fix_verified", | |
| "tickets_resolved", | |
| "tools_used_sequence", | |
| "policies_checked", | |
| "kb_queried", | |
| "logs_checked_for", | |
| "postmortem_written", | |
| "kb_updated", | |
| "steps_taken", | |
| "max_steps", | |
| "cumulative_reward", | |
| "last_action_json", | |
| "last_reward_breakdown", | |
| "known_facts", | |
| "active_policies", | |
| "tool_results", | |
| "active_alerts", | |
| "resource_budget", | |
| "audit_trail", | |
| "current_severity", | |
| "severity_re_eval", | |
| "_pending_reclassification", | |
| "chaos_injector", | |
| "has_escalated", | |
| "total_action_cost", | |
| "evidence_chain", | |
| "crm", | |
| "billing", | |
| "policy_engine", | |
| "history_store", | |
| "runbook_engine", | |
| "stakeholder_mgr", | |
| "customer_queue_mgr", | |
| "persistent_kb", | |
| "knowledge_base", | |
| "suggested_runbook", | |
| "actions_log", | |
| ) | |
| def __init__( | |
| self, | |
| incident: IncidentScenario, | |
| world: WorldState, | |
| *, | |
| crm: object, | |
| billing: object, | |
| policy_engine: PolicyEngine, | |
| history_store: IncidentHistoryStore, | |
| runbook_engine: RunbookEngine, | |
| stakeholder_mgr: StakeholderManager, | |
| customer_queue_mgr: CustomerQueueManager, | |
| persistent_kb: PersistentKnowledgeBase, | |
| knowledge_base: KnowledgeBase, | |
| suggested_runbook: dict[str, object] | None, | |
| ) -> None: | |
| self.incident = incident | |
| self.world = world | |
| self.incident_phase: IncidentPhase = "triage" | |
| self.triage_complete = False | |
| self.investigation_complete = False | |
| self.response_complete = False | |
| self.episode_done = False | |
| self.root_cause_identified = False | |
| self.fix_applied = False | |
| self.fix_verified = False | |
| self.tickets_resolved: list[str] = [] | |
| self.tools_used_sequence: list[str] = [] | |
| self.policies_checked: set[str] = set() | |
| self.kb_queried = False | |
| self.logs_checked_for: set[str] = set() | |
| self.postmortem_written = False | |
| self.kb_updated = False | |
| self.steps_taken = 0 | |
| self.max_steps = incident.max_steps | |
| self.cumulative_reward = 0.0 | |
| self.last_action_json: str | None = None | |
| self.last_reward_breakdown: dict[str, float] = {} | |
| self.known_facts: dict[str, object] = {} | |
| self.active_policies: dict[str, object] = {} | |
| self.tool_results: dict[str, object] | None = None | |
| self.active_alerts: list[str] = [] | |
| self.resource_budget = ResourceBudget() | |
| self.audit_trail = AuditTrail() | |
| self.current_severity = _severity_for_difficulty(incident.difficulty) | |
| self.severity_re_eval = SeverityReEvaluation() | |
| self._pending_reclassification: IncidentSeverity | None = None | |
| self.chaos_injector = ChaosInjector() | |
| self.has_escalated = False | |
| self.total_action_cost = 0.0 | |
| self.evidence_chain = EvidenceChain() | |
| 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 = knowledge_base | |
| self.suggested_runbook = suggested_runbook | |
| self.actions_log: list[ActionRecord] = [] | |
| def available_actions(self) -> list[str]: | |
| """Return actions available in the current incident phase.""" | |
| return sorted(INCIDENT_PHASE_VALID_ACTIONS[self.incident_phase]) | |
| def record_action(self, action_type: str, feedback: str, reward: float) -> None: | |
| """Record action outcome in history and counters.""" | |
| 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 | |
| self.tools_used_sequence.append(action_type) | |
| self.total_action_cost = round( | |
| self.total_action_cost + ACTION_COSTS.get(action_type, 0), 2 | |
| ) | |
| if self.steps_taken >= self.max_steps: | |
| self.episode_done = True | |
| def add_audit_entry( | |
| self, | |
| *, | |
| action_type: str, | |
| target: str, | |
| policy_checked: bool, | |
| compliant: bool, | |
| ) -> None: | |
| """Append compliance audit entry for current step.""" | |
| self.audit_trail.append( | |
| AuditEntry( | |
| step=self.steps_taken, | |
| timestamp_simulated=f"T+{self.steps_taken:03d}", | |
| action_type=action_type, | |
| target=target, | |
| rationale_required=action_type in {"apply_fix", "escalate"}, | |
| policy_checked=policy_checked, | |
| compliant=compliant, | |
| ) | |
| ) | |
| def advance_phase(self) -> None: | |
| """Advance phase when completion conditions are met.""" | |
| if self.incident_phase == "triage" and self.triage_complete: | |
| self.incident_phase = "investigation" | |
| return | |
| if self.incident_phase == "investigation" and self.investigation_complete: | |
| self.incident_phase = "response" | |
| return | |
| if self.incident_phase == "response" and self.response_complete: | |
| self.incident_phase = "resolution" | |
| def all_objectives_complete(self) -> bool: | |
| """Return True when incident is fully resolved.""" | |
| return self.fix_verified and self.postmortem_written and self.kb_updated | |
| def apply_severity_auto_escalation(self) -> None: | |
| """Escalate severity if unresolved for long durations.""" | |
| for step_trigger, new_severity in SEVERITY_ESCALATION.items(): | |
| if self.steps_taken >= step_trigger and not self.response_complete: | |
| self.current_severity = new_severity | |
| def maybe_update_reclassification(self) -> None: | |
| """Check whether evidence implies severity reclassification.""" | |
| reward, _, target = self.severity_re_eval.check_reclassification( | |
| evidence_chain=self.evidence_chain, | |
| current_step=self.steps_taken, | |
| current_severity=self.current_severity, | |
| ) | |
| if reward != 0.0: | |
| self._pending_reclassification = target | |
| def mark_fix_applied(self, action: ApplyFixAction, correct: bool) -> None: | |
| """Mark fix state and progression.""" | |
| self.fix_applied = True | |
| if correct: | |
| self.response_complete = True | |
| self.add_audit_entry( | |
| action_type="apply_fix", | |
| target=action.service_name, | |
| policy_checked="escalation" in self.policies_checked, | |
| compliant=correct, | |
| ) | |
| def to_observation(self) -> Observation: | |
| """Build incident-mode observation payload.""" | |
| 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 | |
| alerts = self.world.service_mesh.generate_alerts(self.steps_taken) | |
| self.active_alerts = [alert.message for alert in alerts] | |
| 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=round(self.world.total_downtime_cost + self.total_action_cost, 2), | |
| ) | |
| def to_info(self) -> dict[str, object]: | |
| """Build incident-mode diagnostics payload.""" | |
| 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": round(self.world.total_downtime_cost + self.total_action_cost, 2), | |
| "reward_breakdown": dict(self.last_reward_breakdown), | |
| "resource_budget": { | |
| "remaining_fix_attempts": self.resource_budget.remaining_fix_attempts, | |
| "remaining_escalations": self.resource_budget.remaining_escalations, | |
| "remaining_notifications": self.resource_budget.remaining_notifications, | |
| }, | |
| "compliance_score": self.audit_trail.grade_compliance(), | |
| "current_severity": self.current_severity, | |
| } | |
| _POLICY_SENSITIVE_ACTIONS = frozenset( | |
| ["apply_fix", "escalate", "notify_stakeholders", "update_kb"] | |
| ) | |
| def _severity_for_difficulty(difficulty: str) -> IncidentSeverity: | |
| if difficulty == "easy": | |
| return "medium" | |
| if difficulty == "medium": | |
| return "high" | |
| return "critical" | |