Spaces:
Sleeping
Sleeping
| """ | |
| Retrieval — embed query, hit LanceDB, apply the RELEVANCE GATE (Safeguard 1). | |
| The gate is the most important defense in this whole system. It runs BEFORE | |
| the LLM is called: if the best retrieved passage scores below the threshold, | |
| we never invoke the LLM, we just return "I don't know" plus a contact pointer. | |
| That kills the worst failure mode (confident hallucinations on topics not in | |
| the corpus) at zero LLM cost. | |
| Score model: vectors are L2-normalised, so cosine similarity = dot product. | |
| LanceDB returns _distance = 1 - cosine_similarity for cosine metric, so | |
| similarity = 1 - _distance. | |
| """ | |
| from __future__ import annotations | |
| from dataclasses import dataclass | |
| from functools import lru_cache | |
| import lancedb | |
| from app.config import SETTINGS | |
| from app.providers.local_bge import LocalBGE | |
| # ── single shared embedder ──────────────────────────────────────────────────── | |
| def get_embedder() -> LocalBGE: | |
| return LocalBGE(SETTINGS.embed_model) | |
| def get_table(): | |
| db = lancedb.connect(str(SETTINGS.lancedb_path)) | |
| return db.open_table(SETTINGS.table_name) | |
| # ── result shape ────────────────────────────────────────────────────────────── | |
| class Passage: | |
| text: str | |
| source_url: str | |
| title: str | |
| heading_path: str | |
| page: int | None | |
| category: str | |
| score: float | |
| rerank_score: float | None = None | |
| class RetrievalResult: | |
| passages: list[Passage] | |
| best_score: float | |
| gate_passed: bool | |
| # ── core ────────────────────────────────────────────────────────────────────── | |
| def retrieve( | |
| query: str, | |
| top_k: int | None = None, | |
| threshold: float | None = None, | |
| ) -> RetrievalResult: | |
| """Two-stage retrieval: dense top-N → cross-encoder rerank → top-K. | |
| The relevance gate runs on the DENSE score of the best chunk (the bar for | |
| "is this in our corpus at all"), not the rerank score. Reranking is a | |
| re-sort, not a recall booster. | |
| """ | |
| top_k = top_k or SETTINGS.top_k | |
| threshold = threshold if threshold is not None else SETTINGS.relevance_threshold | |
| rerank_pool = max(SETTINGS.rerank_top_n, top_k) | |
| embedder = get_embedder() | |
| table = get_table() | |
| vec = embedder.embed_query(query).tolist() | |
| rows = ( | |
| table.search(vec) | |
| .limit(rerank_pool) | |
| .to_list() | |
| ) | |
| passages: list[Passage] = [] | |
| for r in rows: | |
| dist = float(r.get("_distance", 1.0)) | |
| # LanceDB cosine _distance is (1 - cos_sim) for L2-normalised vectors. | |
| score = max(0.0, 1.0 - dist) | |
| passages.append(Passage( | |
| text=r["text"], | |
| source_url=r["source_url"], | |
| title=r["title"], | |
| heading_path=r["heading_path"], | |
| page=None if r.get("page", -1) == -1 else r["page"], | |
| category=r["category"], | |
| score=score, | |
| )) | |
| best_dense = passages[0].score if passages else 0.0 | |
| gate_passed = best_dense >= threshold | |
| # Rerank only when the gate passes; if it failed, we won't use these chunks. | |
| if gate_passed and SETTINGS.rerank_enabled and len(passages) > 1: | |
| from app.providers.reranker import rerank as _rerank | |
| order = _rerank(query, [p.text for p in passages], top_k=top_k) | |
| reranked: list[Passage] = [] | |
| for idx, rscore in order: | |
| p = passages[idx] | |
| p.rerank_score = rscore | |
| reranked.append(p) | |
| passages = reranked | |
| else: | |
| passages = passages[:top_k] | |
| return RetrievalResult( | |
| passages=passages, | |
| best_score=best_dense, | |
| gate_passed=gate_passed, | |
| ) | |