""" ClaimSense — Agent 11: Triage & Routing Agent =============================================== Deterministic rules engine. Makes final triage decision based on outputs of all prior agents. No ML — pure business logic. Outputs: AUTO_SETTLE — low fraud, covered, minor/moderate → auto pay ADJUSTER_REVIEW — standard review required SIU_REFERRAL — Special Investigation Unit — high fraud score DENY — excluded, invalid FNOL, or uncovered peril """ import logging from datetime import datetime log = logging.getLogger(__name__) # ── Triage rules ───────────────────────────────────────────── def _determine_outcome(fnol: dict, coverage: dict, fraud: dict, severity: dict) -> tuple: """ Returns (triage_outcome, priority, reasons) """ reasons = [] outcome = 'ADJUSTER_REVIEW' priority = 'MEDIUM' coverage_status = coverage.get('coverage_status', 'PENDING') fraud_score = int(fraud.get('fraud_score') or 0) fraud_band = fraud.get('fraud_band', 'LOW') severity_band = severity.get('severity_band', 'MODERATE') reserve = float(severity.get('reserve_estimate') or 0) fnol_status = fnol.get('status', 'FNOL_ACCEPTED') fraud_flags = fraud.get('fraud_flags', []) exclusions = coverage.get('exclusions', []) # ── DENY conditions ─────────────────────────────────────── if fnol_status == 'FNOL_INVALID': outcome = 'DENY' priority = 'LOW' reasons.append("FNOL is invalid — missing required fields") return outcome, priority, reasons if coverage_status == 'EXCLUDED': outcome = 'DENY' priority = 'LOW' reasons.append("Peril is excluded from coverage") reasons.extend(exclusions) return outcome, priority, reasons # ── SIU Referral ───────────────────────────────────────── if fraud_score >= 70: outcome = 'SIU_REFERRAL' priority = 'HIGH' reasons.append(f"High fraud score: {fraud_score}/100") reasons.extend(fraud_flags[:3]) return outcome, priority, reasons # ── Auto-settle conditions ──────────────────────────────── if ( coverage_status == 'COVERED' and fraud_band == 'LOW' and fraud_score < 25 and severity_band in ('MINOR', 'MODERATE') and reserve < 25000 and fnol_status == 'FNOL_ACCEPTED' ): outcome = 'AUTO_SETTLE' priority = 'LOW' reasons.append("All checks passed — eligible for automated settlement") reasons.append(f"Fraud score {fraud_score}/100 (LOW band)") reasons.append(f"Reserve estimate ${reserve:,.0f} below auto-settle threshold") return outcome, priority, reasons # ── Adjuster review ─────────────────────────────────────── if severity_band == 'CATASTROPHIC': priority = 'URGENT' reasons.append("CATASTROPHIC severity — executive escalation") elif severity_band == 'MAJOR': priority = 'HIGH' reasons.append("MAJOR severity — senior adjuster required") elif fraud_band == 'MEDIUM': priority = 'HIGH' reasons.append(f"Moderate fraud indicators detected (score: {fraud_score})") elif coverage_status == 'PARTIAL': reasons.append("Partial coverage — adjuster must determine applicable amount") else: reasons.append("Standard claim routed to adjuster queue") return outcome, priority, reasons def _assign_queue(outcome: str, severity_band: str, incident_type: str) -> str: """Map outcome + severity to adjuster queue name.""" if outcome == 'AUTO_SETTLE': return 'AUTOMATED_SETTLEMENT' if outcome == 'SIU_REFERRAL': return 'SPECIAL_INVESTIGATIONS_UNIT' if outcome == 'DENY': return 'CLAIMS_DENIAL_REVIEW' # ADJUSTER_REVIEW if severity_band == 'CATASTROPHIC': return 'MAJOR_LOSS_TEAM' if severity_band == 'MAJOR': return 'SENIOR_ADJUSTER_QUEUE' if incident_type in ('FIRE', 'STRUCTURAL', 'EARTHQUAKE', 'FLOOD'): return 'PROPERTY_DAMAGE_QUEUE' if incident_type in ('LIABILITY',): return 'LIABILITY_CLAIMS_QUEUE' return 'STANDARD_ADJUSTER_QUEUE' def _build_triage_summary(claim_id: str, outcome: str, priority: str, fraud: dict, severity: dict, coverage: dict, reasons: list) -> str: fraud_score = fraud.get('fraud_score', 0) severity_band = severity.get('severity_band', '') reserve = severity.get('reserve_estimate', 0) cov_status = coverage.get('coverage_status', '') outcome_text = { 'AUTO_SETTLE': 'approved for automated settlement', 'ADJUSTER_REVIEW': 'routed to adjuster review', 'SIU_REFERRAL': 'referred to the Special Investigation Unit', 'DENY': 'recommended for denial', }.get(outcome, outcome) return ( f"Claim {claim_id} has been {outcome_text}. " f"Coverage status is {cov_status}. " f"Fraud score: {fraud_score}/100. " f"Severity: {severity_band}. " f"Initial reserve estimate: ${float(reserve):,.0f}. " f"Priority: {priority}. " + (" ".join(reasons[:2]) if reasons else "") ) # ── Main agent function ─────────────────────────────────────── def run_triage_agent(fnol_result: dict, coverage_result: dict, fraud_result: dict, severity_result: dict) -> dict: claim_id = fnol_result.get('claim_id', '') log.info(f"[TRIAGE] Agent 11 running for {claim_id}") outcome, priority, reasons = _determine_outcome( fnol_result, coverage_result, fraud_result, severity_result ) incident_type = fnol_result.get('incident_type', 'OTHER') severity_band = severity_result.get('severity_band', 'MODERATE') adjuster_queue = _assign_queue(outcome, severity_band, incident_type) reserve = float(severity_result.get('reserve_estimate') or 0) fraud_score = int(fraud_result.get('fraud_score') or 0) overall_summary = _build_triage_summary( claim_id, outcome, priority, fraud_result, severity_result, coverage_result, reasons ) # ── Decision factors for AXIOM audit ───────────────────── decision_factors = [ { 'agent': 'FNOL Intake Agent', 'factor': 'FNOL Completeness', 'outcome': fnol_result.get('status', ''), 'detail': ( f"Completeness score: {fnol_result.get('completeness_score', 0)}%. " f"Days to report: {fnol_result.get('days_to_report', 0)}. " f"Issues: {len(fnol_result.get('validation_issues', []))}." ) }, { 'agent': 'Coverage Verification Agent', 'factor': 'Policy Coverage Check', 'outcome': coverage_result.get('coverage_status', ''), 'detail': coverage_result.get('summary', '') }, { 'agent': 'Fraud Signal Agent', 'factor': 'Fraud Risk Scoring', 'outcome': fraud_result.get('fraud_band', ''), 'detail': ( f"Fraud score: {fraud_score}/100. " f"Band: {fraud_result.get('fraud_band', '')}. " f"Flags: {len(fraud_result.get('fraud_flags', []))}." ) }, { 'agent': 'Severity & Reserve Agent', 'factor': 'Severity Classification', 'outcome': severity_band, 'detail': ( f"Severity: {severity_band}. " f"Reserve estimate: ${reserve:,.0f}. " f"{severity_result.get('reserve_note', '')}." ) }, { 'agent': 'Triage & Routing Agent', 'factor': 'Final Triage Decision', 'outcome': outcome, 'detail': " ".join(reasons) }, ] result = { 'claim_id': claim_id, 'status': 'TRIAGE_COMPLETE', 'triage_outcome': outcome, 'triage_priority': priority, 'adjuster_queue': adjuster_queue, 'triage_reasons': reasons, 'overall_summary': overall_summary, 'decision_factors': decision_factors, 'reserve_estimate': reserve, 'fraud_score': fraud_score, 'severity_band': severity_band, 'coverage_status': coverage_result.get('coverage_status', ''), 'triaged_at': datetime.utcnow().isoformat(), } log.info( f"[TRIAGE] {claim_id}: outcome={outcome} priority={priority} " f"queue={adjuster_queue}" ) return result