Spaces:
Sleeping
Sleeping
Deploy: Stack A (NIM brain + Maverick judge + Sarvam voice). D-019.
Browse files- README.md +11 -0
- backend/providers/local_embeddings.py +33 -2
- docs/ingestion_policy.md +99 -0
- entrypoint.sh +22 -8
- rag/retrieve.py +54 -0
- requirements.txt +5 -1
- tools/ingest_reviews.py +182 -0
- tools/set_hf_secrets.py +12 -4
- tools/upload_all_to_dataset.py +44 -0
- tools/upload_corpus_to_dataset.py +44 -0
- tools/upload_vectors_to_dataset.py +44 -0
README.md
CHANGED
|
@@ -72,3 +72,14 @@ See [`docs/01-requirements.md` Β§7](docs/01-requirements.md).
|
|
| 72 |
- **UI**: Streamlit
|
| 73 |
|
| 74 |
Each pick is justified in [`docs/decisions.md`](docs/decisions.md).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
- **UI**: Streamlit
|
| 73 |
|
| 74 |
Each pick is justified in [`docs/decisions.md`](docs/decisions.md).
|
| 75 |
+
|
| 76 |
+
## Deploying changes
|
| 77 |
+
|
| 78 |
+
All extraction, embedding, and Chroma writes for the curated corpus run on the developer's Mac. The deployed HF Space serves pre-built indexes and does not auto-ingest (the one exception is user-uploaded PDFs, which embed on-demand into an isolated `user_uploads_quarantine` collection). Full rationale and the per-stage where-it-runs table: [`docs/ingestion_policy.md`](docs/ingestion_policy.md).
|
| 79 |
+
|
| 80 |
+
After any corpus change, run these four commands sequentially from the project root with the local `.venv` active:
|
| 81 |
+
|
| 82 |
+
- [ ] `.venv/bin/python -m rag.ingest` β rebuild the Chroma index locally
|
| 83 |
+
- [ ] `.venv/bin/python tools/upload_vectors_to_dataset.py` β push `rag/vectors/` to the HF dataset
|
| 84 |
+
- [ ] `.venv/bin/python tools/upload_extracted_to_dataset.py` β push `rag/extracted/` (only if structured JSONs changed)
|
| 85 |
+
- [ ] `.venv/bin/python tools/upload_to_hf.py` β push code to the Space (triggers a Docker rebuild that pulls the dataset)
|
backend/providers/local_embeddings.py
CHANGED
|
@@ -27,11 +27,39 @@ class LocalEmbeddings(EmbeddingsProvider):
|
|
| 27 |
device: Optional[str] = None,
|
| 28 |
):
|
| 29 |
# Lazy import so this module loads fast even if model isn't downloaded
|
|
|
|
| 30 |
from sentence_transformers import SentenceTransformer
|
| 31 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
self.model_name = model_name
|
|
|
|
| 33 |
self.model = SentenceTransformer(model_name, device=device)
|
| 34 |
self.dimension = self.model.get_sentence_embedding_dimension()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
async def embed(
|
| 37 |
self,
|
|
@@ -43,10 +71,13 @@ class LocalEmbeddings(EmbeddingsProvider):
|
|
| 43 |
# BGE recommends a small query-side instruction; not strictly required
|
| 44 |
if input_type == "query":
|
| 45 |
texts = [f"Represent this sentence for searching relevant passages: {t}" for t in texts]
|
| 46 |
-
#
|
|
|
|
|
|
|
|
|
|
| 47 |
vectors = self.model.encode(
|
| 48 |
texts,
|
| 49 |
-
batch_size=
|
| 50 |
show_progress_bar=False,
|
| 51 |
convert_to_numpy=True,
|
| 52 |
normalize_embeddings=True,
|
|
|
|
| 27 |
device: Optional[str] = None,
|
| 28 |
):
|
| 29 |
# Lazy import so this module loads fast even if model isn't downloaded
|
| 30 |
+
import os
|
| 31 |
from sentence_transformers import SentenceTransformer
|
| 32 |
|
| 33 |
+
# Device autodetect: MPS on Apple Silicon when available (2-3x faster
|
| 34 |
+
# than CPU on long chunks), CUDA if present, else CPU. Honor explicit
|
| 35 |
+
# override via constructor arg OR EMBED_DEVICE env var so HF Space
|
| 36 |
+
# (no MPS) and local Mac (with MPS) pick the right path.
|
| 37 |
+
if device is None:
|
| 38 |
+
device = os.environ.get("EMBED_DEVICE", "").strip() or None
|
| 39 |
+
if device is None:
|
| 40 |
+
try:
|
| 41 |
+
import torch
|
| 42 |
+
if torch.backends.mps.is_available():
|
| 43 |
+
device = "mps"
|
| 44 |
+
elif torch.cuda.is_available():
|
| 45 |
+
device = "cuda"
|
| 46 |
+
else:
|
| 47 |
+
device = "cpu"
|
| 48 |
+
except Exception:
|
| 49 |
+
device = "cpu"
|
| 50 |
+
|
| 51 |
self.model_name = model_name
|
| 52 |
+
self.device = device
|
| 53 |
self.model = SentenceTransformer(model_name, device=device)
|
| 54 |
self.dimension = self.model.get_sentence_embedding_dimension()
|
| 55 |
+
# Warm-up call on MPS β first kernel JIT compile is ~3-5s; doing it
|
| 56 |
+
# in __init__ rather than first encode() makes the first user request
|
| 57 |
+
# fast. CPU/CUDA skip this (their first call has no JIT penalty).
|
| 58 |
+
if device == "mps":
|
| 59 |
+
try:
|
| 60 |
+
self.model.encode(["warmup"] * 2, batch_size=2, show_progress_bar=False)
|
| 61 |
+
except Exception:
|
| 62 |
+
pass
|
| 63 |
|
| 64 |
async def embed(
|
| 65 |
self,
|
|
|
|
| 71 |
# BGE recommends a small query-side instruction; not strictly required
|
| 72 |
if input_type == "query":
|
| 73 |
texts = [f"Represent this sentence for searching relevant passages: {t}" for t in texts]
|
| 74 |
+
# Batch size scales by device: MPS / CUDA throughput benefits from
|
| 75 |
+
# bigger batches; CPU prefers smaller to avoid memory pressure on M1.
|
| 76 |
+
# 800-token chunks at batch_size=64 is ~50 MB which fits 8GB M1 fine.
|
| 77 |
+
batch = 64 if self.device in ("mps", "cuda") else 32
|
| 78 |
vectors = self.model.encode(
|
| 79 |
texts,
|
| 80 |
+
batch_size=batch,
|
| 81 |
show_progress_bar=False,
|
| 82 |
convert_to_numpy=True,
|
| 83 |
normalize_embeddings=True,
|
docs/ingestion_policy.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Ingestion Policy (2026-05-14)
|
| 2 |
+
|
| 3 |
+
**Headline:** All extraction, embedding, and Chroma writes for the curated
|
| 4 |
+
policy corpus run on the developer's Mac. The deployed Hugging Face Space
|
| 5 |
+
serves pre-built indexes only. The single exception is user-uploaded PDFs,
|
| 6 |
+
which are embedded on-demand into a SEPARATE `user_uploads_quarantine`
|
| 7 |
+
Chroma collection isolated from the main `policies` corpus.
|
| 8 |
+
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
## Where things run
|
| 12 |
+
|
| 13 |
+
| Pipeline stage | Runs on Mac (`.venv`) | Runs in Space (Docker) |
|
| 14 |
+
|---------------------------------------------|:---------------------:|:----------------------:|
|
| 15 |
+
| Source-PDF download (`rag/corpus/**`) | yes | no |
|
| 16 |
+
| Text extraction (pdfplumber β JSON) | yes | no |
|
| 17 |
+
| Chunking (800-token windows, 120 overlap) | yes | no |
|
| 18 |
+
| Embedding (BAAI/bge-small-en-v1.5) | yes | **only quarantine** |
|
| 19 |
+
| Chroma write β `policies` collection | yes | no |
|
| 20 |
+
| Chroma write β `user_uploads_quarantine` | no | yes (on-demand) |
|
| 21 |
+
| HF dataset push (`rag/vectors/**`, `rag/extracted/**`, `rag/corpus/**`) | yes | no |
|
| 22 |
+
| HF Space push (code only) | yes | no |
|
| 23 |
+
| Serving (retrieval, LLM, voice, frontend) | no | yes |
|
| 24 |
+
|
| 25 |
+
Reading the column "Runs in Space": the only embedding work the deployed
|
| 26 |
+
container ever performs is on-demand quarantine embedding of a user-uploaded
|
| 27 |
+
PDF inside `POST /api/upload-policy`. Every other embedding has been baked
|
| 28 |
+
into the image at Docker build time via `huggingface_hub.snapshot_download`
|
| 29 |
+
of the companion dataset (`rohitsar567/insurance-bot-data`).
|
| 30 |
+
|
| 31 |
+
## Local workflow
|
| 32 |
+
|
| 33 |
+
After any change to the curated corpus, run these on the Mac in order:
|
| 34 |
+
|
| 35 |
+
```bash
|
| 36 |
+
# 1. Re-build the Chroma index against the local PDFs + extracted JSONs.
|
| 37 |
+
.venv/bin/python -m rag.ingest
|
| 38 |
+
|
| 39 |
+
# 2. Push the freshly-built rag/vectors/ to the HF dataset so the next
|
| 40 |
+
# Space rebuild pulls a ready-to-serve index.
|
| 41 |
+
.venv/bin/python tools/upload_vectors_to_dataset.py
|
| 42 |
+
|
| 43 |
+
# 3. Only if structured extraction JSONs changed (rag/extracted/*.json) β
|
| 44 |
+
# e.g. you re-ran an NIM extraction batch.
|
| 45 |
+
.venv/bin/python tools/upload_extracted_to_dataset.py
|
| 46 |
+
|
| 47 |
+
# 4. Push the code to the HF Space. This triggers a Docker rebuild; the
|
| 48 |
+
# build pulls rag/vectors/ + rag/extracted/ + rag/corpus/ from the
|
| 49 |
+
# dataset (see Dockerfile `allow_patterns`).
|
| 50 |
+
.venv/bin/python tools/upload_to_hf.py
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
The four steps are sequential β step 4 must come last because the Space
|
| 54 |
+
rebuild snapshots whatever state the dataset is in at build time. If you
|
| 55 |
+
push code before the vectors are synced, the Space will boot against a
|
| 56 |
+
stale index (or, if the schema changed, fail the entrypoint validation).
|
| 57 |
+
|
| 58 |
+
## Why
|
| 59 |
+
|
| 60 |
+
Prior policy auto-ingested on Space boot if Chroma looked empty or broken.
|
| 61 |
+
That created two bad failure modes:
|
| 62 |
+
|
| 63 |
+
1. **Confusing schema breakage with normal boot.** A breaking Chroma schema
|
| 64 |
+
change (e.g. a chromadb version bump) silently triggered a 20+ minute
|
| 65 |
+
re-ingest during `APP_STARTING`. The Space logged nothing user-visible
|
| 66 |
+
while it churned, then either succeeded (slow, but fine) or failed deep
|
| 67 |
+
inside the embedder. Operators could not tell which.
|
| 68 |
+
2. **Resource cost on the wrong machine.** Free-tier Spaces have a CPU
|
| 69 |
+
budget and a 1 GB image cap. Embedding ~190 PDFs on Space CPU was
|
| 70 |
+
slower than on a developer Mac and pushed boot far past the platform's
|
| 71 |
+
health-check window.
|
| 72 |
+
|
| 73 |
+
Fail-fast is better: `entrypoint.sh` now validates Chroma is readable and
|
| 74 |
+
populated, and exits with a loud error if not. The fix is documented at
|
| 75 |
+
exit time (run `rag.ingest` locally, push vectors, redeploy). Total
|
| 76 |
+
boot-to-serving time on the Space is now seconds, not tens of minutes.
|
| 77 |
+
|
| 78 |
+
## Exception β user uploads
|
| 79 |
+
|
| 80 |
+
`POST /api/upload-policy` accepts an arbitrary PDF from the public web and
|
| 81 |
+
must embed it before the chatbot can answer questions about it. To keep
|
| 82 |
+
this off the main corpus while still allowing the feature:
|
| 83 |
+
|
| 84 |
+
- The endpoint writes into a SEPARATE Chroma collection named
|
| 85 |
+
`user_uploads_quarantine` (created lazily via
|
| 86 |
+
`rag.ingest.get_quarantine_collection`).
|
| 87 |
+
- Every chunk in that collection is tagged with the uploading session's
|
| 88 |
+
`session_id`. Retrieval against the quarantine collection is scoped to
|
| 89 |
+
that `session_id`, so one user's upload never surfaces in another
|
| 90 |
+
user's session β and never surfaces in queries against the main
|
| 91 |
+
`policies` collection at all.
|
| 92 |
+
- The quarantine collection is the ONLY place the deployed Space ever
|
| 93 |
+
writes embeddings. It does not back-propagate to the dataset, does not
|
| 94 |
+
persist across Space rebuilds (it lives only in the Space's working
|
| 95 |
+
Chroma directory), and is not part of any evaluation set.
|
| 96 |
+
|
| 97 |
+
If a user-uploaded PDF turns out to be worth curating, the operator pulls
|
| 98 |
+
it into `rag/corpus/<insurer>/` on the Mac and re-runs the local workflow
|
| 99 |
+
above. There is no in-place promotion from quarantine to `policies`.
|
entrypoint.sh
CHANGED
|
@@ -1,9 +1,16 @@
|
|
| 1 |
#!/bin/sh
|
| 2 |
-
# Container entrypoint:
|
| 3 |
-
# 1.
|
| 4 |
-
#
|
| 5 |
-
#
|
| 6 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
set -e
|
| 9 |
|
|
@@ -46,9 +53,16 @@ except Exception as e:
|
|
| 46 |
print(f'[entrypoint] Chroma load FAILED: {type(e).__name__}: {e}')
|
| 47 |
sys.exit(1)
|
| 48 |
" || (
|
| 49 |
-
echo "[entrypoint]
|
| 50 |
-
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
)
|
| 53 |
|
| 54 |
# Start the server
|
|
|
|
| 1 |
#!/bin/sh
|
| 2 |
+
# Container entrypoint (2026-05-14 policy update):
|
| 3 |
+
# 1. Validate Chroma is readable + populated.
|
| 4 |
+
# 2. If empty/broken: FAIL FAST with a loud error β DO NOT auto-ingest.
|
| 5 |
+
# Ingestion runs on the developer's local Mac (faster CPU, visible
|
| 6 |
+
# progress). The deployed Space serves pre-built indexes only.
|
| 7 |
+
# The single exception is the user_uploads_quarantine collection,
|
| 8 |
+
# which /api/upload-policy embeds on-demand per uploading session.
|
| 9 |
+
# 3. Start uvicorn.
|
| 10 |
+
#
|
| 11 |
+
# Why: previously the Space silently re-ingested for 20+ min during APP_STARTING
|
| 12 |
+
# (output piped through `tail -30` so nothing visible), making schema breakage
|
| 13 |
+
# look identical to "still booting". Fail-fast surfaces ingest gaps immediately.
|
| 14 |
|
| 15 |
set -e
|
| 16 |
|
|
|
|
| 53 |
print(f'[entrypoint] Chroma load FAILED: {type(e).__name__}: {e}')
|
| 54 |
sys.exit(1)
|
| 55 |
" || (
|
| 56 |
+
echo "[entrypoint] ============================================================"
|
| 57 |
+
echo "[entrypoint] FATAL: Chroma vector store is empty or schema-incompatible."
|
| 58 |
+
echo "[entrypoint] Auto-ingest is DISABLED (2026-05-14 policy)."
|
| 59 |
+
echo "[entrypoint]"
|
| 60 |
+
echo "[entrypoint] Fix on the developer Mac:"
|
| 61 |
+
echo "[entrypoint] .venv/bin/python -m rag.ingest"
|
| 62 |
+
echo "[entrypoint] .venv/bin/python tools/upload_extracted_to_dataset.py"
|
| 63 |
+
echo "[entrypoint] # plus sync rag/vectors/ to the dataset, then redeploy"
|
| 64 |
+
echo "[entrypoint] ============================================================"
|
| 65 |
+
exit 1
|
| 66 |
)
|
| 67 |
|
| 68 |
# Start the server
|
rag/retrieve.py
CHANGED
|
@@ -68,6 +68,24 @@ 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,
|
|
@@ -215,6 +233,42 @@ async def retrieve(
|
|
| 215 |
# Regulatory boost is additive; failure shouldn't kill the main result
|
| 216 |
pass
|
| 217 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
return out
|
| 219 |
|
| 220 |
|
|
|
|
| 68 |
return bool(_REGULATORY_TRIGGERS.search(query or ""))
|
| 69 |
|
| 70 |
|
| 71 |
+
# Review-intent triggers: when a query is about insurer reputation /
|
| 72 |
+
# claim experience / customer satisfaction, surface the review chunks
|
| 73 |
+
# (ingested via tools/ingest_reviews.py, doc_type='review').
|
| 74 |
+
_REVIEW_TRIGGERS = _re.compile(
|
| 75 |
+
r"\b(review|reviews|rating|ratings|reputation|complaint|complaints|"
|
| 76 |
+
r"trustpilot|policybazaar|insurancedekho|joinditto|claim experience|"
|
| 77 |
+
r"claim settlement|claim ratio|complaint ratio|customer service|"
|
| 78 |
+
r"good service|bad service|user experience|reddit|youtube|"
|
| 79 |
+
r"feedback|testimonial|sentiment|trust|reliable|reliability)\b",
|
| 80 |
+
flags=_re.IGNORECASE,
|
| 81 |
+
)
|
| 82 |
+
REVIEW_BOOST = 1.15 # slight boost so reviews appear in reputation Qs
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def _is_review_intent(query: str) -> bool:
|
| 86 |
+
return bool(_REVIEW_TRIGGERS.search(query or ""))
|
| 87 |
+
|
| 88 |
+
|
| 89 |
def _build_chunk(cid: str, doc: str, meta: dict, score: float) -> RetrievedChunk:
|
| 90 |
return RetrievedChunk(
|
| 91 |
chunk_id=cid,
|
|
|
|
| 233 |
# Regulatory boost is additive; failure shouldn't kill the main result
|
| 234 |
pass
|
| 235 |
|
| 236 |
+
# Review boost pass β when query asks about insurer reputation / claim
|
| 237 |
+
# experience / service quality. Filters to the relevant insurer if
|
| 238 |
+
# insurer_slugs is set; otherwise queries across all reviews.
|
| 239 |
+
if _is_review_intent(query):
|
| 240 |
+
try:
|
| 241 |
+
where_review: dict = {"doc_type": "review"}
|
| 242 |
+
if insurer_slugs:
|
| 243 |
+
# Combine doc_type + insurer filter
|
| 244 |
+
where_review = {
|
| 245 |
+
"$and": [
|
| 246 |
+
{"doc_type": "review"},
|
| 247 |
+
{"insurer_slug": {"$in": insurer_slugs}},
|
| 248 |
+
]
|
| 249 |
+
}
|
| 250 |
+
rev_res = collection.query(
|
| 251 |
+
query_embeddings=[query_vec],
|
| 252 |
+
n_results=3,
|
| 253 |
+
where=where_review,
|
| 254 |
+
)
|
| 255 |
+
if rev_res["ids"] and rev_res["ids"][0]:
|
| 256 |
+
seen = {c.chunk_id for c in out}
|
| 257 |
+
rev_chunks: list[RetrievedChunk] = []
|
| 258 |
+
for cid, doc, meta, dist in zip(
|
| 259 |
+
rev_res["ids"][0], rev_res["documents"][0],
|
| 260 |
+
rev_res["metadatas"][0], rev_res["distances"][0],
|
| 261 |
+
):
|
| 262 |
+
if cid in seen:
|
| 263 |
+
continue
|
| 264 |
+
boosted = (1.0 - dist) * REVIEW_BOOST
|
| 265 |
+
rev_chunks.append(_build_chunk(cid, doc, meta, boosted))
|
| 266 |
+
merged = sorted(out + rev_chunks, key=lambda c: c.score, reverse=True)
|
| 267 |
+
out = merged[:top_k]
|
| 268 |
+
except Exception:
|
| 269 |
+
# Review boost is additive; failure shouldn't kill the main result
|
| 270 |
+
pass
|
| 271 |
+
|
| 272 |
return out
|
| 273 |
|
| 274 |
|
requirements.txt
CHANGED
|
@@ -15,7 +15,11 @@ requests==2.34.0
|
|
| 15 |
pdfplumber==0.11.4
|
| 16 |
|
| 17 |
# RAG: vector store + structured store
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
duckdb==1.1.3
|
| 20 |
|
| 21 |
# Local embeddings (BGE small) β replaces Voyage in v1 to bypass free-tier rate limits
|
|
|
|
| 15 |
pdfplumber==0.11.4
|
| 16 |
|
| 17 |
# RAG: vector store + structured store
|
| 18 |
+
# 2026-05-14: pinned to 1.5.9 to match the local .venv version that builds
|
| 19 |
+
# rag/vectors. Schema drift between chroma versions caused KeyError('_type')
|
| 20 |
+
# in earlier deploys, wiping the index and forcing a 45-min re-ingest.
|
| 21 |
+
# Bump both local AND this pin together if upgrading.
|
| 22 |
+
chromadb==1.5.9
|
| 23 |
duckdb==1.1.3
|
| 24 |
|
| 25 |
# Local embeddings (BGE small) β replaces Voyage in v1 to bypass free-tier rate limits
|
tools/ingest_reviews.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Ingest insurer reviews into the main Chroma `policies` collection.
|
| 2 |
+
|
| 3 |
+
For each insurer review JSON in `data/reviews/`:
|
| 4 |
+
1. Render the structured review into a natural-language paragraph that
|
| 5 |
+
captures the gist of an insurer's reputation: claim settlement %,
|
| 6 |
+
complaint rate, aggregator ratings, sentiment summary, news flags.
|
| 7 |
+
2. Embed via LocalEmbeddings (BGE-small).
|
| 8 |
+
3. Write to the same Chroma `policies` collection with
|
| 9 |
+
insurer_slug = <slug>
|
| 10 |
+
policy_id = "review_<slug>"
|
| 11 |
+
doc_type = "review"
|
| 12 |
+
source_url = first verified review URL we have
|
| 13 |
+
4. Idempotent: re-running replaces the existing chunk for that insurer.
|
| 14 |
+
|
| 15 |
+
After ingest, `retrieve()` will surface these chunks for queries like
|
| 16 |
+
"is HDFC ERGO's claim experience good?" or "what do users say about
|
| 17 |
+
Care Health?" β semantic recall over reviews, citable back to source.
|
| 18 |
+
|
| 19 |
+
Run AFTER the main rag.ingest finishes (avoids Chroma write contention).
|
| 20 |
+
.venv/bin/python tools/ingest_reviews.py
|
| 21 |
+
"""
|
| 22 |
+
from __future__ import annotations
|
| 23 |
+
|
| 24 |
+
import asyncio
|
| 25 |
+
import json
|
| 26 |
+
from pathlib import Path
|
| 27 |
+
|
| 28 |
+
import chromadb
|
| 29 |
+
from chromadb.config import Settings
|
| 30 |
+
|
| 31 |
+
from backend.config import settings
|
| 32 |
+
from backend.providers.local_embeddings import LocalEmbeddings
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
ROOT = Path(__file__).resolve().parent.parent
|
| 36 |
+
REVIEWS_DIR = ROOT / "data" / "reviews"
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def review_to_paragraph(d: dict) -> str:
|
| 40 |
+
"""Render a structured review JSON into a single English paragraph
|
| 41 |
+
suitable for embedding + retrieval."""
|
| 42 |
+
parts: list[str] = []
|
| 43 |
+
name = d.get("insurer_name") or d.get("insurer_slug")
|
| 44 |
+
parts.append(f"USER REVIEWS AND REPUTATION β {name}.")
|
| 45 |
+
|
| 46 |
+
# Hard claim metrics first β most-cited numbers
|
| 47 |
+
cm = d.get("claim_metrics") or {}
|
| 48 |
+
if cm.get("claim_settlement_ratio_pct") is not None:
|
| 49 |
+
parts.append(
|
| 50 |
+
f"Claim Settlement Ratio: {cm['claim_settlement_ratio_pct']}% "
|
| 51 |
+
f"({cm.get('claim_settlement_ratio_year','recent')}, per IRDAI)."
|
| 52 |
+
)
|
| 53 |
+
if cm.get("complaints_per_10k_policies") is not None:
|
| 54 |
+
parts.append(
|
| 55 |
+
f"Complaints per 10,000 policies: {cm['complaints_per_10k_policies']} "
|
| 56 |
+
f"({cm.get('complaints_year','recent')})."
|
| 57 |
+
)
|
| 58 |
+
if cm.get("incurred_claim_ratio_pct") is not None:
|
| 59 |
+
parts.append(f"Incurred Claim Ratio: {cm['incurred_claim_ratio_pct']}%.")
|
| 60 |
+
|
| 61 |
+
# Aggregator star ratings (Policybazaar, InsuranceDekho, Ditto, etc.)
|
| 62 |
+
agg = d.get("aggregator_ratings") or {}
|
| 63 |
+
for site, info in agg.items():
|
| 64 |
+
star = (info or {}).get("avg_star")
|
| 65 |
+
if star is not None:
|
| 66 |
+
count = info.get("review_count")
|
| 67 |
+
count_part = f" ({count} reviews)" if count else ""
|
| 68 |
+
parts.append(f"{site.replace('_',' ').title()} rating: {star}/5{count_part}.")
|
| 69 |
+
|
| 70 |
+
# Trustpilot
|
| 71 |
+
tp = d.get("trustpilot") or {}
|
| 72 |
+
if tp.get("score") is not None:
|
| 73 |
+
parts.append(f"Trustpilot: {tp['score']}/5 over {tp.get('review_count','few')} reviews.")
|
| 74 |
+
|
| 75 |
+
# Reddit / Youtube sentiment summaries (text fields)
|
| 76 |
+
for key, label in [
|
| 77 |
+
("reddit_sentiment", "Reddit user sentiment"),
|
| 78 |
+
("youtube_coverage", "YouTube coverage"),
|
| 79 |
+
("in_news", "Recent news"),
|
| 80 |
+
]:
|
| 81 |
+
v = d.get(key)
|
| 82 |
+
if isinstance(v, dict):
|
| 83 |
+
summary = v.get("summary") or v.get("note")
|
| 84 |
+
if summary:
|
| 85 |
+
parts.append(f"{label}: {summary}")
|
| 86 |
+
elif isinstance(v, str) and v.strip():
|
| 87 |
+
parts.append(f"{label}: {v.strip()}")
|
| 88 |
+
|
| 89 |
+
# Aggregate score the bot has computed
|
| 90 |
+
agg_score = d.get("aggregate_score")
|
| 91 |
+
if isinstance(agg_score, dict):
|
| 92 |
+
s = agg_score.get("score")
|
| 93 |
+
rationale = agg_score.get("rationale") or ""
|
| 94 |
+
if s is not None:
|
| 95 |
+
parts.append(f"Overall trust score (internal): {s}. {rationale}")
|
| 96 |
+
|
| 97 |
+
parts.append(
|
| 98 |
+
"Use these reviews when a user asks about claim experience, "
|
| 99 |
+
"service quality, or general reputation. Reviews are dated; "
|
| 100 |
+
f"last updated {d.get('last_updated','recent')}."
|
| 101 |
+
)
|
| 102 |
+
return "\n".join(parts)
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def first_verified_url(d: dict) -> str:
|
| 106 |
+
"""Pick a single canonical URL to attach as source_url on the chunk.
|
| 107 |
+
Prefer IRDAI claim-stats page, then Policybazaar, then company."""
|
| 108 |
+
cm = d.get("claim_metrics") or {}
|
| 109 |
+
for k in ("source_irdai_url", "source_secondary_url", "source_company_url"):
|
| 110 |
+
if cm.get(k):
|
| 111 |
+
return cm[k]
|
| 112 |
+
agg = d.get("aggregator_ratings") or {}
|
| 113 |
+
for site_info in agg.values():
|
| 114 |
+
if isinstance(site_info, dict) and site_info.get("url"):
|
| 115 |
+
return site_info["url"]
|
| 116 |
+
return ""
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
async def main():
|
| 120 |
+
files = sorted(REVIEWS_DIR.glob("*.json"))
|
| 121 |
+
if not files:
|
| 122 |
+
print(f"No review JSONs found in {REVIEWS_DIR}")
|
| 123 |
+
return
|
| 124 |
+
|
| 125 |
+
client = chromadb.PersistentClient(
|
| 126 |
+
path=str(settings.VECTORS_DIR),
|
| 127 |
+
settings=Settings(anonymized_telemetry=False),
|
| 128 |
+
)
|
| 129 |
+
coll = client.get_or_create_collection(
|
| 130 |
+
name="policies",
|
| 131 |
+
metadata={"hnsw:space": "cosine"},
|
| 132 |
+
)
|
| 133 |
+
embedder = LocalEmbeddings()
|
| 134 |
+
|
| 135 |
+
ok, skipped = 0, 0
|
| 136 |
+
for f in files:
|
| 137 |
+
try:
|
| 138 |
+
d = json.load(open(f))
|
| 139 |
+
except Exception as e:
|
| 140 |
+
print(f" SKIP {f.name}: {type(e).__name__}: {e}")
|
| 141 |
+
skipped += 1
|
| 142 |
+
continue
|
| 143 |
+
slug = d.get("insurer_slug") or f.stem
|
| 144 |
+
chunk_id = f"review_{slug}"
|
| 145 |
+
text = review_to_paragraph(d)
|
| 146 |
+
if len(text) < 100:
|
| 147 |
+
print(f" SKIP {slug}: rendered text too short ({len(text)} chars)")
|
| 148 |
+
skipped += 1
|
| 149 |
+
continue
|
| 150 |
+
[vec] = await embedder.embed([text], input_type="document")
|
| 151 |
+
|
| 152 |
+
# Replace any prior chunk for this insurer (idempotent)
|
| 153 |
+
try:
|
| 154 |
+
coll.delete(where={"policy_id": chunk_id})
|
| 155 |
+
except Exception:
|
| 156 |
+
pass
|
| 157 |
+
|
| 158 |
+
coll.add(
|
| 159 |
+
ids=[chunk_id],
|
| 160 |
+
documents=[text],
|
| 161 |
+
embeddings=[vec],
|
| 162 |
+
metadatas=[{
|
| 163 |
+
"policy_id": chunk_id,
|
| 164 |
+
"insurer_slug": slug,
|
| 165 |
+
"policy_name": f"{d.get('insurer_name', slug)} reviews",
|
| 166 |
+
"doc_type": "review",
|
| 167 |
+
"source_url": first_verified_url(d),
|
| 168 |
+
"page_start": 0,
|
| 169 |
+
"page_end": 0,
|
| 170 |
+
"chunk_idx": 0,
|
| 171 |
+
"local_path": str(f),
|
| 172 |
+
}],
|
| 173 |
+
)
|
| 174 |
+
print(f" OK {slug:22s} {len(text):4d} chars -> {chunk_id}")
|
| 175 |
+
ok += 1
|
| 176 |
+
|
| 177 |
+
print()
|
| 178 |
+
print(f"Done. Embedded: {ok}, skipped: {skipped}, total: {len(files)}")
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
if __name__ == "__main__":
|
| 182 |
+
asyncio.run(main())
|
tools/set_hf_secrets.py
CHANGED
|
@@ -23,10 +23,18 @@ ROOT = Path(__file__).resolve().parent.parent
|
|
| 23 |
load_dotenv(ROOT / ".env")
|
| 24 |
|
| 25 |
REPO_ID = "rohitsar567/InsuranceBot"
|
| 26 |
-
# Active secrets
|
| 27 |
-
|
| 28 |
-
#
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
|
| 32 |
def main():
|
|
|
|
| 23 |
load_dotenv(ROOT / ".env")
|
| 24 |
|
| 25 |
REPO_ID = "rohitsar567/InsuranceBot"
|
| 26 |
+
# Active secrets read by the running code (post-D-022 + admin panel):
|
| 27 |
+
# - SARVAM / NIM = the live providers
|
| 28 |
+
# - VOYAGE = legacy (kept for back-compat with old extracted artifacts)
|
| 29 |
+
# - OPENROUTER / GROQ = optional cross-provider fallbacks (re-added 2026-05-14)
|
| 30 |
+
# - ADMIN_* = control-panel gate (IP allowlist + password)
|
| 31 |
+
SECRETS_TO_SET = [
|
| 32 |
+
"SARVAM_API_KEY", "VOYAGE_API_KEY", "NVIDIA_NIM_API_KEY",
|
| 33 |
+
"OPENROUTER_API_KEY", "GROQ_API_KEY",
|
| 34 |
+
"ADMIN_IP_ALLOWLIST", "ADMIN_PASSWORD",
|
| 35 |
+
]
|
| 36 |
+
# Truly retired providers β delete from Space to prevent confusion.
|
| 37 |
+
SECRETS_TO_DELETE = ["CEREBRAS_API_KEY", "DEEPSEEK_API_KEY"]
|
| 38 |
|
| 39 |
|
| 40 |
def main():
|
tools/upload_all_to_dataset.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""One-command sync: extracted + corpus + vectors -> insurance-bot-data dataset.
|
| 2 |
+
|
| 3 |
+
Runs the three companion uploaders sequentially. Use this after a full local
|
| 4 |
+
pipeline run on the developer Mac (extract -> corpus refresh -> rag.ingest).
|
| 5 |
+
|
| 6 |
+
.venv/bin/python tools/upload_all_to_dataset.py
|
| 7 |
+
|
| 8 |
+
Each step exits non-zero on failure and short-circuits the rest, so a partial
|
| 9 |
+
sync never silently happens. The Space's Dockerfile snapshot_downloads the
|
| 10 |
+
whole dataset at build time, so all three folders need to be in sync before
|
| 11 |
+
running `tools/upload_to_hf.py`.
|
| 12 |
+
"""
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
import sys
|
| 15 |
+
|
| 16 |
+
from tools import (
|
| 17 |
+
upload_extracted_to_dataset,
|
| 18 |
+
upload_corpus_to_dataset,
|
| 19 |
+
upload_vectors_to_dataset,
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
STEPS = [
|
| 24 |
+
("extracted JSONs", upload_extracted_to_dataset.main),
|
| 25 |
+
("corpus PDFs", upload_corpus_to_dataset.main),
|
| 26 |
+
("Chroma vectors", upload_vectors_to_dataset.main),
|
| 27 |
+
]
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def main() -> int:
|
| 31 |
+
n = len(STEPS)
|
| 32 |
+
for i, (label, fn) in enumerate(STEPS, start=1):
|
| 33 |
+
print(f"\n=== [{i}/{n}] {label} ===")
|
| 34 |
+
rc = fn() or 0
|
| 35 |
+
if rc != 0:
|
| 36 |
+
print(f"\nABORT: step {i}/{n} ({label}) returned exit {rc}. "
|
| 37 |
+
"Fix and re-run; later steps were NOT executed.")
|
| 38 |
+
return rc
|
| 39 |
+
print(f"\nAll {n} dataset folders synced. Next: `tools/upload_to_hf.py` to deploy the Space.")
|
| 40 |
+
return 0
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
if __name__ == "__main__":
|
| 44 |
+
sys.exit(main())
|
tools/upload_corpus_to_dataset.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Sync rag/corpus/ (raw policy PDFs) to insurance-bot-data HF dataset.
|
| 2 |
+
|
| 3 |
+
Run after any corpus refresh (new PDFs added or refreshed via
|
| 4 |
+
rag/download_corpus.py). The Space's Dockerfile pulls rag/corpus/** at build
|
| 5 |
+
time so the deployed container has the source PDFs available for citation +
|
| 6 |
+
ingestion pipelines.
|
| 7 |
+
"""
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
import os
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from dotenv import load_dotenv
|
| 12 |
+
from huggingface_hub import HfApi
|
| 13 |
+
|
| 14 |
+
ROOT = Path(__file__).resolve().parent.parent
|
| 15 |
+
load_dotenv(ROOT / ".env")
|
| 16 |
+
|
| 17 |
+
def main():
|
| 18 |
+
api = HfApi(token=os.environ["HF_TOKEN"])
|
| 19 |
+
corpus_dir = ROOT / "rag" / "corpus"
|
| 20 |
+
if not corpus_dir.exists() or not any(corpus_dir.iterdir()):
|
| 21 |
+
print("ERROR: rag/corpus/ is empty. Run `rag/download_corpus.py` first.")
|
| 22 |
+
return 1
|
| 23 |
+
# Print size + file count before upload
|
| 24 |
+
total = 0
|
| 25 |
+
nfiles = 0
|
| 26 |
+
for p in corpus_dir.rglob("*"):
|
| 27 |
+
if p.is_file():
|
| 28 |
+
total += p.stat().st_size
|
| 29 |
+
nfiles += 1
|
| 30 |
+
print(f"Syncing rag/corpus/ ({total/1024/1024:.1f} MB, {nfiles} files) to insurance-bot-data ...")
|
| 31 |
+
api.upload_folder(
|
| 32 |
+
folder_path=str(corpus_dir),
|
| 33 |
+
path_in_repo="rag/corpus",
|
| 34 |
+
repo_id="rohitsar567/insurance-bot-data",
|
| 35 |
+
repo_type="dataset",
|
| 36 |
+
commit_message="Sync rag/corpus/ β raw policy PDFs (Mac local refresh)",
|
| 37 |
+
ignore_patterns=["**/.DS_Store", "*.tmp"],
|
| 38 |
+
)
|
| 39 |
+
print("Done.")
|
| 40 |
+
return 0
|
| 41 |
+
|
| 42 |
+
if __name__ == "__main__":
|
| 43 |
+
import sys
|
| 44 |
+
sys.exit(main())
|
tools/upload_vectors_to_dataset.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Sync rag/vectors/ (pre-built Chroma index) to insurance-bot-data HF dataset.
|
| 2 |
+
|
| 3 |
+
Run AFTER `.venv/bin/python -m rag.ingest` on the developer Mac. The Space's
|
| 4 |
+
Dockerfile pulls rag/vectors/** at build time so the deployed container has a
|
| 5 |
+
ready-to-serve index. As of 2026-05-14 the Space no longer auto-ingests on
|
| 6 |
+
boot β see entrypoint.sh.
|
| 7 |
+
"""
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
import os
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from dotenv import load_dotenv
|
| 12 |
+
from huggingface_hub import HfApi
|
| 13 |
+
|
| 14 |
+
ROOT = Path(__file__).resolve().parent.parent
|
| 15 |
+
load_dotenv(ROOT / ".env")
|
| 16 |
+
|
| 17 |
+
def main():
|
| 18 |
+
api = HfApi(token=os.environ["HF_TOKEN"])
|
| 19 |
+
vectors_dir = ROOT / "rag" / "vectors"
|
| 20 |
+
if not vectors_dir.exists() or not any(vectors_dir.iterdir()):
|
| 21 |
+
print("ERROR: rag/vectors/ is empty. Run `.venv/bin/python -m rag.ingest` first.")
|
| 22 |
+
return 1
|
| 23 |
+
# Print size + file count before upload
|
| 24 |
+
total = 0
|
| 25 |
+
nfiles = 0
|
| 26 |
+
for p in vectors_dir.rglob("*"):
|
| 27 |
+
if p.is_file():
|
| 28 |
+
total += p.stat().st_size
|
| 29 |
+
nfiles += 1
|
| 30 |
+
print(f"Syncing rag/vectors/ ({total/1024/1024:.1f} MB, {nfiles} files) to insurance-bot-data ...")
|
| 31 |
+
api.upload_folder(
|
| 32 |
+
folder_path=str(vectors_dir),
|
| 33 |
+
path_in_repo="rag/vectors",
|
| 34 |
+
repo_id="rohitsar567/insurance-bot-data",
|
| 35 |
+
repo_type="dataset",
|
| 36 |
+
commit_message="Sync rag/vectors/ β pre-built Chroma index (Mac local ingest)",
|
| 37 |
+
ignore_patterns=["*.tmp", "**/.DS_Store"],
|
| 38 |
+
)
|
| 39 |
+
print("Done. Now run `tools/upload_to_hf.py` to deploy Space.")
|
| 40 |
+
return 0
|
| 41 |
+
|
| 42 |
+
if __name__ == "__main__":
|
| 43 |
+
import sys
|
| 44 |
+
sys.exit(main())
|