Skip to content

Commit 87223ca

Browse files
committed
add document rag demo
Signed-off-by: Arshdeep54 <balarsh535@gmail.com>
1 parent 59162e5 commit 87223ca

24 files changed

Lines changed: 3341 additions & 0 deletions

crates/http/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pub mod handler;
33
use api::VectorDb;
44
use axum::{
55
Router,
6+
extract::DefaultBodyLimit,
67
routing::{get, post},
78
};
89
use defs::BoxError;
@@ -36,6 +37,7 @@ pub fn create_router(db: Arc<VectorDb>) -> Router {
3637
.route("/points/batch", post(batch_insert_handler))
3738
.route("/points/search/batch", post(batch_search_handler))
3839
.with_state(app_state)
40+
.layer(DefaultBodyLimit::max(50 * 1024 * 1024)) // 50MB limit
3941
}
4042

4143
/// Runs the HTTP server on the specified address.

demo/document-rag/.env.example

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
OPENAI_API_KEY=sk-your-api-key-here
2+
VORTEXDB_HOST=vortexdb
3+
VORTEXDB_PORT=3034
4+
EMBEDDING_MODEL=text-embedding-3-small
5+
LLM_MODEL=gpt-4o-mini
6+
CHUNK_SIZE=512
7+
CHUNK_OVERLAP=50
8+
TOP_K=5

demo/document-rag/README.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# VectorDB RAG Demo
2+
3+
A fully containerized Document RAG demo with a dark-themed web UI. Upload documents, chat with your knowledge base.
4+
5+
## Quick Start
6+
7+
```bash
8+
# 1. Fill in your API key
9+
cp .env.example .env
10+
# Edit .env and set OPENAI_API_KEY=sk-your-key-here
11+
12+
# 2. Build and start everything
13+
docker compose up -d --build
14+
15+
# 3. Open browser
16+
open http://localhost:3035
17+
```
18+
19+
That's it! No other setup required.
20+
21+
## Features
22+
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
26+
27+
## Architecture
28+
29+
```
30+
Browser (localhost:3035) → Frontend (nginx)
31+
32+
Backend API (port 8000)
33+
34+
┌───────────────┴───────────────┐
35+
↓ ↓
36+
OpenAI API VortexDB
37+
(embeddings + LLM) (HTTP port 3000)
38+
```
39+
40+
## Configuration
41+
42+
### .env file
43+
44+
```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
50+
CHUNK_SIZE=512
51+
CHUNK_OVERLAP=50
52+
TOP_K=5
53+
```
54+
55+
## Project Structure
56+
57+
```
58+
demo/document-rag/
59+
├── docker-compose.yml
60+
├── .env.example
61+
├── README.md
62+
├── backend/
63+
│ ├── 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
71+
│ ├── requirements.txt
72+
│ └── Dockerfile
73+
└── 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
82+
└── Dockerfile
83+
```
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
FROM python:3.11-slim
2+
3+
WORKDIR /app
4+
5+
COPY requirements.txt .
6+
RUN pip install --no-cache-dir -r requirements.txt
7+
8+
COPY src/ ./src/
9+
10+
RUN mkdir -p /app/uploads
11+
12+
ENV PYTHONPATH=/app
13+
14+
EXPOSE 8000
15+
16+
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +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
9+
python-dotenv==1.0.1
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import re
2+
from typing import List
3+
4+
5+
def chunk_text(text: str, chunk_size: int = 512, chunk_overlap: int = 50) -> List[str]:
6+
"""
7+
Split text into overlapping chunks.
8+
"""
9+
if not text or not text.strip():
10+
return []
11+
12+
text = re.sub(r'\s+', ' ', text).strip()
13+
14+
chunks = []
15+
start = 0
16+
text_len = len(text)
17+
18+
while start < text_len:
19+
end = start + chunk_size
20+
chunk = text[start:end]
21+
22+
if end < text_len:
23+
last_period = chunk.rfind('. ')
24+
last_newline = chunk.rfind('\n')
25+
split_pos = max(last_period, last_newline)
26+
27+
if split_pos > chunk_size // 2:
28+
chunk = chunk[:split_pos + 1]
29+
end = start + split_pos + 1
30+
31+
chunks.append(chunk.strip())
32+
start = end - chunk_overlap if end < text_len else text_len
33+
34+
return [c for c in chunks if c]
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import os
2+
from dotenv import load_dotenv
3+
4+
load_dotenv()
5+
6+
class Config:
7+
OPENAI_API_KEY: str = os.getenv("OPENAI_API_KEY", "")
8+
VORTEXDB_HOST: str = os.getenv("VORTEXDB_HOST", "localhost")
9+
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")
12+
CHUNK_SIZE: int = int(os.getenv("CHUNK_SIZE", "512"))
13+
CHUNK_OVERLAP: int = int(os.getenv("CHUNK_OVERLAP", "50"))
14+
TOP_K: int = int(os.getenv("TOP_K", "5"))
15+
VECTOR_SIZE: int = 1536
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import openai
2+
from openai import OpenAI
3+
from typing import List
4+
from src.config import Config
5+
6+
7+
class Embedder:
8+
def __init__(self, api_key: str):
9+
self.client = OpenAI(api_key=api_key)
10+
self.model = Config.EMBEDDING_MODEL
11+
12+
def embed(self, texts: List[str]) -> List[List[float]]:
13+
"""Generate embeddings for a list of texts."""
14+
if not texts:
15+
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+
24+
def embed_single(self, text: str) -> List[float]:
25+
"""Generate embedding for a single text."""
26+
embeddings = self.embed([text])
27+
return embeddings[0] if embeddings else []
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from pathlib import Path
2+
from PyPDF2 import PdfReader
3+
import docx
4+
5+
6+
SUPPORTED_EXTENSIONS = {'.txt', '.md', '.pdf', '.docx', '.csv'}
7+
8+
9+
def extract_text(file_path: str) -> str:
10+
path = Path(file_path)
11+
ext = path.suffix.lower()
12+
13+
if ext not in SUPPORTED_EXTENSIONS:
14+
raise ValueError(f"Unsupported format: {ext}")
15+
16+
extractors = {
17+
'.txt': extract_txt,
18+
'.md': extract_markdown,
19+
'.pdf': extract_pdf,
20+
'.docx': extract_docx,
21+
'.csv': extract_csv,
22+
}
23+
24+
return extractors[ext](file_path)
25+
26+
27+
def extract_txt(file_path: str) -> str:
28+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
29+
return f.read()
30+
31+
32+
def extract_markdown(file_path: str) -> str:
33+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
34+
return f.read()
35+
36+
37+
def extract_pdf(file_path: str) -> str:
38+
reader = PdfReader(file_path)
39+
text_parts = []
40+
for page in reader.pages:
41+
text = page.extract_text()
42+
if text:
43+
text_parts.append(text)
44+
return "\n\n".join(text_parts)
45+
46+
47+
def extract_docx(file_path: str) -> str:
48+
doc = docx.Document(file_path)
49+
paragraphs = [p.text for p in doc.paragraphs if p.text.strip()]
50+
return "\n\n".join(paragraphs)
51+
52+
53+
def extract_csv(file_path: str) -> str:
54+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
55+
return f.read()
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import openai
2+
from openai import OpenAI
3+
from typing import List, Dict
4+
from src.config import Config
5+
6+
7+
class Generator:
8+
def __init__(self, api_key: str):
9+
self.client = OpenAI(api_key=api_key)
10+
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+
"""
20+
if not context_chunks:
21+
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+
28+
prompt = f"""You are a helpful assistant answering questions based on provided documents.
29+
30+
Context from documents:
31+
{context_text}
32+
33+
Question: {question}
34+
35+
Instructions:
36+
- Answer based ONLY on the context provided above
37+
- If the answer is not in the context, say "I couldn't find this information in the uploaded documents."
38+
- Be concise and helpful
39+
- Cite which document(s) you're using when relevant
40+
41+
Answer:"""
42+
43+
response = self.client.chat.completions.create(
44+
model=self.model,
45+
messages=[
46+
{"role": "system", "content": "You are a helpful assistant that answers questions based on provided documents."},
47+
{"role": "user", "content": prompt}
48+
],
49+
temperature=0.3,
50+
max_tokens=1000
51+
)
52+
53+
return response.choices[0].message.content

0 commit comments

Comments
 (0)