"""Shared deterministic triage policy used by the API and offline runner.""" from __future__ import annotations from models import ActionModel from server.heuristics import ( COMPONENT_TEAM_MAP, infer_component, infer_priority, infer_severity, needs_more_info, suggested_info_type, ) def recommend_action(observation, action_history: list[str] | None = None) -> tuple[ActionModel, str]: """Return the next deterministic action for the current ticket.""" ticket = observation.current_ticket history = set(action_history or []) if ticket is None: return ( ActionModel(action_type="next_ticket", next_ticket={}), "No active ticket is loaded, so moving to the next ticket is safest.", ) duplicate_id = (ticket.suspected_duplicate_ids or [None])[0] component = infer_component(ticket, observation.available_components) severity = infer_severity(ticket, component) priority = infer_priority(ticket, severity) if duplicate_id: if "mark_duplicate" not in history: return ( ActionModel( action_type="mark_duplicate", mark_duplicate={"canonical_ticket_id": str(duplicate_id)}, ), f"Duplicate hints point to {duplicate_id}, so linking it should happen first.", ) return ( ActionModel(action_type="next_ticket", next_ticket={}), "This ticket is already linked as a duplicate, so continuing would only waste steps.", ) if "classify" not in history: return ( ActionModel( action_type="classify", classify={ "severity": severity, "priority": priority, "component": component, }, ), "Classification is still missing, so severity, priority, and component come first.", ) if "assign" not in history: default_team = observation.available_teams[0] if observation.available_teams else "backend-api" team = COMPONENT_TEAM_MAP.get(component, default_team) return ( ActionModel(action_type="assign", assign={"team": team}), f"The inferred component maps best to {team}, so routing is the next step.", ) if severity in {"sev0", "sev1"} and "escalate_incident" not in history: return ( ActionModel( action_type="escalate_incident", escalate_incident={"justification": "High-impact production risk detected"}, ), "This looks like a critical ticket, so escalation should happen before moving on.", ) if needs_more_info(ticket) and "request_info" not in history: info_type = suggested_info_type(ticket) return ( ActionModel( action_type="request_info", request_info={"info_type": info_type}, ), "The report is missing enough evidence that requesting more information is justified.", ) return ( ActionModel(action_type="next_ticket", next_ticket={}), "The important triage steps for this ticket are already covered, so moving on is reasonable.", )