""" ClaimSense — Agent 7: FNOL Intake Agent ======================================== First Notice of Loss parser and validator. - Extracts and normalises incident fields from raw FNOL payload - Validates required fields are present - Checks policy_number format and incident_date is not future - Determines FNOL completeness score - No ML model needed — pure logic/rules """ import os, sys, json, logging from datetime import datetime, date log = logging.getLogger(__name__) # ── Field validation helpers ────────────────────────────────── VALID_INCIDENT_TYPES = { 'FIRE', 'WATER', 'WIND', 'HAIL', 'THEFT', 'VANDALISM', 'LIABILITY', 'EARTHQUAKE', 'FLOOD', 'VEHICLE_IMPACT', 'STRUCTURAL', 'OTHER' } REQUIRED_FIELDS = [ 'policy_number', 'claimant_name', 'incident_date', 'incident_type' ] def _parse_date(val): if not val: return None for fmt in ('%Y-%m-%d', '%m/%d/%Y', '%d-%m-%Y', '%Y/%m/%d'): try: return datetime.strptime(str(val).strip(), fmt).date() except ValueError: continue return None def _days_to_report(incident_dt, reported_dt=None): """Days between incident and FNOL submission — key fraud signal.""" if not incident_dt: return 0 ref = reported_dt or date.today() if isinstance(ref, datetime): ref = ref.date() delta = (ref - incident_dt).days return max(0, delta) def _completeness_score(fnol: dict) -> int: """0–100 completeness score based on fields present.""" bonus_fields = [ 'incident_description', 'incident_address', 'incident_city', 'claimant_email', 'claimant_phone', 'has_police_report', 'has_photos', 'estimated_damage' ] base = len([f for f in REQUIRED_FIELDS if fnol.get(f)]) / len(REQUIRED_FIELDS) bonus = sum(1 for f in bonus_fields if fnol.get(f)) / len(bonus_fields) return int((base * 70) + (bonus * 30)) # ── Main agent function ─────────────────────────────────────── def run_fnol_intake_agent(fnol_payload: dict) -> dict: """ Agent 7 — FNOL Intake. Args: fnol_payload: Raw FNOL submission dict Returns: result dict with status, normalised fields, validation issues """ claim_id = fnol_payload.get('claim_id', '') log.info(f"[FNOL] Agent 7 running for {claim_id}") issues = [] normalised = {} # ── Required field checks ───────────────────────────────── for field in REQUIRED_FIELDS: if not fnol_payload.get(field): issues.append(f"Missing required field: {field}") # ── Policy number ───────────────────────────────────────── policy_number = str(fnol_payload.get('policy_number', '')).strip() normalised['policy_number'] = policy_number # ── Claimant ────────────────────────────────────────────── normalised['claimant_name'] = str(fnol_payload.get('claimant_name', '')).strip() normalised['claimant_email'] = fnol_payload.get('claimant_email') normalised['claimant_phone'] = fnol_payload.get('claimant_phone') normalised['claimant_relation'] = fnol_payload.get('claimant_relation', 'Named Insured') # ── Incident type ───────────────────────────────────────── raw_type = str(fnol_payload.get('incident_type', '')).upper().strip() if raw_type and raw_type not in VALID_INCIDENT_TYPES: # Try to map common synonyms synonyms = { 'FLOOD': 'WATER', 'BURST PIPE': 'WATER', 'LEAK': 'WATER', 'BREAK-IN': 'THEFT', 'BURGLARY': 'THEFT', 'ROBBERY': 'THEFT', 'STORM': 'WIND', 'HURRICANE': 'WIND', 'TORNADO': 'WIND', 'ACCIDENT': 'LIABILITY', 'INJURY': 'LIABILITY', } mapped = synonyms.get(raw_type) if mapped: raw_type = mapped else: raw_type = 'OTHER' normalised['incident_type'] = raw_type or 'OTHER' # ── Incident date ───────────────────────────────────────── incident_dt = _parse_date(fnol_payload.get('incident_date')) if incident_dt: if incident_dt > date.today(): issues.append("incident_date is in the future") incident_dt = None elif (date.today() - incident_dt).days > 1825: # 5 years issues.append("incident_date is more than 5 years ago — verify") else: if fnol_payload.get('incident_date'): issues.append(f"Could not parse incident_date: {fnol_payload.get('incident_date')}") normalised['incident_date'] = incident_dt.isoformat() if incident_dt else None normalised['days_to_report'] = _days_to_report(incident_dt) # ── Late reporting flag ─────────────────────────────────── late_report = normalised['days_to_report'] > 30 if normalised['days_to_report'] > 90: issues.append( f"Very late FNOL: {normalised['days_to_report']} days after incident — fraud signal" ) # ── Incident details ────────────────────────────────────── normalised['incident_description'] = fnol_payload.get('incident_description') normalised['incident_address'] = fnol_payload.get('incident_address') normalised['incident_city'] = fnol_payload.get('incident_city') normalised['incident_state'] = fnol_payload.get('incident_state') normalised['estimated_damage'] = fnol_payload.get('estimated_damage') normalised['has_police_report'] = bool(fnol_payload.get('has_police_report', False)) normalised['has_photos'] = bool(fnol_payload.get('has_photos', False)) # ── Completeness ───────────────────────────────────────── completeness = _completeness_score({**fnol_payload, **normalised}) # ── Status determination ────────────────────────────────── critical_issues = [i for i in issues if 'Missing required' in i] if critical_issues: status = 'FNOL_INVALID' elif issues: status = 'FNOL_PARTIAL' else: status = 'FNOL_ACCEPTED' result = { 'claim_id': claim_id, 'status': status, 'normalised_fields': normalised, 'completeness_score': completeness, 'late_report': late_report, 'days_to_report': normalised['days_to_report'], 'validation_issues': issues, 'incident_type': normalised['incident_type'], 'incident_date': normalised['incident_date'], 'policy_number': policy_number, 'claimant_name': normalised['claimant_name'], } log.info( f"[FNOL] {claim_id}: {status} | " f"completeness={completeness} | " f"days_to_report={normalised['days_to_report']} | " f"issues={len(issues)}" ) return result