belfastbuild-backend / backend /app /opencode_client.py
pritamdeka
commit
21f359b
Raw
History Blame Contribute Delete
2.84 kB
"""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",
}