bshepp commited on
Commit ·
4ba0371
1
Parent(s): 28f1212
Add HF Space deployment (Dockerfile, nginx, startup script) and auto-detect WebSocket URL
Browse files- .dockerignore +21 -0
- Dockerfile +71 -0
- space/README.md +35 -0
- space/nginx.conf +80 -0
- space/start.sh +24 -0
- src/backend/app/config.py +6 -1
- src/frontend/src/hooks/useAgentWebSocket.ts +15 -2
.dockerignore
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__
|
| 2 |
+
*.pyc
|
| 3 |
+
.git
|
| 4 |
+
.gitignore
|
| 5 |
+
node_modules
|
| 6 |
+
.next
|
| 7 |
+
*.md
|
| 8 |
+
!src/frontend/package.json
|
| 9 |
+
!space/README.md
|
| 10 |
+
src/backend/validation/
|
| 11 |
+
src/backend/tracks/
|
| 12 |
+
src/backend/test_*.py
|
| 13 |
+
src/backend/analyze_*.py
|
| 14 |
+
src/backend/check_*.py
|
| 15 |
+
notebooks/
|
| 16 |
+
demo/
|
| 17 |
+
competition/
|
| 18 |
+
models/
|
| 19 |
+
docs/
|
| 20 |
+
*.jsonl
|
| 21 |
+
src/backend/validation/results/
|
Dockerfile
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ── Stage 1: Build the Next.js frontend ──────────────────────────
|
| 2 |
+
FROM node:20-slim AS frontend-build
|
| 3 |
+
|
| 4 |
+
WORKDIR /app/frontend
|
| 5 |
+
COPY src/frontend/package.json src/frontend/package-lock.json* ./
|
| 6 |
+
RUN npm install --frozen-lockfile 2>/dev/null || npm install
|
| 7 |
+
|
| 8 |
+
COPY src/frontend/ ./
|
| 9 |
+
|
| 10 |
+
# Build-time env: WebSocket and API go through the same origin via nginx
|
| 11 |
+
ENV NEXT_PUBLIC_WS_URL=""
|
| 12 |
+
ENV NEXT_PUBLIC_API_URL=""
|
| 13 |
+
|
| 14 |
+
RUN npm run build
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
# ── Stage 2: Production image ────────────────────────────────────
|
| 18 |
+
FROM python:3.10-slim
|
| 19 |
+
|
| 20 |
+
# System deps: nginx + node (for Next.js SSR)
|
| 21 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 22 |
+
nginx \
|
| 23 |
+
curl \
|
| 24 |
+
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
| 25 |
+
&& apt-get install -y --no-install-recommends nodejs \
|
| 26 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 27 |
+
|
| 28 |
+
# ── Python backend ───────────────────────────────────────────────
|
| 29 |
+
WORKDIR /app/backend
|
| 30 |
+
COPY src/backend/requirements.txt ./
|
| 31 |
+
|
| 32 |
+
# Install Python deps (skip torch/transformers — we use API mode only)
|
| 33 |
+
RUN pip install --no-cache-dir \
|
| 34 |
+
fastapi==0.115.0 \
|
| 35 |
+
"uvicorn[standard]==0.30.6" \
|
| 36 |
+
websockets==12.0 \
|
| 37 |
+
pydantic-settings==2.5.2 \
|
| 38 |
+
python-dotenv==1.0.1 \
|
| 39 |
+
openai==1.51.0 \
|
| 40 |
+
httpx==0.27.2 \
|
| 41 |
+
chromadb==0.5.7 \
|
| 42 |
+
sentence-transformers==3.1.1 \
|
| 43 |
+
python-multipart==0.0.10
|
| 44 |
+
|
| 45 |
+
# Copy backend source
|
| 46 |
+
COPY src/backend/app/ ./app/
|
| 47 |
+
COPY src/backend/data/ ./data/
|
| 48 |
+
|
| 49 |
+
# ── Frontend built artifacts ─────────────────────────────────────
|
| 50 |
+
WORKDIR /app/frontend
|
| 51 |
+
COPY --from=frontend-build /app/frontend/.next ./.next
|
| 52 |
+
COPY --from=frontend-build /app/frontend/node_modules ./node_modules
|
| 53 |
+
COPY --from=frontend-build /app/frontend/package.json ./
|
| 54 |
+
COPY --from=frontend-build /app/frontend/public ./public 2>/dev/null || true
|
| 55 |
+
COPY src/frontend/next.config.js ./
|
| 56 |
+
|
| 57 |
+
# ── Nginx config ─────────────────────────────────────────────────
|
| 58 |
+
COPY space/nginx.conf /etc/nginx/nginx.conf
|
| 59 |
+
|
| 60 |
+
# ── Startup script ───────────────────────────────────────────────
|
| 61 |
+
COPY space/start.sh /app/start.sh
|
| 62 |
+
RUN chmod +x /app/start.sh
|
| 63 |
+
|
| 64 |
+
# HF Spaces expects port 7860
|
| 65 |
+
EXPOSE 7860
|
| 66 |
+
|
| 67 |
+
# Health check
|
| 68 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
|
| 69 |
+
CMD curl -f http://localhost:7860/api/health || exit 1
|
| 70 |
+
|
| 71 |
+
CMD ["/app/start.sh"]
|
space/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: CDS Agent
|
| 3 |
+
emoji: 🏥
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
fullWidth: true
|
| 9 |
+
custom_domains:
|
| 10 |
+
- demo.briansheppard.com
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
# CDS Agent — Agentic Clinical Decision Support
|
| 14 |
+
|
| 15 |
+
An agentic pipeline that orchestrates **MedGemma** (HAI-DEF) across six specialized clinical reasoning steps, augmented with drug safety APIs and guideline RAG, to produce comprehensive decision support reports in real time.
|
| 16 |
+
|
| 17 |
+
## Architecture
|
| 18 |
+
|
| 19 |
+
```
|
| 20 |
+
Frontend (Next.js 14) ←WebSocket→ Backend (FastAPI)
|
| 21 |
+
│
|
| 22 |
+
Orchestrator (6-step pipeline)
|
| 23 |
+
├── Step 1: Parse Patient Data (MedGemma)
|
| 24 |
+
├── Step 2: Clinical Reasoning (MedGemma)
|
| 25 |
+
├── Step 3: Drug Interaction Check (OpenFDA + RxNorm)
|
| 26 |
+
├── Step 4: Guideline Retrieval (ChromaDB RAG)
|
| 27 |
+
├── Step 5: Conflict Detection (MedGemma)
|
| 28 |
+
└── Step 6: Synthesis (MedGemma)
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
## Links
|
| 32 |
+
|
| 33 |
+
- **Code:** [github.com/bshepp/clinical-decision-support-agent](https://github.com/bshepp/clinical-decision-support-agent)
|
| 34 |
+
- **Model:** [google/medgemma-27b-text-it](https://huggingface.co/google/medgemma-27b-text-it)
|
| 35 |
+
- **Competition:** [MedGemma Impact Challenge](https://www.kaggle.com/competitions/med-gemma-impact-challenge)
|
space/nginx.conf
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
worker_processes auto;
|
| 2 |
+
pid /tmp/nginx.pid;
|
| 3 |
+
|
| 4 |
+
events {
|
| 5 |
+
worker_connections 1024;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
http {
|
| 9 |
+
include /etc/nginx/mime.types;
|
| 10 |
+
default_type application/octet-stream;
|
| 11 |
+
|
| 12 |
+
# Temp paths writable without root
|
| 13 |
+
client_body_temp_path /tmp/client_body;
|
| 14 |
+
proxy_temp_path /tmp/proxy;
|
| 15 |
+
fastcgi_temp_path /tmp/fastcgi;
|
| 16 |
+
uwsgi_temp_path /tmp/uwsgi;
|
| 17 |
+
scgi_temp_path /tmp/scgi;
|
| 18 |
+
|
| 19 |
+
sendfile on;
|
| 20 |
+
tcp_nopush on;
|
| 21 |
+
keepalive_timeout 65;
|
| 22 |
+
|
| 23 |
+
# Increase timeouts for long-running pipeline (~5 min max)
|
| 24 |
+
proxy_connect_timeout 300s;
|
| 25 |
+
proxy_send_timeout 300s;
|
| 26 |
+
proxy_read_timeout 300s;
|
| 27 |
+
|
| 28 |
+
# Larger buffers for clinical reports
|
| 29 |
+
proxy_buffer_size 128k;
|
| 30 |
+
proxy_buffers 4 256k;
|
| 31 |
+
proxy_busy_buffers_size 256k;
|
| 32 |
+
|
| 33 |
+
map $http_upgrade $connection_upgrade {
|
| 34 |
+
default upgrade;
|
| 35 |
+
'' close;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
server {
|
| 39 |
+
listen 7860;
|
| 40 |
+
server_name _;
|
| 41 |
+
|
| 42 |
+
# ── WebSocket: /ws/* → FastAPI backend ──────────────────
|
| 43 |
+
location /ws/ {
|
| 44 |
+
proxy_pass http://127.0.0.1:8002;
|
| 45 |
+
proxy_http_version 1.1;
|
| 46 |
+
proxy_set_header Upgrade $http_upgrade;
|
| 47 |
+
proxy_set_header Connection $connection_upgrade;
|
| 48 |
+
proxy_set_header Host $host;
|
| 49 |
+
proxy_set_header X-Real-IP $remote_addr;
|
| 50 |
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
| 51 |
+
proxy_set_header X-Forwarded-Proto $scheme;
|
| 52 |
+
proxy_read_timeout 600s;
|
| 53 |
+
proxy_send_timeout 600s;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
# ── REST API: /api/* → FastAPI backend ──────────────────
|
| 57 |
+
location /api/ {
|
| 58 |
+
proxy_pass http://127.0.0.1:8002;
|
| 59 |
+
proxy_set_header Host $host;
|
| 60 |
+
proxy_set_header X-Real-IP $remote_addr;
|
| 61 |
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
| 62 |
+
proxy_set_header X-Forwarded-Proto $scheme;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
# ── Everything else → Next.js frontend ─────────────────
|
| 66 |
+
location / {
|
| 67 |
+
proxy_pass http://127.0.0.1:3000;
|
| 68 |
+
proxy_set_header Host $host;
|
| 69 |
+
proxy_set_header X-Real-IP $remote_addr;
|
| 70 |
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
| 71 |
+
proxy_set_header X-Forwarded-Proto $scheme;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
# ── Next.js static assets ──────────────────────────────
|
| 75 |
+
location /_next/ {
|
| 76 |
+
proxy_pass http://127.0.0.1:3000;
|
| 77 |
+
proxy_set_header Host $host;
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
}
|
space/start.sh
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
set -e
|
| 3 |
+
|
| 4 |
+
echo "=== CDS Agent — Starting services ==="
|
| 5 |
+
|
| 6 |
+
# ── 1. Start FastAPI backend ────────────────────────────────────
|
| 7 |
+
echo "[1/3] Starting FastAPI backend on :8002 ..."
|
| 8 |
+
cd /app/backend
|
| 9 |
+
uvicorn app.main:app \
|
| 10 |
+
--host 0.0.0.0 \
|
| 11 |
+
--port 8002 \
|
| 12 |
+
--workers 1 \
|
| 13 |
+
--timeout-keep-alive 300 \
|
| 14 |
+
&
|
| 15 |
+
|
| 16 |
+
# ── 2. Start Next.js frontend ──────────────────────────────────
|
| 17 |
+
echo "[2/3] Starting Next.js frontend on :3000 ..."
|
| 18 |
+
cd /app/frontend
|
| 19 |
+
PORT=3000 node_modules/.bin/next start -p 3000 &
|
| 20 |
+
|
| 21 |
+
# ── 3. Start nginx reverse proxy ───────────────────────────────
|
| 22 |
+
echo "[3/3] Starting nginx on :7860 ..."
|
| 23 |
+
sleep 3 # Give backend/frontend a moment to bind
|
| 24 |
+
nginx -g 'daemon off;'
|
src/backend/app/config.py
CHANGED
|
@@ -14,7 +14,12 @@ class Settings(BaseSettings):
|
|
| 14 |
debug: bool = True
|
| 15 |
|
| 16 |
# CORS
|
| 17 |
-
cors_origins: List[str] = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
# MedGemma
|
| 20 |
medgemma_model_id: str = "google/medgemma"
|
|
|
|
| 14 |
debug: bool = True
|
| 15 |
|
| 16 |
# CORS
|
| 17 |
+
cors_origins: List[str] = [
|
| 18 |
+
"http://localhost:3000",
|
| 19 |
+
"http://localhost:5173",
|
| 20 |
+
"https://demo.briansheppard.com",
|
| 21 |
+
"https://bshepp-cds-agent.hf.space",
|
| 22 |
+
]
|
| 23 |
|
| 24 |
# MedGemma
|
| 25 |
medgemma_model_id: str = "google/medgemma"
|
src/frontend/src/hooks/useAgentWebSocket.ts
CHANGED
|
@@ -26,7 +26,20 @@ interface UseAgentWebSocketReturn {
|
|
| 26 |
submitCase: (submission: CaseSubmission) => void;
|
| 27 |
}
|
| 28 |
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
export function useAgentWebSocket(): UseAgentWebSocketReturn {
|
| 32 |
const [steps, setSteps] = useState<Step[]>([]);
|
|
@@ -47,7 +60,7 @@ export function useAgentWebSocket(): UseAgentWebSocketReturn {
|
|
| 47 |
wsRef.current.close();
|
| 48 |
}
|
| 49 |
|
| 50 |
-
const ws = new WebSocket(
|
| 51 |
wsRef.current = ws;
|
| 52 |
|
| 53 |
ws.onopen = () => {
|
|
|
|
| 26 |
submitCase: (submission: CaseSubmission) => void;
|
| 27 |
}
|
| 28 |
|
| 29 |
+
function getWsUrl(): string {
|
| 30 |
+
// If explicitly set via env, use it
|
| 31 |
+
const envUrl = process.env.NEXT_PUBLIC_WS_URL;
|
| 32 |
+
if (envUrl) return envUrl;
|
| 33 |
+
|
| 34 |
+
// In browser: derive from current location (works for any deployment)
|
| 35 |
+
if (typeof window !== "undefined") {
|
| 36 |
+
const proto = window.location.protocol === "https:" ? "wss:" : "ws:";
|
| 37 |
+
return `${proto}//${window.location.host}/ws/agent`;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
// SSR fallback
|
| 41 |
+
return "ws://localhost:8002/ws/agent";
|
| 42 |
+
}
|
| 43 |
|
| 44 |
export function useAgentWebSocket(): UseAgentWebSocketReturn {
|
| 45 |
const [steps, setSteps] = useState<Step[]>([]);
|
|
|
|
| 60 |
wsRef.current.close();
|
| 61 |
}
|
| 62 |
|
| 63 |
+
const ws = new WebSocket(getWsUrl());
|
| 64 |
wsRef.current = ws;
|
| 65 |
|
| 66 |
ws.onopen = () => {
|