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 "
No data yet.
" 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 = "
📋 Extracted Claim Data
" for k,label in labels: v = data.get(k) if v and str(v) not in ("null","None",""): html += f"
{label}
{v}
" html += "
" docs = [d for d in data.get("supporting_documents",[]) if d and d!="null"] if docs: html += "
Documents: " html += " · ".join(f"{d}" for d in docs) + "
" flags = [f for f in data.get("key_flags",[]) if f and f!="null"] if flags: html += "
⚑ Flags: " html += " · ".join(f"{f}" for f in flags) + "
" photo_findings = data.get("photo_findings") or [] if photo_findings: html += "
📷 Photo Analysis:" for i, pf in enumerate(photo_findings, 1): desc = pf.get("damage_description","") plate = pf.get("number_plate_text") plate_html = f" — plate: {plate}" if plate and str(plate) not in ("null","None") else "" html += f"
Photo {i}: {desc}{plate_html}
" html += "
" html += "
" return html def render_fnol(data): if not data: return "
Run FNOL Analysis first.
" 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"""
📝 FNOL Assessment
{summary}
{validity}
Claim validity
{score}/100
Validity score
{complexity}
Complexity
⚖️ Fault Assessment
{fault_pct}%
Probability insured is NOT at fault
{fault.get("rationale","")}
{cov_icon} Coverage: {coverage.get("coverage_confidence","N/A")} confidence — {coverage.get("coverage_notes","")}
""" 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"
{vm_icon} Vehicle Match: {vm_label}
Claim reg: {claim_reg} · Photo plate(s): {photo_plates_str}
" if escalate and esc_rsn: html += f"
🚨 Escalation Required: {esc_rsn}
" 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"
📜 Policy Condition Checks
" html += f"
✅ {passes} passed   ❌ {fails} failed   ⚠️ {warns} warnings
" 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"
{icon_map.get(st,'❓')} {st} — {c.get('rule','')}
{c.get('finding','')}
" if questions: html += "
❓ Key Questions for Claimant
" html += "".join(f"
• {q}
" for q in questions) + "
" # 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 += "
⚡ Immediate Actions Required
" html += "".join(f"
→ {a}
" for a in filtered) + "
" html += "
" return html def render_reserve(data, claim_type): if not data: return "
Run Reserve Recommendation first.
" 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"""
💰 Reserve Recommendation — {claim_type} {conf} CONFIDENCE
ℹ️ Note: 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.
Expected Reserve
€{mid:,}
Subject to surveyor confirmation
{data.get("rationale","")}
""" if data.get("key_drivers"): html += "
📊 Key Reserve Drivers
" html += "".join(f"{d}" for d in data["key_drivers"]) + "
" 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 += "
⚑ Reserve Increase Triggers
" html += "".join(f"
• {t}
" for t in triggers) + "
" review = data.get("recommended_review", "Minimum 30 days from today") html += f"
Next reserve review: {review}
" if data.get("ibnr_note"): html += f"
IBNR note: {data['ibnr_note']}
" html += "
" return html def render_fraud(data): if not data: return "
Run Risk Detection first.
" 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"""
{icon} Risk Level: {level}
Risk score: {score}/100
⚡ {action}{data.get("action_reason","")}
""" if fnol_c: html += f"
FNOL Consistency: {fnol_c} — {data.get('fnol_notes','')}
" 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"
{title}
" html += "".join(f"
• {i}
" for i in items) + "
" html += "
" return html def render_routing(data): if not data: return "
Run Adjudication Routing first.
" 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"""
{lbl_map.get(decision,decision)}
Confidence: {conf}%
{data.get("decision_rationale","")}
{data.get("estimated_settlement_days",14)}
Est. days to settle
{data.get("settlement_amount_recommendation","Per adjuster")}
Settlement amount
{data.get("next_handler","")}
Assigned to
""" factors = data.get("decision_factors",[]) if factors: html += "
Decision Factors
" for f in factors: ic = {"POSITIVE":"✅","NEGATIVE":"❌","NEUTRAL":"➡️"}.get(f.get("impact","NEUTRAL"),"➡️") html += f"
{ic} {f.get('factor','')}
" html += "
" conds = [c for c in data.get("conditions",[]) if c and c!="None"] if conds: html += "
📋 Conditions Before Payment
" html += "".join(f"
• {c}
" for c in conds) + "
" if data.get("audit_trail"): html += f"
Audit trail: {data['audit_trail']}
" html += "
" return html def render_survey(data): if not data: return "
Run Survey first.
" estimate = data.get("surveyor_cost_estimate", 0) html = f"""
🔧 Surveyor Assessment
ℹ️ Note: Repair/settlement amount is determined solely by the surveyor.
Inspection Findings: {data.get("inspection_findings","")}
Damage Description: {data.get("damage_description","")}
Repair Recommendation: {data.get("surveyor_repair_recommendation","")}
€{estimate:,}
Surveyor Cost Estimate
Overall Assessment: {data.get("surveyor_overall_assessment","")}
""" discrep = data.get("discrepancies",[]) if discrep: html += "
Discrepancies noted:" html += "".join(f"
• {d}
" for d in discrep) + "
" html += "
" return html def render_final_reserve(data): if not data: return "
Run Final Reserve first.
" 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"""
{icon}
{data.get("rationale","")}
""" if ready: html += f"""
€{final:,}
Final Settlement Amount
{target}
Final Settlement Target Date
Recommendation: {data.get("settlement_recommendation","")}
""" else: outstanding = data.get("outstanding_items",[]) if outstanding: html += "
Outstanding items before settlement:
" html += "".join(f"
→ {item}
" for item in outstanding) html += "
" return html def render_kpis(data): if not data: return "
Run KPI report first.
" 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"""
📈 Claims KPI Dashboard
{total}
Days to Settle
{sla}
SLA Target (days)
{sla_icon}
SLA Met
{rating}
Performance
""" stages = data.get("stage_breakdown",[]) if stages: html += "
Stage Breakdown
" 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"""
{s.get("stage","")}{d} days
""" html += "
" 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"
{label}: {v}
" if data.get("performance_notes"): html += f"
{data['performance_notes']}
" html += "
" 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("""
🔍 Claims AI Diagnostic
AI-powered claims assessment · FNOL · Policy review · Reserve · Fraud detection · Adjudication
POC Demo — PwC Malta
""") with gr.Tabs() as tabs: # ════ STEP 1: Claims Type ════ with gr.Tab("① Claims Type", id=0): gr.HTML("
Welcome to the Claims AI Diagnostic. Select your claims type and organisation, then upload a real claim document or proceed to document extraction.
") gr.HTML("
Select Claims Type
Motor claims — choose your line of business
") 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("
For the purposes of the PwC demo — upload a claim form and any supporting documents. Claude AI will extract all key fields automatically. For Motor — Third Party (Bumper to Bumper) claims, also upload Malta's Bumper to Bumper accident report form if available, plus any photos of damage.
") gr.HTML("
Upload & Extract Claim Document
PDF, JPG, PNG — AI reads and extracts every visible field
") 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("
Upload a document and click Extract.
") 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), "
⚠️ Please upload a document.
", {}, "" return yield gr.update(), "
⏳ Extracting document(s)...
", {}, "" data, raw, err = analyse_document(fp, ct, org, bumper_fp, damage_fps) if err and not data: yield render_claim_fields(None), f"
❌ {err}
", {}, "" return yield render_claim_fields(data), "
✅ Extraction complete — proceed to FNOL Analysis.
", 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("
First Notice of Loss assessment. AI reviews the claim against every policy condition, assigns a validity score, determines coverage, and assesses fault probability.
") gr.HTML("
FNOL Analysis & Policy Comparison
") fnol_btn = gr.Button("📋 Run FNOL Analysis", variant="primary") fnol_status = gr.HTML("") fnol_out = gr.HTML("
Click Run FNOL Analysis.
") def do_fnol(ed, raw, ct, org, pf): yield gr.update(), "
⏳ Running FNOL assessment...
" data, err = run_fnol_policy(ed, raw, ct, org, pf) yield render_fnol(data), "
✅ FNOL complete.
" 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("
Initial reserve setting. 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.
") gr.HTML("
Claims Reserve Recommendation
") reserve_btn = gr.Button("💰 Generate Reserve Recommendation", variant="primary") reserve_status = gr.HTML("") reserve_out = gr.HTML("
Click Generate Reserve Recommendation.
") def do_reserve(ed, fd, ct, org): yield gr.update(), "
⏳ Calculating reserve...
" data, err = run_reserve(ed, fd, ct, org) yield render_reserve(data, ct), "
✅ Reserve complete.
" 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("
AI risk analysis cross-referencing FNOL findings. Checks for red flags, inconsistencies, and fraud indicators.
") gr.HTML("
Risk Assessment
") fraud_btn = gr.Button("🔍 Run Risk Detection", variant="primary") fraud_status = gr.HTML("") fraud_out = gr.HTML("
Click Run Risk Detection.
") def do_fraud(ed, fd, ct): yield gr.update(), "
⏳ Analysing risk indicators...
" data, err = run_fraud(ed, fd, ct) yield render_fraud(data), "
✅ Risk analysis complete.
" 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("
Automated adjudication decision. AI evaluates all prior findings and recommends a payment routing with rationale and audit trail.
") gr.HTML("
Adjudication & Payment Routing
") routing_btn = gr.Button("⚖️ Run Adjudication Decision", variant="primary") routing_status = gr.HTML("") routing_out = gr.HTML("
Click Run Adjudication Decision.
") def do_routing(ed, fd, fraud_d, res_d, ct, org): yield gr.update(), "
⏳ Running adjudication...
" data, err = run_routing(ed, fd, fraud_d, res_d, ct, org) yield render_routing(data), "
✅ Routing decision complete.
" 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("
Surveyor inspection. Upload the surveyor report if available. The repair/settlement amount is determined solely by the surveyor — not self-reported by the claimant.
") gr.HTML("
Surveyor Assessment
") 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("
Click Run Survey Assessment.
") def do_survey(ed, fd, ct, org, sf): yield gr.update(), "
⏳ Running survey assessment...
" data, err = run_survey(ed, fd, ct, org, sf) yield render_survey(data), "
✅ Survey complete.
" 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("
Final reserve recommendation. Based on surveyor findings, AI recommends whether to proceed to payment or request additional information, with a final settlement target date.
") gr.HTML("
Final Reserve & Settlement Decision
") fr_btn = gr.Button("📊 Generate Final Recommendation", variant="primary") fr_status = gr.HTML("") fr_out = gr.HTML("
Click Generate Final Recommendation.
") def do_final_reserve(ed, fd, res_d, sur_d, ct, org): yield gr.update(), "
⏳ Generating final recommendation...
" data, err = run_final_reserve(ed, fd, res_d, sur_d, ct, org) yield render_final_reserve(data), "
✅ Final recommendation complete.
" 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("
Claim performance metrics. Enter claim open and settlement dates to generate SLA compliance, stage breakdown, and overall performance rating.
") gr.HTML("
KPI Dashboard
") 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("
Enter dates and click Generate KPI Report.
") def do_kpis(ed, fd, res_d, rout_d, ct, org, od, sd): yield gr.update(), "
⏳ Calculating KPIs...
" data, err = run_kpis(ed, fd, res_d, rout_d, ct, org, od, sd) yield render_kpis(data), "
✅ KPI report complete.
" 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("
Pre-built demo scenarios for presentation purposes. Click a scenario to pre-load data, then navigate through the tabs.
") gr.HTML("
Quick Demo Scenarios
") 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"""
✓ Demo loaded: {d.get('claim_type','')}
Claimant
{d.get('claimant_name','')}
Policy
{d.get('policy_number','')}
Location
{d.get('incident_location','')}
{d.get('incident_description','')[:180]}...
✓ Data pre-loaded — go to ① Claims Type tab to proceed
""" 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)