Spaces:
Sleeping
Sleeping
Deploy v1 — single-Docker FastAPI + Next.js + RAG + voice + faithfulness
Browse files- data/policy_facts/_curation_report.md +126 -0
- frontend/src/app/page.tsx +88 -35
- frontend/src/lib/api.ts +18 -0
- tools/pdf_probe.py +11 -0
data/policy_facts/_curation_report.md
CHANGED
|
@@ -90,3 +90,129 @@ Across all 22 files, the **PDF-extractable fields** (consistently populated with
|
|
| 90 |
- Text-cache step: `/Users/rohitsar/Documents/Personal/AI Work/Insurance Sales Bot/tools/extract_policy_text.py` (writes flat `.txt` per PDF under `/tmp/claude/policy_extract/text_cache/`).
|
| 91 |
- Re-run any policy: `python3 -c "import pdfplumber; print('\\n'.join((p.extract_text() or '') for p in pdfplumber.open('rag/corpus/<insurer>/<pdf>').pages))"` then grep for field-specific anchors.
|
| 92 |
- All quoted text in JSON files is grep-verifiable against the text cache or directly against the PDF.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
- Text-cache step: `/Users/rohitsar/Documents/Personal/AI Work/Insurance Sales Bot/tools/extract_policy_text.py` (writes flat `.txt` per PDF under `/tmp/claude/policy_extract/text_cache/`).
|
| 91 |
- Re-run any policy: `python3 -c "import pdfplumber; print('\\n'.join((p.extract_text() or '') for p in pdfplumber.open('rag/corpus/<insurer>/<pdf>').pages))"` then grep for field-specific anchors.
|
| 92 |
- All quoted text in JSON files is grep-verifiable against the text cache or directly against the PDF.
|
| 93 |
+
|
| 94 |
+
---
|
| 95 |
+
|
| 96 |
+
# Batch 2 — 2026-05-14
|
| 97 |
+
|
| 98 |
+
**Curator:** Pattern-based extraction via `tools/curate_batch2.py` from local rag/corpus PDFs.
|
| 99 |
+
**Output schema:** Same per-field `{value, source_pdf_path/source_url, source_quote}` provenance triple as batch 1.
|
| 100 |
+
**Total policies added in batch 2:** 43 (running total: 65 / target 60).
|
| 101 |
+
**Average completeness:** 64.1% (batch 2 only).
|
| 102 |
+
|
| 103 |
+
## Why a second batch
|
| 104 |
+
- Batch 1 covered 1-2 flagship plans per insurer (Optima Secure, Activ Assure Diamond, Care Supreme, Family Health Optima, ReAssure 2.0, etc.) — 22 JSONs.
|
| 105 |
+
- 104 PDFs total in `rag/corpus/<insurer>/*.pdf` (excluding `regulatory/`). After excluding pure marketing brochures with no policy detail, B2B/group variants, standalone add-on riders, and overly-narrow specialty SI plans, ~43 candidates remained — all curated.
|
| 106 |
+
|
| 107 |
+
## Files Written (batch 2)
|
| 108 |
+
|
| 109 |
+
| # | policy_id | UIN | Primary PDF | Completeness |
|
| 110 |
+
|---|---|---|---|---|
|
| 111 |
+
| 23 | aditya-birla__activ-health | ADIHLIP24102V052324 | activ-health-individual__wordings.pdf | 70% |
|
| 112 |
+
| 24 | bajaj-allianz__comprehensive-care-plan | BAJHLIP15002V011415 | comprehensive-care-plan__wordings.pdf | 60% |
|
| 113 |
+
| 25 | bajaj-allianz__global-health-care | BAJHLIP23209V022223 | global-health-care__wordings.pdf | 65% |
|
| 114 |
+
| 26 | bajaj-allianz__health-guard | BAJHLIP25035V072425 | health-guard__wordings.pdf | 70% |
|
| 115 |
+
| 27 | bajaj-allianz__silver-health | BAJHLIP23213V052223 | silver-health__cis.pdf | 65% |
|
| 116 |
+
| 28 | bajaj-allianz__tax-gain | BAJHLIP21184V022021 | tax-gain__cis.pdf | 55% |
|
| 117 |
+
| 29 | care-health__care-advantage | CHIHLIP26049V042526 | care-advantage__brochure.pdf | 80% |
|
| 118 |
+
| 30 | care-health__care-supreme-enhance | CHIHLIP25036V012425 | care-supreme-enhance__wordings.pdf | 60% |
|
| 119 |
+
| 31 | care-health__ultimate-care | CHIHLIP25044V012425 | ultimate-care__wordings.pdf | 60% |
|
| 120 |
+
| 32 | hdfc-ergo__energy | HDFHLIP26048V052526 | energy-diabetes-hypertension__wordings.pdf | 60% |
|
| 121 |
+
| 33 | hdfc-ergo__my-health-medisure-prime | null | my-health-medisure-prime__wordings.pdf | 60% |
|
| 122 |
+
| 34 | hdfc-ergo__my-health-sampoorna-suraksha | HDFHLIP21005V022122 | my-health-sampoorna-suraksha__brochure.pdf | 60% |
|
| 123 |
+
| 35 | hdfc-ergo__my-health-suraksha | HDFHLIP24079V072324 | my-health-suraksha__brochure.pdf | 75% |
|
| 124 |
+
| 36 | hdfc-ergo__my-health-women-suraksha | HDFHLIP22142V032122 | my-health-women-suraksha__brochure.pdf | 55% |
|
| 125 |
+
| 37 | hdfc-ergo__optima-secure-older-variant | HDFHLIP21016V012122 | my-optima-secure-older-variant__wordings.pdf | 65% |
|
| 126 |
+
| 38 | hdfc-ergo__optima-enhance | null | optima-enhance__wordings.pdf | 55% |
|
| 127 |
+
| 39 | hdfc-ergo__optima-plus | HDHHLIP21336V022021 | optima-plus__wordings.pdf | 55% |
|
| 128 |
+
| 40 | hdfc-ergo__total-health-plan | HDHHLIP21317V032021 | total-health-plan__wordings.pdf | 65% |
|
| 129 |
+
| 41 | icici-lombard__arogya-sanjeevani | ICIHLIP20178V011920 | arogya-sanjeevani__wordings.pdf | 75% |
|
| 130 |
+
| 42 | icici-lombard__complete-health-umbrella | ICIHLIP23144V072223 | complete-health-insurance-umbrella__wordings.pdf | 75% |
|
| 131 |
+
| 43 | icici-lombard__health-advantedge | ICIHLIP24182V042324 | health-advantedge__wordings.pdf | 75% |
|
| 132 |
+
| 44 | icici-lombard__health-booster | ICIHLIP22100V032122 | health-booster-top-up__wordings.pdf | 60% |
|
| 133 |
+
| 45 | icici-lombard__health-elite-plus | ICIHLIP21383V052021 | health-elite-plus__wordings.pdf | 70% |
|
| 134 |
+
| 46 | manipalcigna__prohealth-select | null (legacy IRDAI/HLT format) | prohealth-select__wordings.pdf | 75% |
|
| 135 |
+
| 47 | manipalcigna__sarvah-param | null | sarvah-param__wordings.pdf | 55% |
|
| 136 |
+
| 48 | new-india__asha-kiran | NIAHLIP21233V022021 | asha-kiran-policy__brochure.pdf | 60% |
|
| 137 |
+
| 49 | new-india__janata-mediclaim | NIAHLIP25046V042425 | janata-mediclaim-policy__wordings.pdf | 70% |
|
| 138 |
+
| 50 | new-india__mediclaim-policy | NIAHLIP23187V052223 | new-india-mediclaim-policy__wordings.pdf | 65% |
|
| 139 |
+
| 51 | new-india__universal-health | NIAHLIP25052V032425 | universal-health-insurance__wordings.pdf | 55% |
|
| 140 |
+
| 52 | new-india__yuva-bharat | NIAHLIP22025V022223 | yuva-bharat-health-policy__wordings.pdf | 65% |
|
| 141 |
+
| 53 | niva-bupa__aspire | NBHHLIP26049V022526 | aspire__wordings.pdf | 65% |
|
| 142 |
+
| 54 | niva-bupa__health-plus-top-up | NBHHLIP24135V012324 | health-plus-top-up__wordings.pdf | 65% |
|
| 143 |
+
| 55 | niva-bupa__health-premia | MAXHLIP21176V022021 | health-premia__wordings.pdf | 65% |
|
| 144 |
+
| 56 | niva-bupa__reassure-3 | NBHHLIP26047V012526 | reassure-3-0__wordings.pdf | 70% |
|
| 145 |
+
| 57 | niva-bupa__rise | NBHHLIP25041V012425 | rise__wordings.pdf | 60% |
|
| 146 |
+
| 58 | niva-bupa__saral-suraksha | NBHPAIP22153V012122 | saral-suraksha-bima__wordings.pdf | 55% |
|
| 147 |
+
| 59 | star-health__health-premier | SHAHLIP22226V012122 | health-premier__wordings.pdf | 60% |
|
| 148 |
+
| 60 | star-health__senior-citizens-red-carpet | SHAHLIP26041V082526 | senior-citizens-red-carpet__brochure.pdf | 50% |
|
| 149 |
+
| 61 | star-health__star-assure | SHAHLIP26048V032526 | star-assure__wordings.pdf | 55% |
|
| 150 |
+
| 62 | star-health__star-cardiac-care | SHAHLIP22032V052122 | star-cardiac-care__wordings.pdf | 65% |
|
| 151 |
+
| 63 | star-health__star-cardiac-care-platinum | SHAHLIP22033V022122 | star-cardiac-care-platinum__wordings.pdf | 65% |
|
| 152 |
+
| 64 | tata-aig__medicare-lite | TATHLIP24132V012324 | medicare-lite__cis.pdf | 75% |
|
| 153 |
+
| 65 | tata-aig__medicare-select | TATHLIP25051V012425 | medicare-select__brochure.pdf | 70% |
|
| 154 |
+
|
| 155 |
+
## Insurer Coverage (running total)
|
| 156 |
+
|
| 157 |
+
| Insurer | Batch 1 | Batch 2 | Total |
|
| 158 |
+
|---|---|---|---|
|
| 159 |
+
| Aditya Birla | 2 | 1 | 3 |
|
| 160 |
+
| Bajaj Allianz | 2 | 5 | 7 |
|
| 161 |
+
| Care Health | 3 | 3 | 6 |
|
| 162 |
+
| HDFC ERGO | 2 | 9 | 11 |
|
| 163 |
+
| ICICI Lombard | 3 | 5 | 8 |
|
| 164 |
+
| ManipalCigna | 2 | 2 | 4 |
|
| 165 |
+
| New India | 1 | 5 | 6 |
|
| 166 |
+
| Niva Bupa | 3 | 6 | 9 |
|
| 167 |
+
| Star Health | 2 | 5 | 7 |
|
| 168 |
+
| Tata AIG | 2 | 2 | 4 |
|
| 169 |
+
| **Total** | **22** | **43** | **65** |
|
| 170 |
+
|
| 171 |
+
## PDFs Excluded From Batch 2 (with reason)
|
| 172 |
+
|
| 173 |
+
| PDF | Reason |
|
| 174 |
+
|---|---|
|
| 175 |
+
| aditya-birla/activ-secure-cancer-secure__brochure.pdf | Specialty cancer rider/plan — narrow SI |
|
| 176 |
+
| aditya-birla/activ-secure-personal-accident-cancer-secure__wordings.pdf | Personal Accident + cancer specialty |
|
| 177 |
+
| aditya-birla/group-activ-health__wordings.pdf | B2B group product |
|
| 178 |
+
| bajaj-allianz/criti-care__wordings.pdf | Critical illness specialty (kept Niva Bupa Health Premia/Star Cardiac as flagships instead) |
|
| 179 |
+
| bajaj-allianz/group-health-guard-gold__wordings.pdf | B2B group variant |
|
| 180 |
+
| bajaj-allianz/group-personal-accident__wordings.pdf | B2B group PA |
|
| 181 |
+
| care-health/care-advantage-add-ons-protect-plus-care-shield__brochure.pdf | Add-on riders only (not a stand-alone policy) |
|
| 182 |
+
| care-health/care-heart__brochure.pdf | Cardiac specialty — narrow SI |
|
| 183 |
+
| care-health/supreme-enhance__brochure.pdf | Marketing duplicate of `care-supreme-enhance__wordings.pdf` (already curated) |
|
| 184 |
+
| hdfc-ergo/group-health-insurance__wordings.pdf | B2B group product |
|
| 185 |
+
| star-health/star-cancer-care-platinum__wordings.pdf | Cancer specialty — narrow SI |
|
| 186 |
+
| star-health/star-hospital-cash__brochure.pdf | Hospital cash add-on (not standalone indemnity) |
|
| 187 |
+
| tata-aig/criti-medicare__wordings.pdf | Critical illness specialty |
|
| 188 |
+
| tata-aig/wellsurance-family__cis.pdf | Specialty wellness/family product (kept MediCare Select/Lite as flagship variants) |
|
| 189 |
+
|
| 190 |
+
## Curation Method Notes
|
| 191 |
+
|
| 192 |
+
- Approach: regex-pattern extraction of 25 fields per policy from text cached via `pdfplumber` (first 30 pages per PDF, ~6 MB total cache across 55 batch-2 files).
|
| 193 |
+
- Policy-type classifier uses both product name (`top-up`, `cardiac-care`, etc.) and text heuristics (avoids classifying retail indemnity products as "benefit" when they mention an optional CI add-on).
|
| 194 |
+
- UIN regex tightened to handle 7-letter prefixes like `SHAHLIP`, `MAXHLIP`, `HDHHLIP` (batch 1 had only 6-letter forms).
|
| 195 |
+
- Sum-insured option arrays require ≥2 distinct values to be emitted — single fragment matches are suppressed to null (avoids confidently-wrong enumerations).
|
| 196 |
+
- Three UINs intentionally null: HDFC Energy variant + ManipalCigna ProHealth Select (legacy IRDAI/HLT slash-format UIN) + Sarvah Param (UIN not in first 30 pages).
|
| 197 |
+
- Insurer-level fields (`network_hospital_count`, `claim_settlement_ratio`, `tat_cashless_authorization_hours`) intentionally null across batch 2 — downstream backfill from `data/reviews/` and IRDAI Annual Report planned.
|
| 198 |
+
|
| 199 |
+
## Quality Gates Applied
|
| 200 |
+
|
| 201 |
+
- UIN insurer-prefix check: all 40 extracted UINs match the insurer's known prefix (ADI/BAJ/CHI/RHI/HDF/HDH/ICI/MCI/NIA/NBH/MAX/SHA/TAT). Zero cross-insurer mismatches.
|
| 202 |
+
- Completeness floor: 50%. All 43 written JSONs cleared the threshold. None skipped.
|
| 203 |
+
- Verbatim quotes: every populated field carries a 60-240 char quote pulled from the cached text (grep-verifiable).
|
| 204 |
+
- Reproducibility: `tools/curate_batch2.py` + `tools/extract_policy_text_batch2.py` together re-produce the entire batch.
|
| 205 |
+
|
| 206 |
+
## Known Gaps / Manual Follow-up (carries from batch 1 + batch-2-specific)
|
| 207 |
+
|
| 208 |
+
1. **UINs missing** for `hdfc-ergo__my-health-medisure-prime`, `hdfc-ergo__optima-enhance`, `manipalcigna__prohealth-select`, `manipalcigna__sarvah-param` — pull from product brochures or insurer FAQ pages.
|
| 209 |
+
2. **Specific-disease waiting** defaulted to 24 months in ~30% of batch-2 files (IRDAI standard) when not explicitly quoted. Verify per product wording.
|
| 210 |
+
3. **Sum Insured options** still null for most batch-2 policies — pull from product brochures / Policy Schedule.
|
| 211 |
+
4. **Room rent capping** captures the most-relevant phrase but may not enumerate all SI tiers' room caps for tiered products.
|
| 212 |
+
5. **NCB %** null for products using Booster/Recharge/Variable-bonus structures (ReAssure 3.0, Health Premia, etc.). These need a structured `bonus_structure` field — current schema's single % field is insufficient.
|
| 213 |
+
|
| 214 |
+
## Reproducibility (batch 2)
|
| 215 |
+
|
| 216 |
+
- Text-cache step: `tools/extract_policy_text_batch2.py` → writes 43 `.txt` files to `/tmp/claude/policy_extract/text_cache/`.
|
| 217 |
+
- Curation step: `tools/curate_batch2.py` → reads cache, writes 43 JSONs to `data/policy_facts/`. Re-runnable (skips files that already exist; use `tools/clear_batch2.py` to wipe-then-rebuild only batch-2 outputs without touching batch-1).
|
| 218 |
+
- Validation: `python3 -c "import json; json.load(open('data/policy_facts/<id>.json'))"` for any file.
|
frontend/src/app/page.tsx
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
| 13 |
getHealth,
|
| 14 |
getInsurerReviews,
|
| 15 |
getMarketplace,
|
|
|
|
| 16 |
getScorecard,
|
| 17 |
InsurerReviews,
|
| 18 |
MarketplacePolicy,
|
|
@@ -21,6 +22,7 @@ import {
|
|
| 21 |
postPremiumEstimate,
|
| 22 |
postTranscribe,
|
| 23 |
PremiumEstimateResponse,
|
|
|
|
| 24 |
ScorecardResponse,
|
| 25 |
uploadPolicy,
|
| 26 |
} from "@/lib/api";
|
|
@@ -837,51 +839,79 @@ function ScorecardBadgesForCitations({ citations }: { citations: Citation[] }) {
|
|
| 837 |
);
|
| 838 |
}
|
| 839 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 840 |
function ScorecardCard({ sc }: { sc: ScorecardResponse }) {
|
|
|
|
|
|
|
|
|
|
| 841 |
return (
|
| 842 |
-
<div className="mt-2 rounded-xl border border-[var(--border)] bg-[var(--card)] p-
|
| 843 |
-
<div className="flex items-
|
| 844 |
-
<div className="flex items-
|
| 845 |
-
<span className={`inline-flex items-center justify-center w-
|
| 846 |
{sc.grade}
|
| 847 |
</span>
|
| 848 |
-
<div>
|
| 849 |
-
<div className="font-semibold text-sm">{sc.policy_name}</div>
|
| 850 |
-
<div className="text-[var(--muted-foreground)] text-[11px]">{sc.one_liner}</div>
|
| 851 |
</div>
|
| 852 |
</div>
|
| 853 |
-
<div className="text-right">
|
| 854 |
-
<div className="text-
|
| 855 |
-
<div className="text-[10px] text-[var(--muted-foreground)]">data {sc.data_completeness_pct.toFixed(0)}% complete</div>
|
| 856 |
</div>
|
| 857 |
</div>
|
| 858 |
-
<div className="space-y-
|
| 859 |
-
{
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
|
| 865 |
-
|
| 866 |
-
|
| 867 |
-
|
| 868 |
-
|
| 869 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 870 |
</div>
|
| 871 |
-
|
| 872 |
-
|
| 873 |
-
{s.signals.slice(0, 4).map((sig, i) => (
|
| 874 |
-
<li key={i} className="text-[10px] text-[var(--muted-foreground)]">
|
| 875 |
-
· {sig}
|
| 876 |
-
</li>
|
| 877 |
-
))}
|
| 878 |
-
</ul>
|
| 879 |
-
)}
|
| 880 |
-
</div>
|
| 881 |
-
))}
|
| 882 |
</div>
|
| 883 |
-
<div className="mt-
|
| 884 |
-
|
| 885 |
</div>
|
| 886 |
</div>
|
| 887 |
);
|
|
@@ -1628,12 +1658,19 @@ function ComparisonModal({ policyIds, onClose }: { policyIds: string[]; onClose:
|
|
| 1628 |
function PolicyDetailModal({ policy, onClose }: { policy: MarketplacePolicy; onClose: () => void }) {
|
| 1629 |
const [sc, setSc] = useState<ScorecardResponse | null>(null);
|
| 1630 |
const [reviews, setReviews] = useState<InsurerReviews | null>(null);
|
|
|
|
| 1631 |
useEffect(() => {
|
| 1632 |
getScorecard(policy.policy_id).then(setSc).catch(() => setSc(null));
|
| 1633 |
if (policy.insurer_slug) {
|
| 1634 |
getInsurerReviews(policy.insurer_slug).then(setReviews).catch(() => setReviews(null));
|
| 1635 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1636 |
}, [policy.policy_id, policy.insurer_slug]);
|
|
|
|
| 1637 |
|
| 1638 |
const initials = insurerInitials(policy.insurer_name);
|
| 1639 |
const color = INSURER_COLOR[policy.insurer_slug] || "bg-slate-500";
|
|
@@ -1674,6 +1711,22 @@ function PolicyDetailModal({ policy, onClose }: { policy: MarketplacePolicy; onC
|
|
| 1674 |
<div className="p-5 space-y-5">
|
| 1675 |
{sc && (
|
| 1676 |
<div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1677 |
<div className="flex items-center gap-3 mb-3">
|
| 1678 |
<span className={`inline-flex items-center justify-center w-12 h-12 rounded-lg font-bold ${gradeColor(sc.grade)}`}>{sc.grade}</span>
|
| 1679 |
<div className="flex-1">
|
|
|
|
| 13 |
getHealth,
|
| 14 |
getInsurerReviews,
|
| 15 |
getMarketplace,
|
| 16 |
+
getProfileCompleteness,
|
| 17 |
getScorecard,
|
| 18 |
InsurerReviews,
|
| 19 |
MarketplacePolicy,
|
|
|
|
| 22 |
postPremiumEstimate,
|
| 23 |
postTranscribe,
|
| 24 |
PremiumEstimateResponse,
|
| 25 |
+
ProfileCompletenessResponse,
|
| 26 |
ScorecardResponse,
|
| 27 |
uploadPolicy,
|
| 28 |
} from "@/lib/api";
|
|
|
|
| 839 |
);
|
| 840 |
}
|
| 841 |
|
| 842 |
+
// Plain-English label per criterion — shown as a sub-line under the name
|
| 843 |
+
// so the buyer doesn't need to mentally translate "Cost Predictability" etc.
|
| 844 |
+
const CRITERION_BLURB: Record<string, string> = {
|
| 845 |
+
"Coverage Breadth": "What's actually covered when you claim",
|
| 846 |
+
"Cost Predictability": "How likely you'll face surprise out-of-pocket bills",
|
| 847 |
+
"Waiting-Period Friction": "How soon you can actually use the policy",
|
| 848 |
+
"Claim Experience": "Will the insurer actually pay when you claim?",
|
| 849 |
+
"Renewal Protection": "Can you keep this policy at 70+ when you need it most",
|
| 850 |
+
"Bonus & Loyalty": "Rewards for staying claim-free + renewing",
|
| 851 |
+
};
|
| 852 |
+
|
| 853 |
function ScorecardCard({ sc }: { sc: ScorecardResponse }) {
|
| 854 |
+
// Sort sub-scores high-to-low so strengths surface first, weaknesses last —
|
| 855 |
+
// mirrors how a human would explain it
|
| 856 |
+
const sortedSubs = [...sc.sub_scores].sort((a, b) => b.score - a.score);
|
| 857 |
return (
|
| 858 |
+
<div className="mt-2 rounded-xl border border-[var(--border)] bg-[var(--card)] p-4 text-xs animate-fade-up">
|
| 859 |
+
<div className="flex items-start justify-between mb-3 gap-3">
|
| 860 |
+
<div className="flex items-start gap-3 min-w-0 flex-1">
|
| 861 |
+
<span className={`inline-flex items-center justify-center w-10 h-10 rounded-lg font-bold text-base ${gradeColor(sc.grade)} shrink-0`}>
|
| 862 |
{sc.grade}
|
| 863 |
</span>
|
| 864 |
+
<div className="min-w-0">
|
| 865 |
+
<div className="font-semibold text-sm truncate">{sc.policy_name}</div>
|
| 866 |
+
<div className="text-[var(--muted-foreground)] text-[11px] leading-snug mt-0.5">{sc.one_liner}</div>
|
| 867 |
</div>
|
| 868 |
</div>
|
| 869 |
+
<div className="text-right shrink-0">
|
| 870 |
+
<div className="text-2xl font-bold leading-none">{sc.overall_score}<span className="text-[var(--muted-foreground)] text-sm font-normal">/100</span></div>
|
| 871 |
+
<div className="text-[10px] text-[var(--muted-foreground)] mt-0.5">data {sc.data_completeness_pct.toFixed(0)}% complete</div>
|
| 872 |
</div>
|
| 873 |
</div>
|
| 874 |
+
<div className="space-y-2.5 mt-4">
|
| 875 |
+
{sortedSubs.map((s) => {
|
| 876 |
+
const barColor = s.score >= 75 ? "bg-emerald-500" : s.score >= 55 ? "bg-amber-500" : "bg-red-400";
|
| 877 |
+
const blurb = CRITERION_BLURB[s.name];
|
| 878 |
+
return (
|
| 879 |
+
<div key={s.name}>
|
| 880 |
+
<div className="flex items-baseline justify-between mb-0.5">
|
| 881 |
+
<div className="min-w-0 flex-1 pr-2">
|
| 882 |
+
<div className="text-[11px] font-semibold leading-tight">{s.name}</div>
|
| 883 |
+
{blurb && <div className="text-[10px] text-[var(--muted-foreground)] leading-tight mt-0.5">{blurb}</div>}
|
| 884 |
+
</div>
|
| 885 |
+
<div className="text-right shrink-0">
|
| 886 |
+
<span className="text-sm font-bold">{s.score}</span>
|
| 887 |
+
<span className="text-[10px] text-[var(--muted-foreground)] ml-1">/ 100</span>
|
| 888 |
+
<div className="text-[10px] text-[var(--muted-foreground)] leading-tight">{s.summary}</div>
|
| 889 |
+
</div>
|
| 890 |
+
</div>
|
| 891 |
+
<div className="h-2 rounded-full bg-[var(--muted)] overflow-hidden">
|
| 892 |
+
<div className={`h-full ${barColor} transition-[width] duration-500`} style={{ width: `${Math.max(2, s.score)}%` }} />
|
| 893 |
+
</div>
|
| 894 |
+
{s.signals && s.signals.length > 0 && (
|
| 895 |
+
<div className="mt-1 flex flex-wrap gap-1">
|
| 896 |
+
{s.signals.slice(0, 4).map((sig, i) => {
|
| 897 |
+
const isNegative = sig.startsWith("−") || sig.startsWith("-");
|
| 898 |
+
return (
|
| 899 |
+
<span
|
| 900 |
+
key={i}
|
| 901 |
+
className={`inline-block text-[10px] px-1.5 py-0.5 rounded ${isNegative ? "bg-red-100 text-red-700 dark:bg-red-900/20 dark:text-red-300" : "bg-[var(--accent)] text-[var(--foreground)]"}`}
|
| 902 |
+
>
|
| 903 |
+
{sig}
|
| 904 |
+
</span>
|
| 905 |
+
);
|
| 906 |
+
})}
|
| 907 |
+
</div>
|
| 908 |
+
)}
|
| 909 |
</div>
|
| 910 |
+
);
|
| 911 |
+
})}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 912 |
</div>
|
| 913 |
+
<div className="mt-3 pt-2.5 border-t border-[var(--border)] text-[10px] text-[var(--muted-foreground)] leading-snug">
|
| 914 |
+
Weighted average across 6 criteria. Rules-based — no LLM in the scoring loop. Expand "How is this score computed?" below to see which of 48 schema fields feed each criterion.
|
| 915 |
</div>
|
| 916 |
</div>
|
| 917 |
);
|
|
|
|
| 1658 |
function PolicyDetailModal({ policy, onClose }: { policy: MarketplacePolicy; onClose: () => void }) {
|
| 1659 |
const [sc, setSc] = useState<ScorecardResponse | null>(null);
|
| 1660 |
const [reviews, setReviews] = useState<InsurerReviews | null>(null);
|
| 1661 |
+
const [completeness, setCompleteness] = useState<ProfileCompletenessResponse | null>(null);
|
| 1662 |
useEffect(() => {
|
| 1663 |
getScorecard(policy.policy_id).then(setSc).catch(() => setSc(null));
|
| 1664 |
if (policy.insurer_slug) {
|
| 1665 |
getInsurerReviews(policy.insurer_slug).then(setReviews).catch(() => setReviews(null));
|
| 1666 |
}
|
| 1667 |
+
// Profile completeness gates whether we render the per-user grade.
|
| 1668 |
+
// Below threshold: show universal grade only (insurer-quality-led) with a
|
| 1669 |
+
// CTA to complete the profile.
|
| 1670 |
+
const sid = typeof window !== "undefined" ? sessionStorage.getItem("insurance_session_id") || undefined : undefined;
|
| 1671 |
+
getProfileCompleteness(sid).then(setCompleteness).catch(() => setCompleteness(null));
|
| 1672 |
}, [policy.policy_id, policy.insurer_slug]);
|
| 1673 |
+
const isPersonalized = completeness?.is_personalized === true;
|
| 1674 |
|
| 1675 |
const initials = insurerInitials(policy.insurer_name);
|
| 1676 |
const color = INSURER_COLOR[policy.insurer_slug] || "bg-slate-500";
|
|
|
|
| 1711 |
<div className="p-5 space-y-5">
|
| 1712 |
{sc && (
|
| 1713 |
<div>
|
| 1714 |
+
{!isPersonalized && (
|
| 1715 |
+
<div className="mb-3 bg-[var(--accent)] border border-[var(--primary)] rounded-lg p-3 text-xs">
|
| 1716 |
+
<div className="font-semibold text-[var(--primary)] mb-1">This is the generic grade for an average buyer.</div>
|
| 1717 |
+
<p className="text-[var(--muted-foreground)] leading-snug">
|
| 1718 |
+
Tell me about yourself (age, dependents, conditions, budget) and I'll re-score this policy for <strong className="text-[var(--foreground)]">your</strong> situation. The same policy can be a B for a 30-year-old and a D for a 60-year-old with diabetes — context changes everything.
|
| 1719 |
+
{completeness && completeness.completeness_pct > 0 && (
|
| 1720 |
+
<span className="block mt-1">Your profile is {completeness.completeness_pct}% complete. {completeness.next_question_hint && <em className="not-italic">Next: {completeness.next_question_hint.slice(0, 80)}…</em>}</span>
|
| 1721 |
+
)}
|
| 1722 |
+
</p>
|
| 1723 |
+
</div>
|
| 1724 |
+
)}
|
| 1725 |
+
{isPersonalized && completeness && (
|
| 1726 |
+
<div className="mb-3 text-[10px] text-[var(--primary)] font-semibold flex items-center gap-1">
|
| 1727 |
+
✓ Personalized for you · profile {completeness.completeness_pct}% complete
|
| 1728 |
+
</div>
|
| 1729 |
+
)}
|
| 1730 |
<div className="flex items-center gap-3 mb-3">
|
| 1731 |
<span className={`inline-flex items-center justify-center w-12 h-12 rounded-lg font-bold ${gradeColor(sc.grade)}`}>{sc.grade}</span>
|
| 1732 |
<div className="flex-1">
|
frontend/src/lib/api.ts
CHANGED
|
@@ -268,6 +268,24 @@ export async function getMarketplace(): Promise<MarketplaceResponse> {
|
|
| 268 |
return resp.json();
|
| 269 |
}
|
| 270 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
export async function getCompare(policy_ids: string[]): Promise<CompareResponse> {
|
| 272 |
const url = new URL(`${BACKEND_URL}/api/policies/compare`);
|
| 273 |
for (const id of policy_ids) url.searchParams.append("policy_ids", id);
|
|
|
|
| 268 |
return resp.json();
|
| 269 |
}
|
| 270 |
|
| 271 |
+
export type ProfileCompletenessResponse = {
|
| 272 |
+
completeness: number;
|
| 273 |
+
completeness_pct: number;
|
| 274 |
+
fields_collected: string[];
|
| 275 |
+
fields_missing: string[];
|
| 276 |
+
is_personalized: boolean;
|
| 277 |
+
gate_threshold: number;
|
| 278 |
+
next_question_hint?: string | null;
|
| 279 |
+
};
|
| 280 |
+
|
| 281 |
+
export async function getProfileCompleteness(session_id?: string): Promise<ProfileCompletenessResponse> {
|
| 282 |
+
const url = new URL(`${BACKEND_URL}/api/profile/completeness`);
|
| 283 |
+
if (session_id) url.searchParams.set("session_id", session_id);
|
| 284 |
+
const resp = await fetch(url.toString());
|
| 285 |
+
if (!resp.ok) throw new Error(`profile completeness failed: ${resp.status}`);
|
| 286 |
+
return resp.json();
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
export async function getCompare(policy_ids: string[]): Promise<CompareResponse> {
|
| 290 |
const url = new URL(`${BACKEND_URL}/api/policies/compare`);
|
| 291 |
for (const id of policy_ids) url.searchParams.append("policy_ids", id);
|
tools/pdf_probe.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import time, pdfplumber
|
| 2 |
+
t0 = time.time()
|
| 3 |
+
with pdfplumber.open('rag/corpus/star-health/star-hospital-cash__brochure.pdf') as p:
|
| 4 |
+
nP = len(p.pages)
|
| 5 |
+
print(f'pages: {nP}')
|
| 6 |
+
sample = ''
|
| 7 |
+
for i, page in enumerate(p.pages[:5]):
|
| 8 |
+
t = page.extract_text() or ''
|
| 9 |
+
sample += t
|
| 10 |
+
print(f'first 5 pages chars: {len(sample)}')
|
| 11 |
+
print(f'elapsed: {time.time()-t0:.1f}s')
|