from __future__ import annotations import httpx from app.core.config import get_settings from app.services.knowledge import EvidenceState, GroundedAnswer, KnowledgeHealth, SourceCard _GROUNDED_ANSWERABILITY = {"answerable", "partially_answerable"} class RAGcoreKnowledgeProvider: """Adapter for the central RAGcore service, against its real `/v1/*` contract (see `docs/contracts/openapi.yaml` in the RAGcore checkout -- RAGcore is built and owned separately, MobilityOps only ever talks to its documented HTTP API). Authenticates as a service account via `Authorization: Bearer ` (RAGcore's session-cookie auth is for its own browser admin UI only). Any connection error, timeout, non-2xx response, or malformed body degrades to `evidence_state: "unavailable"` rather than raising -- this is the adapter that actually exercises the architecture's reliability boundary: RAGcore failure disables knowledge answers only, never fabricates an answer, never affects the rest of the app. Known gap, not fixable from this side: RAGcore's ingest pipeline currently tags every chunk's `language` payload field as `"en"` regardless of actual document language (the `/v1/uploads` contract has no per-file language field for a caller to set correctly). Filtering search/answer requests by requested UI language would therefore silently exclude genuinely-relevant nl-BE/fr-BE content, so this adapter deliberately does not filter by language -- retrieval relies on the embedding model's cross-lingual matching. """ name = "ragcore" def __init__(self) -> None: self._settings = get_settings() def _client(self) -> httpx.Client: headers = {} if self._settings.ragcore_api_token: headers["Authorization"] = f"Bearer {self._settings.ragcore_api_token}" return httpx.Client( base_url=self._settings.ragcore_base_url, headers=headers, timeout=self._settings.ragcore_http_timeout_seconds, ) def health(self, language: str = "en-GB") -> KnowledgeHealth: try: with self._client() as client: response = client.get("/health/ready") body = response.json() available = response.status_code == 200 and body.get("status") == "ok" detail = ( "RAGcore reachable and ready." if available else f"RAGcore degraded: {body.get('status', 'unknown')}" ) except (httpx.HTTPError, ValueError) as exc: available = False detail = f"RAGcore unavailable: {type(exc).__name__}: {exc}" return KnowledgeHealth( provider=self.name, available=available, detail=detail, tenant=self._settings.ragcore_tenant, workspace=self._settings.ragcore_workspace, collection=self._settings.ragcore_collection, # RAGcore's retrieval API has no corpus-size endpoint to query honestly from # here; left at 0 rather than approximated from a capped search result count. document_count=0, ) def ask(self, question: str, correlation_id: str, language: str = "en-GB") -> GroundedAnswer: unavailable = GroundedAnswer( answer="", evidence_state="unavailable", sources=[], provider=self.name, correlation_id=correlation_id, ) if not self._settings.ragcore_space_id: return unavailable try: with self._client() as client: response = client.post( "/v1/answers", json={ "query": question, "requested_space_ids": [self._settings.ragcore_space_id], }, ) if response.status_code != 200: return unavailable body = response.json() except (httpx.HTTPError, ValueError): return unavailable try: citations = {c["id"]: c for c in body.get("citations", [])} sources = [ SourceCard( document_id=str(citation["document_id"]), title=citation["title"], version=str(citation["document_version_id"]), section=citation.get("section") or "", excerpt=citation["excerpt"], ) for citation in citations.values() ] answerability = body.get("answerability", "not_answerable") is_grounded = answerability in _GROUNDED_ANSWERABILITY and sources evidence_state: EvidenceState = "grounded" if is_grounded else "insufficient" return GroundedAnswer( answer=body.get("answer", "") if evidence_state == "grounded" else "", evidence_state=evidence_state, sources=sources if evidence_state == "grounded" else [], provider=self.name, correlation_id=correlation_id, ) except (TypeError, KeyError, ValueError): return unavailable