File size: 3,148 Bytes
a86363e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ebe90cc
 
a86363e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ebe90cc
 
a86363e
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# ProbabilityRAG — Hugging Face Spaces (Docker SDK, free CPU basic: 2 vCPU / 16 GB).
# Two stages: build the React bundle, then a CPU-only Python image that bakes the vector
# index + model weights so cold start is model-load only (no indexing at boot).

# ---------- Stage 1: build the frontend ----------
FROM node:22-alpine AS web
WORKDIR /web
# Copy manifests first so `npm ci` is cached until deps actually change.
COPY web/package.json web/package-lock.json ./
RUN npm ci
COPY web/ ./
RUN npm run build          # -> /web/dist

# ---------- Stage 2: Python runtime ----------
FROM python:3.12-slim AS app

# HF hub caches model weights here; a stable path lets the BGE-M3 + reranker downloads that
# happen at build time bake into the image (cold start = load from disk, no network fetch).
ENV HF_HOME=/opt/hf \
    KMP_DUPLICATE_LIB_OK=TRUE \
    HF_HUB_DISABLE_SYMLINKS_WARNING=1 \
    PROBRAG_PUBLIC=1 \
    PROBRAG_MODELS=glm-flash \
    PYTHONUNBUFFERED=1

WORKDIR /app

# curl is only needed to fetch the source PDF below; drop the apt lists to keep the layer small.
RUN apt-get update && apt-get install -y --no-install-recommends curl \
    && rm -rf /var/lib/apt/lists/*

# CPU torch FIRST, from PyTorch's CPU wheel index — requirements.txt pins torch==2.11.0+cu128
# (a local-GPU build) which does NOT exist on PyPI and would never resolve on a CPU host.
# Then install everything else EXCEPT the torch line (grep it out) so pip doesn't try to
# "correct" our CPU torch back to the CUDA pin.
COPY requirements.txt ./
RUN pip install --no-cache-dir torch==2.11.0 --index-url https://download.pytorch.org/whl/cpu \
    && grep -v '^torch' requirements.txt > /tmp/req-nocuda.txt \
    && pip install --no-cache-dir -r /tmp/req-nocuda.txt

# Only what the index build needs — app/ is copied AFTER the build steps below, so pure
# server-code changes reuse the cached PDF + index layers (~2 min rebuild instead of ~20).
COPY src/ ./src/
COPY scripts/ ./scripts/
COPY data/chunks/ ./data/chunks/

# Grinstead & Snell "Introduction to Probability" — GFDL-licensed, legally redistributable.
# Hosted by Dartmouth; verified to return %PDF (see DEPLOY.md attribution). The /pdf citation
# viewer needs it, so a failed download must fail the build loudly (-f -> non-zero on 4xx/5xx).
ARG PDF_URL=https://math.dartmouth.edu/~prob/prob/prob.pdf
RUN mkdir -p data/raw \
    && curl -fSL "$PDF_URL" -o data/raw/grinstead_snell.pdf \
    && head -c 4 data/raw/grinstead_snell.pdf | grep -q '%PDF'   # sanity: it's really a PDF

# Build the vector index AT BUILD TIME. This downloads BGE-M3 + bge-reranker weights into
# HF_HOME and writes qdrant_storage/, so the running container never indexes or fetches models.
RUN python scripts/embed_store.py --build

# Server code + built frontend land after the heavy layers (see COPY note above).
COPY app/ ./app/
COPY --from=web /web/dist ./web/dist

# HF Spaces routes traffic to the container's app_port (7860). ZAI_API_KEY is injected as a
# Space secret at runtime — deliberately NOT baked into the image.
EXPOSE 7860
CMD ["uvicorn", "app.server:app", "--host", "0.0.0.0", "--port", "7860"]