ITNovaML/PCAgentinAI
0
1"""2ClaimSense — Agent 8: Coverage Verification Agent3===================================================4Verifies whether the claimed incident is covered under the policy.5- Works for both linked (submission_id present) and standalone flows6- Checks incident type vs coverage type eligibility matrix7- Checks deductible, limits, exclusions8- No ML — pure rules engine9"""10 11import logging12from datetime import datetime, date13 14log = logging.getLogger(__name__)15 16# ── Coverage eligibility matrix ───────────────────────────────17# Maps coverage_type → set of covered incident types18COVERAGE_MATRIX = {19 'HO-3': {20 'FIRE', 'WIND', 'HAIL', 'THEFT', 'VANDALISM',21 'LIABILITY', 'VEHICLE_IMPACT', 'WATER', 'STRUCTURAL', 'OTHER'22 },23 'HO-5': {24 'FIRE', 'WIND', 'HAIL', 'THEFT', 'VANDALISM',25 'LIABILITY', 'VEHICLE_IMPACT', 'WATER', 'STRUCTURAL',26 'EARTHQUAKE', 'OTHER'27 },28 'HO-4': { # Renters29 'FIRE', 'THEFT', 'VANDALISM', 'LIABILITY', 'WATER', 'OTHER'30 },31 'HO-6': { # Condo32 'FIRE', 'THEFT', 'VANDALISM', 'LIABILITY', 'WATER', 'STRUCTURAL', 'OTHER'33 },34 'DP-3': { # Dwelling35 'FIRE', 'WIND', 'HAIL', 'VANDALISM', 'VEHICLE_IMPACT', 'OTHER'36 },37 'WC-3': {38 'LIABILITY', 'OTHER'39 },40}41 42# Incident types typically excluded (require endorsement)43FLOOD_EXCLUDED = {'HO-3', 'HO-4', 'HO-5', 'HO-6', 'DP-3'}44EARTHQUAKE_EXCLUDED = {'HO-3', 'HO-4', 'HO-6', 'DP-3'}45 46 47def run_coverage_verify_agent(fnol_result: dict, submission: dict) -> dict:48 """49 Agent 8 — Coverage Verification.50 51 Args:52 fnol_result: Output from Agent 7 (FNOL Intake)53 submission: Submission/policy data dict (may be empty for standalone)54 55 Returns:56 result dict with coverage_status, applicable_limit, deductible, exclusions57 """58 claim_id = fnol_result.get('claim_id', '')59 incident_type = fnol_result.get('incident_type', 'OTHER')60 policy_number = fnol_result.get('policy_number', '')61 62 log.info(f"[COVERAGE] Agent 8 running for {claim_id}")63 64 # ── Pull policy details ───────────────────────────────────65 # Works for both linked (from DB via submission) and standalone66 coverage_type = (67 submission.get('_coverage_type_code') or68 submission.get('coverage_type_code') or69 (submission.get('policy_request') or {}).get('coverage_type') or70 'HO-3' # safe default71 )72 73 coverage_limit = float(74 submission.get('_requested_coverage_limit') or75 (submission.get('policy_request') or {}).get('requested_coverage_limit') or76 30000077 )78 deductible = float(79 submission.get('_requested_deductible') or80 (submission.get('policy_request') or {}).get('requested_deductible') or81 250082 )83 estimated_damage = float(84 fnol_result.get('normalised_fields', {}).get('estimated_damage') or 085 )86 87 coverage_type = str(coverage_type).upper().strip()88 exclusions = []89 partial_notes = []90 91 # ── Check eligibility matrix ──────────────────────────────92 covered_perils = COVERAGE_MATRIX.get(coverage_type, COVERAGE_MATRIX['HO-3'])93 94 if incident_type == 'FLOOD' and coverage_type in FLOOD_EXCLUDED:95 exclusions.append(96 f"FLOOD damage is excluded under {coverage_type}. "97 "Separate flood insurance (NFIP) required."98 )99 elif incident_type == 'EARTHQUAKE' and coverage_type in EARTHQUAKE_EXCLUDED:100 exclusions.append(101 f"EARTHQUAKE is excluded under {coverage_type}. "102 "Earthquake endorsement required."103 )104 elif incident_type not in covered_perils:105 exclusions.append(106 f"Incident type {incident_type} is not a covered peril under {coverage_type}."107 )108 109 # ── Limit checks ─────────────────────────────────────────110 applicable_limit = coverage_limit111 if estimated_damage > coverage_limit:112 partial_notes.append(113 f"Estimated damage ${estimated_damage:,.0f} exceeds "114 f"coverage limit ${coverage_limit:,.0f}. "115 f"Maximum payable: ${coverage_limit:,.0f}."116 )117 118 # ── Deductible check ──────────────────────────────────────119 net_payable = max(0, min(estimated_damage, coverage_limit) - deductible)120 if estimated_damage > 0 and estimated_damage <= deductible:121 partial_notes.append(122 f"Claimed amount ${estimated_damage:,.0f} does not exceed "123 f"deductible ${deductible:,.0f}. Claim may not result in payment."124 )125 126 # ── Final coverage status ─────────────────────────────────127 if exclusions:128 coverage_status = 'EXCLUDED'129 elif partial_notes:130 coverage_status = 'PARTIAL'131 else:132 coverage_status = 'COVERED'133 134 # ── Summary ───────────────────────────────────────────────135 if coverage_status == 'COVERED':136 summary = (137 f"Incident type {incident_type} is fully covered under {coverage_type}. "138 f"Coverage limit: ${coverage_limit:,.0f}. "139 f"Deductible: ${deductible:,.0f}. "140 f"Estimated net payable: ${net_payable:,.0f}."141 )142 elif coverage_status == 'PARTIAL':143 summary = (144 f"Incident {incident_type} is covered under {coverage_type} "145 f"but with limitations. " + " ".join(partial_notes)146 )147 else:148 summary = (149 f"Incident {incident_type} is NOT covered under {coverage_type}. "150 + " ".join(exclusions)151 )152 153 result = {154 'claim_id': claim_id,155 'status': f'COVERAGE_{coverage_status}',156 'coverage_status': coverage_status,157 'coverage_type': coverage_type,158 'policy_number': policy_number,159 'applicable_limit': applicable_limit,160 'deductible': deductible,161 'estimated_damage': estimated_damage,162 'net_payable_est': net_payable,163 'exclusions': exclusions,164 'partial_notes': partial_notes,165 'summary': summary,166 }167 168 log.info(169 f"[COVERAGE] {claim_id}: {coverage_status} | "170 f"type={coverage_type} | limit=${coverage_limit:,.0f} | "171 f"exclusions={len(exclusions)}"172 )173 return result174 