Spaces:
Sleeping
Sleeping
| """RAG benchmark store: ChromaDB (in-memory) + BGE-small embeddings. | |
| Loads `app/benchmarks/seed_clauses.json` at warmup, embeds with sentence-transformers | |
| BGE-small, and exposes `fetch_benchmark_matches()` returning BenchmarkMatch objects | |
| that carry semantic similarity scores (0-1). | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import logging | |
| import threading | |
| from pathlib import Path | |
| from app.schemas import BenchmarkMatch, BenchmarkRef, ClauseType | |
| logger = logging.getLogger(__name__) | |
| _BENCHMARK_PATH = Path(__file__).parents[1] / "benchmarks" / "seed_clauses.json" | |
| _BGE_MODEL = "BAAI/bge-small-en-v1.5" | |
| _COLLECTION_NAME = "benchmark_clauses" | |
| _warmup_lock = threading.Lock() | |
| _warmed = False | |
| _collection = None | |
| _embedder = None | |
| _by_type: dict[str, list[BenchmarkRef]] = {} | |
| def _load_seed() -> list[BenchmarkRef]: | |
| if not _BENCHMARK_PATH.exists(): | |
| logger.warning("seed clauses file not found at %s", _BENCHMARK_PATH) | |
| return [] | |
| raw = json.loads(_BENCHMARK_PATH.read_text(encoding="utf-8")) | |
| return [BenchmarkRef(**item) for item in raw] | |
| def warmup() -> None: | |
| global _warmed, _collection, _embedder, _by_type | |
| with _warmup_lock: | |
| if _warmed: | |
| return | |
| seed = _load_seed() | |
| for ref in seed: | |
| _by_type.setdefault(ref.clause_type, []).append(ref) | |
| logger.info("Loaded %d benchmark clauses into type index", len(seed)) | |
| try: | |
| import chromadb | |
| from sentence_transformers import SentenceTransformer | |
| except Exception as exc: | |
| logger.warning( | |
| "Chroma or sentence-transformers unavailable (%s); type-only retrieval active", | |
| exc, | |
| ) | |
| _warmed = True | |
| return | |
| try: | |
| _embedder = SentenceTransformer(_BGE_MODEL) | |
| client = chromadb.EphemeralClient() | |
| try: | |
| client.delete_collection(_COLLECTION_NAME) | |
| except Exception: | |
| pass | |
| _collection = client.create_collection(_COLLECTION_NAME) | |
| if seed: | |
| ids = [ref.id for ref in seed] | |
| docs = [ref.text for ref in seed] | |
| metas = [ | |
| {"clause_type": ref.clause_type, "doc_type": ref.doc_type} | |
| for ref in seed | |
| ] | |
| embeddings = _embedder.encode(docs, normalize_embeddings=True).tolist() | |
| _collection.add(ids=ids, documents=docs, embeddings=embeddings, metadatas=metas) | |
| logger.info( | |
| "ChromaDB vector store ready — %d clauses indexed with BGE-small embeddings", | |
| len(seed), | |
| ) | |
| except Exception as exc: | |
| logger.exception("Chroma initialization failed (%s); falling back to type-only", exc) | |
| _collection = None | |
| _embedder = None | |
| _warmed = True | |
| def fetch_benchmark_matches( | |
| clause_text: str, clause_type: ClauseType, k: int = 2 | |
| ) -> list[BenchmarkMatch]: | |
| """Return up to k BenchmarkMatch objects ranked by semantic similarity.""" | |
| same_type = _by_type.get(clause_type, []) | |
| if not same_type: | |
| return [] | |
| if _collection is None or _embedder is None: | |
| # Fallback: return type-only matches with zero similarity | |
| return [BenchmarkMatch(ref=ref, similarity=0.0) for ref in same_type[:k]] | |
| try: | |
| query_vec = _embedder.encode([clause_text], normalize_embeddings=True).tolist() | |
| n = min(max(k * 3, 6), len(same_type)) | |
| result = _collection.query( | |
| query_embeddings=query_vec, | |
| n_results=n, | |
| where={"clause_type": clause_type}, | |
| ) | |
| hit_ids = result.get("ids", [[]])[0] | |
| distances = result.get("distances", [[]])[0] | |
| by_id: dict[str, BenchmarkRef] = {ref.id: ref for ref in same_type} | |
| matches: list[BenchmarkMatch] = [] | |
| for hit_id, dist in zip(hit_ids, distances): | |
| if hit_id in by_id: | |
| # ChromaDB cosine distance: 0=identical, 2=opposite → similarity = 1 - dist/2 | |
| sim = max(0.0, min(1.0, 1.0 - dist / 2.0)) | |
| matches.append(BenchmarkMatch(ref=by_id[hit_id], similarity=sim)) | |
| return matches[:k] if matches else [BenchmarkMatch(ref=r, similarity=0.0) for r in same_type[:k]] | |
| except Exception as exc: | |
| logger.warning("semantic retrieval failed (%s); returning type-only", exc) | |
| return [BenchmarkMatch(ref=ref, similarity=0.0) for ref in same_type[:k]] | |
| # Legacy shim for any callers that still use the old signature | |
| def fetch_benchmarks_by_text(clause_text: str, clause_type: ClauseType, k: int = 2): | |
| return [m.ref for m in fetch_benchmark_matches(clause_text, clause_type, k)] | |
| def fetch_benchmarks(clause_type: ClauseType, k: int = 2): | |
| refs = _by_type.get(clause_type, []) | |
| return refs[:k] | |