NathanPereira commited on
Commit
9ccd45c
·
verified ·
1 Parent(s): d1e5701

Upload 7 files

Browse files
Files changed (7) hide show
  1. .dockerignore +9 -0
  2. Dockerfile +50 -0
  3. README.md +37 -7
  4. backend/ingest.py +27 -0
  5. backend/main.py +82 -0
  6. requirements.txt +25 -0
  7. startup.sh +17 -0
.dockerignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ frontend/node_modules
2
+ frontend/dist
3
+ **/__pycache__
4
+ *.pyc
5
+ .git
6
+ .env
7
+ *.db
8
+ qdrant_data/
9
+ .venv
Dockerfile ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ---- Frontend build stage (React -> static files) ----
2
+ FROM node:20-slim AS frontend
3
+ WORKDIR /frontend
4
+ # Copy only manifest first for layer caching
5
+ COPY frontend/package*.json ./
6
+ RUN npm ci
7
+ COPY frontend/ ./
8
+ # Produces /frontend/dist (Vite) — change to build/ if you use CRA
9
+ RUN npm run build
10
+
11
+ # ---- Runtime stage (CUDA for imaging models on T4) ----
12
+ FROM nvidia/cuda:12.4.1-cudnn-runtime-ubuntu22.04
13
+
14
+ # System deps as root BEFORE creating the non-root user
15
+ RUN apt-get update && apt-get install -y --no-install-recommends \
16
+ python3.11 python3-pip python3.11-venv \
17
+ build-essential curl git ffmpeg libgl1 libglib2.0-0 \
18
+ && rm -rf /var/lib/apt/lists/*
19
+
20
+ RUN ln -sf /usr/bin/python3.11 /usr/bin/python
21
+
22
+ # Spaces runs your container as UID 1000 — create the matching user
23
+ RUN useradd -m -u 1000 user
24
+ ENV HOME=/home/user \
25
+ PATH=/home/user/.local/bin:$PATH \
26
+ # Model/cache dir. Ephemeral on free tier — fine, models re-download on boot.
27
+ HF_HOME=/home/user/.cache/huggingface \
28
+ PYTHONUNBUFFERED=1
29
+
30
+ WORKDIR /app
31
+
32
+ # Python deps (do NOT run any GPU/nvidia-smi command here — no GPU at build time)
33
+ COPY --chown=user requirements.txt ./
34
+ RUN pip install --no-cache-dir --upgrade pip \
35
+ && pip install --no-cache-dir -r requirements.txt
36
+
37
+ # App code
38
+ COPY --chown=user backend/ ./backend/
39
+ COPY --chown=user startup.sh ./startup.sh
40
+
41
+ # Built React static files from stage 1
42
+ COPY --chown=user --from=frontend /frontend/dist ./static
43
+
44
+ RUN chmod +x ./startup.sh && chown -R user:user /app
45
+ USER user
46
+
47
+ # Spaces exposes a single public port; must match app_port in README
48
+ EXPOSE 7860
49
+
50
+ CMD ["./startup.sh"]
README.md CHANGED
@@ -1,11 +1,41 @@
1
  ---
2
- title: MultiagentMedicalAssistant
3
- emoji: 👀
4
- colorFrom: indigo
5
- colorTo: purple
6
  sdk: docker
7
- pinned: false
8
- license: mit
 
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Multi-Agent Medical Assistant
3
+ emoji: 🩺
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: docker
7
+ app_port: 7860
8
+ suggested_hardware: t4-small
9
+ short_description: Multi-agent clinical assistant with RAG and imaging analysis
10
  ---
11
 
12
+ # Multi-Agent Medical Assistant
13
+
14
+ Multi-agent clinical assistant: diagnosis, retrieval, reasoning, and imaging
15
+ agents behind a FastAPI backend with a React frontend, deployed as a single
16
+ Docker Space.
17
+
18
+ ## Architecture on Spaces
19
+
20
+ Everything runs in one container on port 7860:
21
+
22
+ - **FastAPI** — single entrypoint, serves both the API and the built React app.
23
+ - **React** — built to static files at image-build time, served by FastAPI.
24
+ - **Embedded Qdrant** — local path, no separate server. Rebuilt on each boot.
25
+ - **SQLite** — file-based, no Postgres server.
26
+ - **Imaging models** — loaded lazily on the T4 GPU on first request.
27
+ - **LLM agents** — call a hosted API; keys come from Space secrets.
28
+
29
+ ## Configuration
30
+
31
+ Set these under **Settings → Variables & secrets**:
32
+
33
+ - `LLM_API_KEY` — your hosted LLM provider key
34
+ - `ELEVENLABS_API_KEY` — for voice output (optional)
35
+
36
+ ## Notes
37
+
38
+ - Free tier disk is ephemeral: the vector store is rebuilt from seed docs on
39
+ every restart. User uploads do not persist across restarts.
40
+ - Requires a paid GPU tier (t4-small) for imaging inference. Pause the Space
41
+ when not in use to control cost.
backend/ingest.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Rebuilds the embedded Qdrant vector store on each boot (free tier = no persistence).
3
+ Run as: python -m backend.ingest
4
+
5
+ Keep this idempotent and fast — it runs on every cold start. Point it at seed
6
+ documents baked into the image (e.g. backend/seed_docs/) rather than user uploads,
7
+ since uploads won't survive a restart anyway.
8
+ """
9
+ import os
10
+
11
+
12
+ def main():
13
+ qdrant_path = os.environ.get("QDRANT_PATH", "/home/user/qdrant_data")
14
+
15
+ # Embedded Qdrant — no server process, just a local path.
16
+ # from qdrant_client import QdrantClient
17
+ # client = QdrantClient(path=qdrant_path)
18
+ #
19
+ # 1. read seed docs (Docling parse)
20
+ # 2. chunk + embed (your HF embedding model)
21
+ # 3. client.recreate_collection(...) then client.upsert(...)
22
+
23
+ print(f"[ingest] Would rebuild collection at {qdrant_path}")
24
+
25
+
26
+ if __name__ == "__main__":
27
+ main()
backend/main.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Single-port entrypoint for the Multi-Agent Medical Assistant on HF Spaces.
3
+
4
+ Everything is served from one FastAPI app on port 7860:
5
+ - /api/* your agent + RAG + imaging routes
6
+ - /health liveness probe
7
+ - /* the React single-page app (static build)
8
+
9
+ Design constraints baked in:
10
+ - Hosted LLM API (keys read from Space Secrets via env vars)
11
+ - Embedded Qdrant + SQLite (no external DB servers)
12
+ - Imaging models loaded lazily on first request (no GPU work at import time)
13
+ """
14
+ import os
15
+ from pathlib import Path
16
+
17
+ from fastapi import FastAPI
18
+ from fastapi.middleware.cors import CORSMiddleware
19
+ from fastapi.staticfiles import StaticFiles
20
+ from fastapi.responses import FileResponse
21
+
22
+ # ---- Secrets / config (set these in Space Settings -> Variables & secrets) ----
23
+ # e.g. OPENAI_API_KEY, ANTHROPIC_API_KEY, ELEVENLABS_API_KEY
24
+ LLM_API_KEY = os.environ.get("LLM_API_KEY") # rename to match your provider
25
+ ELEVENLABS_API_KEY = os.environ.get("ELEVENLABS_API_KEY")
26
+
27
+ STATIC_DIR = Path(__file__).resolve().parent.parent / "static"
28
+
29
+ app = FastAPI(title="Multi-Agent Medical Assistant")
30
+
31
+ # Frontend is served from the same origin, so CORS is mostly a no-op here,
32
+ # but keep it permissive if you ever call the API from elsewhere.
33
+ app.add_middleware(
34
+ CORSMiddleware,
35
+ allow_origins=["*"],
36
+ allow_methods=["*"],
37
+ allow_headers=["*"],
38
+ )
39
+
40
+
41
+ @app.get("/health")
42
+ def health():
43
+ return {"status": "ok"}
44
+
45
+
46
+ # ---------------------------------------------------------------------------
47
+ # API ROUTES
48
+ # Import your existing routers here. Keep heavy imports (torch, imaging models)
49
+ # INSIDE the route handlers or a lazy loader — never at module top level, so the
50
+ # container starts fast and the build never touches the GPU.
51
+ # ---------------------------------------------------------------------------
52
+ # from backend.agents.router import router as agents_router
53
+ # app.include_router(agents_router, prefix="/api")
54
+
55
+
56
+ # ---- Lazy imaging-model loader example (T4 GPU) ----------------------------
57
+ _models = {}
58
+
59
+ def get_imaging_model(name: str):
60
+ """Load a model once, on first use, onto the GPU."""
61
+ if name not in _models:
62
+ import torch # imported lazily so build stage never needs CUDA
63
+ # _models[name] = load_your_model(name).to(
64
+ # "cuda" if torch.cuda.is_available() else "cpu"
65
+ # )
66
+ raise NotImplementedError("Wire up your imaging model loader here.")
67
+ return _models[name]
68
+
69
+
70
+ # ---------------------------------------------------------------------------
71
+ # STATIC FRONTEND (mounted LAST so it doesn't shadow /api and /health)
72
+ # ---------------------------------------------------------------------------
73
+ if STATIC_DIR.exists():
74
+ app.mount("/assets", StaticFiles(directory=STATIC_DIR / "assets"), name="assets")
75
+
76
+ @app.get("/{full_path:path}")
77
+ def serve_spa(full_path: str):
78
+ # Serve real files if they exist, else fall back to index.html (SPA routing)
79
+ candidate = STATIC_DIR / full_path
80
+ if full_path and candidate.is_file():
81
+ return FileResponse(candidate)
82
+ return FileResponse(STATIC_DIR / "index.html")
requirements.txt ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- Web / serving ---
2
+ fastapi>=0.110
3
+ uvicorn[standard]>=0.29
4
+ python-multipart>=0.0.9
5
+
6
+ # --- Agents / RAG ---
7
+ langchain>=0.2
8
+ qdrant-client>=1.9 # embedded mode via path=
9
+ docling # document parsing
10
+ sentence-transformers # embeddings (or your HF embedding model)
11
+
12
+ # --- Imaging (GPU T4) ---
13
+ # Pin torch to a CUDA 12.x wheel compatible with the base image.
14
+ torch>=2.2
15
+ torchvision>=0.17
16
+ opencv-python-headless
17
+ nibabel
18
+ monai
19
+
20
+ # --- Voice (optional) ---
21
+ # elevenlabs
22
+
23
+ # --- Hosted LLM client (pick the one you use) ---
24
+ # openai
25
+ # anthropic
startup.sh ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Free tier = ephemeral disk. Everything under these paths is wiped on restart,
5
+ # so we rebuild the vector store on every boot. Keep this idempotent.
6
+ export QDRANT_PATH="${QDRANT_PATH:-/home/user/qdrant_data}"
7
+ export SQLITE_PATH="${SQLITE_PATH:-/home/user/app.db}"
8
+ mkdir -p "$QDRANT_PATH"
9
+
10
+ echo "[startup] Rebuilding vector store (ephemeral disk, no persistence)..."
11
+ # This should ingest your seed docs into embedded Qdrant. Make it safe to re-run.
12
+ python -m backend.ingest || {
13
+ echo "[startup] Ingestion failed — starting app anyway (empty index)."
14
+ }
15
+
16
+ echo "[startup] Launching FastAPI on 0.0.0.0:7860"
17
+ exec uvicorn backend.main:app --host 0.0.0.0 --port 7860 --workers 1