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 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] = ["http://localhost:3000", "http://localhost:5173"]
 
 
 
 
 
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
- const WS_URL = process.env.NEXT_PUBLIC_WS_URL || "ws://localhost:8002/ws/agent";
 
 
 
 
 
 
 
 
 
 
 
 
 
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(WS_URL);
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 = () => {