Spaces:
Sleeping
fix(marketplace+voice+routing): KI-136 + KI-141 + KI-142 + KI-143 + KI-144 — full bundle
Browse filesFive distinct fixes shipped together for a single rebuild cycle:
KI-141 — hybrid alias dedup. /api/coverage + /api/policies/all now collapse
curated marketing-rename entries (Activ One, Optima Secure, etc.) under
their PDF-backed parent card, with aliases surfaced as 'Also marketed as:'
on the marketplace cards. Frontend page.tsx + api.ts add aliases[] field
on MarketplacePolicy. Card count went 158 -> ~137.
KI-142 — UIN-primary merge rule. /api/coverage and /api/policies/all now
build uin_to_parent maps from extracted JSONs and merge curated entries
based on UIN equality. Falls back to source_pdf gate when UIN matches
fail. 1 UIN = 1 marketplace card invariant.
KI-136 — named-SKU comparison routing. orchestrator.py
should_route_to_fact_find now accepts query and exempts intent=comparison
when the question mentions >=2 named insurers/SKUs. Empty profile no
longer traps SKU-specific compare questions in fact_find_brain.
Verified: 'Compare HDFC ERGO Optima Secure and Niva Bupa Aspire on
maternity' now routes to nim-chain comparison brain with citations.
KI-143 — bajaj group-health-guard slug correction. PDF labeled 'Gold'
in slug was actually 'Health Guard (Group) - Silver Plan' per the PDF
cover. Renamed PDF, extracted JSON, curated facts, Chroma metadata
(43 chunks), .gitattributes LFS line. The separate
health-guard-gold-individual product is untouched.
KI-144 — reliance-general -> indusind-general migration. The 3
INDUSIND-branded products (HealthGain, Hospi Care, Group Mediclaim) are
issued by IndusInd General Insurance (formerly Reliance General per
IRDAI Reg No. 103). Migrated insurer_slug for these 3, kept
personal-accident under reliance-general. Added insurer_meta entry for
indusind-general. Added legacy_issuer field for retrieval continuity.
164 Chroma chunks migrated.
Dockerfile DATASET_CACHE_BUST bumped to 2026-05-15-ki144-v1 so the next
Space rebuild pulls the freshly-updated Chroma binary.
- .gitattributes +1 -1
- 40-data/policy_facts/{bajaj-allianz__group-health-guard-gold__wordings.json → bajaj-allianz__group-health-guard-silver__wordings.json} +25 -25
- 40-data/policy_facts/{reliance-general__group-mediclaim__wordings.json → indusind-general__group-mediclaim__wordings.json} +5 -4
- 40-data/policy_facts/{reliance-general__health-gain__wordings.json → indusind-general__health-gain__wordings.json} +5 -4
- 40-data/policy_facts/{reliance-general__hospi-care__wordings.json → indusind-general__hospi-care__wordings.json} +5 -4
- Dockerfile +1 -1
- backend/main.py +307 -2
- backend/orchestrator.py +32 -0
- frontend/src/app/page.tsx +10 -1
- frontend/src/lib/api.ts +1 -0
|
@@ -42,7 +42,7 @@ rag/corpus/bajaj-allianz/comprehensive-care-plan__wordings.pdf filter=lfs diff=l
|
|
| 42 |
rag/corpus/bajaj-allianz/criti-care__wordings.pdf filter=lfs diff=lfs merge=lfs -text
|
| 43 |
rag/corpus/bajaj-allianz/extra-care-plus__wordings.pdf filter=lfs diff=lfs merge=lfs -text
|
| 44 |
rag/corpus/bajaj-allianz/global-health-care__wordings.pdf filter=lfs diff=lfs merge=lfs -text
|
| 45 |
-
rag/corpus/bajaj-allianz/group-health-guard-
|
| 46 |
rag/corpus/bajaj-allianz/group-personal-accident__wordings.pdf filter=lfs diff=lfs merge=lfs -text
|
| 47 |
rag/corpus/bajaj-allianz/health-guard-gold-individual__wordings.pdf filter=lfs diff=lfs merge=lfs -text
|
| 48 |
rag/corpus/bajaj-allianz/health-guard__wordings.pdf filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 42 |
rag/corpus/bajaj-allianz/criti-care__wordings.pdf filter=lfs diff=lfs merge=lfs -text
|
| 43 |
rag/corpus/bajaj-allianz/extra-care-plus__wordings.pdf filter=lfs diff=lfs merge=lfs -text
|
| 44 |
rag/corpus/bajaj-allianz/global-health-care__wordings.pdf filter=lfs diff=lfs merge=lfs -text
|
| 45 |
+
rag/corpus/bajaj-allianz/group-health-guard-silver__wordings.pdf filter=lfs diff=lfs merge=lfs -text
|
| 46 |
rag/corpus/bajaj-allianz/group-personal-accident__wordings.pdf filter=lfs diff=lfs merge=lfs -text
|
| 47 |
rag/corpus/bajaj-allianz/health-guard-gold-individual__wordings.pdf filter=lfs diff=lfs merge=lfs -text
|
| 48 |
rag/corpus/bajaj-allianz/health-guard__wordings.pdf filter=lfs diff=lfs merge=lfs -text
|
|
@@ -1,121 +1,121 @@
|
|
| 1 |
{
|
| 2 |
-
"policy_id": "bajaj-allianz__group-health-guard-
|
| 3 |
-
"policy_name": "
|
| 4 |
"insurer_slug": "bajaj-allianz",
|
| 5 |
"uin_code": {
|
| 6 |
"value": "BAJHLGP21181V022021",
|
| 7 |
-
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-
|
| 8 |
"source_quote": "UIN: BAJHLGP21181V022021"
|
| 9 |
},
|
| 10 |
"min_entry_age": {
|
| 11 |
"value": null,
|
| 12 |
-
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-
|
| 13 |
"source_quote": ""
|
| 14 |
},
|
| 15 |
"max_entry_age": {
|
| 16 |
"value": null,
|
| 17 |
-
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-
|
| 18 |
"source_quote": ""
|
| 19 |
},
|
| 20 |
"max_renewal_age": {
|
| 21 |
"value": null,
|
| 22 |
-
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-
|
| 23 |
"source_quote": ""
|
| 24 |
},
|
| 25 |
"pre_existing_disease_waiting_months": {
|
| 26 |
"value": 36,
|
| 27 |
-
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-
|
| 28 |
"source_quote": "pre-existing Disease (PED) and its direct complications shall be excluded until the expiry of 36 months"
|
| 29 |
},
|
| 30 |
"initial_waiting_period_days": {
|
| 31 |
"value": null,
|
| 32 |
-
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-
|
| 33 |
"source_quote": ""
|
| 34 |
},
|
| 35 |
"maternity_coverage": {
|
| 36 |
"value": null,
|
| 37 |
-
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-
|
| 38 |
"source_quote": ""
|
| 39 |
},
|
| 40 |
"maternity_waiting_months": {
|
| 41 |
"value": null,
|
| 42 |
-
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-
|
| 43 |
"source_quote": ""
|
| 44 |
},
|
| 45 |
"ayush_coverage": {
|
| 46 |
"value": null,
|
| 47 |
-
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-
|
| 48 |
"source_quote": ""
|
| 49 |
},
|
| 50 |
"cashless_treatment_supported": {
|
| 51 |
"value": true,
|
| 52 |
-
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-
|
| 53 |
"source_quote": "Cashless facility"
|
| 54 |
},
|
| 55 |
"copayment_pct": {
|
| 56 |
"value": null,
|
| 57 |
-
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-
|
| 58 |
"source_quote": ""
|
| 59 |
},
|
| 60 |
"no_claim_bonus_pct": {
|
| 61 |
"value": 100,
|
| 62 |
-
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-
|
| 63 |
"source_quote": "cumulative bonus (if any) is exhausted due to claims lodged during the Policy\nyear, then it is agreed that 100%"
|
| 64 |
},
|
| 65 |
"restoration_benefit": {
|
| 66 |
"value": null,
|
| 67 |
-
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-
|
| 68 |
"source_quote": ""
|
| 69 |
},
|
| 70 |
"room_rent_capping": {
|
| 71 |
"value": null,
|
| 72 |
-
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-
|
| 73 |
"source_quote": ""
|
| 74 |
},
|
| 75 |
"pre_hospitalization_days": {
|
| 76 |
"value": 60,
|
| 77 |
-
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-
|
| 78 |
"source_quote": "Pre-Hospitalisation\nThe Medical Expenses incurred during the 60 days"
|
| 79 |
},
|
| 80 |
"post_hospitalization_days": {
|
| 81 |
"value": 90,
|
| 82 |
-
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-
|
| 83 |
"source_quote": "Post-Hospitalisation\nThe Medical Expenses incurred during the 90 days"
|
| 84 |
},
|
| 85 |
"day_care_treatments_count": {
|
| 86 |
"value": null,
|
| 87 |
-
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-
|
| 88 |
"source_quote": ""
|
| 89 |
},
|
| 90 |
"network_hospital_count": {
|
| 91 |
"value": null,
|
| 92 |
-
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-
|
| 93 |
"source_quote": ""
|
| 94 |
},
|
| 95 |
"sum_insured_options": {
|
| 96 |
"value": null,
|
| 97 |
-
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-
|
| 98 |
"source_quote": ""
|
| 99 |
},
|
| 100 |
"claim_settlement_ratio": {
|
| 101 |
"value": null,
|
| 102 |
-
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-
|
| 103 |
"source_quote": ""
|
| 104 |
},
|
| 105 |
"tat_cashless_authorization_hours": {
|
| 106 |
"value": null,
|
| 107 |
-
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-
|
| 108 |
"source_quote": ""
|
| 109 |
},
|
| 110 |
"policy_type": {
|
| 111 |
"value": "group",
|
| 112 |
-
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-
|
| 113 |
"source_quote": "classified as group from PDF heuristics"
|
| 114 |
},
|
| 115 |
"_meta": {
|
| 116 |
"curated_at": "2026-05-14",
|
| 117 |
"primary_source_pdf": "rag/corpus/bajaj-allianz/group-health-guard-gold__wordings.pdf",
|
| 118 |
"completeness_pct": 32,
|
| 119 |
-
"notes": "Curated by tools/curate_remaining.py
|
| 120 |
}
|
| 121 |
}
|
|
|
|
| 1 |
{
|
| 2 |
+
"policy_id": "bajaj-allianz__group-health-guard-silver__wordings",
|
| 3 |
+
"policy_name": "Health Guard (Group) - Silver Plan",
|
| 4 |
"insurer_slug": "bajaj-allianz",
|
| 5 |
"uin_code": {
|
| 6 |
"value": "BAJHLGP21181V022021",
|
| 7 |
+
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-silver__wordings.pdf",
|
| 8 |
"source_quote": "UIN: BAJHLGP21181V022021"
|
| 9 |
},
|
| 10 |
"min_entry_age": {
|
| 11 |
"value": null,
|
| 12 |
+
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-silver__wordings.pdf",
|
| 13 |
"source_quote": ""
|
| 14 |
},
|
| 15 |
"max_entry_age": {
|
| 16 |
"value": null,
|
| 17 |
+
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-silver__wordings.pdf",
|
| 18 |
"source_quote": ""
|
| 19 |
},
|
| 20 |
"max_renewal_age": {
|
| 21 |
"value": null,
|
| 22 |
+
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-silver__wordings.pdf",
|
| 23 |
"source_quote": ""
|
| 24 |
},
|
| 25 |
"pre_existing_disease_waiting_months": {
|
| 26 |
"value": 36,
|
| 27 |
+
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-silver__wordings.pdf",
|
| 28 |
"source_quote": "pre-existing Disease (PED) and its direct complications shall be excluded until the expiry of 36 months"
|
| 29 |
},
|
| 30 |
"initial_waiting_period_days": {
|
| 31 |
"value": null,
|
| 32 |
+
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-silver__wordings.pdf",
|
| 33 |
"source_quote": ""
|
| 34 |
},
|
| 35 |
"maternity_coverage": {
|
| 36 |
"value": null,
|
| 37 |
+
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-silver__wordings.pdf",
|
| 38 |
"source_quote": ""
|
| 39 |
},
|
| 40 |
"maternity_waiting_months": {
|
| 41 |
"value": null,
|
| 42 |
+
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-silver__wordings.pdf",
|
| 43 |
"source_quote": ""
|
| 44 |
},
|
| 45 |
"ayush_coverage": {
|
| 46 |
"value": null,
|
| 47 |
+
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-silver__wordings.pdf",
|
| 48 |
"source_quote": ""
|
| 49 |
},
|
| 50 |
"cashless_treatment_supported": {
|
| 51 |
"value": true,
|
| 52 |
+
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-silver__wordings.pdf",
|
| 53 |
"source_quote": "Cashless facility"
|
| 54 |
},
|
| 55 |
"copayment_pct": {
|
| 56 |
"value": null,
|
| 57 |
+
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-silver__wordings.pdf",
|
| 58 |
"source_quote": ""
|
| 59 |
},
|
| 60 |
"no_claim_bonus_pct": {
|
| 61 |
"value": 100,
|
| 62 |
+
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-silver__wordings.pdf",
|
| 63 |
"source_quote": "cumulative bonus (if any) is exhausted due to claims lodged during the Policy\nyear, then it is agreed that 100%"
|
| 64 |
},
|
| 65 |
"restoration_benefit": {
|
| 66 |
"value": null,
|
| 67 |
+
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-silver__wordings.pdf",
|
| 68 |
"source_quote": ""
|
| 69 |
},
|
| 70 |
"room_rent_capping": {
|
| 71 |
"value": null,
|
| 72 |
+
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-silver__wordings.pdf",
|
| 73 |
"source_quote": ""
|
| 74 |
},
|
| 75 |
"pre_hospitalization_days": {
|
| 76 |
"value": 60,
|
| 77 |
+
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-silver__wordings.pdf",
|
| 78 |
"source_quote": "Pre-Hospitalisation\nThe Medical Expenses incurred during the 60 days"
|
| 79 |
},
|
| 80 |
"post_hospitalization_days": {
|
| 81 |
"value": 90,
|
| 82 |
+
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-silver__wordings.pdf",
|
| 83 |
"source_quote": "Post-Hospitalisation\nThe Medical Expenses incurred during the 90 days"
|
| 84 |
},
|
| 85 |
"day_care_treatments_count": {
|
| 86 |
"value": null,
|
| 87 |
+
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-silver__wordings.pdf",
|
| 88 |
"source_quote": ""
|
| 89 |
},
|
| 90 |
"network_hospital_count": {
|
| 91 |
"value": null,
|
| 92 |
+
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-silver__wordings.pdf",
|
| 93 |
"source_quote": ""
|
| 94 |
},
|
| 95 |
"sum_insured_options": {
|
| 96 |
"value": null,
|
| 97 |
+
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-silver__wordings.pdf",
|
| 98 |
"source_quote": ""
|
| 99 |
},
|
| 100 |
"claim_settlement_ratio": {
|
| 101 |
"value": null,
|
| 102 |
+
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-silver__wordings.pdf",
|
| 103 |
"source_quote": ""
|
| 104 |
},
|
| 105 |
"tat_cashless_authorization_hours": {
|
| 106 |
"value": null,
|
| 107 |
+
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-silver__wordings.pdf",
|
| 108 |
"source_quote": ""
|
| 109 |
},
|
| 110 |
"policy_type": {
|
| 111 |
"value": "group",
|
| 112 |
+
"source_pdf_path": "rag/corpus/bajaj-allianz/group-health-guard-silver__wordings.pdf",
|
| 113 |
"source_quote": "classified as group from PDF heuristics"
|
| 114 |
},
|
| 115 |
"_meta": {
|
| 116 |
"curated_at": "2026-05-14",
|
| 117 |
"primary_source_pdf": "rag/corpus/bajaj-allianz/group-health-guard-gold__wordings.pdf",
|
| 118 |
"completeness_pct": 32,
|
| 119 |
+
"notes": "Curated by tools/curate_remaining.py — pattern-based extraction from local PDF"
|
| 120 |
}
|
| 121 |
}
|
|
@@ -1,7 +1,7 @@
|
|
| 1 |
{
|
| 2 |
-
"policy_id": "
|
| 3 |
"policy_name": "IndusInd Group Mediclaim",
|
| 4 |
-
"insurer_slug": "
|
| 5 |
"source_pdf_path": "rag/corpus/reliance-general/group-mediclaim__wordings.pdf",
|
| 6 |
"uin_code": {
|
| 7 |
"value": "RELHLGP21523V022021",
|
|
@@ -134,5 +134,6 @@
|
|
| 134 |
"unit": null,
|
| 135 |
"source_pdf_path": "rag/corpus/reliance-general/group-mediclaim__wordings.pdf",
|
| 136 |
"source_quote": "not extracted from this PDF (field absent or LLM unable to infer)"
|
| 137 |
-
}
|
| 138 |
-
|
|
|
|
|
|
| 1 |
{
|
| 2 |
+
"policy_id": "indusind-general__group-mediclaim__wordings",
|
| 3 |
"policy_name": "IndusInd Group Mediclaim",
|
| 4 |
+
"insurer_slug": "indusind-general",
|
| 5 |
"source_pdf_path": "rag/corpus/reliance-general/group-mediclaim__wordings.pdf",
|
| 6 |
"uin_code": {
|
| 7 |
"value": "RELHLGP21523V022021",
|
|
|
|
| 134 |
"unit": null,
|
| 135 |
"source_pdf_path": "rag/corpus/reliance-general/group-mediclaim__wordings.pdf",
|
| 136 |
"source_quote": "not extracted from this PDF (field absent or LLM unable to infer)"
|
| 137 |
+
},
|
| 138 |
+
"legacy_issuer": "Reliance General Insurance"
|
| 139 |
+
}
|
|
@@ -1,7 +1,7 @@
|
|
| 1 |
{
|
| 2 |
-
"policy_id": "
|
| 3 |
"policy_name": "IndusInd HealthGain Policy",
|
| 4 |
-
"insurer_slug": "
|
| 5 |
"source_pdf_path": "rag/corpus/reliance-general/health-gain__wordings.pdf",
|
| 6 |
"uin_code": {
|
| 7 |
"value": "INIHLIP26040V042526",
|
|
@@ -134,5 +134,6 @@
|
|
| 134 |
"unit": null,
|
| 135 |
"source_pdf_path": "rag/corpus/reliance-general/health-gain__wordings.pdf",
|
| 136 |
"source_quote": "not extracted from this PDF (field absent or LLM unable to infer)"
|
| 137 |
-
}
|
| 138 |
-
|
|
|
|
|
|
| 1 |
{
|
| 2 |
+
"policy_id": "indusind-general__health-gain__wordings",
|
| 3 |
"policy_name": "IndusInd HealthGain Policy",
|
| 4 |
+
"insurer_slug": "indusind-general",
|
| 5 |
"source_pdf_path": "rag/corpus/reliance-general/health-gain__wordings.pdf",
|
| 6 |
"uin_code": {
|
| 7 |
"value": "INIHLIP26040V042526",
|
|
|
|
| 134 |
"unit": null,
|
| 135 |
"source_pdf_path": "rag/corpus/reliance-general/health-gain__wordings.pdf",
|
| 136 |
"source_quote": "not extracted from this PDF (field absent or LLM unable to infer)"
|
| 137 |
+
},
|
| 138 |
+
"legacy_issuer": "Reliance General Insurance"
|
| 139 |
+
}
|
|
@@ -1,7 +1,7 @@
|
|
| 1 |
{
|
| 2 |
-
"policy_id": "
|
| 3 |
"policy_name": "IndusInd Hospi Care Insurance",
|
| 4 |
-
"insurer_slug": "
|
| 5 |
"source_pdf_path": "rag/corpus/reliance-general/hospi-care__wordings.pdf",
|
| 6 |
"uin_code": {
|
| 7 |
"value": "RELHLIP20027V012021",
|
|
@@ -134,5 +134,6 @@
|
|
| 134 |
"unit": null,
|
| 135 |
"source_pdf_path": "rag/corpus/reliance-general/hospi-care__wordings.pdf",
|
| 136 |
"source_quote": "not extracted from this PDF (field absent or LLM unable to infer)"
|
| 137 |
-
}
|
| 138 |
-
|
|
|
|
|
|
| 1 |
{
|
| 2 |
+
"policy_id": "indusind-general__hospi-care__wordings",
|
| 3 |
"policy_name": "IndusInd Hospi Care Insurance",
|
| 4 |
+
"insurer_slug": "indusind-general",
|
| 5 |
"source_pdf_path": "rag/corpus/reliance-general/hospi-care__wordings.pdf",
|
| 6 |
"uin_code": {
|
| 7 |
"value": "RELHLIP20027V012021",
|
|
|
|
| 134 |
"unit": null,
|
| 135 |
"source_pdf_path": "rag/corpus/reliance-general/hospi-care__wordings.pdf",
|
| 136 |
"source_quote": "not extracted from this PDF (field absent or LLM unable to infer)"
|
| 137 |
+
},
|
| 138 |
+
"legacy_issuer": "Reliance General Insurance"
|
| 139 |
+
}
|
|
@@ -70,7 +70,7 @@ COPY 40-data ./40-data
|
|
| 70 |
# (7356 chunks from a prior ingest) instead of the freshly-uploaded
|
| 71 |
# cleaned one (3799 chunks). Bump CACHE_BUST manually each time the
|
| 72 |
# dataset is re-uploaded; the value just needs to change.
|
| 73 |
-
ARG DATASET_CACHE_BUST=2026-05-15-
|
| 74 |
RUN echo "Dataset cache bust: ${DATASET_CACHE_BUST}" && python -c "\
|
| 75 |
from huggingface_hub import snapshot_download; \
|
| 76 |
snapshot_download(\
|
|
|
|
| 70 |
# (7356 chunks from a prior ingest) instead of the freshly-uploaded
|
| 71 |
# cleaned one (3799 chunks). Bump CACHE_BUST manually each time the
|
| 72 |
# dataset is re-uploaded; the value just needs to change.
|
| 73 |
+
ARG DATASET_CACHE_BUST=2026-05-15-ki144-v1
|
| 74 |
RUN echo "Dataset cache bust: ${DATASET_CACHE_BUST}" && python -c "\
|
| 75 |
from huggingface_hub import snapshot_download; \
|
| 76 |
snapshot_download(\
|
|
@@ -140,6 +140,12 @@ class InsurerCoverage(BaseModel):
|
|
| 140 |
home_url: str # insurer's main website (manually curated, verified)
|
| 141 |
policy_count: int
|
| 142 |
sample_policies: list[PolicyEntry]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
|
| 144 |
|
| 145 |
class CoverageResponse(BaseModel):
|
|
@@ -557,6 +563,7 @@ async def coverage():
|
|
| 557 |
"national-insurance": ("National Insurance Company", "https://nationalinsurance.nic.co.in/"),
|
| 558 |
"new-india": ("New India Assurance", "https://www.newindia.co.in/"),
|
| 559 |
"niva-bupa": ("Niva Bupa Health Insurance", "https://www.nivabupa.com/"),
|
|
|
|
| 560 |
"oriental-insurance": ("Oriental Insurance Company", "https://orientalinsurance.org.in/"),
|
| 561 |
"reliance-general": ("Reliance General Insurance", "https://www.reliancegeneral.co.in/"),
|
| 562 |
"royal-sundaram": ("Royal Sundaram General Insurance", "https://www.royalsundaram.in/"),
|
|
@@ -592,9 +599,103 @@ async def coverage():
|
|
| 592 |
seen_product_keys: set[str] = set()
|
| 593 |
seen_policy_ids: set[str] = set()
|
| 594 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 595 |
# by_insurer entries:
|
| 596 |
# products: set of product_keys (matches /api/policies/all card count)
|
| 597 |
# names: ordered dict of policy_NAME -> first product_key (for sample display)
|
|
|
|
|
|
|
| 598 |
# KI-135 (2026-05-15) — track product_keys (not names) for counting so the
|
| 599 |
# ~1 within-insurer policy_name collision (e.g. new-india Floater listed
|
| 600 |
# as both extracted + curated_facts) doesn't collapse the count below the
|
|
@@ -618,8 +719,10 @@ async def coverage():
|
|
| 618 |
name = data.get("policy_name", "") or pid
|
| 619 |
url = data.get("source_pdf_url", "")
|
| 620 |
if slug not in by_insurer:
|
| 621 |
-
by_insurer[slug] = {"products": set(), "names": [], "chunks": 0}
|
| 622 |
by_insurer[slug]["products"].add(pkey)
|
|
|
|
|
|
|
| 623 |
if name not in by_insurer[slug]["names"]:
|
| 624 |
by_insurer[slug]["names"].append(name)
|
| 625 |
by_insurer[slug]["chunks"] += 1
|
|
@@ -634,6 +737,10 @@ async def coverage():
|
|
| 634 |
continue
|
| 635 |
if any(eid.startswith(curated_pid + "__") for eid in seen_policy_ids):
|
| 636 |
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
| 637 |
seen_policy_ids.add(curated_pid)
|
| 638 |
slug = data.get("insurer_slug", "")
|
| 639 |
if slug == "regulatory":
|
|
@@ -647,8 +754,12 @@ async def coverage():
|
|
| 647 |
name = data.get("policy_name", "") or curated_pid
|
| 648 |
url = data.get("source_pdf_url", "")
|
| 649 |
if slug not in by_insurer:
|
| 650 |
-
by_insurer[slug] = {"products": set(), "names": [], "chunks": 0}
|
| 651 |
by_insurer[slug]["products"].add(pkey)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 652 |
if name not in by_insurer[slug]["names"]:
|
| 653 |
by_insurer[slug]["names"].append(name)
|
| 654 |
by_insurer[slug]["chunks"] += 1
|
|
@@ -680,6 +791,7 @@ async def coverage():
|
|
| 680 |
home_url=home_url,
|
| 681 |
policy_count=product_count,
|
| 682 |
sample_policies=sample_entries,
|
|
|
|
| 683 |
)
|
| 684 |
)
|
| 685 |
|
|
@@ -1073,6 +1185,12 @@ class MarketplacePolicy(BaseModel):
|
|
| 1073 |
maternity_coverage: Optional[bool] = None
|
| 1074 |
cashless_treatment_supported: Optional[bool] = None
|
| 1075 |
room_rent_capping: Optional[str] = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1076 |
|
| 1077 |
|
| 1078 |
class MarketplaceResponse(BaseModel):
|
|
@@ -1151,8 +1269,14 @@ def _load_curated_facts() -> dict[str, dict]:
|
|
| 1151 |
`{field: {value, source_pdf_path, source_quote}}` shape. We unwrap to a
|
| 1152 |
flat `{field: value}` dict for the marketplace endpoint, preserving the
|
| 1153 |
provenance in a `_facts_provenance` field for transparency.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1154 |
"""
|
| 1155 |
import json as _json
|
|
|
|
| 1156 |
facts: dict[str, dict] = {}
|
| 1157 |
facts_dir = settings.CORPUS_DIR.parent.parent / "40-data" / "policy_facts"
|
| 1158 |
if not facts_dir.exists():
|
|
@@ -1165,6 +1289,7 @@ def _load_curated_facts() -> dict[str, dict]:
|
|
| 1165 |
policy_id = d.get("policy_id") or f.stem
|
| 1166 |
flat: dict = {}
|
| 1167 |
provenance: dict = {}
|
|
|
|
| 1168 |
for k, v in d.items():
|
| 1169 |
if k.startswith("_") or k in ("policy_id", "policy_name", "insurer_slug"):
|
| 1170 |
flat[k] = v
|
|
@@ -1177,9 +1302,19 @@ def _load_curated_facts() -> dict[str, dict]:
|
|
| 1177 |
"source_quote": v.get("source_quote"),
|
| 1178 |
"source_url": v.get("source_url"),
|
| 1179 |
}
|
|
|
|
|
|
|
| 1180 |
else:
|
| 1181 |
flat[k] = v
|
| 1182 |
flat["_facts_provenance"] = provenance
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1183 |
# Try a couple of policy_id permutations to maximise lookup hit rate
|
| 1184 |
facts[policy_id] = flat
|
| 1185 |
# Some extracted JSONs use `_wordings` suffix; the curated files don't
|
|
@@ -1189,6 +1324,23 @@ def _load_curated_facts() -> dict[str, dict]:
|
|
| 1189 |
return facts
|
| 1190 |
|
| 1191 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1192 |
def _merge_curated(extracted: dict, curated: dict | None) -> dict:
|
| 1193 |
"""Curated facts override LLM extraction for every field they populate.
|
| 1194 |
LLM extraction fills the long tail. Provenance pointers survive in the
|
|
@@ -1251,6 +1403,7 @@ async def policies_all(session_id: Optional[str] = None):
|
|
| 1251 |
"national-insurance": ("National Insurance Company", "https://nationalinsurance.nic.co.in/"),
|
| 1252 |
"new-india": ("New India Assurance", "https://www.newindia.co.in/"),
|
| 1253 |
"niva-bupa": ("Niva Bupa Health Insurance", "https://www.nivabupa.com/"),
|
|
|
|
| 1254 |
"oriental-insurance": ("Oriental Insurance Company", "https://orientalinsurance.org.in/"),
|
| 1255 |
"reliance-general": ("Reliance General Insurance", "https://www.reliancegeneral.co.in/"),
|
| 1256 |
"royal-sundaram": ("Royal Sundaram General Insurance", "https://www.royalsundaram.in/"),
|
|
@@ -1293,6 +1446,146 @@ async def policies_all(session_id: Optional[str] = None):
|
|
| 1293 |
key=lambda fp: (_DOCTYPE_RANK.get(_doctype_of(fp.stem), 99), fp.stem),
|
| 1294 |
)
|
| 1295 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1296 |
seen_product_keys: set[str] = set()
|
| 1297 |
seen_policy_ids: set[str] = set()
|
| 1298 |
out = []
|
|
@@ -1371,6 +1664,9 @@ async def policies_all(session_id: Optional[str] = None):
|
|
| 1371 |
maternity_coverage=_coerce_bool(data.get("maternity_coverage")),
|
| 1372 |
cashless_treatment_supported=_coerce_bool(data.get("cashless_treatment_supported")),
|
| 1373 |
room_rent_capping=data.get("room_rent_capping") if isinstance(data.get("room_rent_capping"), str) else None,
|
|
|
|
|
|
|
|
|
|
| 1374 |
))
|
| 1375 |
except Exception as e:
|
| 1376 |
# One malformed extraction should not kill the whole feed
|
|
@@ -1390,6 +1686,11 @@ async def policies_all(session_id: Optional[str] = None):
|
|
| 1390 |
# Also skip if any extracted ID matches with a suffix
|
| 1391 |
if any(eid.startswith(curated_policy_id + "__") for eid in seen_policy_ids):
|
| 1392 |
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1393 |
seen_policy_ids.add(curated_policy_id)
|
| 1394 |
slug = data.get("insurer_slug", "")
|
| 1395 |
name, home = insurer_meta.get(slug, (slug, ""))
|
|
@@ -1440,6 +1741,10 @@ async def policies_all(session_id: Optional[str] = None):
|
|
| 1440 |
maternity_coverage=_coerce_bool(data.get("maternity_coverage")),
|
| 1441 |
cashless_treatment_supported=_coerce_bool(data.get("cashless_treatment_supported")),
|
| 1442 |
room_rent_capping=data.get("room_rent_capping") if isinstance(data.get("room_rent_capping"), str) else None,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1443 |
))
|
| 1444 |
except Exception as e:
|
| 1445 |
print(f"[marketplace] skipping curated {curated_policy_id}: {type(e).__name__}: {str(e)[:120]}")
|
|
|
|
| 140 |
home_url: str # insurer's main website (manually curated, verified)
|
| 141 |
policy_count: int
|
| 142 |
sample_policies: list[PolicyEntry]
|
| 143 |
+
# KI-141 (2026-05-15) — backward-compatible default empty. Per-product
|
| 144 |
+
# alias list isn't actually surfaced on the coverage card today, but the
|
| 145 |
+
# field is mirrored from MarketplacePolicy so callers that union the two
|
| 146 |
+
# endpoints see a consistent schema. Total aliases collapsed into this
|
| 147 |
+
# insurer's parents — useful for QA + future UI surfacing.
|
| 148 |
+
alias_count: int = 0
|
| 149 |
|
| 150 |
|
| 151 |
class CoverageResponse(BaseModel):
|
|
|
|
| 563 |
"national-insurance": ("National Insurance Company", "https://nationalinsurance.nic.co.in/"),
|
| 564 |
"new-india": ("New India Assurance", "https://www.newindia.co.in/"),
|
| 565 |
"niva-bupa": ("Niva Bupa Health Insurance", "https://www.nivabupa.com/"),
|
| 566 |
+
"indusind-general": ("IndusInd General Insurance (formerly Reliance General)", "https://www.indusind.com/general-insurance/"),
|
| 567 |
"oriental-insurance": ("Oriental Insurance Company", "https://orientalinsurance.org.in/"),
|
| 568 |
"reliance-general": ("Reliance General Insurance", "https://www.reliancegeneral.co.in/"),
|
| 569 |
"royal-sundaram": ("Royal Sundaram General Insurance", "https://www.royalsundaram.in/"),
|
|
|
|
| 599 |
seen_product_keys: set[str] = set()
|
| 600 |
seen_policy_ids: set[str] = set()
|
| 601 |
|
| 602 |
+
# KI-141 (2026-05-15) — pre-compute the alias mapping (curated marketing
|
| 603 |
+
# renames whose source PDF maps to an extracted parent). These curated
|
| 604 |
+
# entries collapse onto the parent card; they DO NOT count separately.
|
| 605 |
+
# Same algorithm as /api/policies/all so the totals stay in sync.
|
| 606 |
+
#
|
| 607 |
+
# KI-142 (2026-05-15, REFACTORED) — UIN-primary invariant: 1 unique UIN
|
| 608 |
+
# = 1 unique marketplace card. Mirrors the /api/policies/all algorithm
|
| 609 |
+
# so the coverage policy_count stays in lockstep with the marketplace
|
| 610 |
+
# card count. See the long-form comment block in that endpoint for the
|
| 611 |
+
# full algorithm rationale.
|
| 612 |
+
extracted_stems_cov = {fp.stem for fp in sorted_files}
|
| 613 |
+
|
| 614 |
+
# Phase A — extracted parents claim their UINs first.
|
| 615 |
+
uin_to_parent_cov: dict[str, str] = {}
|
| 616 |
+
extracted_uin_cov: dict[str, str] = {}
|
| 617 |
+
for fp in sorted_files:
|
| 618 |
+
try:
|
| 619 |
+
_d = _json.loads(fp.read_text())
|
| 620 |
+
except Exception:
|
| 621 |
+
continue
|
| 622 |
+
_u = _d.get("uin_code")
|
| 623 |
+
if isinstance(_u, dict):
|
| 624 |
+
_u = _u.get("value")
|
| 625 |
+
_u = (_u or "").strip() if isinstance(_u, str) else ""
|
| 626 |
+
if _u:
|
| 627 |
+
extracted_uin_cov[fp.stem] = _u
|
| 628 |
+
uin_to_parent_cov.setdefault(_u, fp.stem)
|
| 629 |
+
|
| 630 |
+
direct_parent_cov: dict[str, str] = {}
|
| 631 |
+
curated_canonical_ids_cov: list[str] = []
|
| 632 |
+
|
| 633 |
+
# Phase B — walk curated entries deterministically (sorted by policy_id).
|
| 634 |
+
for curated_pid, cdata in sorted(curated_facts.items()):
|
| 635 |
+
if curated_pid != cdata.get("policy_id", curated_pid):
|
| 636 |
+
continue
|
| 637 |
+
if any(curated_pid.endswith(f"__{dt}")
|
| 638 |
+
for dt in ("wordings", "brochure", "cis", "prospectus")):
|
| 639 |
+
continue
|
| 640 |
+
curated_canonical_ids_cov.append(curated_pid)
|
| 641 |
+
|
| 642 |
+
curated_uin = cdata.get("uin_code")
|
| 643 |
+
if isinstance(curated_uin, dict):
|
| 644 |
+
curated_uin = curated_uin.get("value")
|
| 645 |
+
curated_uin = (curated_uin or "").strip() if isinstance(curated_uin, str) else ""
|
| 646 |
+
|
| 647 |
+
parent_id: str | None = None
|
| 648 |
+
if curated_uin and curated_uin in uin_to_parent_cov \
|
| 649 |
+
and uin_to_parent_cov[curated_uin] != curated_pid:
|
| 650 |
+
parent_id = uin_to_parent_cov[curated_uin]
|
| 651 |
+
elif curated_uin:
|
| 652 |
+
uin_to_parent_cov[curated_uin] = curated_pid
|
| 653 |
+
|
| 654 |
+
if parent_id is None:
|
| 655 |
+
# KI-142 — fall back to source_pdf gate when UIN doesn't match
|
| 656 |
+
# any prior claimant (curated UIN may be more accurate than
|
| 657 |
+
# extracted; multi-variant PDFs naturally share one filing).
|
| 658 |
+
fb_parent = _source_pdf_to_policy_id(cdata.get("_primary_source_pdf"))
|
| 659 |
+
if fb_parent and fb_parent in extracted_stems_cov and fb_parent != curated_pid:
|
| 660 |
+
parent_id = fb_parent
|
| 661 |
+
|
| 662 |
+
if parent_id:
|
| 663 |
+
direct_parent_cov[curated_pid] = parent_id
|
| 664 |
+
|
| 665 |
+
# Phase C — chain-compress (see /api/policies/all for rationale).
|
| 666 |
+
aliased_curated_ids_cov: set[str] = set()
|
| 667 |
+
parent_pkey_alias_count: dict[str, int] = {}
|
| 668 |
+
|
| 669 |
+
def _terminal_parent_cov(start: str) -> str | None:
|
| 670 |
+
seen_chain: set[str] = set()
|
| 671 |
+
cur = start
|
| 672 |
+
while True:
|
| 673 |
+
nxt = direct_parent_cov.get(cur)
|
| 674 |
+
if not nxt:
|
| 675 |
+
return cur if cur != start else None
|
| 676 |
+
if nxt in seen_chain or nxt == start:
|
| 677 |
+
return None
|
| 678 |
+
seen_chain.add(nxt)
|
| 679 |
+
cur = nxt
|
| 680 |
+
|
| 681 |
+
for curated_pid in curated_canonical_ids_cov:
|
| 682 |
+
if curated_pid not in direct_parent_cov:
|
| 683 |
+
continue
|
| 684 |
+
terminal = _terminal_parent_cov(curated_pid)
|
| 685 |
+
if not terminal:
|
| 686 |
+
continue
|
| 687 |
+
if terminal in extracted_stems_cov:
|
| 688 |
+
terminal_pkey = _product_key_of_cov(terminal)
|
| 689 |
+
else:
|
| 690 |
+
terminal_pkey = terminal
|
| 691 |
+
aliased_curated_ids_cov.add(curated_pid)
|
| 692 |
+
parent_pkey_alias_count[terminal_pkey] = parent_pkey_alias_count.get(terminal_pkey, 0) + 1
|
| 693 |
+
|
| 694 |
# by_insurer entries:
|
| 695 |
# products: set of product_keys (matches /api/policies/all card count)
|
| 696 |
# names: ordered dict of policy_NAME -> first product_key (for sample display)
|
| 697 |
+
# aliases: KI-141 — count of curated marketing-rename entries merged
|
| 698 |
+
# into this insurer's parent cards (for the alias_count field).
|
| 699 |
# KI-135 (2026-05-15) — track product_keys (not names) for counting so the
|
| 700 |
# ~1 within-insurer policy_name collision (e.g. new-india Floater listed
|
| 701 |
# as both extracted + curated_facts) doesn't collapse the count below the
|
|
|
|
| 719 |
name = data.get("policy_name", "") or pid
|
| 720 |
url = data.get("source_pdf_url", "")
|
| 721 |
if slug not in by_insurer:
|
| 722 |
+
by_insurer[slug] = {"products": set(), "names": [], "chunks": 0, "aliases": 0}
|
| 723 |
by_insurer[slug]["products"].add(pkey)
|
| 724 |
+
# KI-141 — accumulate alias count from the pre-pass
|
| 725 |
+
by_insurer[slug]["aliases"] += parent_pkey_alias_count.get(pkey, 0)
|
| 726 |
if name not in by_insurer[slug]["names"]:
|
| 727 |
by_insurer[slug]["names"].append(name)
|
| 728 |
by_insurer[slug]["chunks"] += 1
|
|
|
|
| 737 |
continue
|
| 738 |
if any(eid.startswith(curated_pid + "__") for eid in seen_policy_ids):
|
| 739 |
continue
|
| 740 |
+
# KI-141 — skip curated entries that have already been collapsed into
|
| 741 |
+
# a pass-1 parent's alias list.
|
| 742 |
+
if curated_pid in aliased_curated_ids_cov:
|
| 743 |
+
continue
|
| 744 |
seen_policy_ids.add(curated_pid)
|
| 745 |
slug = data.get("insurer_slug", "")
|
| 746 |
if slug == "regulatory":
|
|
|
|
| 754 |
name = data.get("policy_name", "") or curated_pid
|
| 755 |
url = data.get("source_pdf_url", "")
|
| 756 |
if slug not in by_insurer:
|
| 757 |
+
by_insurer[slug] = {"products": set(), "names": [], "chunks": 0, "aliases": 0}
|
| 758 |
by_insurer[slug]["products"].add(pkey)
|
| 759 |
+
# KI-142 — accumulate alias count for curated parents (curated entries
|
| 760 |
+
# that themselves became the claimant of a new UIN, with later curated
|
| 761 |
+
# siblings aliasing onto them).
|
| 762 |
+
by_insurer[slug]["aliases"] += parent_pkey_alias_count.get(pkey, 0)
|
| 763 |
if name not in by_insurer[slug]["names"]:
|
| 764 |
by_insurer[slug]["names"].append(name)
|
| 765 |
by_insurer[slug]["chunks"] += 1
|
|
|
|
| 791 |
home_url=home_url,
|
| 792 |
policy_count=product_count,
|
| 793 |
sample_policies=sample_entries,
|
| 794 |
+
alias_count=info.get("aliases", 0),
|
| 795 |
)
|
| 796 |
)
|
| 797 |
|
|
|
|
| 1185 |
maternity_coverage: Optional[bool] = None
|
| 1186 |
cashless_treatment_supported: Optional[bool] = None
|
| 1187 |
room_rent_capping: Optional[str] = None
|
| 1188 |
+
# KI-141 (2026-05-15) — marketing-rename aliases that share the same
|
| 1189 |
+
# source PDF (e.g. "Activ One" and "Activ Health" both point to the
|
| 1190 |
+
# activ-health-individual__wordings.pdf parent). Default empty list so
|
| 1191 |
+
# the field is backward-compatible. Frontend renders these as small
|
| 1192 |
+
# "Also known as: X, Y" sub-labels under the parent card title.
|
| 1193 |
+
aliases: list[str] = Field(default_factory=list)
|
| 1194 |
|
| 1195 |
|
| 1196 |
class MarketplaceResponse(BaseModel):
|
|
|
|
| 1269 |
`{field: {value, source_pdf_path, source_quote}}` shape. We unwrap to a
|
| 1270 |
flat `{field: value}` dict for the marketplace endpoint, preserving the
|
| 1271 |
provenance in a `_facts_provenance` field for transparency.
|
| 1272 |
+
|
| 1273 |
+
KI-141 (2026-05-15) — also computes `_primary_source_pdf`, the most-common
|
| 1274 |
+
`source_pdf_path` across this curated entry's fields. Used by both
|
| 1275 |
+
/api/policies/all and /api/coverage to alias-merge marketing-rename
|
| 1276 |
+
curated entries into their extracted-JSON parent card.
|
| 1277 |
"""
|
| 1278 |
import json as _json
|
| 1279 |
+
from collections import Counter
|
| 1280 |
facts: dict[str, dict] = {}
|
| 1281 |
facts_dir = settings.CORPUS_DIR.parent.parent / "40-data" / "policy_facts"
|
| 1282 |
if not facts_dir.exists():
|
|
|
|
| 1289 |
policy_id = d.get("policy_id") or f.stem
|
| 1290 |
flat: dict = {}
|
| 1291 |
provenance: dict = {}
|
| 1292 |
+
all_source_pdfs: list[str] = []
|
| 1293 |
for k, v in d.items():
|
| 1294 |
if k.startswith("_") or k in ("policy_id", "policy_name", "insurer_slug"):
|
| 1295 |
flat[k] = v
|
|
|
|
| 1302 |
"source_quote": v.get("source_quote"),
|
| 1303 |
"source_url": v.get("source_url"),
|
| 1304 |
}
|
| 1305 |
+
if v.get("source_pdf_path"):
|
| 1306 |
+
all_source_pdfs.append(v["source_pdf_path"])
|
| 1307 |
else:
|
| 1308 |
flat[k] = v
|
| 1309 |
flat["_facts_provenance"] = provenance
|
| 1310 |
+
# KI-141 — pick the most-common source PDF path as this curated
|
| 1311 |
+
# entry's "primary source". When this path's extracted-JSON parent
|
| 1312 |
+
# already exists, the marketplace collapses this curated entry into
|
| 1313 |
+
# the parent's aliases list instead of emitting a separate card.
|
| 1314 |
+
flat["_primary_source_pdf"] = (
|
| 1315 |
+
Counter(all_source_pdfs).most_common(1)[0][0]
|
| 1316 |
+
if all_source_pdfs else None
|
| 1317 |
+
)
|
| 1318 |
# Try a couple of policy_id permutations to maximise lookup hit rate
|
| 1319 |
facts[policy_id] = flat
|
| 1320 |
# Some extracted JSONs use `_wordings` suffix; the curated files don't
|
|
|
|
| 1324 |
return facts
|
| 1325 |
|
| 1326 |
|
| 1327 |
+
def _source_pdf_to_policy_id(pdf_path: str | None) -> str | None:
|
| 1328 |
+
"""KI-141 — map a curated `source_pdf_path` like
|
| 1329 |
+
'rag/corpus/aditya-birla/activ-health-individual__wordings.pdf' to the
|
| 1330 |
+
extracted-JSON policy_id 'aditya-birla__activ-health-individual__wordings'.
|
| 1331 |
+
|
| 1332 |
+
Returns None if the input is empty/None.
|
| 1333 |
+
"""
|
| 1334 |
+
if not pdf_path:
|
| 1335 |
+
return None
|
| 1336 |
+
s = pdf_path
|
| 1337 |
+
if s.startswith("rag/corpus/"):
|
| 1338 |
+
s = s[len("rag/corpus/"):]
|
| 1339 |
+
if s.endswith(".pdf"):
|
| 1340 |
+
s = s[: -len(".pdf")]
|
| 1341 |
+
return s.replace("/", "__")
|
| 1342 |
+
|
| 1343 |
+
|
| 1344 |
def _merge_curated(extracted: dict, curated: dict | None) -> dict:
|
| 1345 |
"""Curated facts override LLM extraction for every field they populate.
|
| 1346 |
LLM extraction fills the long tail. Provenance pointers survive in the
|
|
|
|
| 1403 |
"national-insurance": ("National Insurance Company", "https://nationalinsurance.nic.co.in/"),
|
| 1404 |
"new-india": ("New India Assurance", "https://www.newindia.co.in/"),
|
| 1405 |
"niva-bupa": ("Niva Bupa Health Insurance", "https://www.nivabupa.com/"),
|
| 1406 |
+
"indusind-general": ("IndusInd General Insurance (formerly Reliance General)", "https://www.indusind.com/general-insurance/"),
|
| 1407 |
"oriental-insurance": ("Oriental Insurance Company", "https://orientalinsurance.org.in/"),
|
| 1408 |
"reliance-general": ("Reliance General Insurance", "https://www.reliancegeneral.co.in/"),
|
| 1409 |
"royal-sundaram": ("Royal Sundaram General Insurance", "https://www.royalsundaram.in/"),
|
|
|
|
| 1446 |
key=lambda fp: (_DOCTYPE_RANK.get(_doctype_of(fp.stem), 99), fp.stem),
|
| 1447 |
)
|
| 1448 |
|
| 1449 |
+
# KI-141 (2026-05-15) — alias-dedup pre-pass. Curated "marketing rename"
|
| 1450 |
+
# entries that re-describe the SAME IRDAI-filed product collapse onto a
|
| 1451 |
+
# single marketplace card; the marketing names surface as `aliases`.
|
| 1452 |
+
#
|
| 1453 |
+
# KI-142 (2026-05-15, REFACTORED) — UIN-primary invariant: 1 unique UIN
|
| 1454 |
+
# = 1 unique marketplace card. The PDF-based gate is now a fallback for
|
| 1455 |
+
# entries that lack a UIN.
|
| 1456 |
+
#
|
| 1457 |
+
# Algorithm (two phases so PDF-backed extracted entries always claim
|
| 1458 |
+
# their UIN before any curated rename does):
|
| 1459 |
+
# Phase A: walk extracted/*.json (sorted by doctype rank, then stem).
|
| 1460 |
+
# Each extracted parent claims its uin_code into `uin_to_parent`.
|
| 1461 |
+
# Phase B: walk curated_facts (sorted by policy_id for determinism).
|
| 1462 |
+
# For each canonical curated entry (skip lookup-permutation aliases
|
| 1463 |
+
# and entries that ARE __wordings/__brochure/__cis themselves):
|
| 1464 |
+
# 1. Read curated UIN (scalar OR nested .value form).
|
| 1465 |
+
# 2. If UIN non-empty AND already in `uin_to_parent` (claimant !=
|
| 1466 |
+
# self) → alias of that parent.
|
| 1467 |
+
# 3. Else if UIN non-empty → claim it (so subsequent curated
|
| 1468 |
+
# siblings with the same UIN alias onto THIS entry in pass 2).
|
| 1469 |
+
# 4. Else (UIN empty) OR (UIN had no prior claimant) → fall back
|
| 1470 |
+
# to the source_pdf gate: if `_primary_source_pdf` maps to an
|
| 1471 |
+
# extracted parent stem, alias under that parent.
|
| 1472 |
+
# 5. Otherwise the curated entry stays as a standalone card.
|
| 1473 |
+
#
|
| 1474 |
+
# Multi-variant wordings PDFs with a single filed UIN (e.g.
|
| 1475 |
+
# manipalcigna prohealth-insurance-all-variants.pdf — the PDF text
|
| 1476 |
+
# confirms only ONE UIN `MCIHLIP24011V072324` is filed for that
|
| 1477 |
+
# product) correctly collapse all sub-product curated entries onto one
|
| 1478 |
+
# card. Distinct-UIN siblings under a shared PDF would surface as
|
| 1479 |
+
# separate cards because their UINs claim independent parents.
|
| 1480 |
+
extracted_stems = {fp.stem for fp in sorted_files}
|
| 1481 |
+
|
| 1482 |
+
# Phase A — extracted parents claim their UINs first.
|
| 1483 |
+
uin_to_parent: dict[str, str] = {}
|
| 1484 |
+
extracted_uin: dict[str, str] = {} # kept for downstream introspection
|
| 1485 |
+
for fp in sorted_files:
|
| 1486 |
+
try:
|
| 1487 |
+
_d = _json.loads(fp.read_text())
|
| 1488 |
+
except Exception:
|
| 1489 |
+
continue
|
| 1490 |
+
_u = _d.get("uin_code")
|
| 1491 |
+
if isinstance(_u, dict):
|
| 1492 |
+
_u = _u.get("value")
|
| 1493 |
+
_u = (_u or "").strip() if isinstance(_u, str) else ""
|
| 1494 |
+
if _u:
|
| 1495 |
+
extracted_uin[fp.stem] = _u
|
| 1496 |
+
uin_to_parent.setdefault(_u, fp.stem)
|
| 1497 |
+
|
| 1498 |
+
# Direct-parent map for each curated entry (built in Phase B), then
|
| 1499 |
+
# chain-compressed in Phase C so transitive aliases (e.g. activ-one →
|
| 1500 |
+
# activ-health → activ-health-individual__wordings) flatten onto the
|
| 1501 |
+
# ultimate extracted parent.
|
| 1502 |
+
direct_parent: dict[str, str] = {}
|
| 1503 |
+
curated_canonical_ids: list[str] = []
|
| 1504 |
+
|
| 1505 |
+
# Phase B — walk curated entries deterministically (sorted by policy_id).
|
| 1506 |
+
for curated_policy_id, cdata in sorted(curated_facts.items()):
|
| 1507 |
+
# Skip the __wordings/__brochure/__cis lookup-permutation aliases
|
| 1508 |
+
# that _load_curated_facts adds for hit-rate (canonical policy_id is
|
| 1509 |
+
# stored in the JSON's "policy_id" field).
|
| 1510 |
+
if curated_policy_id != cdata.get("policy_id", curated_policy_id):
|
| 1511 |
+
continue
|
| 1512 |
+
# Skip curated entries that ARE their own __wordings/__brochure/__cis
|
| 1513 |
+
# (doctype-permutation curated files, not marketing renames; pass-2
|
| 1514 |
+
# dedup handles them via the seen_policy_ids prefix check).
|
| 1515 |
+
if any(curated_policy_id.endswith(f"__{dt}")
|
| 1516 |
+
for dt in ("wordings", "brochure", "cis", "prospectus")):
|
| 1517 |
+
continue
|
| 1518 |
+
curated_canonical_ids.append(curated_policy_id)
|
| 1519 |
+
|
| 1520 |
+
# Read curated UIN (scalar OR nested {value, source_pdf_path, ...}).
|
| 1521 |
+
curated_uin = cdata.get("uin_code")
|
| 1522 |
+
if isinstance(curated_uin, dict):
|
| 1523 |
+
curated_uin = curated_uin.get("value")
|
| 1524 |
+
curated_uin = (curated_uin or "").strip() if isinstance(curated_uin, str) else ""
|
| 1525 |
+
|
| 1526 |
+
parent_id: str | None = None
|
| 1527 |
+
if curated_uin and curated_uin in uin_to_parent \
|
| 1528 |
+
and uin_to_parent[curated_uin] != curated_policy_id:
|
| 1529 |
+
# UIN-primary path: collapse onto the prior claimant of this UIN
|
| 1530 |
+
# (different policy_name same regulator filing = pure rename).
|
| 1531 |
+
parent_id = uin_to_parent[curated_uin]
|
| 1532 |
+
elif curated_uin:
|
| 1533 |
+
# New UIN — this curated entry becomes the claimant so any
|
| 1534 |
+
# later curated sibling with the same UIN aliases onto it.
|
| 1535 |
+
uin_to_parent[curated_uin] = curated_policy_id
|
| 1536 |
+
|
| 1537 |
+
if parent_id is None and not curated_uin:
|
| 1538 |
+
# KI-142 (user rule, 2026-05-15): source-PDF fallback only fires
|
| 1539 |
+
# when the curated entry has NO UIN at all. If UIN exists but
|
| 1540 |
+
# doesn't match any extracted parent, they're different
|
| 1541 |
+
# regulator-filed products and must stay as separate cards —
|
| 1542 |
+
# source-PDF coincidence (multi-variant wordings) does NOT merge.
|
| 1543 |
+
fb_parent = _source_pdf_to_policy_id(cdata.get("_primary_source_pdf"))
|
| 1544 |
+
if fb_parent and fb_parent in extracted_stems and fb_parent != curated_policy_id:
|
| 1545 |
+
parent_id = fb_parent
|
| 1546 |
+
|
| 1547 |
+
if parent_id:
|
| 1548 |
+
direct_parent[curated_policy_id] = parent_id
|
| 1549 |
+
|
| 1550 |
+
# Phase C — chain-compress direct_parent so every curated alias points
|
| 1551 |
+
# at its terminal parent (an extracted stem, or a curated parent that
|
| 1552 |
+
# itself has no parent). Detect cycles defensively. After compression
|
| 1553 |
+
# we emit one alias entry per curated descendant onto the terminal
|
| 1554 |
+
# parent's product_key.
|
| 1555 |
+
parent_pkey_aliases: dict[str, list[str]] = {}
|
| 1556 |
+
aliased_curated_ids: set[str] = set()
|
| 1557 |
+
|
| 1558 |
+
def _terminal_parent(start: str) -> str | None:
|
| 1559 |
+
"""Walk direct_parent until we hit an extracted stem or a curated id
|
| 1560 |
+
with no further parent. Returns None on cycle (defensive)."""
|
| 1561 |
+
seen_chain: set[str] = set()
|
| 1562 |
+
cur = start
|
| 1563 |
+
while True:
|
| 1564 |
+
nxt = direct_parent.get(cur)
|
| 1565 |
+
if not nxt:
|
| 1566 |
+
return cur if cur != start else None
|
| 1567 |
+
if nxt in seen_chain or nxt == start:
|
| 1568 |
+
return None # cycle — drop the alias attempt
|
| 1569 |
+
seen_chain.add(nxt)
|
| 1570 |
+
cur = nxt
|
| 1571 |
+
|
| 1572 |
+
for curated_policy_id in curated_canonical_ids:
|
| 1573 |
+
if curated_policy_id not in direct_parent:
|
| 1574 |
+
continue
|
| 1575 |
+
terminal = _terminal_parent(curated_policy_id)
|
| 1576 |
+
if not terminal:
|
| 1577 |
+
continue
|
| 1578 |
+
# Alias-target product_key: extracted stems use _product_key_of()
|
| 1579 |
+
# (strips __doctype). Curated terminals use the policy_id directly.
|
| 1580 |
+
if terminal in extracted_stems:
|
| 1581 |
+
terminal_pkey = _product_key_of(terminal)
|
| 1582 |
+
else:
|
| 1583 |
+
terminal_pkey = terminal
|
| 1584 |
+
alias_name = (curated_facts.get(curated_policy_id, {}).get("policy_name")
|
| 1585 |
+
or curated_policy_id)
|
| 1586 |
+
parent_pkey_aliases.setdefault(terminal_pkey, []).append(alias_name)
|
| 1587 |
+
aliased_curated_ids.add(curated_policy_id)
|
| 1588 |
+
|
| 1589 |
seen_product_keys: set[str] = set()
|
| 1590 |
seen_policy_ids: set[str] = set()
|
| 1591 |
out = []
|
|
|
|
| 1664 |
maternity_coverage=_coerce_bool(data.get("maternity_coverage")),
|
| 1665 |
cashless_treatment_supported=_coerce_bool(data.get("cashless_treatment_supported")),
|
| 1666 |
room_rent_capping=data.get("room_rent_capping") if isinstance(data.get("room_rent_capping"), str) else None,
|
| 1667 |
+
# KI-141 — merge marketing-rename curated entries onto this
|
| 1668 |
+
# parent card. Sorted for deterministic output.
|
| 1669 |
+
aliases=sorted(parent_pkey_aliases.get(product_key, [])),
|
| 1670 |
))
|
| 1671 |
except Exception as e:
|
| 1672 |
# One malformed extraction should not kill the whole feed
|
|
|
|
| 1686 |
# Also skip if any extracted ID matches with a suffix
|
| 1687 |
if any(eid.startswith(curated_policy_id + "__") for eid in seen_policy_ids):
|
| 1688 |
continue
|
| 1689 |
+
# KI-141 — skip curated entries that have already been collapsed onto
|
| 1690 |
+
# a pass-1 parent card via the aliases mechanism (e.g. Activ One →
|
| 1691 |
+
# Activ Health Individual Wordings).
|
| 1692 |
+
if curated_policy_id in aliased_curated_ids:
|
| 1693 |
+
continue
|
| 1694 |
seen_policy_ids.add(curated_policy_id)
|
| 1695 |
slug = data.get("insurer_slug", "")
|
| 1696 |
name, home = insurer_meta.get(slug, (slug, ""))
|
|
|
|
| 1741 |
maternity_coverage=_coerce_bool(data.get("maternity_coverage")),
|
| 1742 |
cashless_treatment_supported=_coerce_bool(data.get("cashless_treatment_supported")),
|
| 1743 |
room_rent_capping=data.get("room_rent_capping") if isinstance(data.get("room_rent_capping"), str) else None,
|
| 1744 |
+
# KI-142 — curated entries can ALSO be UIN-claimants when no
|
| 1745 |
+
# extracted parent owns their UIN. In that case their later
|
| 1746 |
+
# curated siblings alias onto them and surface here.
|
| 1747 |
+
aliases=sorted(parent_pkey_aliases.get(curated_policy_id, [])),
|
| 1748 |
))
|
| 1749 |
except Exception as e:
|
| 1750 |
print(f"[marketplace] skipping curated {curated_policy_id}: {type(e).__name__}: {str(e)[:120]}")
|
|
@@ -73,6 +73,32 @@ INDIC_KEYWORDS = (
|
|
| 73 |
)
|
| 74 |
|
| 75 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
def _phrase_present(phrase: str, q: str) -> bool:
|
| 77 |
"""Word-boundary phrase match. KI-023 (2026-05-14) — replaces naive
|
| 78 |
substring matching that incorrectly tripped triggers like "hi" on words
|
|
@@ -204,6 +230,7 @@ def should_route_to_fact_find(
|
|
| 204 |
profile_is_empty: bool,
|
| 205 |
in_fact_find_continuation: bool,
|
| 206 |
free_form_session: bool,
|
|
|
|
| 207 |
) -> bool:
|
| 208 |
"""Pure decision function for the fact-find routing branch.
|
| 209 |
|
|
@@ -217,6 +244,10 @@ def should_route_to_fact_find(
|
|
| 217 |
if in_fact_find_continuation:
|
| 218 |
return True
|
| 219 |
if profile_is_empty and intent in CONTEXT_DEPENDENT_INTENTS:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
return True
|
| 221 |
return False
|
| 222 |
|
|
@@ -476,6 +507,7 @@ async def handle_turn(
|
|
| 476 |
profile_is_empty=profile_is_empty,
|
| 477 |
in_fact_find_continuation=in_fact_find_continuation,
|
| 478 |
free_form_session=session.free_form_session,
|
|
|
|
| 479 |
)
|
| 480 |
|
| 481 |
if treat_as_fact_find:
|
|
|
|
| 73 |
)
|
| 74 |
|
| 75 |
|
| 76 |
+
# KI-136 (2026-05-15) — named-SKU detector. Comparisons that name >=2
|
| 77 |
+
# specific insurers/policies are policy-fact lookups, not profile-dependent
|
| 78 |
+
# recommendations — same carve-out spirit as KI-018 for qa intent.
|
| 79 |
+
_NAMED_INSURER_TOKENS = (
|
| 80 |
+
"hdfc ergo", "niva bupa", "star health", "care health", "icici lombard",
|
| 81 |
+
"bajaj allianz", "tata aig", "aditya birla", "manipal cigna", "sbi general",
|
| 82 |
+
"new india", "national insurance", "oriental insurance", "united india",
|
| 83 |
+
"reliance general", "go digit", "iffco tokio", "raheja qbe",
|
| 84 |
+
"cholamandalam", "acko", "royal sundaram",
|
| 85 |
+
# Common product names
|
| 86 |
+
"optima secure", "optima restore", "aspire", "activ assure", "activ one",
|
| 87 |
+
"activ health", "health companion", "young star", "senior citizen red carpet",
|
| 88 |
+
"arogya sanjeevani", "reassure", "health guard", "prohealth", "elevate",
|
| 89 |
+
"medicare", "criti", "saral suraksha", "comprehensive care",
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def _names_two_specific_policies(q: str) -> bool:
|
| 94 |
+
"""KI-136 — return True if the user's text mentions >=2 named SKUs/insurers,
|
| 95 |
+
indicating a policy-fact lookup rather than a profile-dependent
|
| 96 |
+
recommendation."""
|
| 97 |
+
ql = q.lower()
|
| 98 |
+
hits = sum(1 for tok in _NAMED_INSURER_TOKENS if tok in ql)
|
| 99 |
+
return hits >= 2
|
| 100 |
+
|
| 101 |
+
|
| 102 |
def _phrase_present(phrase: str, q: str) -> bool:
|
| 103 |
"""Word-boundary phrase match. KI-023 (2026-05-14) — replaces naive
|
| 104 |
substring matching that incorrectly tripped triggers like "hi" on words
|
|
|
|
| 230 |
profile_is_empty: bool,
|
| 231 |
in_fact_find_continuation: bool,
|
| 232 |
free_form_session: bool,
|
| 233 |
+
query: str = "",
|
| 234 |
) -> bool:
|
| 235 |
"""Pure decision function for the fact-find routing branch.
|
| 236 |
|
|
|
|
| 244 |
if in_fact_find_continuation:
|
| 245 |
return True
|
| 246 |
if profile_is_empty and intent in CONTEXT_DEPENDENT_INTENTS:
|
| 247 |
+
# KI-136 (2026-05-15) — exempt named-SKU comparisons. They're policy-fact
|
| 248 |
+
# lookups (route to qa), not profile-dependent recommendations.
|
| 249 |
+
if intent == "comparison" and _names_two_specific_policies(query):
|
| 250 |
+
return False
|
| 251 |
return True
|
| 252 |
return False
|
| 253 |
|
|
|
|
| 507 |
profile_is_empty=profile_is_empty,
|
| 508 |
in_fact_find_continuation=in_fact_find_continuation,
|
| 509 |
free_form_session=session.free_form_session,
|
| 510 |
+
query=user_text,
|
| 511 |
)
|
| 512 |
|
| 513 |
if treat_as_fact_find:
|
|
@@ -1951,7 +1951,11 @@ function MarketplacePanel({
|
|
| 1951 |
const insurers = Array.from(new Set(data.policies.map((p) => p.insurer_slug))).sort();
|
| 1952 |
|
| 1953 |
const filtered = data.policies.filter((p) => {
|
| 1954 |
-
if (search
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1955 |
if (insurerFilter !== "all" && p.insurer_slug !== insurerFilter) return false;
|
| 1956 |
if (grade !== "all" && p.grade !== grade) return false;
|
| 1957 |
if (p.pre_existing_disease_waiting_months && p.pre_existing_disease_waiting_months > maxPED) return false;
|
|
@@ -2321,6 +2325,11 @@ function PolicyCard({
|
|
| 2321 |
<div className="flex-1 min-w-0">
|
| 2322 |
<div className="text-xs text-[var(--muted-foreground)] truncate">{policy.insurer_name}</div>
|
| 2323 |
<div className="font-semibold text-sm truncate group-hover:text-[var(--primary)] transition">{policy.policy_name}</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2324 |
</div>
|
| 2325 |
{/* Score badge ONLY when we have a profile — otherwise CTA pill */}
|
| 2326 |
{isPersonalized ? (
|
|
|
|
| 1951 |
const insurers = Array.from(new Set(data.policies.map((p) => p.insurer_slug))).sort();
|
| 1952 |
|
| 1953 |
const filtered = data.policies.filter((p) => {
|
| 1954 |
+
if (search) {
|
| 1955 |
+
const q = search.toLowerCase();
|
| 1956 |
+
const aliasHit = (p.aliases || []).some((a) => a.toLowerCase().includes(q));
|
| 1957 |
+
if (!p.policy_name.toLowerCase().includes(q) && !p.insurer_name.toLowerCase().includes(q) && !aliasHit) return false;
|
| 1958 |
+
}
|
| 1959 |
if (insurerFilter !== "all" && p.insurer_slug !== insurerFilter) return false;
|
| 1960 |
if (grade !== "all" && p.grade !== grade) return false;
|
| 1961 |
if (p.pre_existing_disease_waiting_months && p.pre_existing_disease_waiting_months > maxPED) return false;
|
|
|
|
| 2325 |
<div className="flex-1 min-w-0">
|
| 2326 |
<div className="text-xs text-[var(--muted-foreground)] truncate">{policy.insurer_name}</div>
|
| 2327 |
<div className="font-semibold text-sm truncate group-hover:text-[var(--primary)] transition">{policy.policy_name}</div>
|
| 2328 |
+
{policy.aliases && policy.aliases.length > 0 && (
|
| 2329 |
+
<div className="text-xs text-slate-500 italic mt-0.5 truncate">
|
| 2330 |
+
Also marketed as: {policy.aliases.join(", ")}
|
| 2331 |
+
</div>
|
| 2332 |
+
)}
|
| 2333 |
</div>
|
| 2334 |
{/* Score badge ONLY when we have a profile — otherwise CTA pill */}
|
| 2335 |
{isPersonalized ? (
|
|
@@ -274,6 +274,7 @@ export type CompareResponse = {
|
|
| 274 |
export type MarketplacePolicy = {
|
| 275 |
policy_id: string;
|
| 276 |
policy_name: string;
|
|
|
|
| 277 |
insurer_slug: string;
|
| 278 |
insurer_name: string;
|
| 279 |
insurer_home_url: string;
|
|
|
|
| 274 |
export type MarketplacePolicy = {
|
| 275 |
policy_id: string;
|
| 276 |
policy_name: string;
|
| 277 |
+
aliases?: string[];
|
| 278 |
insurer_slug: string;
|
| 279 |
insurer_name: string;
|
| 280 |
insurer_home_url: string;
|