Spaces:
Sleeping
Sleeping
Deploy v1 — single-Docker FastAPI + Next.js + RAG + voice + faithfulness
Browse files- rag/ingest.py +4 -0
- rag/retrieve.py +79 -25
- tools/extract_policy_text_batch2.py +76 -0
rag/ingest.py
CHANGED
|
@@ -185,6 +185,10 @@ async def ingest_one(
|
|
| 185 |
policy_name = manifest_entry.get("policy_name", pdf_path.stem)
|
| 186 |
doc_type = manifest_entry.get("doc_type", "unknown")
|
| 187 |
source_url = manifest_entry.get("url", "")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
|
| 189 |
# Skip if already ingested
|
| 190 |
existing = collection.get(where={"policy_id": policy_id}, limit=1)
|
|
|
|
| 185 |
policy_name = manifest_entry.get("policy_name", pdf_path.stem)
|
| 186 |
doc_type = manifest_entry.get("doc_type", "unknown")
|
| 187 |
source_url = manifest_entry.get("url", "")
|
| 188 |
+
# PDFs under rag/corpus/regulatory/ are IRDAI / Govt mandates. Tag them
|
| 189 |
+
# so retrieve.py can apply the regulatory-intent boost.
|
| 190 |
+
if pdf_path.parent.name == "regulatory" or insurer_slug == "regulatory":
|
| 191 |
+
doc_type = "regulatory"
|
| 192 |
|
| 193 |
# Skip if already ingested
|
| 194 |
existing = collection.get(where={"policy_id": policy_id}, limit=1)
|
rag/retrieve.py
CHANGED
|
@@ -47,6 +47,43 @@ def get_collection():
|
|
| 47 |
)
|
| 48 |
|
| 49 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
async def retrieve(
|
| 51 |
query: str,
|
| 52 |
top_k: int = settings.RAG_TOP_K,
|
|
@@ -58,6 +95,12 @@ async def retrieve(
|
|
| 58 |
|
| 59 |
Optional filters narrow retrieval to specific policies/insurers (used
|
| 60 |
by comparison and per-policy Q&A flows).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
"""
|
| 62 |
embedder = embedder or VoyageEmbeddings()
|
| 63 |
[query_vec] = await embedder.embed([query], input_type="query")
|
|
@@ -69,6 +112,8 @@ async def retrieve(
|
|
| 69 |
where["insurer_slug"] = {"$in": insurer_slugs}
|
| 70 |
|
| 71 |
collection = get_collection()
|
|
|
|
|
|
|
| 72 |
res = collection.query(
|
| 73 |
query_embeddings=[query_vec],
|
| 74 |
n_results=top_k,
|
|
@@ -76,32 +121,41 @@ async def retrieve(
|
|
| 76 |
)
|
| 77 |
|
| 78 |
out: list[RetrievedChunk] = []
|
| 79 |
-
if
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
insurer_slug=meta.get("insurer_slug", ""),
|
| 96 |
-
policy_name=meta.get("policy_name", ""),
|
| 97 |
-
doc_type=meta.get("doc_type", ""),
|
| 98 |
-
source_url=meta.get("source_url", ""),
|
| 99 |
-
page_start=int(meta.get("page_start", 0)),
|
| 100 |
-
page_end=int(meta.get("page_end", 0)),
|
| 101 |
-
chunk_idx=int(meta.get("chunk_idx", 0)),
|
| 102 |
-
score=score,
|
| 103 |
)
|
| 104 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
return out
|
| 106 |
|
| 107 |
|
|
|
|
| 47 |
)
|
| 48 |
|
| 49 |
|
| 50 |
+
import re as _re
|
| 51 |
+
|
| 52 |
+
# Queries containing these keywords trigger a parallel regulatory-only
|
| 53 |
+
# retrieval whose top results are boosted ×1.2 and merged into the final
|
| 54 |
+
# context. This ensures the brain sees IRDAI mandates whenever the user
|
| 55 |
+
# asks about compliance, legality, or what's "allowed" — even when the
|
| 56 |
+
# policy chunks would otherwise dominate raw cosine.
|
| 57 |
+
_REGULATORY_TRIGGERS = _re.compile(
|
| 58 |
+
r"\b(irdai|irda|regulation|regulator|regulatory|mandate|mandatory|"
|
| 59 |
+
r"allowed|prohibited|legal|illegal|unenforceable|cap|capped|ceiling|"
|
| 60 |
+
r"master circular|section\s+\d+|compliance|non[- ]?compliant|"
|
| 61 |
+
r"required|must|rule|statute|act|government)\b",
|
| 62 |
+
flags=_re.IGNORECASE,
|
| 63 |
+
)
|
| 64 |
+
REGULATORY_BOOST = 1.2 # multiplier applied to regulatory chunk scores
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _is_regulatory_intent(query: str) -> bool:
|
| 68 |
+
return bool(_REGULATORY_TRIGGERS.search(query or ""))
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def _build_chunk(cid: str, doc: str, meta: dict, score: float) -> RetrievedChunk:
|
| 72 |
+
return RetrievedChunk(
|
| 73 |
+
chunk_id=cid,
|
| 74 |
+
text=doc,
|
| 75 |
+
policy_id=meta.get("policy_id", ""),
|
| 76 |
+
insurer_slug=meta.get("insurer_slug", ""),
|
| 77 |
+
policy_name=meta.get("policy_name", ""),
|
| 78 |
+
doc_type=meta.get("doc_type", ""),
|
| 79 |
+
source_url=meta.get("source_url", ""),
|
| 80 |
+
page_start=int(meta.get("page_start", 0)),
|
| 81 |
+
page_end=int(meta.get("page_end", 0)),
|
| 82 |
+
chunk_idx=int(meta.get("chunk_idx", 0)),
|
| 83 |
+
score=score,
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
|
| 87 |
async def retrieve(
|
| 88 |
query: str,
|
| 89 |
top_k: int = settings.RAG_TOP_K,
|
|
|
|
| 95 |
|
| 96 |
Optional filters narrow retrieval to specific policies/insurers (used
|
| 97 |
by comparison and per-policy Q&A flows).
|
| 98 |
+
|
| 99 |
+
For queries with regulatory intent (IRDAI / mandate / allowed / etc.),
|
| 100 |
+
runs a SECOND retrieval restricted to doc_type='regulatory' chunks and
|
| 101 |
+
merges the top 3 of those (score-boosted ×1.2) into the result set.
|
| 102 |
+
This ensures the brain sees regulatory ceilings even when policy
|
| 103 |
+
chunks dominate raw cosine.
|
| 104 |
"""
|
| 105 |
embedder = embedder or VoyageEmbeddings()
|
| 106 |
[query_vec] = await embedder.embed([query], input_type="query")
|
|
|
|
| 112 |
where["insurer_slug"] = {"$in": insurer_slugs}
|
| 113 |
|
| 114 |
collection = get_collection()
|
| 115 |
+
|
| 116 |
+
# Standard retrieval
|
| 117 |
res = collection.query(
|
| 118 |
query_embeddings=[query_vec],
|
| 119 |
n_results=top_k,
|
|
|
|
| 121 |
)
|
| 122 |
|
| 123 |
out: list[RetrievedChunk] = []
|
| 124 |
+
if res["ids"] and res["ids"][0]:
|
| 125 |
+
for cid, doc, meta, dist in zip(
|
| 126 |
+
res["ids"][0], res["documents"][0],
|
| 127 |
+
res["metadatas"][0], res["distances"][0],
|
| 128 |
+
):
|
| 129 |
+
out.append(_build_chunk(cid, doc, meta, 1.0 - dist))
|
| 130 |
+
|
| 131 |
+
# Regulatory boost pass — only when the query is about IRDAI / regulations,
|
| 132 |
+
# and only when not already filtered to specific policies (otherwise the
|
| 133 |
+
# caller is asking about a specific policy, not regulations).
|
| 134 |
+
if _is_regulatory_intent(query) and not policy_ids and not insurer_slugs:
|
| 135 |
+
try:
|
| 136 |
+
reg_res = collection.query(
|
| 137 |
+
query_embeddings=[query_vec],
|
| 138 |
+
n_results=3,
|
| 139 |
+
where={"doc_type": "regulatory"},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
)
|
| 141 |
+
if reg_res["ids"] and reg_res["ids"][0]:
|
| 142 |
+
seen = {c.chunk_id for c in out}
|
| 143 |
+
reg_chunks: list[RetrievedChunk] = []
|
| 144 |
+
for cid, doc, meta, dist in zip(
|
| 145 |
+
reg_res["ids"][0], reg_res["documents"][0],
|
| 146 |
+
reg_res["metadatas"][0], reg_res["distances"][0],
|
| 147 |
+
):
|
| 148 |
+
if cid in seen:
|
| 149 |
+
continue
|
| 150 |
+
boosted = (1.0 - dist) * REGULATORY_BOOST
|
| 151 |
+
reg_chunks.append(_build_chunk(cid, doc, meta, boosted))
|
| 152 |
+
# Merge and re-sort by score, then trim back to top_k
|
| 153 |
+
merged = sorted(out + reg_chunks, key=lambda c: c.score, reverse=True)
|
| 154 |
+
out = merged[:top_k]
|
| 155 |
+
except Exception:
|
| 156 |
+
# Regulatory boost is additive; failure shouldn't kill the main result
|
| 157 |
+
pass
|
| 158 |
+
|
| 159 |
return out
|
| 160 |
|
| 161 |
|
tools/extract_policy_text_batch2.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Extract text from policy PDFs (batch 2) into the same text cache."""
|
| 2 |
+
import os, sys, json
|
| 3 |
+
import pdfplumber
|
| 4 |
+
|
| 5 |
+
BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 6 |
+
CACHE = "/tmp/claude/policy_extract/text_cache"
|
| 7 |
+
os.makedirs(CACHE, exist_ok=True)
|
| 8 |
+
|
| 9 |
+
# Batch 2: PDFs not yet covered, filtered by retail/standalone relevance
|
| 10 |
+
BATCH2 = [
|
| 11 |
+
("aditya-birla", "rag/corpus/aditya-birla/activ-health-individual__wordings.pdf"),
|
| 12 |
+
("bajaj-allianz", "rag/corpus/bajaj-allianz/comprehensive-care-plan__wordings.pdf"),
|
| 13 |
+
("bajaj-allianz", "rag/corpus/bajaj-allianz/global-health-care__wordings.pdf"),
|
| 14 |
+
("bajaj-allianz", "rag/corpus/bajaj-allianz/health-guard__wordings.pdf"),
|
| 15 |
+
("bajaj-allianz", "rag/corpus/bajaj-allianz/silver-health__cis.pdf"),
|
| 16 |
+
("bajaj-allianz", "rag/corpus/bajaj-allianz/tax-gain__cis.pdf"),
|
| 17 |
+
("care-health", "rag/corpus/care-health/care-advantage__brochure.pdf"),
|
| 18 |
+
("care-health", "rag/corpus/care-health/care-supreme-enhance__wordings.pdf"),
|
| 19 |
+
("care-health", "rag/corpus/care-health/ultimate-care__wordings.pdf"),
|
| 20 |
+
("hdfc-ergo", "rag/corpus/hdfc-ergo/energy-diabetes-hypertension__wordings.pdf"),
|
| 21 |
+
("hdfc-ergo", "rag/corpus/hdfc-ergo/my-health-medisure-prime__wordings.pdf"),
|
| 22 |
+
("hdfc-ergo", "rag/corpus/hdfc-ergo/my-health-sampoorna-suraksha__brochure.pdf"),
|
| 23 |
+
("hdfc-ergo", "rag/corpus/hdfc-ergo/my-health-suraksha__brochure.pdf"),
|
| 24 |
+
("hdfc-ergo", "rag/corpus/hdfc-ergo/my-health-women-suraksha__brochure.pdf"),
|
| 25 |
+
("hdfc-ergo", "rag/corpus/hdfc-ergo/my-optima-secure-older-variant__wordings.pdf"),
|
| 26 |
+
("hdfc-ergo", "rag/corpus/hdfc-ergo/optima-enhance__wordings.pdf"),
|
| 27 |
+
("hdfc-ergo", "rag/corpus/hdfc-ergo/optima-plus__wordings.pdf"),
|
| 28 |
+
("hdfc-ergo", "rag/corpus/hdfc-ergo/total-health-plan__wordings.pdf"),
|
| 29 |
+
("icici-lombard", "rag/corpus/icici-lombard/arogya-sanjeevani__wordings.pdf"),
|
| 30 |
+
("icici-lombard", "rag/corpus/icici-lombard/complete-health-insurance-umbrella__wordings.pdf"),
|
| 31 |
+
("icici-lombard", "rag/corpus/icici-lombard/health-advantedge__wordings.pdf"),
|
| 32 |
+
("icici-lombard", "rag/corpus/icici-lombard/health-booster-top-up__wordings.pdf"),
|
| 33 |
+
("icici-lombard", "rag/corpus/icici-lombard/health-elite-plus__wordings.pdf"),
|
| 34 |
+
("manipalcigna", "rag/corpus/manipalcigna/prohealth-select__wordings.pdf"),
|
| 35 |
+
("manipalcigna", "rag/corpus/manipalcigna/sarvah-param__wordings.pdf"),
|
| 36 |
+
("new-india", "rag/corpus/new-india/asha-kiran-policy__brochure.pdf"),
|
| 37 |
+
("new-india", "rag/corpus/new-india/janata-mediclaim-policy__wordings.pdf"),
|
| 38 |
+
("new-india", "rag/corpus/new-india/new-india-mediclaim-policy__wordings.pdf"),
|
| 39 |
+
("new-india", "rag/corpus/new-india/universal-health-insurance__wordings.pdf"),
|
| 40 |
+
("new-india", "rag/corpus/new-india/yuva-bharat-health-policy__wordings.pdf"),
|
| 41 |
+
("niva-bupa", "rag/corpus/niva-bupa/aspire__wordings.pdf"),
|
| 42 |
+
("niva-bupa", "rag/corpus/niva-bupa/health-plus-top-up__wordings.pdf"),
|
| 43 |
+
("niva-bupa", "rag/corpus/niva-bupa/health-premia__wordings.pdf"),
|
| 44 |
+
("niva-bupa", "rag/corpus/niva-bupa/reassure-3-0__wordings.pdf"),
|
| 45 |
+
("niva-bupa", "rag/corpus/niva-bupa/rise__wordings.pdf"),
|
| 46 |
+
("niva-bupa", "rag/corpus/niva-bupa/saral-suraksha-bima__wordings.pdf"),
|
| 47 |
+
("star-health", "rag/corpus/star-health/health-premier__wordings.pdf"),
|
| 48 |
+
("star-health", "rag/corpus/star-health/senior-citizens-red-carpet__brochure.pdf"),
|
| 49 |
+
("star-health", "rag/corpus/star-health/star-assure__wordings.pdf"),
|
| 50 |
+
("star-health", "rag/corpus/star-health/star-cardiac-care-platinum__wordings.pdf"),
|
| 51 |
+
("star-health", "rag/corpus/star-health/star-cardiac-care__wordings.pdf"),
|
| 52 |
+
("tata-aig", "rag/corpus/tata-aig/medicare-lite__cis.pdf"),
|
| 53 |
+
("tata-aig", "rag/corpus/tata-aig/medicare-select__brochure.pdf"),
|
| 54 |
+
]
|
| 55 |
+
|
| 56 |
+
for insurer, rel in BATCH2:
|
| 57 |
+
src = os.path.join(BASE, rel)
|
| 58 |
+
name = os.path.basename(rel).replace(".pdf", ".txt")
|
| 59 |
+
dst = os.path.join(CACHE, f"{insurer}__{name}")
|
| 60 |
+
if os.path.exists(dst):
|
| 61 |
+
print(f"skip {os.path.basename(dst)}")
|
| 62 |
+
continue
|
| 63 |
+
if not os.path.exists(src):
|
| 64 |
+
print(f"MISSING {src}")
|
| 65 |
+
continue
|
| 66 |
+
try:
|
| 67 |
+
with pdfplumber.open(src) as pdf:
|
| 68 |
+
# Extract up to 30 pages worth (most policy details in first ~20)
|
| 69 |
+
pages = pdf.pages[:30]
|
| 70 |
+
text = "\n".join((p.extract_text() or "") for p in pages)
|
| 71 |
+
with open(dst, "w", encoding="utf-8") as f:
|
| 72 |
+
f.write(text)
|
| 73 |
+
print(f"OK {os.path.basename(dst)} ({len(text)} chars, {len(pages)} pages)")
|
| 74 |
+
except Exception as e:
|
| 75 |
+
print(f"ERR {src}: {e}")
|
| 76 |
+
print("Done.")
|