Spaces:
Sleeping
Sleeping
File size: 10,509 Bytes
5dbdf6e e2d09f9 5dbdf6e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 | """Generate the Information Source Map β a detailed catalog of what knowledge
the corpus contains, used for both reviewer-facing explainability and
faithfulness verification at runtime.
Outputs two artifacts after ingestion + extraction have run:
1. 70-docs/information_source_map.md
Human-readable per-policy catalog: insurer, policy, doc type, chunk count,
pages covered, extracted-field summary, source URL. The "what does the bot
know" reference.
2. rag/source_map.json
Machine-readable per-chunk index: {chunk_id, policy_id, page_range,
extracted_terms, primary_topics}. Used by faithfulness verifier to look up
whether a claim could plausibly trace to a chunk.
Run:
python -m rag.source_map
"""
from __future__ import annotations
import json
import re
import time
from collections import defaultdict
from pathlib import Path
import chromadb
import duckdb
from chromadb.config import Settings as ChromaSettings
from backend.config import settings
ROOT = settings.CORPUS_DIR.parent.parent
MD_OUTPUT = ROOT / "docs" / "information_source_map.md"
JSON_OUTPUT = settings.VECTORS_DIR.parent / "source_map.json"
# Topic keywords used to tag chunks with the high-level concepts they cover.
# Used for the JSON catalog + chunk-routing in retrieval.
TOPIC_KEYWORDS: dict[str, list[str]] = {
"waiting_period": ["waiting period", "pre-existing", "PED", "specific waiting", "initial waiting"],
"coverage_scope": ["covered", "covers", "inpatient", "outpatient", "OPD", "domiciliary"],
"exclusions": ["exclusion", "excluded", "not covered", "shall not pay", "permanent exclusion"],
"claim_process": ["claim", "settlement", "TAT", "turnaround time", "reimbursement", "cashless"],
"sum_insured": ["sum insured", "sum assured", "policy limit", "annual limit"],
"room_rent": ["room rent", "ICU", "private room", "single room"],
"copayment": ["co-payment", "copay", "deductible", "patient share"],
"maternity": ["maternity", "pregnancy", "delivery", "newborn"],
"ayush": ["AYUSH", "Ayurveda", "Yoga", "Unani", "Siddha", "Homeopathy"],
"critical_illness": ["critical illness", "cancer", "stroke", "heart attack", "kidney failure"],
"network": ["network hospital", "cashless", "network of hospitals", "empanelled"],
"ncb": ["no claim bonus", "NCB", "cumulative bonus", "renewal bonus"],
"restoration": ["restoration", "refill", "recharge"],
"geography": ["pan-india", "worldwide", "overseas", "geographic"],
"tax_section_80d": ["80D", "tax benefit", "tax deduction", "income tax"],
"renewal": ["renewal", "renewability", "lifelong", "guaranteed renewal"],
}
def chroma_collection():
client = chromadb.PersistentClient(
path=str(settings.VECTORS_DIR),
settings=ChromaSettings(anonymized_telemetry=False),
)
return client.get_or_create_collection(
name="policies",
metadata={"hnsw:space": "cosine"},
)
def load_extracted_policies() -> dict[str, dict]:
"""Map policy_id -> extracted JSON from DuckDB."""
out: dict[str, dict] = {}
db = settings.STRUCTURED_DB
if not db.exists():
return out
con = duckdb.connect(str(db), read_only=True)
try:
rows = con.execute("SELECT policy_id, data_json FROM policies").fetchall()
for pid, data in rows:
try:
out[pid] = json.loads(data)
except Exception:
pass
finally:
con.close()
return out
def tag_topics(text: str) -> list[str]:
"""Return the topics this chunk text covers."""
t = text.lower()
return [topic for topic, kws in TOPIC_KEYWORDS.items() if any(kw.lower() in t for kw in kws)]
def summarize_fields(p: dict) -> dict:
"""Pick high-leverage fields for the per-policy summary in the markdown."""
def get(k, default="β"):
v = p.get(k, default)
if v is None or v == "" or v == []:
return default
return v
return {
"policy_name": get("policy_name"),
"insurer_name": get("insurer_name"),
"policy_type": get("policy_type"),
"min_entry_age": get("min_entry_age"),
"max_entry_age": get("max_entry_age"),
"sum_insured_options": get("sum_insured_options"),
"pre_existing_disease_waiting_months": get("pre_existing_disease_waiting_months"),
"maternity_waiting_months": get("maternity_waiting_months"),
"ayush_coverage": get("ayush_coverage"),
"room_rent_capping": get("room_rent_capping"),
"copayment_pct": get("copayment_pct"),
"no_claim_bonus_pct": get("no_claim_bonus_pct"),
"network_hospital_count": get("network_hospital_count"),
"extraction_confidence_pct": get("extraction_confidence_pct"),
}
def build_machine_index() -> dict:
"""Per-chunk index used by faithfulness verifier."""
coll = chroma_collection()
total = coll.count()
if total == 0:
return {"total_chunks": 0, "chunks": []}
PAGE = 500
chunks_out: list[dict] = []
for offset in range(0, total, PAGE):
res = coll.get(limit=PAGE, offset=offset, include=["documents", "metadatas"])
for cid, doc, meta in zip(res["ids"], res["documents"], res["metadatas"]):
chunks_out.append({
"chunk_id": cid,
"policy_id": meta.get("policy_id", ""),
"insurer_slug": meta.get("insurer_slug", ""),
"policy_name": meta.get("policy_name", ""),
"doc_type": meta.get("doc_type", ""),
"page_start": meta.get("page_start"),
"page_end": meta.get("page_end"),
"topics": tag_topics(doc),
})
return {
"generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"total_chunks": total,
"chunks": chunks_out,
}
def build_markdown(machine: dict, extracted: dict[str, dict]) -> str:
"""Human-readable per-policy + per-topic catalog."""
# Group chunks by policy
by_policy: dict[str, list[dict]] = defaultdict(list)
for c in machine.get("chunks", []):
by_policy[c["policy_id"]].append(c)
# Per-policy summary
policies_md = []
for pid in sorted(by_policy.keys()):
chunks = by_policy[pid]
meta = chunks[0]
pages = sorted(set(c["page_start"] for c in chunks if c["page_start"]))
topic_counts: dict[str, int] = defaultdict(int)
for c in chunks:
for t in c.get("topics", []):
topic_counts[t] += 1
topic_summary = ", ".join(f"{t}({n})" for t, n in sorted(topic_counts.items(), key=lambda kv: -kv[1])[:8])
# Extracted-field summary
ext = extracted.get(pid, {})
f = summarize_fields(ext) if ext else {}
field_lines = []
for k, v in f.items():
if v not in ("β", None, ""):
field_lines.append(f" - **{k}**: {v}")
field_block = "\n".join(field_lines) if field_lines else " - (extraction not yet run for this policy)"
policies_md.append(
f"### {meta['policy_name']} \n"
f"_{meta['insurer_slug']} Β· {meta['doc_type']} Β· {len(chunks)} chunks Β· pages {min(pages) if pages else '?'}-{max(pages) if pages else '?'}_\n\n"
f"**Topics covered:** {topic_summary or '(none auto-tagged)'}\n\n"
f"**Extracted fields:**\n{field_block}\n\n"
f"`policy_id`: `{pid}`\n"
)
# Per-topic inverted index
topic_to_policies: dict[str, set[str]] = defaultdict(set)
for c in machine.get("chunks", []):
for t in c.get("topics", []):
topic_to_policies[t].add(c["policy_id"])
topic_md = []
for topic in sorted(topic_to_policies.keys()):
pols = sorted(topic_to_policies[topic])
topic_md.append(f"- **{topic}** β covered in {len(pols)} policies: {', '.join(pols[:8])}{', β¦' if len(pols) > 8 else ''}")
md = f"""# Information Source Map
| Field | Value |
| --- | --- |
| Generated | {machine.get('generated_at', 'never')} |
| Total chunks in vector store | {machine.get('total_chunks', 0)} |
| Policies indexed | {len(by_policy)} |
| Topics auto-tagged | {len(TOPIC_KEYWORDS)} |
## 0. Purpose
This document is the **authoritative catalog of what the bot can answer**. Every chunk in the Chroma vector store is summarized here, grouped by policy. For each policy, the high-value extracted fields are listed alongside.
A reviewer can use this file to answer two questions:
1. **"Could the bot know this?"** β look up the policy + topic.
2. **"Is the bot's answer plausibly grounded?"** β cross-reference the policy_id and field in the runtime audit log.
This artifact is regenerated after every ingestion or extraction run via `python -m rag.source_map`.
## 1. Topic inverted index β what is covered, where
{chr(10).join(topic_md) if topic_md else '_(no topics indexed yet β has ingestion run?)_'}
## 2. Per-policy catalog
{(chr(10) + chr(10)).join(policies_md) if policies_md else '_(no policies indexed yet)_'}
---
## 3. Machine-readable index
A JSON form of this catalog is at `rag/source_map.json` β used by the faithfulness verifier to look up whether a claim could plausibly trace to a chunk before allowing it through.
## 4. Coverage gaps (transparent)
These are areas where the corpus is thin. Bot questions on these should refuse:
- **Regulatory documents (IRDAI):** Deferred β see `decisions.md` D-017. The bot's faithfulness Gate 1 (retrieval floor) refuses these correctly.
- **Premium pricing:** Out of scope (advisor, not broker). See `decisions.md` D-007.
- **Categories beyond Health (Life, Motor, Travel):** Out of scope v1.
- **Star Health policies (11 PDFs):** Star Health's CDN actively blocks scripted downloads. Mitigation pending in v2.
"""
return md
def main():
extracted = load_extracted_policies()
machine = build_machine_index()
JSON_OUTPUT.parent.mkdir(parents=True, exist_ok=True)
JSON_OUTPUT.write_text(json.dumps(machine, indent=2))
md = build_markdown(machine, extracted)
MD_OUTPUT.parent.mkdir(parents=True, exist_ok=True)
MD_OUTPUT.write_text(md)
print(f"Wrote:")
print(f" {MD_OUTPUT.relative_to(ROOT)} ({len(md)} bytes)")
print(f" {JSON_OUTPUT.relative_to(ROOT)} ({machine.get('total_chunks', 0)} chunks)")
print(f"Policies indexed: {len({c['policy_id'] for c in machine.get('chunks', [])})}")
if __name__ == "__main__":
main()
|