PriyankaPWC's picture
Update app.py
1e3b574 verified
Raw
History Blame Contribute Delete
71.5 kB
import subprocess, sys
def _ensure(pkg, import_name=None):
name = import_name or pkg
try:
__import__(name)
except ImportError:
subprocess.check_call([sys.executable, "-m", "pip", "install", pkg,
"--quiet", "--break-system-packages"],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
_ensure("pymupdf", "fitz")
_ensure("pypdf")
import gradio as gr
import os, base64, json, urllib.request, urllib.error, re
CLAIMS_TYPES = [
"Motor β€” Third Party (Bumper to Bumper)",
"Motor β€” Own Damage (Comprehensive)",
]
DEMO_CLAIMS = {
"Motor β€” Third Party (Bumper to Bumper)": {
"claimant_name": "John Azzopardi",
"policy_number": "MTR-2024-88421",
"claim_reference": "CLM-2024-001847",
"claim_date": "12 March 2025",
"incident_date": "10 March 2025",
"claim_type": "Third Party Motor β€” Rear End Collision",
"incident_location": "Msida Valley Road, Malta",
"incident_description": "Claimant's vehicle was stationary at traffic lights when struck from behind by third party vehicle (Toyota Corolla, reg ABC-123). Rear bumper and boot lid damaged. No bodily injury reported. Police report filed. Third party admitted fault at scene.",
"claimant_contact": "+356 9912 3456",
"insurer_name": "",
"supporting_documents": ["Police report", "Repair estimate", "Photos of damage", "Witness statement"],
"key_flags": ["Third party admitted liability", "No bodily injury", "Low complexity"]
},
"Motor β€” Own Damage (Comprehensive)": {
"claimant_name": "Maria Camilleri",
"policy_number": "MTR-2024-77310",
"claim_reference": "CLM-2024-002103",
"claim_date": "5 April 2025",
"incident_date": "3 April 2025",
"claim_type": "Own Damage β€” Comprehensive",
"incident_location": "St Julian's Bypass, Malta",
"incident_description": "Claimant lost control on wet road and collided with central barrier. Front end damage including bonnet, radiator, and front bumper. Airbags deployed. Vehicle towed. Claimant uninjured. No third party involved.",
"claimant_contact": "+356 7734 8821",
"insurer_name": "",
"supporting_documents": ["Police accident report", "Two repair estimates", "Vehicle photos", "Towing invoice"],
"key_flags": ["Single vehicle accident", "High repair cost", "Airbag deployment β€” severity check needed"]
},
}
STAGES = [
{"name": "FNOL / Claim Intake", "key": "fnol"},
{"name": "Validation & Triage", "key": "valid"},
{"name": "Investigation & Evidence", "key": "invest"},
{"name": "Coverage & Adjudication", "key": "adjud"},
{"name": "Damage Assessment & Valuation", "key": "val"},
{"name": "Settlement & Payment Routing", "key": "settle"},
{"name": "Closure & Reporting", "key": "close"},
]
POLICY_RULES = {
"Motor β€” Third Party (Bumper to Bumper)": [
"Third party liability must be established before payment",
"Claim must be reported within 24 hours of incident",
"Police report mandatory for all motor claims",
"Repair estimates from approved repairers only",
"No bodily injury component β€” separate policy required",
"Vehicle must have valid roadworthiness certificate",
],
"Motor β€” Own Damage (Comprehensive)": [
"Vehicle must be roadworthy and have valid certificate at time of loss",
"Driver must hold valid licence for vehicle class",
"Own-damage excess applies β€” standard €500",
"Repairs must be authorised before commencement",
"Independent surveyor report required for claims exceeding €3,000 β€” initial reserve allocated pending survey outcome",
"Airbag deployment triggers mandatory engineering inspection",
],
}
RESERVE_BENCHMARKS = {
"Motor β€” Third Party (Bumper to Bumper)": {"low": 1500, "mid": 3000, "high": 6000, "avg_duration": "3–6 weeks"},
"Motor β€” Own Damage (Comprehensive)": {"low": 2000, "mid": 5500, "high": 12000, "avg_duration": "4–8 weeks"},
}
CSS = """
.gradio-container { max-width: 1200px !important; margin: 0 auto !important; font-family: 'Segoe UI', Arial, sans-serif !important; }
footer { display: none !important; }
.info-bar { background:#FFF4EF; border:1.5px solid #FC5108; border-radius:8px; padding:12px 16px; font-size:13px; color:#555; margin:16px 0 20px; line-height:1.5; }
.step-title { font-size:18px; font-weight:700; color:#0E2841; margin-bottom:3px; }
.step-sub { font-size:13px; color:#777; margin-bottom:16px; }
.result-panel { background:#f8f9fa; border:1px solid #e4e4e4; border-radius:10px; padding:18px 20px; margin:10px 0; }
.result-panel-title { font-size:14px; font-weight:700; color:#0E2841; margin-bottom:14px; padding-bottom:10px; border-bottom:1px solid #e4e4e4; }
.field-grid { display:grid; grid-template-columns:160px 1fr; gap:0; }
.field-key { color:#888; font-size:12px; font-weight:600; padding:6px 0; border-bottom:1px solid #f0f0f0; text-transform:uppercase; letter-spacing:.3px; }
.field-val { color:#111; font-size:13px; padding:6px 0 6px 12px; border-bottom:1px solid #f0f0f0; }
.policy-pass { background:#d4edda; border:1.5px solid #28a745; border-radius:8px; padding:10px 14px; margin:6px 0; }
.policy-fail { background:#f8d7da; border:1.5px solid #dc3545; border-radius:8px; padding:10px 14px; margin:6px 0; }
.policy-warn { background:#fff3cd; border:1.5px solid #ffc107; border-radius:8px; padding:10px 14px; margin:6px 0; }
.policy-unknown { background:#e8eaf6; border:1.5px solid #7986cb; border-radius:8px; padding:10px 14px; margin:6px 0; }
.stat-row { display:grid; grid-template-columns:repeat(3,1fr); gap:10px; margin:14px 0; }
.stat-box { background:#fff; border:1px solid #eee; border-radius:10px; padding:14px; text-align:center; }
.stat-num { font-size:26px; font-weight:700; }
.stat-lbl { font-size:11px; color:#888; margin-top:3px; text-transform:uppercase; letter-spacing:.4px; }
.fraud-low { background:#d4edda; border:1.5px solid #28a745; border-radius:10px; padding:18px 20px; }
.fraud-medium { background:#fff3cd; border:1.5px solid #ffc107; border-radius:10px; padding:18px 20px; }
.fraud-high { background:#f8d7da; border:1.5px solid #dc3545; border-radius:10px; padding:18px 20px; }
.action-chip { display:inline-block; padding:6px 18px; border-radius:7px; font-size:14px; font-weight:700; margin-bottom:10px; }
.chip-approve { background:#28a745; color:white; }
.chip-review { background:#ffc107; color:#333; }
.chip-investigate { background:#FC5108; color:white; }
.chip-decline { background:#dc3545; color:white; }
.reserve-box { background:#fff; border:1.5px solid #ddd; border-radius:10px; padding:18px 20px; margin:10px 0; }
.reserve-grid { display:grid; grid-template-columns:1fr; gap:12px; margin:14px 0; }
.reserve-col { text-align:center; padding:14px 10px; border-radius:8px; }
.reserve-mid { background:#fff3cd; border:1px solid #ffc107; }
.reserve-amount { font-size:22px; font-weight:700; margin-bottom:4px; }
.reserve-label { font-size:11px; text-transform:uppercase; letter-spacing:.4px; font-weight:600; }
.reserve-mid .reserve-amount { color:#856404; } .reserve-mid .reserve-label { color:#856404; }
.routing-approve { background:#d4edda; border:2px solid #28a745; border-radius:12px; padding:20px 22px; margin:10px 0; }
.routing-review { background:#fff3cd; border:2px solid #ffc107; border-radius:12px; padding:20px 22px; margin:10px 0; }
.routing-investigate{ background:#fff4ee; border:2px solid #FC5108; border-radius:12px; padding:20px 22px; margin:10px 0; }
.routing-decline { background:#f8d7da; border:2px solid #dc3545; border-radius:12px; padding:20px 22px; margin:10px 0; }
.coverage-banner-yes { background:#d4edda; border:1.5px solid #28a745; border-radius:8px; padding:12px 16px; margin:10px 0; font-size:13px; color:#155724; }
.coverage-banner-no { background:#f8d7da; border:1.5px solid #dc3545; border-radius:8px; padding:12px 16px; margin:10px 0; font-size:13px; color:#721c24; }
.coverage-banner-unk { background:#fff3cd; border:1.5px solid #ffc107; border-radius:8px; padding:12px 16px; margin:10px 0; font-size:13px; color:#856404; }
.escalation-banner { background:#f8d7da; border:1.5px solid #dc3545; border-radius:8px; padding:12px 16px; margin:10px 0; font-size:13px; color:#721c24; font-weight:600; }
.fault-banner { background:#e8f4fd; border:1.5px solid #4a90d9; border-radius:8px; padding:14px 16px; margin:10px 0; }
"""
# ─── AI HELPERS ───────────────────────────────────────────────
def call_claude(prompt, system="", image_b64=None, image_type="image/jpeg"):
api_key = os.getenv("ANTHROPIC_API_KEY")
if not api_key: return None, "No ANTHROPIC_API_KEY in Secrets"
try:
content = ([{"type":"image","source":{"type":"base64","media_type":image_type,"data":image_b64}},
{"type":"text","text":prompt}] if image_b64 else prompt)
payload = json.dumps({
"model": "claude-opus-4-5", "max_tokens": 2500,
"system": system or "You are a senior insurance AI consultant.",
"messages": [{"role":"user","content":content}]
}).encode()
req = urllib.request.Request(
"https://api.anthropic.com/v1/messages", data=payload,
headers={"x-api-key":api_key,"anthropic-version":"2023-06-01","content-type":"application/json"},
method="POST")
with urllib.request.urlopen(req, timeout=90) as r:
return json.loads(r.read())["content"][0]["text"].strip(), None
except urllib.error.HTTPError as e:
return None, f"HTTP {e.code}: {e.read().decode()[:300]}"
except Exception as e:
return None, str(e)
def call_gemini(prompt, system=""):
api_key = os.getenv("GEMINI_API_KEY")
if not api_key: return None, "No GEMINI_API_KEY"
try:
payload = json.dumps({
"contents":[{"parts":[{"text": f"{system}\n\n{prompt}" if system else prompt}]}],
"generationConfig":{"maxOutputTokens":2500}
}).encode()
url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={api_key}"
req = urllib.request.Request(url, data=payload, headers={"content-type":"application/json"}, method="POST")
with urllib.request.urlopen(req, timeout=90) as r:
return json.loads(r.read())["candidates"][0]["content"]["parts"][0]["text"].strip(), None
except Exception as e: return None, str(e)
def ai_json(prompt, system="", image_b64=None, image_type="image/jpeg"):
r, err = call_claude(prompt, system, image_b64, image_type)
if not r and not image_b64:
r, err = call_gemini(prompt, system)
if not r: return None, err or "AI unavailable"
clean = re.sub(r"```json|```","",r).strip()
try: return json.loads(clean), None
except: return None, f"JSON parse error: {clean[:200]}"
def ai_text(prompt, system=""):
r, _ = call_claude(prompt, system)
if r: return r
r2, _ = call_gemini(prompt, system)
return r2 or ""
def extract_file(filepath):
if not filepath: return "", "No file"
ext = os.path.splitext(filepath)[1].lower()
if ext in [".txt",".csv"]:
try: return open(filepath, errors="ignore").read()[:5000], ""
except Exception as e: return "", str(e)
if ext in [".jpg",".jpeg",".png",".webp",".bmp"]:
try:
raw = open(filepath,"rb").read()
if len(raw)>4_000_000: return "", "Image >4MB"
b64 = base64.b64encode(raw).decode()
mime = {"jpg":"image/jpeg","jpeg":"image/jpeg","png":"image/png","webp":"image/webp","bmp":"image/bmp"}.get(ext.lstrip("."),"image/jpeg")
r, err = call_claude("Extract ALL visible text from this insurance document. Return every field and value as 'Field: Value' pairs.",
system="You are an expert OCR for insurance docs. Extract everything.", image_b64=b64, image_type=mime)
return (r or ""), (err if not r else "")
except Exception as e: return "", str(e)
if ext == ".pdf":
text = ""
try:
import fitz
doc = fitz.open(filepath)
for page in doc: text += page.get_text()
doc.close()
if text.strip(): return text[:5000].strip(), ""
except: pass
try:
from pypdf import PdfReader
for page in PdfReader(filepath).pages: text += (page.extract_text() or "")
if text.strip(): return text[:5000].strip(), ""
except: pass
try:
import fitz
pdoc = fitz.open(filepath)
pix = pdoc[0].get_pixmap(dpi=150)
img_bytes = pix.tobytes("png")
pdoc.close()
b64v = base64.b64encode(img_bytes).decode()
result, _ = call_claude(
"Extract ALL visible text from this insurance document. Return every field and value as Field: Value pairs.",
system="You are an expert OCR for insurance documents.",
image_b64=b64v, image_type="image/png")
if result: return result, ""
except: pass
return "", "Could not read PDF."
return "", f"Unsupported file type: {ext}"
def analyse_damage_photo(filepath):
if not filepath: return {}, "No file"
ext = os.path.splitext(filepath)[1].lower().lstrip(".")
if ext not in ["jpg","jpeg","png","webp","bmp"]:
return {}, f"Unsupported image type: {ext}"
try:
raw = open(filepath,"rb").read()
if len(raw) > 4_000_000: return {}, "Image >4MB"
b64 = base64.b64encode(raw).decode()
mime = {"jpg":"image/jpeg","jpeg":"image/jpeg","png":"image/png","webp":"image/webp","bmp":"image/bmp"}.get(ext,"image/jpeg")
prompt = ('This is a photo submitted as part of an insurance claim. Return ONLY JSON:\n'
'{"damage_description":"1-2 sentences describing damage and severity",'
'"number_plate_visible":true,"number_plate_text":"plate text or null"}')
r, err = call_claude(prompt, system="You are an expert motor damage assessor. Return only valid JSON.", image_b64=b64, image_type=mime)
if not r: return {}, err or "AI unavailable"
clean = re.sub(r"```json|```","",r).strip()
try: return json.loads(clean), ""
except: return {"damage_description": r, "number_plate_visible": False, "number_plate_text": None}, ""
except Exception as e:
return {}, str(e)
def analyse_document(filepath, claim_type, org, bumper_form_path=None, damage_photo_paths=None):
text, err = extract_file(filepath)
if err and not text and not bumper_form_path and not (damage_photo_paths or []):
return None, "", f"❌ {err}"
extra_sections = []
photo_findings = []
if bumper_form_path:
b2b_text, _ = extract_file(bumper_form_path)
if b2b_text:
extra_sections.append(f"--- Bumper to Bumper Form ---\n{b2b_text[:2500]}")
for p in (damage_photo_paths or []):
if not p: continue
finding, _ = analyse_damage_photo(p)
if finding:
photo_findings.append(finding)
desc = finding.get("damage_description","")
plate = finding.get("number_plate_text")
section = f"--- Damage Photo ---\n{desc}"
if plate:
section += f"\nNumber plate: {plate}"
extra_sections.append(section)
combined_text = (text or "").strip()
if extra_sections:
combined_text = (combined_text + "\n\n" + "\n\n".join(extra_sections)).strip()
if not combined_text:
return None, "", "❌ No text extracted."
prompt = f"""Insurance claim document for {org or 'insurer'}, type: {claim_type}.
Raw text: {combined_text[:4500]}
Return ONLY JSON (no markdown):
{{"claimant_name":"...","policy_number":"...","claim_reference":"...","claim_date":"...","incident_date":"...","claim_type":"...","incident_location":"...","incident_description":"...","supporting_documents":["..."],"claimant_contact":"...","insurer_name":"...","vehicle_registration":"...","third_party_vehicle_registration":"...","key_flags":["..."]}}"""
data, e2 = ai_json(prompt, system="Return only valid JSON.")
data = data or {}
if photo_findings:
data["photo_findings"] = photo_findings
return data, combined_text, e2 or ""
def run_fnol_policy(claim_data, raw_text, claim_type, org, policy_file=None):
policy_text = ""
if policy_file:
policy_text, _ = extract_file(policy_file)
if not policy_text:
rules = POLICY_RULES.get(claim_type, [])
policy_text = f"Standard {claim_type} policy conditions:\n" + "\n".join(f"- {r}" for r in rules)
claim_ctx = json.dumps(claim_data) if claim_data else raw_text[:2000]
prompt = f"""Senior claims assessor reviewing FNOL for a {claim_type} claim at {org or 'insurer'}.
CLAIM: {claim_ctx}
POLICY: {policy_text[:3000]}
Also assess fault probability: what is the probability (0-100%) that the insured is NOT at fault?
Return ONLY JSON:
{{"fnol_summary":"2-3 sentence summary","claim_validity":"VALID|POTENTIALLY VALID|REQUIRES INVESTIGATION|POTENTIALLY INVALID","validity_reason":"one sentence","policy_checks":[{{"rule":"...","status":"PASS|FAIL|WARNING|UNKNOWN","finding":"..."}}],"coverage_assessment":{{"likely_covered":true,"coverage_confidence":"HIGH|MEDIUM|LOW","coverage_notes":"..."}},"vehicle_match":{{"status":"MATCH|MISMATCH|NOT_DETECTED|NO_PHOTO_PROVIDED|NOT_APPLICABLE","claim_vehicle_registration":"...","photo_plate_numbers":["..."],"notes":"..."}},"fault_assessment":{{"probability_not_at_fault":85,"rationale":"one sentence explaining the fault assessment"}},"estimated_claim_validity_score":75,"complexity":"LOW|MEDIUM|HIGH","complexity_reason":"...","key_questions":["..."],"immediate_actions":["..."],"escalation_needed":false,"escalation_reason":null}}"""
data, err = ai_json(prompt, system="You are an expert claims assessor. Return only valid JSON.")
if data: return data, ""
rules = POLICY_RULES.get(claim_type, [])
return {"fnol_summary":"Manual review required.","claim_validity":"REQUIRES INVESTIGATION","validity_reason":"Automated analysis unavailable.","policy_checks":[{"rule":r,"status":"UNKNOWN","finding":"Manual check required"} for r in rules],"coverage_assessment":{"likely_covered":None,"coverage_confidence":"LOW","coverage_notes":"Manual assessment required"},"vehicle_match":{"status":"NOT_DETECTED","claim_vehicle_registration":(claim_data or {}).get("vehicle_registration"),"photo_plate_numbers":[],"notes":"Automated analysis unavailable."},"fault_assessment":{"probability_not_at_fault":50,"rationale":"Insufficient data for automated fault assessment."},"estimated_claim_validity_score":50,"complexity":"MEDIUM","complexity_reason":"Cannot determine.","key_questions":["Verify all claim details manually"],"immediate_actions":["Review claim file manually"],"escalation_needed":False,"escalation_reason":None}, ""
def run_reserve(claim_data, fnol_data, claim_type, org):
bench = RESERVE_BENCHMARKS.get(claim_type, {"low":1000,"mid":5000,"high":15000,"avg_duration":"unknown"})
prompt = f"""PwC actuarial consultant setting FNOL reserves for a {claim_type} claim.
Benchmarks: Expected €{bench['mid']:,} | Duration: {bench['avg_duration']}
Note: The repair amount is determined by a surveyor β€” not self-reported. An initial reserve estimate is uploaded per claim type and reviewed by the surveyor.
CLAIM: {json.dumps(claim_data)[:2000]}
FNOL: {json.dumps({k:fnol_data.get(k) for k in ["claim_validity","complexity","estimated_claim_validity_score"]})}
Reserve increase triggers to consider (use exactly these):
- Legal proceedings initiated
- Medical complications arise
- Third party injury claims submitted
- Evidence of underinsurance identified
- Liability remains disputed after investigation
- Repair costs exceed initial surveyor estimate
Next reserve review: minimum 30 days from today.
Return ONLY JSON:
{{"reserve_expected":0,"confidence":"HIGH|MEDIUM|LOW","currency":"EUR","rationale":"2-3 sentences β€” note that amount is subject to surveyor confirmation","key_drivers":["..."],"adjustment_triggers":["Legal proceedings initiated","Medical complications arise","Third party injury claims submitted","Evidence of underinsurance identified","Liability remains disputed after investigation","Repair costs exceed initial surveyor estimate"],"recommended_review":"Minimum 30 days from today","ibnr_note":"..."}}"""
data, err = ai_json(prompt, system="You are an actuarial expert. Return only valid JSON.")
if data: return data, ""
return {"reserve_expected":bench["mid"],"confidence":"MEDIUM","currency":"EUR","rationale":f"Initial benchmark reserve for {claim_type}. Subject to surveyor confirmation.","key_drivers":["Claim type benchmark"],"adjustment_triggers":["Legal proceedings initiated","Medical complications arise","Third party injury claims submitted","Evidence of underinsurance identified","Liability remains disputed after investigation","Repair costs exceed initial surveyor estimate"],"recommended_review":"Minimum 30 days from today","ibnr_note":"Standard IBNR provisions apply."}, ""
def run_fraud(claim_data, fnol_data, claim_type):
prompt = f"""Senior fraud investigator analysing a {claim_type} claim.
Context: {json.dumps({"claim":claim_data,"fnol":fnol_data})[:3500]}
Return ONLY JSON:
{{"fraud_risk_level":"LOW|MEDIUM|HIGH","risk_score":45,"risk_factors":["..."],"positive_indicators":["..."],"red_flags":["..."],"recommended_action":"APPROVE|REVIEW|INVESTIGATE|DECLINE","action_reason":"one sentence","data_gaps":["..."],"fnol_consistency":"CONSISTENT|INCONSISTENT|PARTIALLY CONSISTENT","fnol_notes":"..."}}"""
data, err = ai_json(prompt, system="You are an expert fraud detection AI. Return only valid JSON.")
if data: return data, ""
return {"fraud_risk_level":"MEDIUM","risk_score":45,"risk_factors":["Insufficient data"],"positive_indicators":["Document provided"],"red_flags":[],"recommended_action":"REVIEW","action_reason":"Manual review recommended.","data_gaps":["API key required"],"fnol_consistency":"PARTIALLY CONSISTENT","fnol_notes":"Unable to cross-reference."}, ""
def run_routing(claim_data, fnol_data, fraud_data, reserve_data, claim_type, org):
prompt = f"""Claims adjudicator routing decision for a {claim_type} claim at {org or 'insurer'}.
CLAIM: {json.dumps(claim_data)[:1000]}
FNOL: validity={fnol_data.get('claim_validity')}, score={fnol_data.get('estimated_claim_validity_score')}, complexity={fnol_data.get('complexity')}
FRAUD: risk={fraud_data.get('fraud_risk_level')}, score={fraud_data.get('risk_score')}, action={fraud_data.get('recommended_action')}
RESERVE: expected=€{reserve_data.get('reserve_expected',0):,}, confidence={reserve_data.get('confidence')}
Return ONLY JSON:
{{"routing_decision":"STRAIGHT_TO_PAYMENT|FAST_TRACK_REVIEW|STANDARD_REVIEW|FULL_INVESTIGATION|DECLINE","confidence_score":85,"decision_rationale":"2-3 sentences","decision_factors":[{{"factor":"...","impact":"POSITIVE|NEGATIVE|NEUTRAL","weight":"HIGH|MEDIUM|LOW"}}],"estimated_settlement_days":5,"settlement_amount_recommendation":"...","conditions":["..."],"next_handler":"Automated Payment System|Junior Adjuster|Senior Adjuster|Special Investigations Unit|Legal Team","audit_trail":"one sentence"}}"""
data, err = ai_json(prompt, system="You are an expert claims adjudicator. Return only valid JSON.")
if data: return data, ""
return {"routing_decision":"STANDARD_REVIEW","confidence_score":60,"decision_rationale":"Manual review required.","decision_factors":[{"factor":"Manual review required","impact":"NEUTRAL","weight":"HIGH"}],"estimated_settlement_days":14,"settlement_amount_recommendation":"Per adjuster","conditions":["Manual review required"],"next_handler":"Senior Adjuster","audit_trail":"Claim routed for manual review."}, ""
def run_survey(claim_data, fnol_data, claim_type, org, survey_file=None):
survey_text = ""
if survey_file:
survey_text, _ = extract_file(survey_file)
prompt = f"""Surveyor assessment for a {claim_type} claim at {org or 'insurer'}.
{'Surveyor report content: ' + survey_text[:2000] if survey_text else 'Generate a sample surveyor assessment.'}
CLAIM: {json.dumps(claim_data)[:1000]}
Note: The repair/settlement amount is determined solely by the surveyor β€” not self-reported.
Return ONLY JSON:
{{"inspection_findings":"2-3 sentences","damage_description":"...","surveyor_repair_recommendation":"...","surveyor_cost_estimate":0,"currency":"EUR","discrepancies":["..."],"surveyor_overall_assessment":"...","approved_repairer":true,"additional_inspections_required":false}}"""
data, err = ai_json(prompt, system="You are an expert motor surveyor. Return only valid JSON.")
if data: return data, ""
return {"inspection_findings":"Manual surveyor inspection required.","damage_description":"To be confirmed by surveyor.","surveyor_repair_recommendation":"Pending inspection.","surveyor_cost_estimate":0,"currency":"EUR","discrepancies":[],"surveyor_overall_assessment":"Awaiting surveyor report.","approved_repairer":None,"additional_inspections_required":None}, ""
def run_final_reserve(claim_data, fnol_data, reserve_data, survey_data, claim_type, org):
surveyor_estimate = survey_data.get("surveyor_cost_estimate", 0)
prompt = f"""Final reserve recommendation for a {claim_type} claim at {org or 'insurer'}.
CLAIM: {json.dumps(claim_data)[:800]}
INITIAL RESERVE: €{reserve_data.get('reserve_expected',0):,}
SURVEYOR ESTIMATE: €{surveyor_estimate:,}
FNOL validity: {fnol_data.get('claim_validity')} | Complexity: {fnol_data.get('complexity')}
Is sufficient information available to proceed to settlement?
If yes β€” recommend final settlement amount and target date.
If no β€” list specifically what is still outstanding.
Return ONLY JSON:
{{"ready_to_settle":true,"final_reserve":0,"currency":"EUR","settlement_recommendation":"...","outstanding_items":["..."],"final_settlement_target_date":"...","rationale":"2-3 sentences"}}"""
data, err = ai_json(prompt, system="You are a senior claims manager. Return only valid JSON.")
if data: return data, ""
return {"ready_to_settle":False,"final_reserve":0,"currency":"EUR","settlement_recommendation":"Manual review required.","outstanding_items":["Surveyor report","Policy verification"],"final_settlement_target_date":"TBD","rationale":"Insufficient information for automated recommendation."}, ""
def run_kpis(claim_data, fnol_data, reserve_data, routing_data, claim_type, org, open_date, settlement_date):
prompt = f"""Generate KPI summary for a {claim_type} claim at {org or 'insurer'}.
Claim opened: {open_date or 'Not specified'}
Claim settled: {settlement_date or 'Not specified'}
FNOL score: {fnol_data.get('estimated_claim_validity_score',50)}/100
Routing: {routing_data.get('routing_decision','STANDARD_REVIEW')}
Reserve: €{reserve_data.get('reserve_expected',0):,}
Return ONLY JSON:
{{"total_days_to_settle":0,"sla_target_days":30,"sla_met":true,"stage_breakdown":[{{"stage":"...","days":0}}],"reserve_accuracy":"...","fraud_indicators_triggered":0,"customer_touchpoints":0,"performance_rating":"GREEN|AMBER|RED","performance_notes":"..."}}"""
data, err = ai_json(prompt, system="You are a claims analytics expert. Return only valid JSON.")
if data: return data, ""
return {"total_days_to_settle":0,"sla_target_days":30,"sla_met":None,"stage_breakdown":[],"reserve_accuracy":"TBD","fraud_indicators_triggered":0,"customer_touchpoints":0,"performance_rating":"AMBER","performance_notes":"Insufficient data for KPI calculation."}, ""
# ─── HTML RENDERERS ───────────────────────────────────────────
def render_claim_fields(data):
if not data: return "<div style='color:#aaa;padding:16px'>No data yet.</div>"
labels = [("claimant_name","Claimant"),("policy_number","Policy No."),("claim_reference","Reference"),
("claim_date","Claim Date"),("incident_date","Incident Date"),("claim_type","Type"),
("incident_location","Location"),("incident_description","Description"),
("claimant_contact","Contact"),("insurer_name","Insurer"),
("vehicle_registration","Vehicle Reg."),("third_party_vehicle_registration","Third Party Reg.")]
html = "<div class='result-panel'><div class='result-panel-title'>πŸ“‹ Extracted Claim Data</div><div class='field-grid'>"
for k,label in labels:
v = data.get(k)
if v and str(v) not in ("null","None",""):
html += f"<div class='field-key'>{label}</div><div class='field-val'>{v}</div>"
html += "</div>"
docs = [d for d in data.get("supporting_documents",[]) if d and d!="null"]
if docs:
html += "<div style='margin-top:12px;padding-top:10px;border-top:1px solid #eee'><span style='font-size:12px;font-weight:700;color:#555'>Documents: </span>"
html += " Β· ".join(f"<span style='background:#e8f4fd;color:#0066cc;border-radius:4px;padding:2px 8px;font-size:12px'>{d}</span>" for d in docs) + "</div>"
flags = [f for f in data.get("key_flags",[]) if f and f!="null"]
if flags:
html += "<div style='margin-top:8px'><span style='font-size:12px;font-weight:700;color:#FC5108'>βš‘ Flags: </span>"
html += " Β· ".join(f"<span style='font-size:12px;color:#555'>{f}</span>" for f in flags) + "</div>"
photo_findings = data.get("photo_findings") or []
if photo_findings:
html += "<div style='margin-top:8px;padding-top:10px;border-top:1px solid #eee'><span style='font-size:12px;font-weight:700;color:#555'>πŸ“· Photo Analysis:</span>"
for i, pf in enumerate(photo_findings, 1):
desc = pf.get("damage_description","")
plate = pf.get("number_plate_text")
plate_html = f" β€” plate: <b style='color:#0066cc'>{plate}</b>" if plate and str(plate) not in ("null","None") else ""
html += f"<div style='font-size:12px;color:#555;padding:3px 0'>Photo {i}: {desc}{plate_html}</div>"
html += "</div>"
html += "</div>"
return html
def render_fnol(data):
if not data: return "<div style='color:#aaa;padding:16px'>Run FNOL Analysis first.</div>"
validity = data.get("claim_validity","REQUIRES INVESTIGATION")
score = data.get("estimated_claim_validity_score",50)
summary = data.get("fnol_summary","")
coverage = data.get("coverage_assessment",{})
checks = data.get("policy_checks",[])
questions = data.get("key_questions",[])
actions = data.get("immediate_actions",[])
escalate = data.get("escalation_needed",False)
esc_rsn = data.get("escalation_reason","")
complexity= data.get("complexity","MEDIUM")
fault = data.get("fault_assessment",{})
v_color = {"VALID":"#155724","POTENTIALLY VALID":"#0c5460","REQUIRES INVESTIGATION":"#856404","POTENTIALLY INVALID":"#721c24"}.get(validity,"#856404")
bar_col = "#28a745" if score>=70 else "#ffc107" if score>=40 else "#dc3545"
cx_col = {"LOW":"#155724","MEDIUM":"#856404","HIGH":"#721c24"}.get(complexity,"#856404")
cx_bg = {"LOW":"#d4edda","MEDIUM":"#fff3cd","HIGH":"#f8d7da"}.get(complexity,"#fff3cd")
cov_icon= "βœ…" if coverage.get("likely_covered")==True else "❌" if coverage.get("likely_covered")==False else "❓"
cov_cls = "coverage-banner-yes" if coverage.get("likely_covered")==True else "coverage-banner-no" if coverage.get("likely_covered")==False else "coverage-banner-unk"
fault_pct = fault.get("probability_not_at_fault", 50)
fault_col = "#155724" if fault_pct >= 70 else "#856404" if fault_pct >= 40 else "#721c24"
fault_bg = "#d4edda" if fault_pct >= 70 else "#fff3cd" if fault_pct >= 40 else "#f8d7da"
html = f"""<div class='result-panel'>
<div class='result-panel-title'>πŸ“ FNOL Assessment</div>
<div style='font-size:13px;color:#444;line-height:1.6;margin-bottom:16px'>{summary}</div>
<div class='stat-row'>
<div class='stat-box'><div class='stat-num' style='color:{v_color};font-size:15px'>{validity}</div><div class='stat-lbl'>Claim validity</div></div>
<div class='stat-box'>
<div class='stat-num' style='color:#FC5108'>{score}<span style='font-size:16px'>/100</span></div>
<div style='background:#eee;border-radius:4px;height:6px;margin:6px 0'><div style='width:{score}%;background:{bar_col};height:100%;border-radius:4px'></div></div>
<div class='stat-lbl'>Validity score</div>
</div>
<div class='stat-box'><div style='display:inline-block;background:{cx_bg};color:{cx_col};padding:4px 12px;border-radius:6px;font-size:14px;font-weight:700'>{complexity}</div><div class='stat-lbl' style='margin-top:6px'>Complexity</div></div>
</div>
<div class='fault-banner'>
<div style='font-size:13px;font-weight:700;color:#0E2841;margin-bottom:6px'>βš–οΈ Fault Assessment</div>
<div style='display:flex;align-items:center;gap:16px'>
<div style='background:{fault_bg};color:{fault_col};border-radius:8px;padding:10px 20px;text-align:center'>
<div style='font-size:28px;font-weight:700;color:{fault_col}'>{fault_pct}%</div>
<div style='font-size:11px;text-transform:uppercase;letter-spacing:.4px'>Probability insured is NOT at fault</div>
</div>
<div style='font-size:13px;color:#555;flex:1'>{fault.get("rationale","")}</div>
</div>
</div>
<div class='{cov_cls}'><b>{cov_icon} Coverage: {coverage.get("coverage_confidence","N/A")} confidence</b> β€” {coverage.get("coverage_notes","")}</div>"""
vm = data.get("vehicle_match", {})
vm_status = vm.get("status")
if vm_status and vm_status != "NOT_APPLICABLE":
vm_icon = {"MATCH":"βœ…","MISMATCH":"🚨","NOT_DETECTED":"❓","NO_PHOTO_PROVIDED":"πŸ“·"}.get(vm_status,"❓")
vm_cls = {"MATCH":"coverage-banner-yes","MISMATCH":"coverage-banner-no","NOT_DETECTED":"coverage-banner-unk","NO_PHOTO_PROVIDED":"coverage-banner-unk"}.get(vm_status,"coverage-banner-unk")
vm_label = {"MATCH":"Number plate matches","MISMATCH":"Number plate MISMATCH","NOT_DETECTED":"No plate detected in photos","NO_PHOTO_PROVIDED":"No damage photo provided"}.get(vm_status, vm_status)
claim_reg = vm.get("claim_vehicle_registration") or "β€”"
photo_plates = [p for p in (vm.get("photo_plate_numbers") or []) if p]
photo_plates_str = ", ".join(photo_plates) if photo_plates else "none detected"
html += f"<div class='{vm_cls}'><b>{vm_icon} Vehicle Match: {vm_label}</b><br>Claim reg: <b>{claim_reg}</b> Β· Photo plate(s): <b>{photo_plates_str}</b></div>"
if escalate and esc_rsn:
html += f"<div class='escalation-banner'>🚨 Escalation Required: {esc_rsn}</div>"
passes = sum(1 for c in checks if c.get("status")=="PASS")
fails = sum(1 for c in checks if c.get("status")=="FAIL")
warns = sum(1 for c in checks if c.get("status")=="WARNING")
html += f"<div style='font-size:13px;font-weight:700;color:#0E2841;margin:14px 0 8px'>πŸ“œ Policy Condition Checks</div>"
html += f"<div style='font-size:12px;color:#555;margin-bottom:10px'>βœ… {passes} passed &nbsp; ❌ {fails} failed &nbsp; ⚠️ {warns} warnings</div>"
cls_map = {"PASS":"policy-pass","FAIL":"policy-fail","WARNING":"policy-warn","UNKNOWN":"policy-unknown"}
icon_map = {"PASS":"βœ…","FAIL":"❌","WARNING":"⚠️","UNKNOWN":"❓"}
for c in checks:
st = c.get("status","UNKNOWN")
html += f"<div class='{cls_map.get(st,'policy-unknown')}'><div style='font-size:12px;font-weight:700;margin-bottom:3px'>{icon_map.get(st,'❓')} {st} β€” {c.get('rule','')}</div><div style='font-size:12px;opacity:.9'>{c.get('finding','')}</div></div>"
if questions:
html += "<div style='margin-top:14px'><div style='font-size:13px;font-weight:700;color:#0E2841;margin-bottom:6px'>❓ Key Questions for Claimant</div>"
html += "".join(f"<div style='font-size:12px;color:#444;padding:3px 0'>β€’ {q}</div>" for q in questions) + "</div>"
# Immediate actions β€” filter out the removed items
REMOVE_ACTIONS = [
"second repair estimate", "witness statement from sarah clarke",
"cityauto repairs", "approved repairer", "non-approved repairer"
]
if actions:
filtered = [a for a in actions if not any(kw in a.lower() for kw in REMOVE_ACTIONS)]
if filtered:
html += "<div style='margin-top:10px'><div style='font-size:13px;font-weight:700;color:#0E2841;margin-bottom:6px'>⚑ Immediate Actions Required</div>"
html += "".join(f"<div style='font-size:12px;color:#444;padding:3px 0'>β†’ {a}</div>" for a in filtered) + "</div>"
html += "</div>"
return html
def render_reserve(data, claim_type):
if not data: return "<div style='color:#aaa;padding:16px'>Run Reserve Recommendation first.</div>"
mid = data.get("reserve_expected", 0)
conf = data.get("confidence","MEDIUM")
conf_color = {"HIGH":"#155724","MEDIUM":"#856404","LOW":"#721c24"}.get(conf,"#856404")
conf_bg = {"HIGH":"#d4edda","MEDIUM":"#fff3cd","LOW":"#f8d7da"}.get(conf,"#fff3cd")
html = f"""<div class='reserve-box'>
<div style='font-size:14px;font-weight:700;color:#0E2841;margin-bottom:14px;padding-bottom:10px;border-bottom:1px solid #eee'>
πŸ’° Reserve Recommendation β€” {claim_type}
<span style='float:right;background:{conf_bg};color:{conf_color};padding:3px 10px;border-radius:6px;font-size:11px;font-weight:700'>{conf} CONFIDENCE</span>
</div>
<div style='background:#fff8e1;border:1px solid #ffc107;border-radius:8px;padding:12px 16px;margin-bottom:12px;font-size:13px;color:#856404'>
<b>ℹ️ Note:</b> An initial reserve estimate is uploaded per claim type and reviewed by the surveyor. The final repair amount is determined solely by the surveyor β€” not self-reported.
</div>
<div class='reserve-grid'>
<div class='reserve-col reserve-mid'><div class='reserve-label'>Expected Reserve</div><div class='reserve-amount'>€{mid:,}</div><div style='font-size:11px;color:#856404;margin-top:3px'>Subject to surveyor confirmation</div></div>
</div>
<div style='font-size:13px;color:#444;line-height:1.6;margin-bottom:12px'>{data.get("rationale","")}</div>"""
if data.get("key_drivers"):
html += "<div style='margin-bottom:10px'><div style='font-size:12px;font-weight:700;color:#333;margin-bottom:5px'>πŸ“Š Key Reserve Drivers</div>"
html += "".join(f"<span style='background:#e8f4fd;color:#0066cc;border-radius:4px;padding:3px 10px;font-size:12px;margin:2px;display:inline-block'>{d}</span>" for d in data["key_drivers"]) + "</div>"
triggers = data.get("adjustment_triggers", [
"Legal proceedings initiated",
"Medical complications arise",
"Third party injury claims submitted",
"Evidence of underinsurance identified",
"Liability remains disputed after investigation",
"Repair costs exceed initial surveyor estimate"
])
html += "<div style='margin-bottom:10px'><div style='font-size:12px;font-weight:700;color:#721c24;margin-bottom:5px'>βš‘ Reserve Increase Triggers</div>"
html += "".join(f"<div style='font-size:12px;color:#555;padding:2px 0'>β€’ {t}</div>" for t in triggers) + "</div>"
review = data.get("recommended_review", "Minimum 30 days from today")
html += f"<div style='font-size:12px;color:#555;padding:8px 12px;background:#f8f9fa;border-radius:6px;margin-bottom:8px'><b>Next reserve review:</b> {review}</div>"
if data.get("ibnr_note"):
html += f"<div style='font-size:12px;color:#555;padding:8px 12px;background:#f8f9fa;border-radius:6px'><b>IBNR note:</b> {data['ibnr_note']}</div>"
html += "</div>"
return html
def render_fraud(data):
if not data: return "<div style='color:#aaa;padding:16px'>Run Risk Detection first.</div>"
level = data.get("fraud_risk_level","MEDIUM").upper()
score = data.get("risk_score",50)
action = data.get("recommended_action","REVIEW")
cls = {"LOW":"fraud-low","MEDIUM":"fraud-medium","HIGH":"fraud-high"}.get(level,"fraud-medium")
icon = {"LOW":"🟒","MEDIUM":"🟑","HIGH":"πŸ”΄"}.get(level,"🟑")
a_col = {"APPROVE":"chip-approve","REVIEW":"chip-review","INVESTIGATE":"chip-investigate","DECLINE":"chip-decline"}.get(action,"chip-review")
bar_col= "#28a745" if score<35 else "#ffc107" if score<65 else "#dc3545"
fnol_c = data.get("fnol_consistency","")
fc_col = {"CONSISTENT":"#155724","INCONSISTENT":"#721c24","PARTIALLY CONSISTENT":"#856404"}.get(fnol_c,"#555")
html = f"""<div class='{cls}'>
<div style='font-size:17px;font-weight:700;margin-bottom:12px'>{icon} Risk Level: <b>{level}</b></div>
<div style='background:rgba(0,0,0,0.08);border-radius:4px;height:10px;overflow:hidden;margin-bottom:10px'>
<div style='width:{score}%;background:{bar_col};height:100%;border-radius:4px'></div>
</div>
<div style='font-size:12px;color:#555;margin-bottom:12px'>Risk score: <b>{score}/100</b></div>
<div style='margin-bottom:12px'><span class='action-chip {a_col}'>⚑ {action}</span><span style='font-size:13px;color:#555;margin-left:10px'>{data.get("action_reason","")}</span></div>"""
if fnol_c:
html += f"<div style='font-size:12px;margin-bottom:12px;padding:8px 12px;background:rgba(255,255,255,0.5);border-radius:6px'><b>FNOL Consistency:</b> <span style='color:{fc_col};font-weight:700'>{fnol_c}</span> β€” {data.get('fnol_notes','')}</div>"
for title, key, col in [("🚩 Red Flags","red_flags","#721c24"),("⚠️ Risk Factors","risk_factors","#856404"),("βœ… Legitimacy Indicators","positive_indicators","#155724"),("πŸ“‹ Data Gaps","data_gaps","#444")]:
items = data.get(key,[])
if items:
html += f"<div style='margin-bottom:10px'><div style='font-size:12px;font-weight:700;color:{col};margin-bottom:4px'>{title}</div>"
html += "".join(f"<div style='font-size:12px;color:#555;padding:2px 0'>β€’ {i}</div>" for i in items) + "</div>"
html += "</div>"
return html
def render_routing(data):
if not data: return "<div style='color:#aaa;padding:16px'>Run Adjudication Routing first.</div>"
decision = data.get("routing_decision","STANDARD_REVIEW")
conf = data.get("confidence_score",60)
cls_map = {"STRAIGHT_TO_PAYMENT":"routing-approve","FAST_TRACK_REVIEW":"routing-review","STANDARD_REVIEW":"routing-review","FULL_INVESTIGATION":"routing-investigate","DECLINE":"routing-decline"}
lbl_map = {"STRAIGHT_TO_PAYMENT":"βœ… Straight to Payment","FAST_TRACK_REVIEW":"⚑ Fast Track Review","STANDARD_REVIEW":"πŸ‘ Standard Review","FULL_INVESTIGATION":"πŸ” Full Investigation","DECLINE":"❌ Decline"}
bar_col = "#28a745" if conf>=70 else "#ffc107" if conf>=40 else "#dc3545"
html = f"""<div class='{cls_map.get(decision,"routing-review")}'>
<div style='font-size:18px;font-weight:700;margin-bottom:8px'>{lbl_map.get(decision,decision)}</div>
<div style='background:rgba(0,0,0,0.1);border-radius:4px;height:8px;margin-bottom:6px'><div style='width:{conf}%;background:{bar_col};height:100%;border-radius:4px'></div></div>
<div style='font-size:12px;color:#555;margin-bottom:12px'>Confidence: <b>{conf}%</b></div>
<div style='font-size:13px;color:#333;line-height:1.6;margin-bottom:14px'>{data.get("decision_rationale","")}</div>
<div style='display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:14px'>
<div style='background:rgba(255,255,255,0.6);border-radius:8px;padding:12px;text-align:center'><div style='font-size:22px;font-weight:700;color:#0E2841'>{data.get("estimated_settlement_days",14)}</div><div style='font-size:11px;color:#888;text-transform:uppercase'>Est. days to settle</div></div>
<div style='background:rgba(255,255,255,0.6);border-radius:8px;padding:12px;text-align:center'><div style='font-size:13px;font-weight:700;color:#0E2841'>{data.get("settlement_amount_recommendation","Per adjuster")}</div><div style='font-size:11px;color:#888;text-transform:uppercase'>Settlement amount</div></div>
<div style='background:rgba(255,255,255,0.6);border-radius:8px;padding:12px;text-align:center'><div style='font-size:12px;font-weight:700;color:#0E2841'>{data.get("next_handler","")}</div><div style='font-size:11px;color:#888;text-transform:uppercase'>Assigned to</div></div>
</div>"""
factors = data.get("decision_factors",[])
if factors:
html += "<div style='margin-bottom:12px'><div style='font-size:12px;font-weight:700;color:#333;margin-bottom:6px'>Decision Factors</div>"
for f in factors:
ic = {"POSITIVE":"βœ…","NEGATIVE":"❌","NEUTRAL":"➑️"}.get(f.get("impact","NEUTRAL"),"➑️")
html += f"<div style='font-size:12px;color:#444;padding:3px 0'>{ic} {f.get('factor','')}</div>"
html += "</div>"
conds = [c for c in data.get("conditions",[]) if c and c!="None"]
if conds:
html += "<div style='margin-bottom:10px'><div style='font-size:12px;font-weight:700;color:#555;margin-bottom:4px'>πŸ“‹ Conditions Before Payment</div>"
html += "".join(f"<div style='font-size:12px;color:#555;padding:2px 0'>β€’ {c}</div>" for c in conds) + "</div>"
if data.get("audit_trail"):
html += f"<div style='font-size:11px;color:#777;padding:8px 12px;background:rgba(255,255,255,0.5);border-radius:6px'><b>Audit trail:</b> {data['audit_trail']}</div>"
html += "</div>"
return html
def render_survey(data):
if not data: return "<div style='color:#aaa;padding:16px'>Run Survey first.</div>"
estimate = data.get("surveyor_cost_estimate", 0)
html = f"""<div class='result-panel'>
<div class='result-panel-title'>πŸ”§ Surveyor Assessment</div>
<div style='background:#fff8e1;border:1px solid #ffc107;border-radius:8px;padding:10px 14px;margin-bottom:12px;font-size:13px;color:#856404'>
<b>ℹ️ Note:</b> Repair/settlement amount is determined solely by the surveyor.
</div>
<div style='font-size:13px;color:#444;margin-bottom:10px'><b>Inspection Findings:</b> {data.get("inspection_findings","")}</div>
<div style='font-size:13px;color:#444;margin-bottom:10px'><b>Damage Description:</b> {data.get("damage_description","")}</div>
<div style='font-size:13px;color:#444;margin-bottom:10px'><b>Repair Recommendation:</b> {data.get("surveyor_repair_recommendation","")}</div>
<div style='background:#f0fdf4;border:1.5px solid #16a34a;border-radius:8px;padding:14px;text-align:center;margin:12px 0'>
<div style='font-size:28px;font-weight:700;color:#155724'>€{estimate:,}</div>
<div style='font-size:12px;color:#155724;text-transform:uppercase;letter-spacing:.4px'>Surveyor Cost Estimate</div>
</div>
<div style='font-size:13px;color:#444;margin-bottom:8px'><b>Overall Assessment:</b> {data.get("surveyor_overall_assessment","")}</div>"""
discrep = data.get("discrepancies",[])
if discrep:
html += "<div style='margin-top:8px'><b style='font-size:12px;color:#721c24'>Discrepancies noted:</b>"
html += "".join(f"<div style='font-size:12px;color:#555;padding:2px 0'>β€’ {d}</div>" for d in discrep) + "</div>"
html += "</div>"
return html
def render_final_reserve(data):
if not data: return "<div style='color:#aaa;padding:16px'>Run Final Reserve first.</div>"
ready = data.get("ready_to_settle", False)
final = data.get("final_reserve", 0)
target = data.get("final_settlement_target_date","TBD")
cls = "routing-approve" if ready else "routing-investigate"
icon = "βœ… Ready to Proceed to Payment" if ready else "⏳ Additional Information Required"
html = f"""<div class='{cls}'>
<div style='font-size:17px;font-weight:700;margin-bottom:10px'>{icon}</div>
<div style='font-size:13px;color:#333;margin-bottom:12px'>{data.get("rationale","")}</div>"""
if ready:
html += f"""<div style='display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px'>
<div style='background:rgba(255,255,255,0.6);border-radius:8px;padding:14px;text-align:center'>
<div style='font-size:26px;font-weight:700;color:#155724'>€{final:,}</div>
<div style='font-size:11px;color:#555;text-transform:uppercase'>Final Settlement Amount</div>
</div>
<div style='background:rgba(255,255,255,0.6);border-radius:8px;padding:14px;text-align:center'>
<div style='font-size:16px;font-weight:700;color:#0E2841'>{target}</div>
<div style='font-size:11px;color:#555;text-transform:uppercase'>Final Settlement Target Date</div>
</div>
</div>
<div style='font-size:13px;color:#333'><b>Recommendation:</b> {data.get("settlement_recommendation","")}</div>"""
else:
outstanding = data.get("outstanding_items",[])
if outstanding:
html += "<div style='font-size:13px;font-weight:700;color:#0E2841;margin-bottom:6px'>Outstanding items before settlement:</div>"
html += "".join(f"<div style='font-size:13px;color:#555;padding:3px 0'>β†’ {item}</div>" for item in outstanding)
html += "</div>"
return html
def render_kpis(data):
if not data: return "<div style='color:#aaa;padding:16px'>Run KPI report first.</div>"
total = data.get("total_days_to_settle", 0)
sla = data.get("sla_target_days", 30)
met = data.get("sla_met")
rating= data.get("performance_rating","AMBER")
r_col = {"GREEN":"#155724","AMBER":"#856404","RED":"#721c24"}.get(rating,"#856404")
r_bg = {"GREEN":"#d4edda","AMBER":"#fff3cd","RED":"#f8d7da"}.get(rating,"#fff3cd")
sla_icon = "βœ…" if met else "❌" if met is False else "❓"
html = f"""<div class='result-panel'>
<div class='result-panel-title'>πŸ“ˆ Claims KPI Dashboard</div>
<div style='display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:16px'>
<div class='stat-box'><div class='stat-num' style='color:#0E2841'>{total}</div><div class='stat-lbl'>Days to Settle</div></div>
<div class='stat-box'><div class='stat-num' style='color:#555'>{sla}</div><div class='stat-lbl'>SLA Target (days)</div></div>
<div class='stat-box'><div class='stat-num'>{sla_icon}</div><div class='stat-lbl'>SLA Met</div></div>
<div class='stat-box'><div style='background:{r_bg};color:{r_col};border-radius:6px;padding:4px 10px;font-size:14px;font-weight:700;display:inline-block'>{rating}</div><div class='stat-lbl' style='margin-top:6px'>Performance</div></div>
</div>"""
stages = data.get("stage_breakdown",[])
if stages:
html += "<div style='margin-bottom:14px'><div style='font-size:13px;font-weight:700;color:#0E2841;margin-bottom:8px'>Stage Breakdown</div>"
for s in stages:
d = s.get("days",0)
bar_w = min(100, int(d/max(total,1)*100)) if total else 0
html += f"""<div style='margin-bottom:6px'>
<div style='display:flex;justify-content:space-between;font-size:12px;color:#555;margin-bottom:2px'><span>{s.get("stage","")}</span><span><b>{d} days</b></span></div>
<div style='background:#eee;border-radius:4px;height:6px'><div style='width:{bar_w}%;background:#4a90d9;height:100%;border-radius:4px'></div></div>
</div>"""
html += "</div>"
extras = [("Reserve Accuracy","reserve_accuracy"),("Fraud Indicators Triggered","fraud_indicators_triggered"),("Customer Touchpoints","customer_touchpoints")]
for label, key in extras:
v = data.get(key)
if v is not None:
html += f"<div style='font-size:12px;color:#555;padding:4px 0;border-bottom:1px solid #f0f0f0'><b>{label}:</b> {v}</div>"
if data.get("performance_notes"):
html += f"<div style='font-size:13px;color:#555;margin-top:12px;padding:10px 14px;background:#f8f9fa;border-radius:6px'>{data['performance_notes']}</div>"
html += "</div>"
return html
# ─── BUILD APP ────────────────────────────────────────────────
def build_app():
with gr.Blocks(title="Claims AI Diagnostic") as demo:
s_ct = gr.State("Motor β€” Third Party (Bumper to Bumper)")
s_org = gr.State("")
s_raw = gr.State("")
s_extract = gr.State({})
s_fnol = gr.State({})
s_reserve = gr.State({})
s_fraud = gr.State({})
s_routing = gr.State({})
s_survey = gr.State({})
s_final_r = gr.State({})
gr.HTML("""<div style='background:linear-gradient(135deg,#0E2841 0%,#1a3d5c 100%);border-radius:12px;padding:24px 28px 20px;margin-bottom:4px;border-bottom:3px solid #FC5108'>
<div style='display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px'>
<div>
<div style='font-size:24px;font-weight:700;color:#ffffff;margin-bottom:4px'>πŸ” Claims AI Diagnostic</div>
<div style='font-size:13px;color:#A8BDD6'>AI-powered claims assessment Β· FNOL Β· Policy review Β· Reserve Β· Fraud detection Β· Adjudication</div>
</div>
<div style='background:rgba(252,81,8,0.2);border:1px solid #FC5108;color:#FC5108;border-radius:6px;padding:5px 14px;font-size:12px;font-weight:600'>POC Demo β€” PwC Malta</div>
</div>
</div>""")
with gr.Tabs() as tabs:
# ════ STEP 1: Claims Type ════
with gr.Tab("β‘  Claims Type", id=0):
gr.HTML("<div class='info-bar'><b>Welcome to the Claims AI Diagnostic.</b> Select your claims type and organisation, then upload a real claim document or proceed to document extraction.</div>")
gr.HTML("<div class='step-title'>Select Claims Type</div><div class='step-sub'>Motor claims β€” choose your line of business</div>")
ct_dd = gr.Dropdown(choices=CLAIMS_TYPES, value="Motor β€” Third Party (Bumper to Bumper)", label="Claims Type")
org_input = gr.Textbox(label="Organisation Name", placeholder="Enter your organisation name")
with gr.Row():
gr.HTML("")
next1 = gr.Button("Next: Upload Document β†’", variant="primary")
# ════ STEP 2: Document Extraction ════
with gr.Tab("β‘‘ Document Extraction", id=1):
gr.HTML("<div class='info-bar'>For the purposes of the PwC demo β€” upload a claim form and any supporting documents. Claude AI will extract all key fields automatically. For <b>Motor β€” Third Party (Bumper to Bumper)</b> claims, also upload Malta's Bumper to Bumper accident report form if available, plus any photos of damage.</div>")
gr.HTML("<div class='step-title'>Upload & Extract Claim Document</div><div class='step-sub'>PDF, JPG, PNG β€” AI reads and extracts every visible field</div>")
with gr.Row():
with gr.Column(scale=3):
doc_file = gr.File(label="πŸ“Ž Claim document (PDF, JPG, PNG)", file_types=[".pdf",".jpg",".jpeg",".png",".txt"], type="filepath")
bumper_form_file = gr.File(label="πŸš— Bumper to Bumper Form (Malta β€” Third Party claims)", file_types=[".pdf",".jpg",".jpeg",".png"], type="filepath")
damage_photos = gr.File(label="πŸ“· Photos of damage (multiple)", file_types=[".jpg",".jpeg",".png",".webp"], file_count="multiple", type="filepath")
with gr.Column(scale=2):
policy_file = gr.File(label="πŸ“œ Policy document (for the purposes of the PwC demo)", file_types=[".pdf",".txt"], type="filepath")
extract_btn = gr.Button("πŸ€– Extract Claim Data with AI", variant="primary")
extract_status = gr.HTML("")
extract_out = gr.HTML("<div style='color:#aaa;padding:16px;font-size:13px'>Upload a document and click Extract.</div>")
def do_extract(fp, ct, org, bumper_fp, damage_fps):
if not fp and not bumper_fp and not damage_fps:
yield render_claim_fields(None), "<div style='color:orange'>⚠️ Please upload a document.</div>", {}, ""
return
yield gr.update(), "<div style='color:#FC5108;font-size:13px'>⏳ Extracting document(s)...</div>", {}, ""
data, raw, err = analyse_document(fp, ct, org, bumper_fp, damage_fps)
if err and not data:
yield render_claim_fields(None), f"<div style='color:red'>❌ {err}</div>", {}, ""
return
yield render_claim_fields(data), "<div style='color:green;font-size:13px'>βœ… Extraction complete β€” proceed to FNOL Analysis.</div>", data or {}, raw or ""
extract_btn.click(do_extract, inputs=[doc_file, ct_dd, org_input, bumper_form_file, damage_photos], outputs=[extract_out, extract_status, s_extract, s_raw])
def toggle_bumper(ct):
return gr.update(visible=(ct == "Motor β€” Third Party (Bumper to Bumper)"))
ct_dd.change(toggle_bumper, inputs=ct_dd, outputs=bumper_form_file)
with gr.Row():
back2 = gr.Button("← Back"); next2 = gr.Button("Next: FNOL & Policy Review β†’", variant="primary")
# ════ STEP 3: FNOL ════
with gr.Tab("β‘’ FNOL & Policy Review", id=2):
gr.HTML("<div class='info-bar'><b>First Notice of Loss assessment.</b> AI reviews the claim against every policy condition, assigns a validity score, determines coverage, and assesses fault probability.</div>")
gr.HTML("<div class='step-title'>FNOL Analysis & Policy Comparison</div>")
fnol_btn = gr.Button("πŸ“‹ Run FNOL Analysis", variant="primary")
fnol_status = gr.HTML("")
fnol_out = gr.HTML("<div style='color:#aaa;padding:16px'>Click Run FNOL Analysis.</div>")
def do_fnol(ed, raw, ct, org, pf):
yield gr.update(), "<div style='color:#FC5108;font-size:13px'>⏳ Running FNOL assessment...</div>"
data, err = run_fnol_policy(ed, raw, ct, org, pf)
yield render_fnol(data), "<div style='color:green;font-size:13px'>βœ… FNOL complete.</div>"
fnol_btn.click(do_fnol, inputs=[s_extract,s_raw,ct_dd,org_input,policy_file], outputs=[fnol_out, fnol_status])
fnol_btn.click(lambda ed,raw,ct,org,pf: run_fnol_policy(ed,raw,ct,org,pf)[0] or {}, inputs=[s_extract,s_raw,ct_dd,org_input,policy_file], outputs=s_fnol)
with gr.Row():
back3 = gr.Button("← Back"); next3 = gr.Button("Next: Reserve β†’", variant="primary")
# ════ STEP 4: Reserve ════
with gr.Tab("β‘£ Reserve Recommendation", id=3):
gr.HTML("<div class='info-bar'><b>Initial reserve setting.</b> An initial reserve estimate is uploaded per claim type and reviewed by the surveyor. The expected reserve is shown below β€” final amount is subject to surveyor confirmation.</div>")
gr.HTML("<div class='step-title'>Claims Reserve Recommendation</div>")
reserve_btn = gr.Button("πŸ’° Generate Reserve Recommendation", variant="primary")
reserve_status = gr.HTML("")
reserve_out = gr.HTML("<div style='color:#aaa;padding:16px'>Click Generate Reserve Recommendation.</div>")
def do_reserve(ed, fd, ct, org):
yield gr.update(), "<div style='color:#FC5108;font-size:13px'>⏳ Calculating reserve...</div>"
data, err = run_reserve(ed, fd, ct, org)
yield render_reserve(data, ct), "<div style='color:green;font-size:13px'>βœ… Reserve complete.</div>"
reserve_btn.click(do_reserve, inputs=[s_extract,s_fnol,ct_dd,org_input], outputs=[reserve_out, reserve_status])
reserve_btn.click(lambda ed,fd,ct,org: run_reserve(ed,fd,ct,org)[0] or {}, inputs=[s_extract,s_fnol,ct_dd,org_input], outputs=s_reserve)
with gr.Row():
back4 = gr.Button("← Back"); next4 = gr.Button("Next: Risk Detection β†’", variant="primary")
# ════ STEP 5: Risk Detection ════
with gr.Tab("β‘€ Risk Detection", id=4):
gr.HTML("<div class='info-bar'><b>AI risk analysis cross-referencing FNOL findings.</b> Checks for red flags, inconsistencies, and fraud indicators.</div>")
gr.HTML("<div class='step-title'>Risk Assessment</div>")
fraud_btn = gr.Button("πŸ” Run Risk Detection", variant="primary")
fraud_status = gr.HTML("")
fraud_out = gr.HTML("<div style='color:#aaa;padding:16px'>Click Run Risk Detection.</div>")
def do_fraud(ed, fd, ct):
yield gr.update(), "<div style='color:#FC5108;font-size:13px'>⏳ Analysing risk indicators...</div>"
data, err = run_fraud(ed, fd, ct)
yield render_fraud(data), "<div style='color:green;font-size:13px'>βœ… Risk analysis complete.</div>"
fraud_btn.click(do_fraud, inputs=[s_extract,s_fnol,s_ct], outputs=[fraud_out, fraud_status])
fraud_btn.click(lambda ed,fd,ct: run_fraud(ed,fd,ct)[0] or {}, inputs=[s_extract,s_fnol,s_ct], outputs=s_fraud)
with gr.Row():
back5 = gr.Button("← Back"); next5 = gr.Button("Next: Adjudication β†’", variant="primary")
# ════ STEP 6: Adjudication ════
with gr.Tab("β‘₯ Adjudication & Routing", id=5):
gr.HTML("<div class='info-bar'><b>Automated adjudication decision.</b> AI evaluates all prior findings and recommends a payment routing with rationale and audit trail.</div>")
gr.HTML("<div class='step-title'>Adjudication & Payment Routing</div>")
routing_btn = gr.Button("βš–οΈ Run Adjudication Decision", variant="primary")
routing_status = gr.HTML("")
routing_out = gr.HTML("<div style='color:#aaa;padding:16px'>Click Run Adjudication Decision.</div>")
def do_routing(ed, fd, fraud_d, res_d, ct, org):
yield gr.update(), "<div style='color:#FC5108;font-size:13px'>⏳ Running adjudication...</div>"
data, err = run_routing(ed, fd, fraud_d, res_d, ct, org)
yield render_routing(data), "<div style='color:green;font-size:13px'>βœ… Routing decision complete.</div>"
routing_btn.click(do_routing, inputs=[s_extract,s_fnol,s_fraud,s_reserve,s_ct,s_org], outputs=[routing_out, routing_status])
routing_btn.click(lambda ed,fd,fraud_d,res_d,ct,org: run_routing(ed,fd,fraud_d,res_d,ct,org)[0] or {}, inputs=[s_extract,s_fnol,s_fraud,s_reserve,s_ct,s_org], outputs=s_routing)
with gr.Row():
back6 = gr.Button("← Back"); next6 = gr.Button("Next: Survey β†’", variant="primary")
# ════ STEP 7: Survey ════
with gr.Tab("⑦ Survey", id=6):
gr.HTML("<div class='info-bar'><b>Surveyor inspection.</b> Upload the surveyor report if available. The repair/settlement amount is determined solely by the surveyor β€” not self-reported by the claimant.</div>")
gr.HTML("<div class='step-title'>Surveyor Assessment</div>")
survey_file = gr.File(label="πŸ“„ Upload Surveyor Report (optional)", file_types=[".pdf",".jpg",".jpeg",".png"], type="filepath")
survey_btn = gr.Button("πŸ”§ Run Survey Assessment", variant="primary")
survey_status = gr.HTML("")
survey_out = gr.HTML("<div style='color:#aaa;padding:16px'>Click Run Survey Assessment.</div>")
def do_survey(ed, fd, ct, org, sf):
yield gr.update(), "<div style='color:#FC5108;font-size:13px'>⏳ Running survey assessment...</div>"
data, err = run_survey(ed, fd, ct, org, sf)
yield render_survey(data), "<div style='color:green;font-size:13px'>βœ… Survey complete.</div>"
survey_btn.click(do_survey, inputs=[s_extract,s_fnol,ct_dd,org_input,survey_file], outputs=[survey_out, survey_status])
survey_btn.click(lambda ed,fd,ct,org,sf: run_survey(ed,fd,ct,org,sf)[0] or {}, inputs=[s_extract,s_fnol,ct_dd,org_input,survey_file], outputs=s_survey)
with gr.Row():
back7 = gr.Button("← Back"); next7 = gr.Button("Next: Final Reserve β†’", variant="primary")
# ════ STEP 8: Final Reserve ════
with gr.Tab("β‘§ Final Reserve Recommendation", id=7):
gr.HTML("<div class='info-bar'><b>Final reserve recommendation.</b> Based on surveyor findings, AI recommends whether to proceed to payment or request additional information, with a final settlement target date.</div>")
gr.HTML("<div class='step-title'>Final Reserve & Settlement Decision</div>")
fr_btn = gr.Button("πŸ“Š Generate Final Recommendation", variant="primary")
fr_status = gr.HTML("")
fr_out = gr.HTML("<div style='color:#aaa;padding:16px'>Click Generate Final Recommendation.</div>")
def do_final_reserve(ed, fd, res_d, sur_d, ct, org):
yield gr.update(), "<div style='color:#FC5108;font-size:13px'>⏳ Generating final recommendation...</div>"
data, err = run_final_reserve(ed, fd, res_d, sur_d, ct, org)
yield render_final_reserve(data), "<div style='color:green;font-size:13px'>βœ… Final recommendation complete.</div>"
fr_btn.click(do_final_reserve, inputs=[s_extract,s_fnol,s_reserve,s_survey,ct_dd,org_input], outputs=[fr_out, fr_status])
fr_btn.click(lambda ed,fd,res_d,sur_d,ct,org: run_final_reserve(ed,fd,res_d,sur_d,ct,org)[0] or {}, inputs=[s_extract,s_fnol,s_reserve,s_survey,ct_dd,org_input], outputs=s_final_r)
with gr.Row():
back8 = gr.Button("← Back"); next8 = gr.Button("Next: KPIs β†’", variant="primary")
# ════ STEP 9: KPIs ════
with gr.Tab("⑨ KPIs & Analytics", id=8):
gr.HTML("<div class='info-bar'><b>Claim performance metrics.</b> Enter claim open and settlement dates to generate SLA compliance, stage breakdown, and overall performance rating.</div>")
gr.HTML("<div class='step-title'>KPI Dashboard</div>")
with gr.Row():
kpi_open = gr.Textbox(label="Claim Open Date", placeholder="e.g. 01 May 2026")
kpi_close = gr.Textbox(label="Settlement Date", placeholder="e.g. 10 June 2026")
kpi_btn = gr.Button("πŸ“ˆ Generate KPI Report", variant="primary")
kpi_status = gr.HTML("")
kpi_out = gr.HTML("<div style='color:#aaa;padding:16px'>Enter dates and click Generate KPI Report.</div>")
def do_kpis(ed, fd, res_d, rout_d, ct, org, od, sd):
yield gr.update(), "<div style='color:#FC5108;font-size:13px'>⏳ Calculating KPIs...</div>"
data, err = run_kpis(ed, fd, res_d, rout_d, ct, org, od, sd)
yield render_kpis(data), "<div style='color:green;font-size:13px'>βœ… KPI report complete.</div>"
kpi_btn.click(do_kpis, inputs=[s_extract,s_fnol,s_reserve,s_routing,ct_dd,org_input,kpi_open,kpi_close], outputs=[kpi_out, kpi_status])
with gr.Row():
back9 = gr.Button("← Back")
# ════ STEP 10: Demo Scenarios ════
with gr.Tab("β‘© Demo Scenarios", id=9):
gr.HTML("<div class='info-bar'><b>Pre-built demo scenarios for presentation purposes.</b> Click a scenario to pre-load data, then navigate through the tabs.</div>")
gr.HTML("<div class='step-title'>Quick Demo Scenarios</div>")
with gr.Row():
demo_btn1 = gr.Button("πŸš— Motor β€” Third Party (Bumper to Bumper)\n€2,850 Β· Low complexity", variant="secondary")
demo_btn2 = gr.Button("πŸš™ Motor β€” Own Damage (Comprehensive)\n€6,400 Β· Medium complexity", variant="secondary")
demo_preview = gr.HTML("")
def load_demo(key):
d = DEMO_CLAIMS.get(key, {})
if not d: return key, gr.update(), {}
html = f"""<div style='background:#f0f7ff;border:1.5px solid #4a90d9;border-radius:10px;padding:14px 18px;margin-top:14px'>
<div style='font-size:13px;font-weight:700;color:#0E2841;margin-bottom:8px'>βœ“ Demo loaded: {d.get('claim_type','')}</div>
<div style='display:grid;grid-template-columns:repeat(3,1fr);gap:8px'>
<div><div style='font-size:11px;color:#888'>Claimant</div><div style='font-size:13px;font-weight:600'>{d.get('claimant_name','')}</div></div>
<div><div style='font-size:11px;color:#888'>Policy</div><div style='font-size:13px;font-weight:600'>{d.get('policy_number','')}</div></div>
<div><div style='font-size:11px;color:#888'>Location</div><div style='font-size:13px;font-weight:600'>{d.get('incident_location','')}</div></div>
</div>
<div style='font-size:12px;color:#555;margin-top:8px'>{d.get('incident_description','')[:180]}...</div>
<div style='font-size:12px;color:#FC5108;margin-top:6px'>βœ“ Data pre-loaded β€” go to β‘  Claims Type tab to proceed</div>
</div>"""
return key, gr.HTML(html), d
demo_btn1.click(lambda: load_demo("Motor β€” Third Party (Bumper to Bumper)"), outputs=[ct_dd, demo_preview, s_extract])
demo_btn2.click(lambda: load_demo("Motor β€” Own Damage (Comprehensive)"), outputs=[ct_dd, demo_preview, s_extract])
# ── Navigation ──
def save1(ct, org): return ct, org, gr.update(selected=1)
next1.click(save1, inputs=[ct_dd, org_input], outputs=[s_ct, s_org, tabs])
next2.click(lambda: gr.update(selected=2), outputs=tabs)
back2.click(lambda: gr.update(selected=0), outputs=tabs)
next3.click(lambda: gr.update(selected=3), outputs=tabs)
back3.click(lambda: gr.update(selected=1), outputs=tabs)
next4.click(lambda: gr.update(selected=4), outputs=tabs)
back4.click(lambda: gr.update(selected=2), outputs=tabs)
next5.click(lambda: gr.update(selected=5), outputs=tabs)
back5.click(lambda: gr.update(selected=3), outputs=tabs)
next6.click(lambda: gr.update(selected=6), outputs=tabs)
back6.click(lambda: gr.update(selected=4), outputs=tabs)
next7.click(lambda: gr.update(selected=7), outputs=tabs)
back7.click(lambda: gr.update(selected=5), outputs=tabs)
next8.click(lambda: gr.update(selected=8), outputs=tabs)
back8.click(lambda: gr.update(selected=6), outputs=tabs)
next8.click(lambda: gr.update(selected=8), outputs=tabs)
back9.click(lambda: gr.update(selected=7), outputs=tabs)
return demo
if __name__ == "__main__":
build_app().launch(css=CSS)