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()