SPB Git forge

spb/trouve-ka

Public ★ Pinned

Trouve-KA — moteur de recherche web indépendant, Québec-first. Crawler distribué, index OpenSearch, ranking bilingue, galerie d'images. En prod : www.trouve-ka.com

40commits 1branches 0releases
9.2 MBsize
maindefault branch
23 days agolast push
Python 51.9% TypeScript 32.7% JavaScript 4.8% CSS 4.1% Shell 2.9% HTML 1.8% SQL 1.5%
3.1 KB · 77 lines python
Raw Blame History
1# Trouve-KA — client du service d'embeddings2# Author: Simon-Pierre Boucher3# Contact: contact@spboucher.ai45"""Client HTTP du service d'embeddings — conçu pour la dégradation gracieuse (§13).67Ne lève JAMAIS : toute erreur (service absent, timeout, réponse invalide)8retourne None et le chemin lexical continue seul. Timeout requête court pour9ne pas pénaliser la latence de recherche; timeout passage plus généreux pour10l'enrichissement en arrière-plan.11"""1213import httpx141516class EmbedClient:17    def __init__(18        self,19        base_url: str | None,20        *,21        query_timeout: float = 0.25,22        passage_timeout: float = 30.0,23        rerank_timeout: float = 0.6,24    ):25        self.base_url = (base_url or "").rstrip("/") or None26        self._query_client = httpx.AsyncClient(timeout=query_timeout) if self.base_url else None27        self._passage_client = httpx.AsyncClient(timeout=passage_timeout) if self.base_url else None28        self._rerank_client = httpx.AsyncClient(timeout=rerank_timeout) if self.base_url else None2930    @property31    def enabled(self) -> bool:32        return self.base_url is not None3334    async def close(self) -> None:35        for c in (self._query_client, self._passage_client, self._rerank_client):36            if c is not None:37                await c.aclose()3839    async def rerank(self, query: str, texts: list[str]) -> list[float] | None:40        """Scores cross-encoder pour le top-N, ou None (l'ordre RRF est conservé)."""41        if not self.base_url or self._rerank_client is None or not texts:42            return None43        try:44            resp = await self._rerank_client.post(45                f"{self.base_url}/rerank", json={"query": query, "texts": texts}46            )47            resp.raise_for_status()48            scores = resp.json()["scores"]49            if not isinstance(scores, list) or len(scores) != len(texts):50                return None51            return [float(s) for s in scores]52        except Exception:53            return None5455    async def embed_query(self, text: str) -> list[float] | None:56        """Vecteur de la requête utilisateur, ou None (fallback lexical)."""57        return await self._embed([text], kind="query", client=self._query_client, single=True)5859    async def embed_passages(self, texts: list[str]) -> list[list[float]] | None:60        """Vecteurs de passages (enrichissement asynchrone), ou None."""61        if not texts:62            return []63        return await self._embed(texts, kind="passage", client=self._passage_client, single=False)6465    async def _embed(self, texts: list[str], *, kind: str, client: httpx.AsyncClient | None, single: bool):66        if not self.base_url or client is None:67            return None68        try:69            resp = await client.post(f"{self.base_url}/embed", json={"texts": texts, "kind": kind})70            resp.raise_for_status()71            vectors = resp.json()["vectors"]72            if not isinstance(vectors, list) or len(vectors) != len(texts):73                return None74            return vectors[0] if single else vectors75        except Exception:76            return None77