Spaces:
Sleeping
Sleeping
| """Pluggable text embeddings. | |
| Three backends, selected by ``EMBED_BACKEND``: | |
| * ``local`` — sentence-transformers (free, no API key); the default for deploy. | |
| * ``gemini`` — Gemini embedding API (tiny install, needs ``GEMINI_API_KEY``). | |
| * ``hash`` — deterministic, dependency-free hashing embedder. Low quality but | |
| lets the whole pipeline run offline for smoke tests. | |
| All backends return L2-normalized ``float32`` vectors, so cosine similarity is a | |
| plain dot product. | |
| """ | |
| from __future__ import annotations | |
| import hashlib | |
| import re | |
| from functools import lru_cache | |
| from typing import List | |
| import numpy as np | |
| import config | |
| _TOKEN_RE = re.compile(r"[a-z0-9]+") | |
| def _normalize(mat: np.ndarray) -> np.ndarray: | |
| mat = np.asarray(mat, dtype=np.float32) | |
| if mat.ndim == 1: | |
| mat = mat[None, :] | |
| norms = np.linalg.norm(mat, axis=1, keepdims=True) | |
| norms[norms == 0] = 1.0 | |
| return mat / norms | |
| # --------------------------------------------------------------------------- # | |
| # local (sentence-transformers) | |
| # --------------------------------------------------------------------------- # | |
| def _local_model(): | |
| from sentence_transformers import SentenceTransformer | |
| return SentenceTransformer(config.EMBED_MODEL_LOCAL) | |
| def _embed_local(texts: List[str]) -> np.ndarray: | |
| model = _local_model() | |
| vecs = model.encode(texts, normalize_embeddings=True, show_progress_bar=False) | |
| return np.asarray(vecs, dtype=np.float32) | |
| # --------------------------------------------------------------------------- # | |
| # gemini | |
| # --------------------------------------------------------------------------- # | |
| def _embed_gemini(texts: List[str]) -> np.ndarray: | |
| from google import genai | |
| client = genai.Client(api_key=config.GEMINI_API_KEY) | |
| out = [] | |
| # The API accepts batches; keep them modest to stay within limits. | |
| for i in range(0, len(texts), 64): | |
| batch = texts[i : i + 64] | |
| resp = client.models.embed_content(model=config.EMBED_MODEL_GEMINI, contents=batch) | |
| out.extend([e.values for e in resp.embeddings]) | |
| return _normalize(np.asarray(out, dtype=np.float32)) | |
| # --------------------------------------------------------------------------- # | |
| # hash (offline fallback) | |
| # --------------------------------------------------------------------------- # | |
| def _embed_hash(texts: List[str]) -> np.ndarray: | |
| dim = config.EMBED_DIM_HASH | |
| out = np.zeros((len(texts), dim), dtype=np.float32) | |
| for row, text in enumerate(texts): | |
| for tok in _TOKEN_RE.findall(text.lower()): | |
| h = int(hashlib.md5(tok.encode("utf-8")).hexdigest(), 16) | |
| out[row, h % dim] += 1.0 | |
| # a second hashed slot reduces collisions | |
| out[row, (h // dim) % dim] += 0.5 | |
| return _normalize(out) | |
| _BACKENDS = {"local": _embed_local, "gemini": _embed_gemini, "hash": _embed_hash} | |
| def _resolve_backend() -> str: | |
| backend = config.EMBED_BACKEND | |
| if backend == "local": | |
| try: | |
| import sentence_transformers # noqa: F401 | |
| except Exception: | |
| print("[embeddings] sentence-transformers not installed; falling back to 'hash'.") | |
| return "hash" | |
| if backend == "gemini" and not config.GEMINI_API_KEY: | |
| print("[embeddings] GEMINI_API_KEY missing; falling back to 'hash'.") | |
| return "hash" | |
| return backend | |
| def active_backend() -> str: | |
| return _resolve_backend() | |
| def embed_texts(texts: List[str]) -> np.ndarray: | |
| if not texts: | |
| return np.zeros((0, config.EMBED_DIM_HASH), dtype=np.float32) | |
| return _BACKENDS[_resolve_backend()](list(texts)) | |
| def embed_query(text: str) -> np.ndarray: | |
| return embed_texts([text])[0] | |