from __future__ import annotations import httpx from app.core.config import get_settings from app.services.knowledge import GroundedAnswer, KnowledgeHealth, SourceCard class RAGcoreKnowledgeProvider: """Adapter for the central RAGcore service. RAGcore is built and owned separately (see contracts/ragcore-contract-assumptions.md). No live RAGcore instance was reachable during this build, so the exact request/response shape below is a best-effort guess at a REST contract; any failure (connection, timeout, malformed response) degrades to `unavailable` rather than raising, per the architecture's reliability boundary: RAGcore failure disables knowledge answers only, never the rest of the app, and never fabricates an answer. """ 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") response.raise_for_status() available = True detail = "RAGcore reachable." except httpx.HTTPError 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, document_count=0, ) def ask(self, question: str, correlation_id: str, language: str = "en-GB") -> GroundedAnswer: try: with self._client() as client: response = client.post( "/api/v1/ask", json={ "tenant": self._settings.ragcore_tenant, "workspace": self._settings.ragcore_workspace, "collection": self._settings.ragcore_collection, "question": question, "correlation_id": correlation_id, "language": language, }, ) response.raise_for_status() body = response.json() except (httpx.HTTPError, ValueError): return GroundedAnswer( answer="", evidence_state="unavailable", sources=[], provider=self.name, correlation_id=correlation_id, ) try: sources = [SourceCard(**s) for s in body.get("sources", [])] evidence_state = body.get("evidence_state", "insufficient") if evidence_state not in ("grounded", "insufficient", "unavailable"): evidence_state = "insufficient" return GroundedAnswer( answer=body.get("answer", ""), evidence_state=evidence_state, sources=sources, provider=self.name, correlation_id=correlation_id, ) except (TypeError, ValueError): return GroundedAnswer( answer="", evidence_state="unavailable", sources=[], provider=self.name, correlation_id=correlation_id, )