Spaces:
Runtime error
Runtime error
| """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 = """<!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <title>Clinical Code Retrieval API</title> | |
| <style> | |
| body {{ font-family: -apple-system, system-ui, sans-serif; max-width: 720px; | |
| margin: 4rem auto; padding: 0 1.5rem; color: #1a1a1a; line-height: 1.5; }} | |
| h1 {{ margin-bottom: 0.25rem; }} | |
| .sub {{ color: #666; margin-top: 0; }} | |
| ul {{ padding-left: 1.2rem; }} | |
| li {{ margin: 0.4rem 0; }} | |
| code {{ background: #f3f3f3; padding: 1px 6px; border-radius: 4px; }} | |
| .pill {{ display: inline-block; background: #eef; color: #225; | |
| padding: 1px 8px; border-radius: 999px; font-size: 0.85em; }} | |
| </style> | |
| </head> | |
| <body> | |
| <h1>Clinical Code Retrieval API</h1> | |
| <p class="sub">Hybrid semantic + graph retrieval for <span class="pill">ICD-10-CM</span> | |
| and <span class="pill">CPT/HCPCS</span>.</p> | |
| <h2>ICD-10-CM API <small>(at root path)</small></h2> | |
| <ul> | |
| <li><a href="/docs">/docs</a> — Swagger UI</li> | |
| <li><a href="/health">/health</a> — service status</li> | |
| <li>POST <code>/retrieve</code> — | |
| <code>{{"conditional_evidence": "...", "k": 5}}</code></li> | |
| <li>POST <code>/retrieve/batch</code> — up to 64 queries</li> | |
| </ul> | |
| <h2>CPT/HCPCS API <small>(at <code>/cpt/*</code>)</small></h2> | |
| <ul> | |
| <li><a href="/cpt/docs">/cpt/docs</a> — Swagger UI</li> | |
| <li><a href="/cpt/health">/cpt/health</a> — service status</li> | |
| <li>POST <code>/cpt/retrieve</code> — | |
| <code>{{"query_text": "...", "k": 5}}</code></li> | |
| <li>POST <code>/cpt/retrieve/batch</code> — up to 64 queries</li> | |
| </ul> | |
| <h2>Diagnostics</h2> | |
| <ul> | |
| <li><a href="/healthz">/healthz</a> — proxy health (proxied ping of both backends)</li> | |
| </ul> | |
| </body> | |
| </html> | |
| """ | |
| 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() | |