Spaces:
Build error
Build error
Upload 7 files
Browse files- .dockerignore +9 -0
- Dockerfile +50 -0
- README.md +37 -7
- backend/ingest.py +27 -0
- backend/main.py +82 -0
- requirements.txt +25 -0
- 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:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
-
|
| 8 |
-
|
|
|
|
| 9 |
---
|
| 10 |
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|