Polish, tests & single-image deploy (Session 5)
Browse filesDeploy (Hugging Face Spaces, Docker SDK):
- Root Dockerfile builds a single image: stage 1 builds the React frontend,
stage 2 runs FastAPI serving the built assets plus the API under /api on
port 7860 (same origin, no CORS, one live URL).
- app/server.py: combined ASGI entrypoint mounting the API at /api and the
static frontend at / (guarded so tests import cleanly without a build).
- README front matter (sdk: docker, app_port: 7860) so a Space builds it
directly; .dockerignore for build-context hygiene.
- backend/.env.example and frontend/.env.example document CORS_ORIGINS,
DOCUASK_DB, and VITE_API_URL for split-hosting setups.
Polish:
- README rewritten for a recruiter: description, chat + dashboard
screenshots, feature list, API table, deploy steps, project structure.
- test_server.py verifies the API is reachable under /api in the combined
app.
Verified the combined server locally: /api/health, static index, and
/api/ask all serve correctly from one origin.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WN4QRr6dTE2W7hQ2SDnLmY
- .dockerignore +11 -0
- .gitignore +3 -0
- Dockerfile +34 -0
- PROJECT_SPEC.md +8 -2
- README.md +101 -25
- backend/.env.example +12 -0
- backend/app/server.py +31 -0
- backend/tests/test_server.py +17 -0
- docs/screenshots/chat.png +0 -0
- docs/screenshots/dashboard.png +0 -0
- frontend/.env.example +7 -0
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Build context hygiene for the root (Hugging Face) image.
|
| 2 |
+
**/node_modules
|
| 3 |
+
**/dist
|
| 4 |
+
**/__pycache__
|
| 5 |
+
**/*.pyc
|
| 6 |
+
**/.pytest_cache
|
| 7 |
+
**/.venv
|
| 8 |
+
**/*.db
|
| 9 |
+
.git
|
| 10 |
+
.github
|
| 11 |
+
docs
|
|
@@ -17,6 +17,9 @@ dist-ssr/
|
|
| 17 |
*.local
|
| 18 |
.vite/
|
| 19 |
|
|
|
|
|
|
|
|
|
|
| 20 |
# Env & secrets
|
| 21 |
.env
|
| 22 |
.env.*
|
|
|
|
| 17 |
*.local
|
| 18 |
.vite/
|
| 19 |
|
| 20 |
+
# Built frontend staged into the backend image (see root Dockerfile)
|
| 21 |
+
backend/static/
|
| 22 |
+
|
| 23 |
# Env & secrets
|
| 24 |
.env
|
| 25 |
.env.*
|
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Single-image build for Hugging Face Spaces (Docker SDK).
|
| 2 |
+
# Stage 1 builds the React frontend; stage 2 runs the FastAPI backend and
|
| 3 |
+
# serves the built frontend from the same origin on port 7860.
|
| 4 |
+
|
| 5 |
+
# ---- Stage 1: build the frontend ----
|
| 6 |
+
FROM node:20-slim AS frontend
|
| 7 |
+
WORKDIR /fe
|
| 8 |
+
COPY frontend/package.json frontend/package-lock.json ./
|
| 9 |
+
RUN npm ci
|
| 10 |
+
COPY frontend/ ./
|
| 11 |
+
RUN npm run build # outputs /fe/dist
|
| 12 |
+
|
| 13 |
+
# ---- Stage 2: backend + static assets ----
|
| 14 |
+
FROM python:3.11-slim
|
| 15 |
+
WORKDIR /app
|
| 16 |
+
|
| 17 |
+
# libgomp1 is required at runtime by faiss-cpu (OpenMP).
|
| 18 |
+
RUN apt-get update \
|
| 19 |
+
&& apt-get install -y --no-install-recommends libgomp1 \
|
| 20 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 21 |
+
|
| 22 |
+
COPY backend/requirements.txt .
|
| 23 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 24 |
+
|
| 25 |
+
COPY backend/ ./
|
| 26 |
+
COPY --from=frontend /fe/dist ./static
|
| 27 |
+
|
| 28 |
+
# HF Spaces expose port 7860. /tmp is writable regardless of the runtime user;
|
| 29 |
+
# mount HF persistent storage and set DOCUASK_DB=/data/docuask.db to keep
|
| 30 |
+
# telemetry across restarts.
|
| 31 |
+
ENV DOCUASK_DB=/tmp/docuask.db
|
| 32 |
+
EXPOSE 7860
|
| 33 |
+
|
| 34 |
+
CMD ["uvicorn", "app.server:root", "--host", "0.0.0.0", "--port", "7860"]
|
|
@@ -119,11 +119,17 @@ Goal: live URL + green CI + a README a recruiter can skim.
|
|
| 119 |
- README: one-paragraph description, screenshot/GIF of the chat + dashboard,
|
| 120 |
live demo link, run instructions, tech stack.
|
| 121 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
**Acceptance criteria**
|
| 123 |
|
| 124 |
-
- [
|
| 125 |
- [ ] Live demo URL works end to end (upload β ask β feedback β dashboard).
|
| 126 |
-
|
|
|
|
|
|
|
| 127 |
|
| 128 |
## Guardrails
|
| 129 |
|
|
|
|
| 119 |
- README: one-paragraph description, screenshot/GIF of the chat + dashboard,
|
| 120 |
live demo link, run instructions, tech stack.
|
| 121 |
|
| 122 |
+
> Note: this project deploys as a single-image **Hugging Face Space** (Docker),
|
| 123 |
+
> which serves the frontend and API from one origin β replacing the split
|
| 124 |
+
> Render/Vercel plan above.
|
| 125 |
+
|
| 126 |
**Acceptance criteria**
|
| 127 |
|
| 128 |
+
- [x] CI badge is green on the default branch.
|
| 129 |
- [ ] Live demo URL works end to end (upload β ask β feedback β dashboard).
|
| 130 |
+
_Deploy config is ready (root `Dockerfile` + README front matter); awaiting
|
| 131 |
+
the owner creating the Hugging Face Space._
|
| 132 |
+
- [x] README has a screenshot; live link is a placeholder until the Space is up.
|
| 133 |
|
| 134 |
## Guardrails
|
| 135 |
|
|
@@ -1,17 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# DocuAsk
|
| 2 |
|
| 3 |
[](https://github.com/Sriyansh-28/docuask/actions/workflows/ci.yml)
|
| 4 |
|
| 5 |
-
|
| 6 |
-
questions in a chat box, and get answers with the **source passage** shown
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
## Run it (one command)
|
| 17 |
|
|
@@ -19,8 +48,8 @@ plus a live feedback/telemetry dashboard.
|
|
| 19 |
docker compose up --build
|
| 20 |
```
|
| 21 |
|
| 22 |
-
Then open **http://localhost:5173**
|
| 23 |
-
|
| 24 |
|
| 25 |
## Run without Docker (dev)
|
| 26 |
|
|
@@ -41,7 +70,7 @@ npm run dev # http://localhost:5173
|
|
| 41 |
```
|
| 42 |
|
| 43 |
In dev the Vite server proxies `/api/*` to the backend, so no CORS setup is
|
| 44 |
-
needed.
|
| 45 |
|
| 46 |
## Tests
|
| 47 |
|
|
@@ -50,36 +79,83 @@ cd backend
|
|
| 50 |
pytest
|
| 51 |
```
|
| 52 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
## Project structure
|
| 54 |
|
| 55 |
```
|
| 56 |
docuask/
|
| 57 |
-
backend/
|
| 58 |
-
app/
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
```
|
| 65 |
|
| 66 |
## Tech stack
|
| 67 |
|
| 68 |
- **Frontend:** React + Vite + Tailwind CSS
|
| 69 |
- **Backend:** FastAPI (Python)
|
|
|
|
|
|
|
| 70 |
- **Tests:** pytest
|
| 71 |
-
- **CI:** GitHub Actions (pytest on every push)
|
| 72 |
-
- **
|
| 73 |
|
| 74 |
## Roadmap
|
| 75 |
|
| 76 |
See [`PROJECT_SPEC.md`](./PROJECT_SPEC.md) for the full plan:
|
| 77 |
|
| 78 |
1. β
Skeleton end to end (health check wired browser β API)
|
| 79 |
-
2. Upload & parse flow (PDF/text β chunked, indexed)
|
| 80 |
-
3. Retrieval + chat loop (BM25 + FAISS, source passages)
|
| 81 |
-
4. Data-collection & feedback layer (SQLite, π/π, `/stats`, dashboard)
|
| 82 |
-
5. Polish, tests,
|
| 83 |
|
| 84 |
## License
|
| 85 |
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: DocuAsk
|
| 3 |
+
emoji: π
|
| 4 |
+
colorFrom: green
|
| 5 |
+
colorTo: gray
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: false
|
| 9 |
+
license: mit
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
# DocuAsk
|
| 13 |
|
| 14 |
[](https://github.com/Sriyansh-28/docuask/actions/workflows/ci.yml)
|
| 15 |
|
| 16 |
+
**DocuAsk** is a full-stack document Q&A web app. Upload a PDF (or paste text),
|
| 17 |
+
ask questions in a chat box, and get answers with the **source passage** shown
|
| 18 |
+
underneath β then rate each answer π/π and watch the usage metrics update on a
|
| 19 |
+
live dashboard. It's a small, end-to-end product loop: a React front end, a
|
| 20 |
+
FastAPI back end, a lightweight retrieval layer, and a SQLite telemetry layer,
|
| 21 |
+
all runnable with one command and deployable as a single container.
|
| 22 |
+
|
| 23 |
+
π **Live demo:** _deploy to a Hugging Face Space (see [Deploy](#deploy-hugging-face-spaces)) and paste the URL here_ β
|
| 24 |
+
`https://huggingface.co/spaces/<your-username>/docuask`
|
| 25 |
+
|
| 26 |
+
## Screenshots
|
| 27 |
+
|
| 28 |
+
| Chat β answer with source passage | Dashboard β live metrics |
|
| 29 |
+
| :---: | :---: |
|
| 30 |
+
|  |  |
|
| 31 |
|
| 32 |
+
## Features
|
| 33 |
+
|
| 34 |
+
- **Upload & parse** β drag-and-drop a PDF or paste text; the backend extracts
|
| 35 |
+
(pypdf), chunks, and indexes it in memory. Encrypted / broken / non-PDF /
|
| 36 |
+
empty / oversize inputs all fail with a clear, friendly message.
|
| 37 |
+
- **Chat with sources** β every answer shows the passage it was drawn from, so
|
| 38 |
+
the retrieval is transparent.
|
| 39 |
+
- **Hybrid retrieval** β BM25 (`rank_bm25`) combined with a FAISS cosine search
|
| 40 |
+
over TF-IDF vectors; deliberately lightweight, no heavyweight model.
|
| 41 |
+
- **Feedback & telemetry** β each question is logged to SQLite with its measured
|
| 42 |
+
latency; π/π feedback and a `/dashboard` page show total questions, median
|
| 43 |
+
latency, and thumbs-up rate live from the DB.
|
| 44 |
|
| 45 |
## Run it (one command)
|
| 46 |
|
|
|
|
| 48 |
docker compose up --build
|
| 49 |
```
|
| 50 |
|
| 51 |
+
Then open **http://localhost:5173**. The frontend calls the API through nginx,
|
| 52 |
+
so there's nothing else to configure.
|
| 53 |
|
| 54 |
## Run without Docker (dev)
|
| 55 |
|
|
|
|
| 70 |
```
|
| 71 |
|
| 72 |
In dev the Vite server proxies `/api/*` to the backend, so no CORS setup is
|
| 73 |
+
needed.
|
| 74 |
|
| 75 |
## Tests
|
| 76 |
|
|
|
|
| 79 |
pytest
|
| 80 |
```
|
| 81 |
|
| 82 |
+
The suite covers `/health`, upload success and every failure path, retrieval and
|
| 83 |
+
`/ask`, `/feedback`, `/stats`, and the combined deploy entrypoint. CI runs it on
|
| 84 |
+
every push.
|
| 85 |
+
|
| 86 |
+
## API
|
| 87 |
+
|
| 88 |
+
| Method | Path | Purpose |
|
| 89 |
+
| ------ | ------------------- | -------------------------------------------------- |
|
| 90 |
+
| GET | `/health` | Liveness probe. |
|
| 91 |
+
| POST | `/documents` | Ingest a PDF file or raw text β `document_id`. |
|
| 92 |
+
| GET | `/documents/{id}` | Document metadata. |
|
| 93 |
+
| POST | `/ask` | `{document_id, question}` β answer + source passage. |
|
| 94 |
+
| POST | `/feedback` | Attach π/π (`up`/`down`) to an interaction. |
|
| 95 |
+
| GET | `/stats` | Totals, median latency, thumbs-up rate, over-time. |
|
| 96 |
+
|
| 97 |
+
## Deploy (Hugging Face Spaces)
|
| 98 |
+
|
| 99 |
+
The root [`Dockerfile`](./Dockerfile) builds a **single image** that serves the
|
| 100 |
+
React frontend and the API from one origin (the API is mounted under `/api`), so
|
| 101 |
+
you get one live URL with no CORS to configure.
|
| 102 |
+
|
| 103 |
+
1. Create a new **Space** β **Docker** SDK (blank template).
|
| 104 |
+
2. Push this repository to the Space (or connect the GitHub repo). The Space
|
| 105 |
+
reads the YAML front matter at the top of this README (`sdk: docker`,
|
| 106 |
+
`app_port: 7860`) and builds the root `Dockerfile`.
|
| 107 |
+
3. Wait for the build; the app comes up at your Space URL. Paste that URL into
|
| 108 |
+
the **Live demo** link above.
|
| 109 |
+
|
| 110 |
+
Telemetry uses `/tmp/docuask.db` by default (resets on restart). To persist it,
|
| 111 |
+
add HF **persistent storage** and set `DOCUASK_DB=/data/docuask.db` in the
|
| 112 |
+
Space's variables.
|
| 113 |
+
|
| 114 |
+
> Prefer split hosting (e.g. static frontend + separate API)? Set
|
| 115 |
+
> `VITE_API_URL` at frontend build time to the API origin and add that origin to
|
| 116 |
+
> the backend's `CORS_ORIGINS`. See the `.env.example` files.
|
| 117 |
+
|
| 118 |
## Project structure
|
| 119 |
|
| 120 |
```
|
| 121 |
docuask/
|
| 122 |
+
backend/ # FastAPI app + tests
|
| 123 |
+
app/
|
| 124 |
+
main.py # API: /health, /documents, /ask, /feedback, /stats
|
| 125 |
+
parsing.py # PDF/text extraction + chunking
|
| 126 |
+
retrieval.py # BM25 + FAISS (TF-IDF) hybrid index
|
| 127 |
+
store.py # in-memory document store
|
| 128 |
+
db.py # SQLite interaction telemetry
|
| 129 |
+
server.py # combined static + API entrypoint (deploy)
|
| 130 |
+
tests/ # pytest suite
|
| 131 |
+
frontend/ # Vite + React + Tailwind
|
| 132 |
+
src/
|
| 133 |
+
App.jsx # layout + hash routing (Chat / Dashboard)
|
| 134 |
+
DocumentUploader.jsx, Chat.jsx, Dashboard.jsx
|
| 135 |
+
Dockerfile # single-image build for Hugging Face Spaces
|
| 136 |
+
docker-compose.yml # local: frontend + backend together
|
| 137 |
+
.github/workflows/ # CI: pytest + frontend build on every push
|
| 138 |
```
|
| 139 |
|
| 140 |
## Tech stack
|
| 141 |
|
| 142 |
- **Frontend:** React + Vite + Tailwind CSS
|
| 143 |
- **Backend:** FastAPI (Python)
|
| 144 |
+
- **Retrieval:** BM25 + FAISS over TF-IDF
|
| 145 |
+
- **Persistence:** SQLite (interaction telemetry)
|
| 146 |
- **Tests:** pytest
|
| 147 |
+
- **CI:** GitHub Actions (pytest + build on every push)
|
| 148 |
+
- **Deploy:** Docker (single-image Hugging Face Space); Docker Compose locally
|
| 149 |
|
| 150 |
## Roadmap
|
| 151 |
|
| 152 |
See [`PROJECT_SPEC.md`](./PROJECT_SPEC.md) for the full plan:
|
| 153 |
|
| 154 |
1. β
Skeleton end to end (health check wired browser β API)
|
| 155 |
+
2. β
Upload & parse flow (PDF/text β chunked, indexed)
|
| 156 |
+
3. β
Retrieval + chat loop (BM25 + FAISS, source passages)
|
| 157 |
+
4. β
Data-collection & feedback layer (SQLite, π/π, `/stats`, dashboard)
|
| 158 |
+
5. β
Polish, tests, deploy (single-image Docker Space)
|
| 159 |
|
| 160 |
## License
|
| 161 |
|
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Backend configuration (copy to .env or set in your host's dashboard).
|
| 2 |
+
|
| 3 |
+
# Comma-separated list of allowed CORS origins. Only needed when the frontend
|
| 4 |
+
# is served from a DIFFERENT origin than the API (split hosting). In the
|
| 5 |
+
# single-image Hugging Face deploy the frontend is same-origin, so this can be
|
| 6 |
+
# left unset.
|
| 7 |
+
# CORS_ORIGINS=https://your-frontend.example.com
|
| 8 |
+
|
| 9 |
+
# Path to the SQLite telemetry database. Defaults to ./docuask.db locally and
|
| 10 |
+
# /tmp/docuask.db in the container image. Point at persistent storage
|
| 11 |
+
# (e.g. /data/docuask.db) to keep interactions across restarts.
|
| 12 |
+
# DOCUASK_DB=/data/docuask.db
|
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Combined ASGI app for single-container deploys (Hugging Face Spaces).
|
| 2 |
+
|
| 3 |
+
Serves the built frontend as static files and mounts the API under ``/api`` so
|
| 4 |
+
the browser only ever talks to one origin (no CORS, one demo URL). The
|
| 5 |
+
frontend's default API base is ``/api``, so no build-time config is needed.
|
| 6 |
+
|
| 7 |
+
Local dev and docker-compose keep using ``app.main`` directly; this module is
|
| 8 |
+
only the entrypoint for the single-image deployment.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
import os
|
| 14 |
+
|
| 15 |
+
from fastapi import FastAPI
|
| 16 |
+
from fastapi.staticfiles import StaticFiles
|
| 17 |
+
|
| 18 |
+
from .main import app as api_app
|
| 19 |
+
|
| 20 |
+
_STATIC_DIR = os.getenv(
|
| 21 |
+
"STATIC_DIR",
|
| 22 |
+
os.path.join(os.path.dirname(os.path.dirname(__file__)), "static"),
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
root = FastAPI(title="DocuAsk")
|
| 26 |
+
root.mount("/api", api_app)
|
| 27 |
+
|
| 28 |
+
# Mounted last so the API prefix wins; guarded so importing this module without
|
| 29 |
+
# a built frontend (e.g. in tests) doesn't fail.
|
| 30 |
+
if os.path.isdir(_STATIC_DIR):
|
| 31 |
+
root.mount("/", StaticFiles(directory=_STATIC_DIR, html=True), name="static")
|
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for the combined deploy entrypoint (Session 5).
|
| 2 |
+
|
| 3 |
+
Ensures the API is reachable under the /api prefix the frontend expects when
|
| 4 |
+
both are served from one origin (Hugging Face Space).
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from fastapi.testclient import TestClient
|
| 8 |
+
|
| 9 |
+
from app.server import root
|
| 10 |
+
|
| 11 |
+
client = TestClient(root)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def test_api_is_mounted_under_api_prefix():
|
| 15 |
+
response = client.get("/api/health")
|
| 16 |
+
assert response.status_code == 200
|
| 17 |
+
assert response.json() == {"status": "ok"}
|
|
|
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Frontend build-time configuration.
|
| 2 |
+
|
| 3 |
+
# Base URL for the API. Leave UNSET for:
|
| 4 |
+
# - local dev (Vite proxies /api to the backend), and
|
| 5 |
+
# - the single-image Hugging Face deploy (API is same-origin under /api).
|
| 6 |
+
# Set it only when hosting the frontend separately from the backend, e.g.:
|
| 7 |
+
# VITE_API_URL=https://your-backend.example.com
|