Spaces:
Sleeping
Sleeping
| """Asynchronous client for the OpenCode NI Spatial API. | |
| The spec describes the spatial gateway as an out-of-band async call keyed on | |
| the NI postcode. For local development, the network round-trip is best | |
| treated as pluggable: if the endpoint is unreachable, we degrade gracefully | |
| to an empty context so the rest of the RAG pipeline can still be exercised. | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| from typing import Any | |
| import httpx | |
| from .config import OpenCodeCredentials | |
| log = logging.getLogger(__name__) | |
| _TIMEOUT_SECONDS = 5.0 | |
| class OpenCodeSpatialClient: | |
| def __init__(self, credentials: OpenCodeCredentials) -> None: | |
| self._credentials = credentials | |
| async def fetch_spatial_context(self, postcode: str) -> dict[str, Any]: | |
| """Call the spatial gateway for *postcode*. | |
| Returns a normalised payload containing any zoning/constraints metadata | |
| the gateway supplied. On any network or HTTP failure we log a warning | |
| and return an empty context — the rest of the RAG pipeline still | |
| functions without it. | |
| """ | |
| headers = { | |
| "X-Client-Id": self._credentials.client_id, | |
| "Authorization": f"Bearer {self._credentials.secret_token}", | |
| "Accept": "application/json", | |
| "User-Agent": "BelfastBuildAI/1.0", | |
| } | |
| params = {"postcode": postcode.upper().strip()} | |
| try: | |
| async with httpx.AsyncClient(timeout=_TIMEOUT_SECONDS, follow_redirects=True) as client: | |
| response = await client.get( | |
| self._credentials.api_endpoint, headers=headers, params=params | |
| ) | |
| except httpx.HTTPError as exc: | |
| log.warning("OpenCode spatial lookup failed (%s); continuing without context", exc) | |
| return {"postcode": postcode, "constraints": [], "source": "offline-fallback"} | |
| if response.status_code >= 400: | |
| log.warning( | |
| "OpenCode spatial lookup returned %s; continuing without context", | |
| response.status_code, | |
| ) | |
| return {"postcode": postcode, "constraints": [], "source": "offline-fallback"} | |
| try: | |
| payload = response.json() | |
| except ValueError: | |
| return {"postcode": postcode, "constraints": [], "source": "offline-fallback"} | |
| if not isinstance(payload, dict): | |
| return {"postcode": postcode, "constraints": [], "source": "offline-fallback"} | |
| constraints = payload.get("constraints") or payload.get("zones") or [] | |
| if not isinstance(constraints, list): | |
| constraints = [] | |
| return { | |
| "postcode": postcode, | |
| "constraints": constraints, | |
| "designations": payload.get("designations", []), | |
| "source": "opencode-ni-spatial", | |
| } | |