vivekchakraverty's picture
Load RAG index eagerly at container startup instead of on first request
a9e0a97 verified
Raw
History Blame Contribute Delete
4.55 kB
"""
Loads the prebuilt local RAG index (rag_index/, built by
scripts/build_index_from_crawler.py) and retrieves anonymized context chunks.
Never crawls or chunks anything itself.
Retrieved chunks carry no title/author/URL/domain β€” the index physically does not
store that metadata β€” so callers can only ever label them "Source 1", "Source 2", ...
"""
from __future__ import annotations
import os
from pathlib import Path
_DEFAULT_INDEX_DIR = Path(__file__).resolve().parent.parent / "rag_index"
_COLLECTION_NAME = "dm_rag"
_EMBED_MODEL = "BAAI/bge-small-en-v1.5"
_collection = None
_embedder = None
_load_attempted = False
def _maybe_download_private_dataset(target_dir: Path) -> None:
"""Optional fallback: pull the index from a private HF Dataset via secrets."""
dataset_repo_id = os.environ.get("RAG_DATASET_ID")
if not dataset_repo_id:
return
hf_token = os.environ.get("RAG_DATASET_TOKEN") or os.environ.get("HF_TOKEN")
from huggingface_hub import snapshot_download
snapshot_download(
repo_id=dataset_repo_id,
repo_type="dataset",
token=hf_token,
local_dir=str(target_dir),
)
def _load() -> None:
global _collection, _embedder, _load_attempted
if _load_attempted:
return
_load_attempted = True
index_dir = _DEFAULT_INDEX_DIR
if not (index_dir / "chroma.sqlite3").exists():
try:
_maybe_download_private_dataset(index_dir)
except Exception as exc:
print(f"[rag] private dataset fallback failed: {exc}")
if not (index_dir / "chroma.sqlite3").exists():
print("[rag] no rag_index/ found β€” RAG stage will be skipped")
return
try:
import chromadb
from sentence_transformers import SentenceTransformer
client = chromadb.PersistentClient(path=str(index_dir))
_collection = client.get_collection(_COLLECTION_NAME)
_embedder = SentenceTransformer(_EMBED_MODEL)
except Exception as exc:
print(f"[rag] failed to load index: {exc}")
_collection = None
_embedder = None
def is_available() -> bool:
_load()
return _collection is not None
def chunk_count() -> int:
_load()
return _collection.count() if _collection is not None else 0
def retrieve(query: str, top_k: int = 8, category: str | list[str] | None = None) -> list[str]:
"""Returns ["Source 1: <text>", "Source 2: <text>", ...], or [] if unavailable.
category optionally filters to a coarse topical bucket ("seo", "social_media",
"online_ads", "general") stored as chunk metadata β€” not identifying information,
so this doesn't weaken the "Source N only" anonymization guarantee. Pass a list
to match any of several categories (e.g. a task's own domain plus "general")."""
_load()
if _collection is None or _embedder is None:
return []
where = None
if isinstance(category, str):
where = {"category": category}
elif category:
where = {"category": {"$in": list(category)}}
try:
query_embedding = _embedder.encode([query], normalize_embeddings=True).tolist()
results = _collection.query(query_embeddings=query_embedding, n_results=top_k, where=where)
except Exception as exc:
print(f"[rag] retrieval failed: {exc}")
return []
documents = results.get("documents", [[]])[0]
return [f"Source {i + 1}: {doc}" for i, doc in enumerate(documents)]
def grounding_block(chunks: list[str]) -> str:
"""Formats retrieved chunks with the "guide, don't limit" framing shared by
every module that grounds against this index: retrieved context informs and
inspires, it isn't a ceiling on what the model may say."""
if not chunks:
return "(no grounding context available for this run)"
intro = (
"These excerpts are here to inform and inspire your recommendations β€” a starting "
"point, not a ceiling. Draw on your own broader expertise freely; you are not "
'limited to what appears below. When a specific recommendation *is* drawn directly '
'from one of these excerpts, cite it as "(Source N)" using the numbering below β€” '
"but only do this for material you actually took from that excerpt, and never state "
"or guess a title, author, publication name, or URL; you do not have that "
"information. Don't hedge or omit a useful recommendation just because it isn't "
"covered here."
)
return intro + "\n\n" + "\n\n".join(chunks)