File size: 8,676 Bytes
821ccae
 
 
 
 
abf3c4b
 
 
821ccae
 
 
 
1eec5a6
 
 
 
821ccae
 
 
 
 
 
 
1874e21
 
 
821ccae
 
 
 
 
 
1eec5a6
821ccae
 
 
 
 
 
 
 
 
4775f5f
 
 
 
 
 
821ccae
 
 
1eec5a6
 
 
 
 
 
 
821ccae
 
1eec5a6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
821ccae
1eec5a6
 
 
 
 
 
 
 
821ccae
 
 
 
 
 
 
 
 
 
 
 
 
 
 
abf3c4b
821ccae
abf3c4b
 
 
821ccae
abf3c4b
821ccae
abf3c4b
 
 
821ccae
c9c222c
 
abf3c4b
 
 
 
 
 
 
 
5ab35ee
abf3c4b
5ab35ee
abf3c4b
5ab35ee
 
 
 
 
 
4775f5f
 
 
 
 
 
5ab35ee
 
abf3c4b
 
5ab35ee
 
 
 
 
abf3c4b
 
 
 
1520970
 
 
24361e0
c9c222c
24361e0
1520970
 
 
abf3c4b
 
 
 
821ccae
1eec5a6
 
 
 
821ccae
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1eec5a6
 
 
 
 
 
821ccae
 
1eec5a6
821ccae
abf3c4b
1eec5a6
 
 
 
 
 
821ccae
 
 
 
1eec5a6
 
 
 
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
---
title: DocuAsk
emoji: πŸ“„
colorFrom: green
colorTo: gray
sdk: static
app_build_command: cd frontend && npm ci && npm run build
app_file: frontend/dist/index.html
pinned: false
license: mit
---

# DocuAsk

[![CI](https://github.com/Sriyansh-28/docuask/actions/workflows/ci.yml/badge.svg)](https://github.com/Sriyansh-28/docuask/actions/workflows/ci.yml)

**DocuAsk** is a full-stack document Q&A web app. Upload a PDF (or paste text),
ask questions in a chat box, and get answers with the **source passage** shown
underneath β€” then rate each answer πŸ‘/πŸ‘Ž and watch the usage metrics update on a
live dashboard. It's a small, end-to-end product loop: a React front end, a
FastAPI back end, a lightweight retrieval layer, and a SQLite telemetry layer,
all runnable with one command and deployable as a single container.

πŸ”— **Live demo:** **[docuask-production-c732.up.railway.app](https://docuask-production-c732.up.railway.app)**
β€” frontend + API from a single Railway container; dashboard at
[`/#/dashboard`](https://docuask-production-c732.up.railway.app/#/dashboard).

## Screenshots

| Chat β€” answer with source passage | Dashboard β€” live metrics |
| :---: | :---: |
| ![Chat view](docs/screenshots/chat.png) | ![Dashboard view](docs/screenshots/dashboard.png) |

## Features

- **Upload & parse** β€” drag-and-drop a PDF or paste text; the backend extracts
  (pypdf), chunks, and indexes it in memory. Encrypted / broken / non-PDF /
  empty / oversize inputs all fail with a clear, friendly message.
- **Chat with sources** β€” every answer shows the passage it was drawn from, so
  the retrieval is transparent.
- **Hybrid retrieval** β€” BM25 (`rank_bm25`) combined with a FAISS cosine search
  over TF-IDF vectors; deliberately lightweight, no heavyweight model.
- **Optional LLM answers (free)** β€” set `LLM_PROVIDER` (`groq` | `gemini` |
  `openrouter`) and `LLM_API_KEY`, and answers become grounded summaries written
  from the retrieved passages (the source passage is still shown). All three are
  free OpenAI-compatible providers with email/Google sign-in. Without a key it
  falls back to an extractive answer, so the app runs with no credentials and no
  cost.
- **Feedback & telemetry** β€” each question is logged to SQLite with its measured
  latency; πŸ‘/πŸ‘Ž feedback and a `/dashboard` page show total questions, median
  latency, and thumbs-up rate live from the DB.

## Run it (one command)

```bash
docker compose up --build
```

Then open **http://localhost:5173**. The frontend calls the API through nginx,
so there's nothing else to configure.

## Run without Docker (dev)

**Backend**

```bash
cd backend
pip install -r requirements.txt
uvicorn app.main:app --reload   # http://localhost:8000
```

**Frontend**

```bash
cd frontend
npm install
npm run dev                     # http://localhost:5173
```

In dev the Vite server proxies `/api/*` to the backend, so no CORS setup is
needed.

## Tests

```bash
cd backend
pytest
```

The suite covers `/health`, upload success and every failure path, retrieval and
`/ask`, `/feedback`, `/stats`, and the combined deploy entrypoint. CI runs it on
every push.

## API

| Method | Path                | Purpose                                            |
| ------ | ------------------- | -------------------------------------------------- |
| GET    | `/health`           | Liveness probe.                                    |
| POST   | `/documents`        | Ingest a PDF file or raw text β†’ `document_id`.     |
| GET    | `/documents/{id}`   | Document metadata.                                 |
| POST   | `/ask`              | `{document_id, question}` β†’ answer + source passage. |
| POST   | `/feedback`         | Attach πŸ‘/πŸ‘Ž (`up`/`down`) to an interaction.       |
| GET    | `/stats`            | Totals, median latency, thumbs-up rate, over-time. |

## Deploy

The frontend deploys to a **Hugging Face Static Space** and the API to any host
that runs a Python web process. They're wired together with one Space variable β€”
no rebuild needed to change the API URL.

### 1. Frontend β†’ Hugging Face Static Space

The README front matter (`sdk: static`) tells HF to run the
`app_build_command` (`cd frontend && npm ci && npm run build`) and serve
`frontend/dist`.

1. Create a new **Space** β†’ **Static** SDK, owner `Sri-28`, name `Docuask`
   (the workflow default; case-sensitive on HF).
2. Push this repo to the Space (the [`sync-to-hf`](./.github/workflows/sync-to-hf.yml)
   workflow does this automatically β€” see below).
3. In the Space's **Settings β†’ Variables**, add `DOCUASK_API_URL` set to your
   deployed API's URL. The frontend reads it at runtime via
   `window.huggingface.variables`, so you can change it without rebuilding.

The Space serves at **https://sri-28-docuask.static.hf.space**.

### 2. Backend β†’ Railway

The API is a standard uvicorn app. On **Railway**:

1. **New Project β†’ Deploy from GitHub repo** β†’ select `docuask`.
2. Open the service β†’ **Settings β†’ Root Directory** = `backend`. This makes
   [`backend/railway.json`](./backend/railway.json) build
   [`backend/Dockerfile`](./backend/Dockerfile) (which installs faiss's
   `libgomp1` and honors Railway's injected `$PORT`).
3. **Settings β†’ Networking β†’ Generate Domain** to get a public URL.
4. *(Optional)* In the service **Variables**, set `LLM_PROVIDER` (e.g. `gemini`)
   and `LLM_API_KEY` (a free key β€” Gemini's is at
   [aistudio.google.com/apikey](https://aistudio.google.com/apikey), Google
   sign-in) to enable LLM-written answers; otherwise the API serves extractive
   answers. Add a **Volume** at `/data` + `DOCUASK_DB=/data/docuask.db` to
   persist telemetry across restarts.
5. Copy the public URL β€” you'll set it as `DOCUASK_API_URL` in the HF Space
   (step 3 above).

The Static Space origin (`https://sri-28-docuask.static.hf.space`) is already in
the backend's default CORS allow-list; add more via the `CORS_ORIGINS` env var.
See [`backend/.env.example`](./backend/.env.example).

> Other hosts work too: any Docker host can build `backend/Dockerfile`, and
> native-Python hosts (Render, …) can use [`backend/Procfile`](./backend/Procfile).

### Keep the demo in sync automatically

The [`sync-to-hf`](./.github/workflows/sync-to-hf.yml) workflow mirrors `main` to
your Space on every push. Configure it once under **Settings β†’ Secrets and
variables β†’ Actions**:

- Secret `HF_TOKEN` β€” a Hugging Face token with write scope. **This is the only
  required step** (username defaults to `Sri-28`, Space to `Docuask`).
- Optionally override the `HF_USERNAME` / `HF_SPACE` variables.

Until `HF_TOKEN` is set the workflow no-ops, so it never fails the branch.

> **One-origin alternative:** the root [`Dockerfile`](./Dockerfile) +
> [`app/server.py`](./backend/app/server.py) build a single image that serves the
> frontend and API together under one origin (API mounted at `/api`, no CORS).
> Use this on any Docker host if you'd rather run one service than two.

## Project structure

```
docuask/
  backend/              # FastAPI app + tests
    app/
      main.py           # API: /health, /documents, /ask, /feedback, /stats
      parsing.py        # PDF/text extraction + chunking
      retrieval.py      # BM25 + FAISS (TF-IDF) hybrid index
      store.py          # in-memory document store
      db.py             # SQLite interaction telemetry
      server.py         # combined static + API entrypoint (deploy)
    tests/              # pytest suite
  frontend/             # Vite + React + Tailwind
    src/
      App.jsx           # layout + hash routing (Chat / Dashboard)
      DocumentUploader.jsx, Chat.jsx, Dashboard.jsx
  Dockerfile            # single-image build for Hugging Face Spaces
  docker-compose.yml    # local: frontend + backend together
  .github/workflows/    # CI: pytest + frontend build on every push
```

## Tech stack

- **Frontend:** React + Vite + Tailwind CSS
- **Backend:** FastAPI (Python)
- **Retrieval:** BM25 + FAISS over TF-IDF
- **Persistence:** SQLite (interaction telemetry)
- **Tests:** pytest
- **CI:** GitHub Actions (pytest + build on every push)
- **Deploy:** Hugging Face Static Space (frontend) + any Python host (API); Docker Compose locally

## Roadmap

See [`PROJECT_SPEC.md`](./PROJECT_SPEC.md) for the full plan:

1. βœ… Skeleton end to end (health check wired browser β†’ API)
2. βœ… Upload & parse flow (PDF/text β†’ chunked, indexed)
3. βœ… Retrieval + chat loop (BM25 + FAISS, source passages)
4. βœ… Data-collection & feedback layer (SQLite, πŸ‘/πŸ‘Ž, `/stats`, dashboard)
5. βœ… Polish, tests, deploy (single-image Docker Space)

## License

[MIT](./LICENSE)