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