import asyncio import logging from functools import lru_cache from ollama import AsyncClient from app.config import get_settings logger = logging.getLogger(__name__) _BACKOFF_SECONDS: tuple[int, ...] = (1, 2, 4) class EmbeddingError(Exception): pass @lru_cache(maxsize=1) def _client() -> AsyncClient: return AsyncClient(host=get_settings().ollama_url) async def embed_texts(texts: list[str], model: str) -> list[list[float]]: """Embed each text via Ollama. Retries individual calls 3x with backoff.""" vectors: list[list[float]] = [] for text in texts: vec = await _embed_one(text, model) vectors.append(vec) return vectors async def _embed_one(text: str, model: str) -> list[float]: last_err: Exception | None = None client = _client() for attempt in range(len(_BACKOFF_SECONDS) + 1): try: response = await client.embeddings(model=model, prompt=text) return list(response["embedding"]) except Exception as exc: last_err = exc if attempt < len(_BACKOFF_SECONDS): wait = _BACKOFF_SECONDS[attempt] logger.warning( "ollama embed retry", extra={"event": "embed_retry", "attempt": attempt + 1, "wait_s": wait, "error": str(exc)}, ) await asyncio.sleep(wait) raise EmbeddingError("embed failed after retries") from last_err async def embedding_dimension(model: str) -> int: """Probe a single embedding to discover the model's vector dimension.""" vec = await _embed_one("dimension probe", model) return len(vec)