diff --git a/Dockerfile b/Dockerfile index f7ab599..e4f65a3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,8 @@ RUN pip install --no-cache-dir -r requirements.txt COPY services/ services/ COPY api/ api/ -# Demo claims (committed). Mount or bake data/generated/ for the full seeded set (issue #47). +# Demo claims (committed). Pre-baked index survives Render redeploys. +COPY data/demo_claims_index.json data/claims_index.json COPY data/samples/ data/samples/ # NOTE: no .env, no secrets copied — injected at runtime by Render diff --git a/api/claims.py b/api/claims.py index ed5564c..1589ef6 100644 --- a/api/claims.py +++ b/api/claims.py @@ -2,6 +2,7 @@ from fastapi.concurrency import run_in_threadpool from pydantic import BaseModel from services.agents.run import start_claim, resume_claim +from services.agents.audit import audit_node from services.ingest.extractor import extract_claim from fastapi import Depends from .auth import require_key @@ -78,7 +79,14 @@ async def decide(claim_id: str, body: DecisionIn): else c["recommendation"]["decision"]) def _resume(): - resume_claim(c["thread_id"], human_decision=final, approver=body.approver) + try: + resume_claim(c["thread_id"], human_decision=final, + approver=body.approver) + except Exception: + # baked demo index may not have a matching LangGraph checkpoint + audit_node({"claim_id": claim_id, + "recommendation": c["recommendation"], + "human_decision": final, "approver": body.approver}) await run_in_threadpool(_resume) store.upsert(claim_id, status="decided", diff --git a/data/demo_claims_index.json b/data/demo_claims_index.json new file mode 100644 index 0000000..0c3a3c7 --- /dev/null +++ b/data/demo_claims_index.json @@ -0,0 +1,114 @@ +{ + "claim_0001": { + "thread_id": "3de5e4ee-ad20-4576-99e5-c13b37dc8590", + "status": "pending", + "recommendation": { + "decision": "approve", + "payout_low_usd": 6500.0, + "payout_high_usd": 26000.0, + "fraud_risk": "low", + "confidence": 0.83, + "rationale": "Applying adjudication rules in order: (1) Coverage is confirmed - covered is true, so no denial on coverage grounds. (2) Fraud risk is low with no flags, so no investigation required. (3) Cost midpoint ($17,250) exceeds deductible ($1,000), so no denial on deductible grounds. (4) Therefore, we approve the claim. Payout range is calculated as repair cost estimates ($7,500-$27,000) minus the $1,000 deductible, with the high end capped at the coverage limit of $26,000. Similar precedent claim_0012 involved vandalism and was approved with a comparable payout of $8,750.", + "cited_precedents": [ + "claim_0012", + "claim_0013", + "claim_0023" + ], + "policy_basis": "Claim meets all approval criteria: incident is covered under policy, fraud risk is low, and estimated repair costs exceed the deductible amount. Payout is subject to $1,000 deductible and $26,000 coverage limit as specified in the policy terms." + }, + "evidence": { + "incident_type": "collision", + "damaged_parts": [ + "front end", + "hood", + "front driver side", + "windshield", + "front bumper", + "driver door" + ], + "severity": "severe", + "visible_pre_existing_damage": false, + "image_inconsistency": true, + "policy_number": "AUTO-42953", + "deductible_usd": 1000.0, + "coverage_limit_usd": 26000.0, + "notes_summary": "Insured reported vandalism at junction with severe damage. However, the photo evidence shows extensive collision damage consistent with a high-speed impact rather than vandalism. The front end is severely crushed, hood crumpled, windshield shattered, and driver door damaged - all typical of a collision. Adjuster notes mention 'severe dmg to unknown' suggesting uncertainty about the incident type. Discrepancy exists between claimed vandalism and visible collision damage.", + "extraction_confidence": 0.65 + }, + "claim_id": "claim_0001" + }, + "claim_0000": { + "thread_id": "b9fcefa6-51b0-4e57-9e1c-f5436a4d11f0", + "status": "pending", + "recommendation": { + "decision": "approve", + "payout_low_usd": 80.0, + "payout_high_usd": 1400.0, + "fraud_risk": "low", + "confidence": 0.92, + "rationale": "Coverage is confirmed and the claim passes all checks. The cost midpoint ($990) exceeds the deductible ($250), satisfying the threshold requirement. Fraud risk is assessed as low with no flags present. All precedent cases with similar image characteristics were approved with no fraud issues. The estimated repair costs fall well within the policy coverage limit of $26,000. Payout range calculated as cost estimates ($330-$1,650) minus the $250 deductible.", + "cited_precedents": [ + "claim_0000", + "claim_0014", + "claim_0032", + "claim_0072", + "claim_0062" + ], + "policy_basis": "Claim is covered under policy with $250 deductible and $26,000 coverage limit. Cost estimate midpoint of $990 exceeds deductible threshold. No fraud indicators or coverage exclusions apply." + }, + "evidence": { + "incident_type": "theft", + "damaged_parts": [ + "rear bumper", + "quarter panel" + ], + "severity": "minor", + "visible_pre_existing_damage": false, + "image_inconsistency": true, + "policy_number": "AUTO-74686", + "deductible_usd": 250.0, + "coverage_limit_usd": 26000.0, + "notes_summary": "Reporting party reports theft incident occurred on driveway with minor damage to unknown parts. Image shows blue vehicle with scrapes and damage to rear bumper/quarter panel area.", + "extraction_confidence": 0.65 + }, + "claim_id": "claim_0000" + }, + "claim_0007": { + "thread_id": "af646410-16fa-4592-97df-b65693ad59a2", + "status": "pending", + "recommendation": { + "decision": "approve", + "payout_low_usd": 6750.0, + "payout_high_usd": 24950.0, + "fraud_risk": "low", + "confidence": 0.85, + "rationale": "Coverage is confirmed with a deductible of $250 and coverage limit of $26,000. The estimated repair cost ranges from $7,000 to $25,200 (midpoint $16,100), which exceeds the deductible threshold. Fraud risk is assessed as low with no flags identified. Following the decision rules in order: (1) coverage is confirmed as covered, (2) fraud risk is low (not medium or high), (3) the midpoint cost of $16,100 exceeds the $250 deductible. Therefore, the claim should be approved. Similar precedents show approved severe damage claims (claim_0059, claim_0006, claim_0012) with comparable severity patterns. Payout calculation: cost range minus $250 deductible = $6,750 to $24,950.", + "cited_precedents": [ + "claim_0059", + "claim_0006", + "claim_0012", + "claim_0061" + ], + "policy_basis": "Claim meets coverage requirements under the policy. The incident is covered, fraud risk is low, and repair costs significantly exceed the deductible amount. Policy allows for approval with payout calculated as estimated costs less applicable deductible." + }, + "evidence": { + "incident_type": "collision", + "damaged_parts": [ + "rear end", + "trunk", + "rear bumper", + "rear quarter panel", + "roof" + ], + "severity": "severe", + "visible_pre_existing_damage": false, + "image_inconsistency": true, + "policy_number": "AUTO-41195", + "deductible_usd": 250.0, + "coverage_limit_usd": 26000.0, + "notes_summary": "Caller reports collision in car park with severe damage. No injuries reported. Image shows catastrophic damage to entire rear section and roof structure with complete collapse - inconsistent with typical car park collision.", + "extraction_confidence": 0.65 + }, + "claim_id": "claim_0007" + } +} \ No newline at end of file diff --git a/web/src/WakeApi.tsx b/web/src/WakeApi.tsx new file mode 100644 index 0000000..ba52229 --- /dev/null +++ b/web/src/WakeApi.tsx @@ -0,0 +1,76 @@ +import { useEffect, useState } from "react"; +import { api } from "./api"; + +const INTERVAL_MS = 2_000; +const MAX_ATTEMPTS = 45; // ~90s — covers Render free-tier cold start + +export default function WakeApi({ children }: { children: React.ReactNode }) { + const [ready, setReady] = useState(false); + const [failed, setFailed] = useState(false); + const [attempt, setAttempt] = useState(0); + + useEffect(() => { + let cancelled = false; + + async function poll() { + for (let i = 0; i < MAX_ATTEMPTS; i++) { + if (cancelled) return; + setAttempt(i + 1); + try { + await api.health(); + setReady(true); + return; + } catch { + await new Promise((r) => setTimeout(r, INTERVAL_MS)); + } + } + if (!cancelled) setFailed(true); + } + + poll(); + return () => { + cancelled = true; + }; + }, []); + + if (ready) return <>{children}>; + + return ( +
+ API didn't wake up in time +
++ The demo API may still be starting. Refresh in a moment, or check + Render dashboard. +
+ + > + ) : ( + <> ++ Waking demo API… +
++ Free-tier container was asleep. First load can take up to ~30s. +
+ {attempt > 3 && ( ++ Still waiting… ({attempt}/{MAX_ATTEMPTS}) +
+ )} + > + )} +