Download src/fgao/rag/retrievers.py from Angshul/FGAO-API: direct link, hf CLI and curl.
- Browser
- Download file 6.03 kB
-
https://huggingface.co/spaces/Angshul/FGAO-API/resolve/main/src/fgao/rag/retrievers.py
- Command line
-
hf download hf://spaces/Angshul/FGAO-API/src/fgao/rag/retrievers.py
-
curl -L -o retrievers.py https://huggingface.co/spaces/Angshul/FGAO-API/resolve/main/src/fgao/rag/retrievers.py
6.03 kB
| """Built-in retrievers and retriever coercion.""" | |
| from __future__ import annotations | |
| import asyncio | |
| import inspect | |
| import math | |
| import re | |
| from collections import Counter | |
| from typing import Any, Callable, List, Optional, Sequence | |
| from .documents import Document, as_document, as_documents | |
| _TOK = re.compile(r"[a-z0-9]+") | |
| _STOP = set("a an the of in on at to for and or is are was were be been by with as from that this which who whom what " | |
| "when where why how did does do it its into than then there their they he she his her".split()) | |
| def tokens(text: str) -> List[str]: | |
| return [t for t in _TOK.findall(text.lower()) if t not in _STOP] | |
| class BM25Retriever: | |
| """In-memory Okapi BM25 over title + text. No dependencies.""" | |
| def __init__(self, documents: Sequence[Any], k1: float = 1.5, b: float = 0.75) -> None: | |
| self.docs = as_documents(documents) | |
| self.k1, self.b = k1, b | |
| self.tf = [Counter(tokens(d.title + " " + d.text)) for d in self.docs] | |
| self.len = [sum(t.values()) for t in self.tf] | |
| self.avg = sum(self.len) / max(len(self.len), 1) | |
| df: Counter = Counter() | |
| for t in self.tf: | |
| df.update(t.keys()) | |
| n = len(self.docs) | |
| self.idf = {w: math.log(1 + (n - c + 0.5) / (c + 0.5)) for w, c in df.items()} | |
| self._post: dict = {} | |
| for i, t in enumerate(self.tf): | |
| for w in t: | |
| self._post.setdefault(w, []).append(i) | |
| def retrieve(self, query: str, k: int) -> List[Document]: | |
| q = tokens(query) | |
| scores: dict = {} | |
| for w in q: | |
| for i in self._post.get(w, ()): | |
| f = self.tf[i][w] | |
| scores[i] = scores.get(i, 0.0) + self.idf[w] * f * (self.k1 + 1) / ( | |
| f + self.k1 * (1 - self.b + self.b * self.len[i] / self.avg)) | |
| ranked = sorted(((s, i) for i, s in scores.items() if s > 0), key=lambda x: (-x[0], x[1])) | |
| return [self.docs[i] for _, i in ranked[:k]] | |
| async def aretrieve(self, query: str, k: int) -> List[Document]: | |
| # in-memory and sub-millisecond for typical corpora: cheaper inline than a thread hop | |
| return self.retrieve(query, k) | |
| async def aretrieve(retriever: Any, query: str, k: int) -> List[Document]: | |
| """Native ``aretrieve`` when available, otherwise ``retrieve`` in a worker thread.""" | |
| fn = getattr(retriever, "aretrieve", None) | |
| if fn is not None: | |
| return list(await fn(query, k)) | |
| return list(await asyncio.to_thread(retriever.retrieve, query, k)) | |
| class FunctionRetriever: | |
| """Wrap ``fn(query, k) -> iterable of Document | str | dict`` (or an ``async def`` function).""" | |
| def __init__(self, fn: Callable[[str, int], Any]) -> None: | |
| self.fn = fn | |
| if inspect.iscoroutinefunction(fn): | |
| self.aretrieve = self._aretrieve # type: ignore[assignment] | |
| def retrieve(self, query: str, k: int) -> List[Document]: | |
| if inspect.iscoroutinefunction(self.fn): | |
| raise TypeError("this retriever is async-only; use the async API") | |
| return as_documents(self.fn(query, k))[:k] | |
| async def _aretrieve(self, query: str, k: int) -> List[Document]: | |
| return as_documents(await self.fn(query, k))[:k] | |
| class DenseRetriever: | |
| """Cosine-similarity retriever over precomputed embeddings. | |
| ``embed(texts: list[str]) -> list[list[float]]`` is any embedding function, | |
| e.g. ``fgao.integrations.huggingface.hf_embedder("BAAI/bge-small-en-v1.5")``. | |
| Uses numpy when installed, plain Python otherwise. | |
| """ | |
| def __init__(self, documents: Sequence[Any], embed: Callable[[List[str]], Any], batch_size: int = 64) -> None: | |
| self.docs = as_documents(documents) | |
| self.embed = embed | |
| vecs: List[List[float]] = [] | |
| texts = [(d.title + "\n" + d.text).strip() for d in self.docs] | |
| for i in range(0, len(texts), batch_size): | |
| vecs.extend(_rows(embed(texts[i:i + batch_size]))) | |
| self._np = _numpy() | |
| if self._np is not None: | |
| m = self._np.asarray(vecs, dtype=float) | |
| self._m = m / (self._np.linalg.norm(m, axis=1, keepdims=True) + 1e-12) | |
| else: | |
| self._m = [_unit(v) for v in vecs] | |
| def retrieve(self, query: str, k: int) -> List[Document]: | |
| (q,) = _rows(self.embed([query])) | |
| if self._np is not None: | |
| qv = self._np.asarray(q, dtype=float) | |
| sims = self._m @ (qv / (self._np.linalg.norm(qv) + 1e-12)) | |
| order = self._np.argsort(-sims, kind="stable")[:k] | |
| return [self.docs[int(i)] for i in order] | |
| qv = _unit(q) | |
| sims = [sum(a * b for a, b in zip(row, qv)) for row in self._m] | |
| order = sorted(range(len(sims)), key=lambda i: (-sims[i], i))[:k] | |
| return [self.docs[i] for i in order] | |
| def _numpy(): | |
| try: | |
| import numpy | |
| return numpy | |
| except ImportError: # pragma: no cover | |
| return None | |
| def _rows(x: Any) -> List[List[float]]: | |
| rows = x.tolist() if hasattr(x, "tolist") else list(x) | |
| return [list(map(float, r)) for r in rows] | |
| def _unit(v: Sequence[float]) -> List[float]: | |
| n = math.sqrt(sum(a * a for a in v)) or 1.0 | |
| return [a / n for a in v] | |
| def as_retriever(x: Any): | |
| """Accept a retriever, a LangChain retriever / vector store, a list of texts or documents | |
| (indexed with BM25), or ``fn(query, k)``.""" | |
| if x is None: | |
| return None | |
| if hasattr(x, "retrieve") and callable(getattr(x, "retrieve")): | |
| return x | |
| if hasattr(x, "similarity_search") or (hasattr(x, "invoke") and callable(getattr(x, "invoke"))): | |
| from ..integrations.langchain import LangChainRetriever | |
| return LangChainRetriever(x) | |
| if isinstance(x, (list, tuple)): | |
| return BM25Retriever(x) | |
| if callable(x): | |
| return FunctionRetriever(x) | |
| raise TypeError(f"cannot use {type(x).__name__} as a retriever") | |
| __all__ = ["BM25Retriever", "FunctionRetriever", "DenseRetriever", "as_retriever", "aretrieve", "as_document", "tokens"] | |