from __future__ import annotations import logging import time from typing import Any from urllib.parse import urlsplit from backend.app.core.config import settings logger = logging.getLogger(__name__) _chroma_singleton = None _chroma_last_connect_attempt_at = 0.0 _chroma_last_warning_at = 0.0 _CHROMA_CONNECT_COOLDOWN_SECONDS = 2.0 _CHROMA_CONNECT_RETRY_DELAYS = (0.0, 0.25, 0.5) class _NoOpChromaClient: """Minimal stub for environments without a running ChromaDB instance. All collection operations return empty results so the app can boot. """ def get_or_create_collection(self, name: str, **kwargs: Any) -> "_NoOpCollection": return _NoOpCollection(name) def get_collection(self, name: str, **kwargs: Any) -> "_NoOpCollection": return _NoOpCollection(name) def list_collections(self) -> list: return [] def heartbeat(self) -> int: return 0 class _NoOpCollection: def __init__(self, name: str) -> None: self.name = name def add(self, **kwargs: Any) -> None: pass def query(self, **kwargs: Any) -> dict: return {"ids": [[]], "documents": [[]], "distances": [[]], "metadatas": [[]]} def get(self, **kwargs: Any) -> dict: return {"ids": [], "documents": [], "metadatas": []} def count(self) -> int: return 0 def delete(self, **kwargs: Any) -> None: pass def upsert(self, **kwargs: Any) -> None: pass def _log_unreachable_warning() -> None: global _chroma_last_warning_at now = time.monotonic() if now - _chroma_last_warning_at < 30: return logger.warning( "ChromaDB is not reachable at %s - using a temporary no-op stub. " "Vector search and RAG will reconnect automatically once Chroma is ready.", settings.safe_chroma_endpoint, ) _chroma_last_warning_at = now def _client_kwargs() -> dict[str, Any]: if not settings.chroma_url: return {"host": settings.chroma_host, "port": settings.chroma_port} parsed = urlsplit(settings.chroma_url) host = parsed.hostname or settings.chroma_host scheme = parsed.scheme.lower() ssl = scheme == "https" port = parsed.port or (443 if ssl else 80) return {"host": host, "port": port, "ssl": ssl} def get_chroma_client(): # type: ignore[return] """Return a ChromaDB client with retry logic. The no-op stub is intentionally temporary so the app can reconnect once Chroma finishes warming up in the background. """ global _chroma_last_connect_attempt_at, _chroma_singleton if _chroma_singleton is not None: return _chroma_singleton now = time.monotonic() if now - _chroma_last_connect_attempt_at < _CHROMA_CONNECT_COOLDOWN_SECONDS: return _NoOpChromaClient() _chroma_last_connect_attempt_at = now for delay in _CHROMA_CONNECT_RETRY_DELAYS: if delay: time.sleep(delay) try: import chromadb client = chromadb.HttpClient(**_client_kwargs()) client.heartbeat() _chroma_singleton = client return client except Exception: continue _log_unreachable_warning() return _NoOpChromaClient()