Spaces:
Runtime error
Runtime error
| from __future__ import annotations | |
| import asyncio | |
| import logging | |
| from typing import Any | |
| import httpx | |
| import numpy as np | |
| from app.config import get_settings | |
| logger = logging.getLogger(__name__) | |
| FAISS_DIM = 384 # match Django's MiniLM output dim (NewsArticle.search_vector) | |
| _NLP_MAX_RETRIES = 3 | |
| _NLP_TIMEOUT_S = 60.0 # generous — accounts for HF Space cold start | |
| def _nlp_service_url() -> str: | |
| settings = get_settings() | |
| url = settings.nlp_service_url.rstrip("/") | |
| if not url: | |
| raise RuntimeError("NLP_SERVICE_URL not configured in settings") | |
| return url | |
| def _nlp_headers() -> dict[str, str]: | |
| settings = get_settings() | |
| if not settings.nlp_service_token: | |
| raise RuntimeError("NLP_SERVICE_TOKEN not configured in settings") | |
| return { | |
| "Authorization": f"Bearer {settings.nlp_service_token}", | |
| "Content-Type": "application/json", | |
| } | |
| async def _nlp_post(path: str, payload: dict) -> Any: | |
| """POST to the shared NLP microservice with retry on timeout/5xx (cold-start safe).""" | |
| url = f"{_nlp_service_url()}{path}" | |
| headers = _nlp_headers() | |
| last_exc: Exception | None = None | |
| async with httpx.AsyncClient(timeout=_NLP_TIMEOUT_S) as client: | |
| for attempt in range(1, _NLP_MAX_RETRIES + 1): | |
| try: | |
| resp = await client.post(url, json=payload, headers=headers) | |
| resp.raise_for_status() | |
| return resp.json() | |
| except (httpx.TimeoutException, httpx.ConnectError) as exc: | |
| last_exc = exc | |
| logger.warning( | |
| "NLP service attempt %d/%d timed out: %s", | |
| attempt, _NLP_MAX_RETRIES, exc, | |
| ) | |
| if attempt < _NLP_MAX_RETRIES: | |
| await asyncio.sleep(2 ** attempt) # 2s, 4s | |
| except httpx.HTTPStatusError as exc: | |
| if exc.response.status_code < 500: | |
| raise # 4xx — don't retry | |
| last_exc = exc | |
| logger.warning( | |
| "NLP service attempt %d/%d HTTP error: %s", | |
| attempt, _NLP_MAX_RETRIES, exc, | |
| ) | |
| if attempt < _NLP_MAX_RETRIES: | |
| await asyncio.sleep(2 ** attempt) | |
| raise RuntimeError( | |
| f"NLP microservice unavailable after {_NLP_MAX_RETRIES} attempts" | |
| ) from last_exc | |
| async def embed(texts: list[str]) -> np.ndarray: | |
| """ | |
| Embed texts via the shared remote MiniLM endpoint (same HF Space Django uses). | |
| Returns float32 array of shape (len(texts), FAISS_DIM), L2-normalised. | |
| """ | |
| result: list[list[float]] = await _nlp_post("/embed", {"texts": texts}) | |
| return np.array(result, dtype="float32") |