PCAgentinAI / agent8_coverage_verify.py
ITNovaML's picture
Add ClaimSense agents + sync all agent files
069dc30
Raw
History Blame Contribute Delete
6.81 kB
"""
ClaimSense β€” Agent 8: Coverage Verification Agent
===================================================
Verifies whether the claimed incident is covered under the policy.
- Works for both linked (submission_id present) and standalone flows
- Checks incident type vs coverage type eligibility matrix
- Checks deductible, limits, exclusions
- No ML β€” pure rules engine
"""
import logging
from datetime import datetime, date
log = logging.getLogger(__name__)
# ── Coverage eligibility matrix ───────────────────────────────
# Maps coverage_type β†’ set of covered incident types
COVERAGE_MATRIX = {
'HO-3': {
'FIRE', 'WIND', 'HAIL', 'THEFT', 'VANDALISM',
'LIABILITY', 'VEHICLE_IMPACT', 'WATER', 'STRUCTURAL', 'OTHER'
},
'HO-5': {
'FIRE', 'WIND', 'HAIL', 'THEFT', 'VANDALISM',
'LIABILITY', 'VEHICLE_IMPACT', 'WATER', 'STRUCTURAL',
'EARTHQUAKE', 'OTHER'
},
'HO-4': { # Renters
'FIRE', 'THEFT', 'VANDALISM', 'LIABILITY', 'WATER', 'OTHER'
},
'HO-6': { # Condo
'FIRE', 'THEFT', 'VANDALISM', 'LIABILITY', 'WATER', 'STRUCTURAL', 'OTHER'
},
'DP-3': { # Dwelling
'FIRE', 'WIND', 'HAIL', 'VANDALISM', 'VEHICLE_IMPACT', 'OTHER'
},
'WC-3': {
'LIABILITY', 'OTHER'
},
}
# Incident types typically excluded (require endorsement)
FLOOD_EXCLUDED = {'HO-3', 'HO-4', 'HO-5', 'HO-6', 'DP-3'}
EARTHQUAKE_EXCLUDED = {'HO-3', 'HO-4', 'HO-6', 'DP-3'}
def run_coverage_verify_agent(fnol_result: dict, submission: dict) -> dict:
"""
Agent 8 β€” Coverage Verification.
Args:
fnol_result: Output from Agent 7 (FNOL Intake)
submission: Submission/policy data dict (may be empty for standalone)
Returns:
result dict with coverage_status, applicable_limit, deductible, exclusions
"""
claim_id = fnol_result.get('claim_id', '')
incident_type = fnol_result.get('incident_type', 'OTHER')
policy_number = fnol_result.get('policy_number', '')
log.info(f"[COVERAGE] Agent 8 running for {claim_id}")
# ── Pull policy details ───────────────────────────────────
# Works for both linked (from DB via submission) and standalone
coverage_type = (
submission.get('_coverage_type_code') or
submission.get('coverage_type_code') or
(submission.get('policy_request') or {}).get('coverage_type') or
'HO-3' # safe default
)
coverage_limit = float(
submission.get('_requested_coverage_limit') or
(submission.get('policy_request') or {}).get('requested_coverage_limit') or
300000
)
deductible = float(
submission.get('_requested_deductible') or
(submission.get('policy_request') or {}).get('requested_deductible') or
2500
)
estimated_damage = float(
fnol_result.get('normalised_fields', {}).get('estimated_damage') or 0
)
coverage_type = str(coverage_type).upper().strip()
exclusions = []
partial_notes = []
# ── Check eligibility matrix ──────────────────────────────
covered_perils = COVERAGE_MATRIX.get(coverage_type, COVERAGE_MATRIX['HO-3'])
if incident_type == 'FLOOD' and coverage_type in FLOOD_EXCLUDED:
exclusions.append(
f"FLOOD damage is excluded under {coverage_type}. "
"Separate flood insurance (NFIP) required."
)
elif incident_type == 'EARTHQUAKE' and coverage_type in EARTHQUAKE_EXCLUDED:
exclusions.append(
f"EARTHQUAKE is excluded under {coverage_type}. "
"Earthquake endorsement required."
)
elif incident_type not in covered_perils:
exclusions.append(
f"Incident type {incident_type} is not a covered peril under {coverage_type}."
)
# ── Limit checks ─────────────────────────────────────────
applicable_limit = coverage_limit
if estimated_damage > coverage_limit:
partial_notes.append(
f"Estimated damage ${estimated_damage:,.0f} exceeds "
f"coverage limit ${coverage_limit:,.0f}. "
f"Maximum payable: ${coverage_limit:,.0f}."
)
# ── Deductible check ──────────────────────────────────────
net_payable = max(0, min(estimated_damage, coverage_limit) - deductible)
if estimated_damage > 0 and estimated_damage <= deductible:
partial_notes.append(
f"Claimed amount ${estimated_damage:,.0f} does not exceed "
f"deductible ${deductible:,.0f}. Claim may not result in payment."
)
# ── Final coverage status ─────────────────────────────────
if exclusions:
coverage_status = 'EXCLUDED'
elif partial_notes:
coverage_status = 'PARTIAL'
else:
coverage_status = 'COVERED'
# ── Summary ───────────────────────────────────────────────
if coverage_status == 'COVERED':
summary = (
f"Incident type {incident_type} is fully covered under {coverage_type}. "
f"Coverage limit: ${coverage_limit:,.0f}. "
f"Deductible: ${deductible:,.0f}. "
f"Estimated net payable: ${net_payable:,.0f}."
)
elif coverage_status == 'PARTIAL':
summary = (
f"Incident {incident_type} is covered under {coverage_type} "
f"but with limitations. " + " ".join(partial_notes)
)
else:
summary = (
f"Incident {incident_type} is NOT covered under {coverage_type}. "
+ " ".join(exclusions)
)
result = {
'claim_id': claim_id,
'status': f'COVERAGE_{coverage_status}',
'coverage_status': coverage_status,
'coverage_type': coverage_type,
'policy_number': policy_number,
'applicable_limit': applicable_limit,
'deductible': deductible,
'estimated_damage': estimated_damage,
'net_payable_est': net_payable,
'exclusions': exclusions,
'partial_notes': partial_notes,
'summary': summary,
}
log.info(
f"[COVERAGE] {claim_id}: {coverage_status} | "
f"type={coverage_type} | limit=${coverage_limit:,.0f} | "
f"exclusions={len(exclusions)}"
)
return result