Skip to content

Commit 272b72c

Browse files
committed
feat(demo): update document-rag demo with pre-ingestion and ratelimiting support
1 parent 87223ca commit 272b72c

23 files changed

Lines changed: 823 additions & 732 deletions

‎demo/document-rag/.env.example‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1-
OPENAI_API_KEY=sk-your-api-key-here
1+
OPENAI_API_KEY=your-api-token
2+
ACCOUNT_ID=your-account-id
3+
4+
EMBEDDING_MODEL=@cf/qwen/qwen3-embedding-0.6b
5+
LLM_MODEL=@cf/openai/gpt-oss-20b
6+
VECTOR_SIZE=1024
7+
28
VORTEXDB_HOST=vortexdb
39
VORTEXDB_PORT=3034
4-
EMBEDDING_MODEL=text-embedding-3-small
5-
LLM_MODEL=gpt-4o-mini
610
CHUNK_SIZE=512
711
CHUNK_OVERLAP=50
812
TOP_K=5
13+
EMBEDDING_BATCH_SIZE=64
14+
DOCS_DIRECTORY=./documents
15+

‎demo/document-rag/README.md‎

Lines changed: 91 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,111 @@
1-
# VectorDB RAG Demo
1+
# VortexDB RAG Demo
22

3-
A fully containerized Document RAG demo with a dark-themed web UI. Upload documents, chat with your knowledge base.
3+
A containerized document RAG demo: offline ingest into VortexDB, query via a FastAPI backend, and chat through a React frontend. Embeddings and LLM calls use [Cloudflare Workers AI](https://developers.cloudflare.com/workers-ai/) through the OpenAI-compatible API.
44

55
## Quick Start
66

77
```bash
8-
# 1. Fill in your API key
8+
# 1. Configure credentials
99
cp .env.example .env
10-
# Edit .env and set OPENAI_API_KEY=sk-your-key-here
10+
# Set OPENAI_API_KEY (Cloudflare API token) and ACCOUNT_ID
1111

12-
# 2. Build and start everything
12+
# 2. Add documents to ingest
13+
cp /path/to/your/files/* documents/
14+
15+
# 3. Start the stack (builds images, ingests docs, starts API + UI)
1316
docker compose up -d --build
1417

15-
# 3. Open browser
18+
# 4. Open the UI
1619
open http://localhost:3035
1720
```
1821

19-
That's it! No other setup required.
22+
On startup, Docker will:
23+
24+
1. Start VortexDB and wait until healthy
25+
2. Run ingest on `./documents` (TXT, MD, PDF, DOCX, CSV)
26+
3. Start the backend API
27+
4. Start the frontend
28+
29+
To re-ingest after adding or changing documents:
30+
31+
```bash
32+
docker compose up ingest --force-recreate
33+
docker compose up -d backend
34+
```
2035

21-
## Features
36+
## Ports
2237

23-
- **File Upload** - Drag & drop or browse documents (PDF, TXT, MD, DOCX, CSV)
24-
- **Chat Interface** - Ask questions, get AI-powered answers
25-
- **Fully Containerized** - VortexDB + Backend + Frontend in Docker
38+
| Service | URL |
39+
|-----------|--------------------------|
40+
| Frontend | http://localhost:3035 |
41+
| Backend | http://localhost:8000 |
42+
| VortexDB | http://localhost:3034 |
2643

2744
## Architecture
2845

2946
```
30-
Browser (localhost:3035) → Frontend (nginx)
47+
documents/ → ingest (one-shot) → VortexDB
48+
↑
49+
Browser (3035) → nginx → backend (8000) ─┘
3150
↓
32-
Backend API (port 8000)
33-
↓
34-
┌───────────────┴───────────────┐
35-
↓ ↓
36-
OpenAI API VortexDB
37-
(embeddings + LLM) (HTTP port 3000)
51+
Cloudflare Workers AI
52+
(embeddings + LLM)
53+
```
54+
55+
## API
56+
57+
| Method | Path | Body | Description |
58+
|--------|----------|-------------------------|--------------------------|
59+
| GET | `/health`| — | Health check |
60+
| POST | `/chat` | `{"query": "..."}` | RAG answer + sources |
61+
| POST | `/query` | `{"query": "..."}` | Retrieval only (no LLM) |
62+
63+
Example:
64+
65+
```bash
66+
curl -X POST http://localhost:8000/chat \
67+
-H "Content-Type: application/json" \
68+
-d '{"query": "What embedding model is used?"}'
3869
```
3970

4071
## Configuration
4172

42-
### .env file
73+
All settings live in `.env`. See `.env.example` for the full list.
4374

4475
```env
45-
OPENAI_API_KEY=sk-your-api-key-here
46-
VORTEXDB_HOST=vortexdb
47-
VORTEXDB_PORT=3000
48-
EMBEDDING_MODEL=text-embedding-3-small
49-
LLM_MODEL=gpt-4o-mini
76+
OPENAI_API_KEY=your-cloudflare-api-token
77+
ACCOUNT_ID=your-account-id
78+
79+
EMBEDDING_MODEL=@cf/qwen/qwen3-embedding-0.6b
80+
LLM_MODEL=@cf/openai/gpt-oss-20b
81+
VECTOR_SIZE=1024
82+
5083
CHUNK_SIZE=512
5184
CHUNK_OVERLAP=50
5285
TOP_K=5
86+
EMBEDDING_BATCH_SIZE=64
87+
DOCS_DIRECTORY=./documents
88+
```
89+
90+
Get credentials from the [Cloudflare dashboard](https://dash.cloudflare.com/) → Workers AI → **Use REST API**. The API token needs Workers AI read access. `VECTOR_SIZE` must match VortexDB's `DIMENSION` in `docker-compose.yml` (default `1024`).
91+
92+
Inside Docker Compose, `VORTEXDB_HOST` and `VORTEXDB_PORT` are overridden to `vortexdb:3000` automatically.
93+
94+
## Local Development (without Docker)
95+
96+
```bash
97+
# Terminal 1 — VortexDB (from repo root)
98+
HTTP_HOST=0.0.0.0 HTTP_PORT=3034 STORAGE_TYPE=inmemory INDEX_TYPE=flat \
99+
DIMENSION=1024 cargo run --release --bin server
100+
101+
# Terminal 2 — ingest
102+
cd backend
103+
pip install -r requirements.txt
104+
cp ../.env.example ../.env # fill in credentials
105+
python -m src.ingestion
106+
107+
# Terminal 3 — API
108+
uvicorn src.main:app --reload --port 8000
53109
```
54110

55111
## Project Structure
@@ -58,26 +114,21 @@ TOP_K=5
58114
demo/document-rag/
59115
├── docker-compose.yml
60116
├── .env.example
61-
├── README.md
117+
├── documents/ # Drop files here for ingest
62118
├── backend/
63119
│ ├── src/
64-
│ │ ├── main.py # FastAPI app
65-
│ │ ├── config.py # Config from env
66-
│ │ ├── chunker.py # Text chunking
67-
│ │ ├── embedder.py # OpenAI embeddings
68-
│ │ ├── generator.py # Chat completion
69-
│ │ ├── extractor.py # Document parsing
70-
│ │ └── vectorstore.py # VortexDB HTTP client
120+
│ │ ├── main.py # FastAPI app
121+
│ │ ├── ingestion.py # Offline ingest CLI
122+
│ │ ├── config.py # Env-based config
123+
│ │ ├── chunker.py
124+
│ │ ├── embedder.py # Cloudflare embeddings
125+
│ │ ├── generator.py # Cloudflare chat completions
126+
│ │ ├── extractor.py
127+
│ │ └── vectorstore.py # VortexDB HTTP client
71128
│ ├── requirements.txt
72129
│ └── Dockerfile
73130
└── frontend/
74-
├── src/
75-
│ ├── App.jsx # Main React component
76-
│ ├── App.css # Styles
77-
│ └── main.jsx # Entry point
78-
├── index.html
79-
├── package.json
80-
├── vite.config.js
81-
├── nginx.conf
131+
├── src/App.jsx
132+
├── nginx.conf # Proxies /api → backend:8000
82133
└── Dockerfile
83134
```

‎demo/document-rag/backend/Dockerfile‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ RUN pip install --no-cache-dir -r requirements.txt
77

88
COPY src/ ./src/
99

10-
RUN mkdir -p /app/uploads
10+
RUN mkdir -p /app/documents
1111

1212
ENV PYTHONPATH=/app
13+
ENV DOCS_DIRECTORY=/app/documents
1314

1415
EXPOSE 8000
1516

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
fastapi==0.109.2
2-
uvicorn[standard]==0.27.1
3-
python-multipart==0.0.9
4-
openai==1.12.0
5-
httpx==0.27.0
6-
pypdf2==3.0.1
7-
python-docx==1.1.0
8-
pydantic==2.6.1
1+
fastapi==0.124.4
2+
uvicorn[standard]==0.41.0
3+
httpx==0.28.1
4+
openai==2.30.0
95
python-dotenv==1.0.1
6+
slowapi==0.1.10
7+
python-multipart==0.0.22
8+
PyPDF2==3.0.1
9+
python-docx==1.1.2

‎demo/document-rag/backend/src/config.py‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,21 @@
33

44
load_dotenv()
55

6+
_account_id = os.getenv("ACCOUNT_ID", "")
7+
8+
69
class Config:
710
OPENAI_API_KEY: str = os.getenv("OPENAI_API_KEY", "")
11+
OPENAI_BASE_URL: str = (
12+
f"https://api.cloudflare.com/client/v4/accounts/{_account_id}/ai/v1"
13+
)
814
VORTEXDB_HOST: str = os.getenv("VORTEXDB_HOST", "localhost")
915
VORTEXDB_PORT: int = int(os.getenv("VORTEXDB_PORT", "3034"))
10-
EMBEDDING_MODEL: str = os.getenv("EMBEDDING_MODEL", "text-embedding-3-small")
11-
LLM_MODEL: str = os.getenv("LLM_MODEL", "gpt-4o-mini")
16+
EMBEDDING_MODEL: str = os.getenv("EMBEDDING_MODEL", "")
17+
LLM_MODEL: str = os.getenv("LLM_MODEL", "")
1218
CHUNK_SIZE: int = int(os.getenv("CHUNK_SIZE", "512"))
1319
CHUNK_OVERLAP: int = int(os.getenv("CHUNK_OVERLAP", "50"))
1420
TOP_K: int = int(os.getenv("TOP_K", "5"))
15-
VECTOR_SIZE: int = 1536
21+
VECTOR_SIZE: int = int(os.getenv("VECTOR_SIZE", ""))
22+
DOCS_DIRECTORY: str = os.getenv("DOCS_DIRECTORY", "./documents")
23+
EMBEDDING_BATCH_SIZE: int = int(os.getenv("EMBEDDING_BATCH_SIZE", "64"))

‎demo/document-rag/backend/src/embedder.py‎

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,31 @@
1-
import openai
2-
from openai import OpenAI
31
from typing import List
2+
3+
from openai import OpenAI
4+
45
from src.config import Config
56

67

78
class Embedder:
8-
def __init__(self, api_key: str):
9-
self.client = OpenAI(api_key=api_key)
9+
def __init__(self):
10+
self.client = OpenAI(
11+
api_key=Config.OPENAI_API_KEY,
12+
base_url=Config.OPENAI_BASE_URL,
13+
)
1014
self.model = Config.EMBEDDING_MODEL
11-
15+
1216
def embed(self, texts: List[str]) -> List[List[float]]:
1317
"""Generate embeddings for a list of texts."""
1418
if not texts:
1519
return []
16-
17-
response = self.client.embeddings.create(
18-
model=self.model,
19-
input=texts
20-
)
21-
22-
return [item.embedding for item in response.data]
23-
20+
21+
embeddings = []
22+
for i in range(0, len(texts), Config.EMBEDDING_BATCH_SIZE):
23+
batch = texts[i : i + Config.EMBEDDING_BATCH_SIZE]
24+
response = self.client.embeddings.create(model=self.model, input=batch)
25+
embeddings.extend(item.embedding for item in response.data)
26+
27+
return embeddings
28+
2429
def embed_single(self, text: str) -> List[float]:
2530
"""Generate embedding for a single text."""
2631
embeddings = self.embed([text])
Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,30 @@
1-
import openai
1+
from typing import Dict, List
2+
23
from openai import OpenAI
3-
from typing import List, Dict
4+
45
from src.config import Config
56

67

78
class Generator:
8-
def __init__(self, api_key: str):
9-
self.client = OpenAI(api_key=api_key)
9+
def __init__(self):
10+
self.client = OpenAI(
11+
api_key=Config.OPENAI_API_KEY,
12+
base_url=Config.OPENAI_BASE_URL,
13+
)
1014
self.model = Config.LLM_MODEL
11-
12-
def generate(
13-
self,
14-
question: str,
15-
context_chunks: List[Dict[str, any]]
16-
) -> str:
17-
"""
18-
Generate answer using RAG prompt.
19-
"""
15+
16+
def generate(self, question: str, context_chunks: List[Dict[str, any]]) -> str:
17+
"""Generate answer using RAG prompt."""
2018
if not context_chunks:
2119
return "No relevant documents found. Please upload a document first."
22-
23-
context_text = "\n\n".join([
24-
f"[Document {i+1}]\n{chunk['text']}"
25-
for i, chunk in enumerate(context_chunks)
26-
])
27-
20+
21+
context_text = "\n\n".join(
22+
[
23+
f"[Document {i + 1}]\n{chunk['text']}"
24+
for i, chunk in enumerate(context_chunks)
25+
]
26+
)
27+
2828
prompt = f"""You are a helpful assistant answering questions based on provided documents.
2929
3030
Context from documents:
@@ -39,15 +39,18 @@ def generate(
3939
- Cite which document(s) you're using when relevant
4040
4141
Answer:"""
42-
42+
4343
response = self.client.chat.completions.create(
4444
model=self.model,
4545
messages=[
46-
{"role": "system", "content": "You are a helpful assistant that answers questions based on provided documents."},
47-
{"role": "user", "content": prompt}
46+
{
47+
"role": "system",
48+
"content": "You are a helpful assistant that answers questions based on provided documents.",
49+
},
50+
{"role": "user", "content": prompt},
4851
],
4952
temperature=0.3,
50-
max_tokens=1000
53+
max_tokens=1000,
5154
)
52-
55+
5356
return response.choices[0].message.content

0 commit comments

Comments
 (0)