PCAgentinAI / agent11_triage.py
ITNovaML's picture
Add ClaimSense agents + sync all agent files
069dc30
Raw
History Blame Contribute Delete
9.31 kB
"""
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