"""Path-based reverse proxy for the HF Space.
Forwards requests to two backend uvicorn workers:
- ICD API on http://127.0.0.1:8000 (served at "/")
- CPT API on http://127.0.0.1:8001 (served at "/cpt/*")
Listens on 0.0.0.0:7860, which is the public port HF Spaces exposes.
"""
from __future__ import annotations
import logging
import os
from aiohttp import ClientSession, ClientTimeout, web
logger = logging.getLogger("proxy")
logging.basicConfig(
level=os.environ.get("PROXY_LOG_LEVEL", "INFO"),
format="%(asctime)s %(levelname)s proxy: %(message)s",
)
ICD_BACKEND = os.environ.get("ICD_BACKEND_URL", "http://127.0.0.1:8000")
CPT_BACKEND = os.environ.get("CPT_BACKEND_URL", "http://127.0.0.1:8001")
PROXY_PORT = int(os.environ.get("PROXY_PORT", "7860"))
PROXY_HOST = os.environ.get("PROXY_HOST", "0.0.0.0")
LANDING_HTML = """
Clinical Code Retrieval API
Clinical Code Retrieval API
Hybrid semantic + graph retrieval for ICD-10-CM
and CPT/HCPCS.
ICD-10-CM API (at root path)
- /docs — Swagger UI
- /health — service status
- POST
/retrieve —
{{"conditional_evidence": "...", "k": 5}}
- POST
/retrieve/batch — up to 64 queries
CPT/HCPCS API (at /cpt/*)
- /cpt/docs — Swagger UI
- /cpt/health — service status
- POST
/cpt/retrieve —
{{"query_text": "...", "k": 5}}
- POST
/cpt/retrieve/batch — up to 64 queries
Diagnostics
- /healthz — proxy health (proxied ping of both backends)
"""
async def _proxy_path(request: web.Request, backend: str, path_override: str | None = None) -> web.StreamResponse:
"""Forward ``request`` to ``backend``.
If ``path_override`` is given, the forwarded URL uses that path
(with the original query string preserved). Used to strip the
"/cpt" prefix before forwarding to the CPT backend.
"""
if path_override is not None:
target_url = f"{backend}{path_override}"
if request.rel_url.query_string:
target_url = f"{target_url}?{request.rel_url.query_string}"
else:
target_url = f"{backend}{request.rel_url}"
headers = {k: v for k, v in request.headers.items() if k.lower() != "host"}
body = await request.read()
timeout = ClientTimeout(total=None, sock_connect=10, sock_read=600)
async with ClientSession(timeout=timeout) as session:
async with session.request(
method=request.method,
url=target_url,
headers=headers,
data=body,
allow_redirects=False,
) as resp:
payload = await resp.read()
proxied_headers = dict(resp.headers)
for h in (
"Content-Length",
"Content-Encoding",
"Transfer-Encoding",
"Connection",
"Keep-Alive",
"Proxy-Authenticate",
"Proxy-Authorization",
"TE",
"Trailer",
"Upgrade",
):
proxied_headers.pop(h, None)
return web.Response(
body=payload,
status=resp.status,
headers=proxied_headers,
)
async def handle_icd(request: web.Request) -> web.StreamResponse:
return await _proxy_path(request, ICD_BACKEND)
async def handle_cpt(request: web.Request) -> web.StreamResponse:
# Strip the "/cpt" prefix so the CPT app sees /retrieve, /docs, etc.
stripped = request.match_info["path"]
new_path = f"/{stripped}" if stripped else "/"
return await _proxy_path(request, CPT_BACKEND, path_override=new_path)
async def handle_landing(request: web.Request) -> web.Response:
return web.Response(text=LANDING_HTML, content_type="text/html")
async def handle_healthz(request: web.Request) -> web.Response:
"""Ping both backends and return a small JSON status."""
timeout = ClientTimeout(total=5)
statuses: dict[str, str] = {}
details: dict[str, str] = {}
async with ClientSession(timeout=timeout) as session:
for name, url in (("icd", ICD_BACKEND), ("cpt", CPT_BACKEND)):
try:
async with session.get(f"{url}/health") as resp:
statuses[name] = "up" if resp.status == 200 else f"http_{resp.status}"
details[name] = f"{url}/health -> {resp.status}"
except Exception as exc: # noqa: BLE001
statuses[name] = "down"
details[name] = f"{url}/health -> {type(exc).__name__}: {exc}"
overall_ok = all(v == "up" for v in statuses.values())
return web.json_response(
{"status": "ok" if overall_ok else "degraded", "backends": statuses, "details": details},
status=200 if overall_ok else 503,
)
def build_app() -> web.Application:
app = web.Application(client_max_size=64 * 1024 * 1024) # 64 MB request body
app.router.add_get("/", handle_landing)
app.router.add_get("/healthz", handle_healthz)
async def handle_cpt_root(request: web.Request) -> web.StreamResponse:
return await _proxy_path(request, CPT_BACKEND, path_override="/")
# Bare /cpt prefix (no trailing slash) goes to the CPT root.
app.router.add_route("*", "/cpt", handle_cpt_root)
# Everything under /cpt/* goes to the CPT backend (prefix stripped).
app.router.add_route("*", "/cpt/{path:.*}", handle_cpt)
# Everything else goes to the ICD backend.
app.router.add_route("*", "/{path:.*}", handle_icd)
return app
def main() -> None:
logger.info("Starting proxy on %s:%s", PROXY_HOST, PROXY_PORT)
logger.info(" ICD backend: %s (served at /)", ICD_BACKEND)
logger.info(" CPT backend: %s (served at /cpt/*)", CPT_BACKEND)
web.run_app(build_app(), host=PROXY_HOST, port=PROXY_PORT, access_log=None)
if __name__ == "__main__":
main()