testtest123 commited on
Commit
653d9a8
·
1 Parent(s): decdb15

chore: add uploads and pdf files to gitignore

Browse files
.gitignore CHANGED
@@ -4,6 +4,8 @@ RAG_VENV/
4
  __pycache__/
5
  *.pyc
6
  data/bm25_indexes/
 
 
7
  *.log
8
 
9
  # --- Frontend (Vite/React) ---
 
4
  __pycache__/
5
  *.pyc
6
  data/bm25_indexes/
7
+ RAG_FULL_APPLICATION_BACKEND/data/uploads/
8
+ *.pdf
9
  *.log
10
 
11
  # --- Frontend (Vite/React) ---
RAG_FULL_APPLICATION_BACKEND/data/uploads/ffc7bc40-6339-46bd-89ac-bcda56535a39/RAG_PIPELINE_BLUEPRINT_V3.md DELETED
@@ -1,1455 +0,0 @@
1
- # 🧠 RAG Pipeline — Production Blueprint V3 (100% Free)
2
-
3
- > **Stack:** FastAPI · React · Supabase pgvector · bge-m3 (HF Space) · Qwen3 · Mistral OCR · Ernie Bot
4
- > **Deploy:** Netlify (Frontend) · Render (Backend) · Supabase (DB + Vectors)
5
- > **Cost:** $0.00
6
- > **Theme:** Green (#22C55E) + Violet (#8B5CF6)
7
-
8
- ---
9
-
10
- ## 📑 Table of Contents
11
- 1. [Full System Architecture](#1-full-system-architecture)
12
- 2. [Tech Stack — All Free](#2-tech-stack--all-free)
13
- 3. [Monorepo Structure](#3-monorepo-structure)
14
- 4. [Supabase Setup](#4-supabase-setup)
15
- 5. [Backend — FastAPI Deep Dive](#5-backend--fastapi-deep-dive)
16
- 6. [File Processing — All Types](#6-file-processing--all-types)
17
- 7. [Chunking Engine — 6 Strategies](#7-chunking-engine--6-strategies)
18
- 8. [Embedding Service](#8-embedding-service)
19
- 9. [All 8 RAG Techniques](#9-all-8-rag-techniques)
20
- 10. [Multi-User Architecture](#10-multi-user-architecture)
21
- 11. [API Endpoints](#11-api-endpoints)
22
- 12. [Frontend — React Deep Dive](#12-frontend--react-deep-dive)
23
- 13. [Docker Setup](#13-docker-setup)
24
- 14. [Environment Variables](#14-environment-variables)
25
- 15. [Deployment Guide](#15-deployment-guide)
26
- 16. [Production Additions](#16-production-additions)
27
-
28
- ---
29
-
30
- ## 1. Full System Architecture
31
-
32
- ```
33
- ┌─────────────────────────────────────────────────────┐
34
- │ NETLIFY — React Frontend │
35
- │ Upload → Technique Select → Chunk Config → Query │
36
- └────────────────────┬────────────────────────────────┘
37
- │ HTTPS + WSS
38
- ┌────────────────────▼────────────────────────────────┐
39
- │ RENDER — FastAPI Backend │
40
- │ │
41
- │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
42
- │ │ /ingest │ │ /query │ │ /evaluate│ │
43
- │ └──────────┘ └──────────┘ └──────────┘ │
44
- │ │
45
- │ ┌────────────────────────────────────────────────┐ │
46
- │ │ Core Services │ │
47
- │ │ FileParser · ChunkEngine · EmbedService │ │
48
- │ │ LLMService · OCRService · ReRankService │ │
49
- │ │ SupabaseClient · CacheService · BM25Service │ │
50
- │ └────────────────────────────────────────────────┘ │
51
- │ │
52
- │ Redis (Render free) Docker container │
53
- └──────┬──────────┬────────────────┬───────────────────┘
54
- │ │ │
55
- ▼ ▼ ▼
56
- ┌──────────┐ ┌─────────────┐ ┌──────────────────────┐
57
- │ Supabase │ │ HF Spaces │ │ HF Spaces │
58
- │ │ │ │ │ │
59
- │ pgvector │ │ bge-m3 │ │ Qwen3 (LLM) │
60
- │ postgres │ │ embeddings │ │ Mistral OCR (PDF/img) │
61
- │ metadata │ │ (free) │ │ Ernie Bot (images) │
62
- │ users │ │ │ │ │
63
- │ chunks │ │ 1K tok cap │ │ │
64
- │ cache │ └─────────────┘ └──────────────────────┘
65
- └──────────┘
66
- ```
67
-
68
- ---
69
-
70
- ## 2. Tech Stack — All Free
71
-
72
- | Layer | Technology | Free Tier |
73
- |-------|-----------|-----------|
74
- | Vector DB | Supabase pgvector | 500MB, unlimited rows |
75
- | Metadata DB | Supabase PostgreSQL | Same instance |
76
- | Embeddings | `lamhieu-lightweight-embeddings.hf.space` bge-m3 | Free HF Space |
77
- | LLM | Qwen3 `Qwen/Qwen3-Demo` | Free HF Space |
78
- | PDF/Image OCR | Mistral OCR `tatendachirume/Mistral-OCR` | Free HF Space |
79
- | Image Understanding | Ernie Bot `baidu-simple-ernie-bot-demo` | Free HF Space |
80
- | Re-ranking | `cross-encoder/ms-marco-MiniLM-L-6-v2` | Runs on Render CPU |
81
- | Backend | Render free tier | 512MB RAM |
82
- | Frontend | Netlify free tier | 100GB bandwidth |
83
- | Cache | Render Redis free | 25MB |
84
- | Containers | Docker + docker-compose | Local dev |
85
-
86
- ---
87
-
88
- ## 3. Monorepo Structure
89
-
90
- ```
91
- rag-pipeline/
92
-
93
- ├── backend/ ← Render deployment
94
- │ ├── app/
95
- │ │ ├── main.py # FastAPI app factory
96
- │ │ ├── config.py # pydantic-settings
97
- │ │ ├── dependencies.py # DI: supabase, redis, etc.
98
- │ │ │
99
- │ │ ├── routers/
100
- │ │ │ ├── auth.py # register, login, refresh
101
- │ │ │ ├── ingest.py # upload, status, documents
102
- │ │ │ ├── query.py # search, history, cache
103
- │ │ │ ├── techniques.py # list techniques
104
- │ │ │ ├── evaluate.py # RAGAs run + report
105
- │ │ │ └── stats.py # index stats
106
- │ │ │
107
- │ │ ├── services/
108
- │ │ │ ├── supabase_client.py # Supabase vector + metadata ops
109
- │ │ │ ├── embed_service.py # bge-m3 via HF Space
110
- │ │ │ ├── llm_service.py # Qwen3 (your existing code)
111
- │ │ │ ├── ocr_service.py # Mistral OCR (your existing code)
112
- │ │ │ ├── ernie_service.py # Ernie Bot (your existing code)
113
- │ │ │ ├── file_parser.py # dispatcher for all file types
114
- │ │ │ ├── chunk_engine.py # 6 chunking strategies
115
- │ │ │ ├── bm25_service.py # keyword search (rank_bm25)
116
- │ │ │ ├── rerank_service.py # cross-encoder re-ranking
117
- │ │ │ └── cache_service.py # Redis query cache
118
- │ │ │
119
- │ │ ├── techniques/
120
- │ │ │ ├── base.py # abstract base + emit_step
121
- │ │ │ ├── hybrid_search.py # BM25 + pgvector → RRF
122
- │ │ │ ├── reranking.py # ANN → cross-encoder
123
- │ │ │ ├── query_expansion.py # HyDE + multi-query
124
- │ │ │ ├── metadata_filter.py # SQL filter + vector search
125
- │ │ │ ├── colbert.py # token-level MaxSim
126
- │ │ │ ├── agentic_rag.py # Qwen3 tool-calling agent
127
- │ │ │ ├── cache_incremental.py # Redis cache + delta ingest
128
- │ │ │ └── ragas_eval.py # RAGAs evaluation
129
- │ │ │
130
- │ │ ├── models/
131
- │ │ │ ├── schemas.py # Pydantic request/response
132
- │ │ │ └── enums.py # TechniqueType, FileType, etc.
133
- │ │ │
134
- │ │ └── utils/
135
- │ │ ├── logger.py # print_with_time (loguru)
136
- │ │ ├── json_utils.py # extract_json_block, repair_json
137
- │ │ ├── retry_utils.py # thread timeout + retry decorator
138
- │ │ ├── hash_utils.py # SHA-256 chunk hashing
139
- │ │ └── ws_manager.py # WebSocket multi-user manager
140
- │ │
141
- │ ├── tests/
142
- │ │ ├── test_ingest.py
143
- │ │ ├── test_query.py
144
- │ │ ├── test_techniques.py
145
- │ │ └── test_parsers.py
146
- │ │
147
- │ ├── requirements.txt
148
- │ ├── Dockerfile # Render uses this
149
- │ └── .env.example # key names only, no values
150
-
151
- ├── frontend/ ← Netlify deployment
152
- │ ├── src/
153
- │ │ ├── main.jsx
154
- │ │ ├── App.jsx
155
- │ │ ├── pages/
156
- │ │ │ ├── LandingPage.jsx # auth + hero (green/violet)
157
- │ │ │ ├── DashboardPage.jsx # document list
158
- │ │ │ ├── PipelinePage.jsx # main RAG UI
159
- │ │ │ └── EvaluatePage.jsx # RAGAs metrics dashboard
160
- │ │ ├── components/
161
- │ │ │ ├── upload/
162
- │ │ │ │ ├── FileDropZone.jsx # drag & drop, all file types
163
- │ │ │ │ └── UploadProgress.jsx
164
- │ │ │ ├── pipeline/
165
- │ │ │ │ ├── PipelineVisualizer.jsx # animated step trace
166
- │ │ │ │ ├── StepCard.jsx # green/violet step cards
167
- │ │ │ │ ├── ChunkSliders.jsx # chunk + overlap sliders
168
- │ │ │ │ └── TechniqueSelector.jsx # 8 technique cards
169
- │ │ │ ├── query/
170
- │ │ │ │ ├── QueryInput.jsx
171
- │ │ │ │ ├── AnswerPanel.jsx
172
- │ │ │ │ └── SourceChunks.jsx
173
- │ │ │ ├── auth/
174
- │ │ │ │ ├── LoginForm.jsx
175
- │ │ │ │ └── RegisterForm.jsx
176
- │ │ │ └── evaluate/
177
- │ │ │ ├── MetricsRadar.jsx # Recharts radar chart
178
- │ │ │ └── EvalTable.jsx
179
- │ │ ├── store/
180
- │ │ │ ├── authStore.js # JWT in-memory (NOT localStorage)
181
- │ │ │ ├── pipelineStore.js
182
- │ │ ��� └── documentStore.js
183
- │ │ ├── hooks/
184
- │ │ │ ├── useAuth.js
185
- │ │ │ ├── useUpload.js
186
- │ │ │ ├── useQuery.js
187
- │ │ │ └── usePipelineWS.js # WebSocket real-time steps
188
- │ │ ├── api/
189
- │ │ │ └── client.js # Axios + JWT interceptor
190
- │ │ └── utils/
191
- │ │ ├── stepColors.js # step → green/violet colors
192
- │ │ └── fileIcons.js
193
- │ ├── package.json
194
- │ ├── vite.config.js
195
- │ ├── tailwind.config.js # green + violet theme
196
- │ ├── netlify.toml
197
- │ └── .env.example
198
-
199
- ├── docker-compose.yml ← Local dev only
200
- ├── .gitignore
201
- └── README.md
202
- ```
203
-
204
- ---
205
-
206
- ## 4. Supabase Setup
207
-
208
- ### Why Supabase (not raw PostgreSQL)
209
-
210
- - Free 500MB, no credit card
211
- - pgvector built-in (vector similarity search)
212
- - Replaces both FAISS and SQLite in one service
213
- - REST + Python client available
214
-
215
- ### Database Schema (all tables in one Supabase project)
216
-
217
- ```sql
218
- -- Users (multi-user support)
219
- CREATE TABLE users (
220
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
221
- username TEXT UNIQUE NOT NULL,
222
- password_hash TEXT NOT NULL,
223
- created_at TIMESTAMPTZ DEFAULT NOW()
224
- );
225
-
226
- -- Documents (one row per uploaded file)
227
- CREATE TABLE documents (
228
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
229
- user_id UUID REFERENCES users(id) ON DELETE CASCADE,
230
- filename TEXT NOT NULL,
231
- file_type TEXT NOT NULL,
232
- technique TEXT NOT NULL,
233
- chunk_strategy TEXT NOT NULL,
234
- chunk_size INT DEFAULT 512,
235
- overlap INT DEFAULT 64,
236
- status TEXT DEFAULT 'pending', -- pending|running|done|failed
237
- chunk_count INT DEFAULT 0,
238
- created_at TIMESTAMPTZ DEFAULT NOW(),
239
- updated_at TIMESTAMPTZ DEFAULT NOW()
240
- );
241
-
242
- -- Chunks (text + metadata per chunk)
243
- CREATE TABLE chunks (
244
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
245
- document_id UUID REFERENCES documents(id) ON DELETE CASCADE,
246
- user_id UUID REFERENCES users(id) ON DELETE CASCADE,
247
- text TEXT NOT NULL,
248
- token_count INT,
249
- source TEXT, -- original filename
250
- page INT, -- page number (PDF)
251
- section TEXT, -- heading (DOCX/MD)
252
- chunk_index INT,
253
- parent_chunk_id UUID, -- for parent-child chunking
254
- text_hash TEXT, -- SHA-256 for incremental ingest
255
- metadata JSONB DEFAULT '{}',
256
- created_at TIMESTAMPTZ DEFAULT NOW()
257
- );
258
-
259
- -- Vectors (pgvector — bge-m3 dim=1024)
260
- CREATE TABLE chunk_vectors (
261
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
262
- chunk_id UUID REFERENCES chunks(id) ON DELETE CASCADE,
263
- document_id UUID REFERENCES documents(id) ON DELETE CASCADE,
264
- user_id UUID REFERENCES users(id) ON DELETE CASCADE,
265
- embedding vector(1024) NOT NULL
266
- );
267
-
268
- -- HNSW index for fast ANN search
269
- CREATE INDEX ON chunk_vectors
270
- USING hnsw (embedding vector_cosine_ops)
271
- WITH (m = 16, ef_construction = 64);
272
-
273
- -- ColBERT token vectors (only populated when ColBERT technique used)
274
- CREATE TABLE colbert_tokens (
275
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
276
- chunk_id UUID REFERENCES chunks(id) ON DELETE CASCADE,
277
- token_text TEXT,
278
- position INT,
279
- embedding vector(1024) NOT NULL
280
- );
281
-
282
- -- Query cache (also stored in Redis, Supabase as overflow)
283
- CREATE TABLE query_cache (
284
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
285
- user_id UUID REFERENCES users(id) ON DELETE CASCADE,
286
- document_id UUID REFERENCES documents(id) ON DELETE CASCADE,
287
- query_hash TEXT NOT NULL,
288
- query_text TEXT,
289
- answer TEXT,
290
- sources JSONB,
291
- technique TEXT,
292
- hit_count INT DEFAULT 0,
293
- created_at TIMESTAMPTZ DEFAULT NOW()
294
- );
295
-
296
- -- RAGAs evaluation reports
297
- CREATE TABLE eval_reports (
298
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
299
- user_id UUID REFERENCES users(id) ON DELETE CASCADE,
300
- document_id UUID REFERENCES documents(id) ON DELETE CASCADE,
301
- faithfulness FLOAT,
302
- answer_relevancy FLOAT,
303
- context_precision FLOAT,
304
- context_recall FLOAT,
305
- per_question JSONB,
306
- created_at TIMESTAMPTZ DEFAULT NOW()
307
- );
308
- ```
309
-
310
- ### Supabase Vector Search Function
311
-
312
- ```sql
313
- -- Used by all retrieval techniques
314
- CREATE OR REPLACE FUNCTION match_chunks(
315
- query_embedding vector(1024),
316
- match_document_id UUID,
317
- match_user_id UUID,
318
- match_count INT DEFAULT 5,
319
- filter_chunk_ids UUID[] DEFAULT NULL
320
- )
321
- RETURNS TABLE (
322
- chunk_id UUID,
323
- text TEXT,
324
- source TEXT,
325
- page INT,
326
- section TEXT,
327
- metadata JSONB,
328
- similarity FLOAT
329
- )
330
- LANGUAGE plpgsql
331
- AS $$
332
- BEGIN
333
- RETURN QUERY
334
- SELECT
335
- c.id,
336
- c.text,
337
- c.source,
338
- c.page,
339
- c.section,
340
- c.metadata,
341
- 1 - (cv.embedding <=> query_embedding) AS similarity
342
- FROM chunk_vectors cv
343
- JOIN chunks c ON c.id = cv.chunk_id
344
- WHERE cv.document_id = match_document_id
345
- AND cv.user_id = match_user_id
346
- AND (filter_chunk_ids IS NULL OR c.id = ANY(filter_chunk_ids))
347
- ORDER BY cv.embedding <=> query_embedding
348
- LIMIT match_count;
349
- END;
350
- $$;
351
- ```
352
-
353
- ---
354
-
355
- ## 5. Backend — FastAPI Deep Dive
356
-
357
- ### `app/main.py`
358
-
359
- ```python
360
- # Key responsibilities:
361
- # - FastAPI app with CORS for Netlify origin
362
- # - Mount all routers
363
- # - Startup: init Supabase client, Redis, load cross-encoder
364
- # - Shutdown: flush Redis pipeline
365
- # - WebSocket: /ws/pipeline/{job_id}?token={jwt}
366
-
367
- app = FastAPI(title="RAG Pipeline API", version="3.0.0")
368
-
369
- # CORS — Netlify + local dev
370
- origins = settings.CORS_ORIGINS.split(",")
371
- app.add_middleware(CORSMiddleware, allow_origins=origins,
372
- allow_methods=["*"], allow_headers=["*"])
373
-
374
- # Routers
375
- app.include_router(auth_router, prefix="/auth")
376
- app.include_router(ingest_router, prefix="/ingest")
377
- app.include_router(query_router, prefix="/query")
378
- app.include_router(technique_router, prefix="/techniques")
379
- app.include_router(evaluate_router, prefix="/evaluate")
380
- app.include_router(stats_router, prefix="/stats")
381
-
382
- @app.websocket("/ws/pipeline/{job_id}")
383
- async def pipeline_ws(websocket, job_id, token):
384
- # Verify JWT, then stream pipeline step events
385
- ...
386
- ```
387
-
388
- ### `app/config.py`
389
-
390
- ```python
391
- class Settings(BaseSettings):
392
- # Supabase
393
- SUPABASE_URL: str # https://xxxx.supabase.co
394
- SUPABASE_KEY: str # anon/service_role key
395
- SUPABASE_DB_URL: str # postgresql://... (direct connection)
396
-
397
- # Embeddings (your existing HF Space)
398
- EMBED_API_URL: str = "https://lamhieu-lightweight-embeddings.hf.space/"
399
- EMBED_MODEL: str = "bge-m3"
400
- EMBED_DIM: int = 1024
401
- EMBED_AUTH_KEY: str = ""
402
- EMBED_MAX_TOKENS: int = 1000 # hard cap — 1K context
403
- EMBED_TIMEOUT: int = 60
404
- EMBED_MAX_RETRIES: int = 3
405
-
406
- # LLM — Qwen3
407
- QWEN3_MODEL_NAME: str = "Qwen/Qwen3-Demo"
408
- QWEN3_THINKING_BUDGET: int = 38
409
- LLM_RESPONSE_TIMEOUT: int = 1080
410
- MAX_LLM_RETRIES: int = 5
411
- MAX_TIMEOUT_RETRIES: int = 10
412
-
413
- # OCR — Mistral
414
- MISTRAL_OCR_SPACE: str = "tatendachirume/Mistral-OCR"
415
- MISTRAL_API_KEY: str
416
-
417
- # Image — Ernie Bot
418
- ERNIE_SPACE_URL: str = "https://baidu-simple-ernie-bot-demo.hf.space/"
419
-
420
- # Redis
421
- REDIS_URL: str
422
- CACHE_TTL_SECONDS: int = 3600
423
-
424
- # Auth
425
- JWT_SECRET_KEY: str
426
- JWT_ALGORITHM: str = "HS256"
427
- JWT_EXPIRE_MINUTES: int = 1440
428
-
429
- # Re-ranking
430
- RERANK_MODEL: str = "cross-encoder/ms-marco-MiniLM-L-6-v2"
431
-
432
- # Rate limiting
433
- RATE_LIMIT_PER_MINUTE: int = 20
434
- RATE_LIMIT_UPLOAD_PER_DAY: int = 50
435
-
436
- # Defaults
437
- DEFAULT_CHUNK_SIZE: int = 512
438
- DEFAULT_OVERLAP: int = 64
439
- DEFAULT_TOP_K: int = 5
440
- MAX_FILE_SIZE_MB: int = 50
441
-
442
- # CORS
443
- CORS_ORIGINS: str # comma-separated
444
- ```
445
-
446
- ### `app/services/supabase_client.py`
447
-
448
- ```python
449
- """
450
- Central Supabase service.
451
- Handles: vector upsert, ANN search, chunk CRUD, metadata queries.
452
- Uses supabase-py client + asyncpg for direct SQL when needed.
453
- """
454
- from supabase import create_client, Client
455
-
456
- class SupabaseService:
457
- def __init__(self):
458
- self.client: Client = create_client(
459
- settings.SUPABASE_URL, settings.SUPABASE_KEY
460
- )
461
-
462
- # ── Chunk Operations ────────────────────────────────────────────────
463
- async def insert_chunks(self, chunks: list[dict]) -> list[str]:
464
- """Insert chunks, return list of chunk_ids"""
465
-
466
- async def get_chunks_by_ids(self, chunk_ids: list[str]) -> list[dict]:
467
- """Fetch chunk text + metadata by IDs"""
468
-
469
- async def get_chunk_hashes(self, document_id: str) -> dict[str, str]:
470
- """Returns {chunk_index: text_hash} for incremental ingest"""
471
-
472
- async def delete_chunks(self, chunk_ids: list[str]):
473
- """Delete chunks + their vectors (CASCADE)"""
474
-
475
- # ── Vector Operations ───────────────────────────────────────────────
476
- async def upsert_vectors(self, vectors: list[dict]):
477
- """
478
- vectors: [{"chunk_id": uuid, "document_id": uuid,
479
- "user_id": uuid, "embedding": [...1024 floats...]}]
480
- """
481
-
482
- async def vector_search(self, query_embedding: list[float],
483
- document_id: str, user_id: str,
484
- top_k: int, filter_chunk_ids: list = None
485
- ) -> list[dict]:
486
- """
487
- Calls match_chunks() SQL function.
488
- Returns: [{chunk_id, text, source, page, section, metadata, similarity}]
489
- """
490
- result = self.client.rpc("match_chunks", {
491
- "query_embedding": query_embedding,
492
- "match_document_id": document_id,
493
- "match_user_id": user_id,
494
- "match_count": top_k,
495
- "filter_chunk_ids": filter_chunk_ids
496
- }).execute()
497
- return result.data
498
-
499
- # ── Metadata Filter ─────────────────────────────────────────────────
500
- async def filter_chunk_ids(self, document_id: str, filters: dict) -> list[str]:
501
- """
502
- Filter chunks by metadata fields.
503
- filters: {"page": {"gte": 5, "lte": 10}, "section": "Intro"}
504
- Returns list of chunk_ids matching the filter.
505
- """
506
-
507
- # ── ColBERT Token Vectors ───────────────────────────────────────────
508
- async def insert_colbert_tokens(self, token_rows: list[dict]):
509
- """Store token-level vectors for ColBERT technique"""
510
-
511
- async def get_colbert_tokens(self, document_id: str) -> list[dict]:
512
- """Fetch all token vectors for MaxSim scoring"""
513
-
514
- # ── Cache ────────────────────────────────────────────────────────────
515
- async def get_cached_query(self, user_id: str,
516
- document_id: str, query_hash: str) -> dict | None:
517
- """Check Supabase query_cache table (overflow from Redis)"""
518
-
519
- async def store_cached_query(self, cache_row: dict):
520
- """Store answer in query_cache table"""
521
- ```
522
-
523
- ### `app/services/embed_service.py` — Your exact code, integrated
524
-
525
- ```python
526
- """
527
- Direct port of your get_embedding_with_retry() function.
528
- Extended to support batch embedding for ingestion.
529
- 1K token hard cap applied before every call.
530
- """
531
- import tiktoken
532
- enc = tiktoken.get_encoding("cl100k_base")
533
-
534
- def truncate_to_1k(text: str) -> str:
535
- tokens = enc.encode(text)
536
- return enc.decode(tokens[:1000]) if len(tokens) > 1000 else text
537
-
538
- def get_embedding(text: str) -> list[float]:
539
- """
540
- Your existing get_embedding_with_retry() — unchanged.
541
- Truncates to 1K tokens before calling HF Space.
542
- Model: bge-m3, dim: 1024
543
- """
544
- text = truncate_to_1k(text)
545
- # ... your exact code from get_embedding_with_retry()
546
-
547
- async def embed_batch(texts: list[str]) -> list[list[float]]:
548
- """
549
- Batch embedding for ingestion.
550
- Processes sequentially in groups of 8 (HF Space rate limit safety).
551
- Each text truncated to 1K tokens.
552
- """
553
- all_embeddings = []
554
- for i in range(0, len(texts), 8):
555
- batch = [truncate_to_1k(t) for t in texts[i:i+8]]
556
- for text in batch:
557
- emb = get_embedding(text)
558
- all_embeddings.append(emb)
559
- return all_embeddings
560
- ```
561
-
562
- ---
563
-
564
- ## 6. File Processing — All Types
565
-
566
- ```
567
- PDF → Mistral OCR (your perform_ocr()) → text per page
568
- JPG/PNG/JPEG → Ernie Bot (your ernie code) → image description text
569
- DOCX → python-docx → paragraphs by heading
570
- TXT → raw read → paragraph split
571
- MD → regex heading split → section chunks
572
- JSON → flatten keys/values → one text per item
573
- ```
574
-
575
- ### `app/services/file_parser.py`
576
-
577
- ```python
578
- async def parse_file(file_path, file_type, job_id, ws_manager) -> list[dict]:
579
- """
580
- Returns: [{"text": str, "metadata": {"source", "page", "section"}}]
581
- Emits WebSocket steps for every file type.
582
- """
583
- match file_type:
584
- case "pdf":
585
- return await parse_pdf(file_path, job_id, ws_manager)
586
- case "jpg" | "jpeg" | "png":
587
- return await parse_image(file_path, job_id, ws_manager)
588
- case "docx":
589
- return parse_docx(file_path)
590
- case "txt":
591
- return parse_txt(file_path)
592
- case "md":
593
- return parse_markdown(file_path)
594
- case "json":
595
- return parse_json(file_path)
596
-
597
- # PDF — uses your perform_ocr() unchanged
598
- async def parse_pdf(file_path, job_id, ws_manager):
599
- await ws_manager.emit(job_id, step="OCR_START", color="#8B5CF6",
600
- detail=f"Sending to Mistral OCR...")
601
- plain_text, markdown_text, images = perform_ocr(
602
- file_path, api_key=settings.MISTRAL_API_KEY)
603
- await ws_manager.emit(job_id, step="OCR_DONE", color="#22C55E",
604
- detail=f"OCR complete: {len(plain_text)} chars")
605
- return split_to_pages(plain_text, markdown_text, str(file_path))
606
-
607
- # Image — uses your Ernie Bot code unchanged
608
- async def parse_image(file_path, job_id, ws_manager):
609
- await ws_manager.emit(job_id, step="IMAGE_ANALYZE", color="#8B5CF6",
610
- detail="Ernie Bot analyzing image...")
611
- description = understand_image(file_path)
612
- return [{"text": description, "metadata": {"source": str(file_path), "page": 1}}]
613
-
614
- # DOCX — python-docx, split by headings
615
- def parse_docx(file_path):
616
- doc = Document(file_path)
617
- sections, current_heading, current_text = [], "", []
618
- for para in doc.paragraphs:
619
- if para.style.name.startswith('Heading'):
620
- if current_text:
621
- sections.append({"text": " ".join(current_text),
622
- "metadata": {"source": str(file_path),
623
- "section": current_heading}})
624
- current_heading, current_text = para.text, []
625
- elif para.text.strip():
626
- current_text.append(para.text)
627
- if current_text:
628
- sections.append({"text": " ".join(current_text),
629
- "metadata": {"source": str(file_path),
630
- "section": current_heading}})
631
- return sections
632
-
633
- # MD — split at headings
634
- def parse_markdown(file_path):
635
- text = Path(file_path).read_text(encoding="utf-8")
636
- parts = re.split(r'\n(?=#+\s)', text)
637
- return [{"text": p.strip(), "metadata": {"source": str(file_path),
638
- "section": re.match(r'^#+\s+(.*)', p).group(1) if re.match(r'^#+\s', p) else ""}}
639
- for p in parts if p.strip()]
640
-
641
- # TXT — paragraph split
642
- def parse_txt(file_path):
643
- text = Path(file_path).read_text(encoding="utf-8")
644
- paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()]
645
- return [{"text": p, "metadata": {"source": str(file_path)}} for p in paragraphs]
646
-
647
- # JSON — flatten per item
648
- def parse_json(file_path):
649
- data = json.loads(Path(file_path).read_text())
650
- items = data if isinstance(data, list) else [data]
651
- docs = []
652
- for item in items:
653
- def flatten(obj, prefix=""):
654
- parts = []
655
- for k, v in obj.items() if isinstance(obj, dict) else enumerate(obj):
656
- full_key = f"{prefix}.{k}" if prefix else str(k)
657
- if isinstance(v, (dict, list)):
658
- parts.extend(flatten(v, full_key))
659
- else:
660
- parts.append(f"{full_key}: {v}")
661
- return parts
662
- text = " | ".join(flatten(item))
663
- docs.append({"text": text, "metadata": {"source": str(file_path),
664
- "original": item}})
665
- return docs
666
- ```
667
-
668
- ---
669
-
670
- ## 7. Chunking Engine — 6 Strategies
671
-
672
- ```python
673
- """
674
- All strategies hard-cap at 1K tokens per chunk.
675
- bge-m3 recommended context: up to 8192, but we cap at 1K for speed/cost.
676
- """
677
- MAX_CHUNK_TOKENS = 1000
678
-
679
- class ChunkEngine:
680
- def __init__(self, chunk_size: int, overlap: int, strategy: str):
681
- self.chunk_size = min(chunk_size, MAX_CHUNK_TOKENS)
682
- self.overlap = min(overlap, self.chunk_size // 4)
683
- self.strategy = strategy
684
- self.enc = tiktoken.get_encoding("cl100k_base")
685
-
686
- def chunk(self, docs: list[dict]) -> list[dict]:
687
- # Each output chunk:
688
- # {chunk_id, text, token_count, source, page, section,
689
- # chunk_index, parent_chunk_id, text_hash, metadata}
690
- match self.strategy:
691
- case "fixed": return self._fixed(docs)
692
- case "semantic": return self._semantic(docs)
693
- case "per_page": return self._per_page(docs)
694
- case "per_item": return self._per_item(docs)
695
- case "recursive": return self._recursive(docs)
696
- case "parent_child": return self._parent_child(docs)
697
-
698
- def _fixed(self, docs):
699
- """Sliding window: step = chunk_size - overlap. Token-accurate."""
700
-
701
- def _semantic(self, docs):
702
- """Use heading sections as natural boundaries. Fixed fallback if too large."""
703
-
704
- def _per_page(self, docs):
705
- """One chunk per PDF page. Fixed fallback for long pages."""
706
-
707
- def _per_item(self, docs):
708
- """One chunk per JSON item (parser already splits)."""
709
-
710
- def _recursive(self, docs):
711
- """Split at: \\n\\n → \\n → '. ' → ' ' until fits in chunk_size."""
712
-
713
- def _parent_child(self, docs):
714
- """
715
- child: chunk_size // 4 tokens → stored in Supabase, used for retrieval
716
- parent: chunk_size tokens → stored in Supabase, sent to LLM
717
- child.parent_chunk_id → parent.id
718
- """
719
- ```
720
-
721
- ---
722
-
723
- ## 8. Embedding Service
724
-
725
- ```python
726
- # app/services/embed_service.py
727
- # Your exact get_embedding_with_retry() function — zero changes
728
- # Calling convention matches your existing code:
729
- #
730
- # get_embedding_with_retry(
731
- # text=text,
732
- # model="bge-m3",
733
- # auth_key=settings.EMBED_AUTH_KEY,
734
- # max_retries=settings.EMBED_MAX_RETRIES,
735
- # timeout_seconds=settings.EMBED_TIMEOUT
736
- # )
737
- #
738
- # Returns: {"data": [[...1024 floats...]], "usage": {...}}
739
- # We extract: result["data"][0]
740
- #
741
- # 1K token truncation applied BEFORE calling — see truncate_to_1k()
742
- ```
743
-
744
- ---
745
-
746
- ## 9. All 8 RAG Techniques
747
-
748
- ### Base class
749
-
750
- ```python
751
- # app/techniques/base.py
752
- class BaseRAGTechnique(ABC):
753
- def __init__(self, supabase, embed_svc, llm_svc, redis, job_id, ws_manager):
754
- ...
755
-
756
- @abstractmethod
757
- async def retrieve(self, query, document_id, user_id, top_k, **kwargs) -> list[dict]:
758
- ...
759
-
760
- @abstractmethod
761
- async def generate(self, query, chunks) -> str:
762
- ...
763
-
764
- async def run(self, request: QueryRequest) -> QueryResponse:
765
- chunks = await self.retrieve(...)
766
- answer = await self.generate(...)
767
- return QueryResponse(...)
768
-
769
- async def emit(self, step, status, color, detail, metadata={}):
770
- """Broadcast step event to frontend via WebSocket"""
771
- await ws_manager.emit(self.job_id, {
772
- "step": step, "status": status,
773
- "color": color, "detail": detail,
774
- "timestamp": datetime.utcnow().isoformat(),
775
- "metadata": metadata
776
- })
777
- ```
778
-
779
- ---
780
-
781
- ### Technique 1 — Hybrid Search
782
-
783
- ```python
784
- # Algorithm: BM25 keyword + pgvector ANN → Reciprocal Rank Fusion (k=60)
785
- # BM25 index built from chunk texts at ingest time, stored as pickle on Render disk
786
-
787
- # Steps emitted:
788
- # 🟣 EMBED "Embedding query (bge-m3)..."
789
- # 🟢 BM25 "BM25 keyword search → {n} candidates"
790
- # 🟢 VECTOR "pgvector ANN search → top-{n}"
791
- # 🟣 RRF "Reciprocal Rank Fusion merging results..."
792
- # 🟢 DONE "Hybrid search → top-{k} returned"
793
-
794
- async def retrieve(self, query, document_id, user_id, top_k, bm25_weight=0.5):
795
- q_vec = get_embedding(truncate_to_1k(query))
796
- bm25_results = bm25_service.search(document_id, query, top_n=top_k * 4)
797
- vector_results = await supabase.vector_search(q_vec, document_id, user_id, top_k * 4)
798
- fused = reciprocal_rank_fusion(bm25_results, vector_results, k=60)
799
- return fused[:top_k]
800
- ```
801
-
802
- ---
803
-
804
- ### Technique 2 — Re-ranking
805
-
806
- ```python
807
- # Algorithm: pgvector top-20 → cross-encoder/ms-marco-MiniLM-L-6-v2 → top-K
808
- # Cross-encoder runs on Render CPU. ~3-8s for 20 pairs. Model cached after first load.
809
-
810
- # Steps emitted:
811
- # 🟣 EMBED "Embedding query..."
812
- # 🟢 RETRIEVE "pgvector: fetching top-20 candidates..."
813
- # 🔴 RERANK "Cross-encoder re-scoring 20 pairs..."
814
- # 🟢 DONE "Re-ranked → top-{k}"
815
-
816
- async def retrieve(self, query, document_id, user_id, top_k):
817
- q_vec = get_embedding(truncate_to_1k(query))
818
- candidates = await supabase.vector_search(q_vec, document_id, user_id, top_k * 4)
819
- pairs = [(query, c["text"]) for c in candidates]
820
- scores = cross_encoder.predict(pairs)
821
- reranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)
822
- return [c for c, _ in reranked[:top_k]]
823
- ```
824
-
825
- ---
826
-
827
- ### Technique 3 — Query Expansion (HyDE)
828
-
829
- ```python
830
- # Algorithm:
831
- # 1. Qwen3 generates hypothetical answer → embed it (HyDE)
832
- # 2. Qwen3 generates 3 query variants → embed each
833
- # 3. FAISS search with all 4 vectors, deduplicate, rank
834
-
835
- # Steps emitted:
836
- # 🟣 HYDE "Qwen3 generating hypothetical answer..."
837
- # 🟣 EXPAND "Generating 3 query variants..."
838
- # 🟢 EMBED "Embedding 4 expanded queries..."
839
- # 🟢 SEARCH "pgvector search with all variants..."
840
- # 🟣 MERGE "Deduplicating {n} results..."
841
- # 🟢 DONE "Query expansion → top-{k}"
842
- ```
843
-
844
- ---
845
-
846
- ### Technique 4 — Metadata Filtering
847
-
848
- ```python
849
- # Algorithm:
850
- # 1. User sets filters (page range, section, source file, custom JSON fields)
851
- # 2. Supabase SQL pre-filters chunk IDs
852
- # 3. pgvector search restricted to those IDs
853
-
854
- # Supported filters:
855
- # page: {gte: 5, lte: 10}
856
- # section: "Introduction"
857
- # source: "contract.docx"
858
- # file_type: "pdf"
859
- # metadata->>'custom_key': "value" (JSONB field)
860
-
861
- # Steps emitted:
862
- # 🟤 FILTER "SQL filter: {filters} → {n} qualifying chunks"
863
- # 🟣 EMBED "Embedding query..."
864
- # 🟢 SEARCH "pgvector search in filtered subset..."
865
- # 🟢 DONE "Metadata-filtered → top-{k}"
866
- ```
867
-
868
- ---
869
-
870
- ### Technique 5 — ColBERT (Multi-vector MaxSim)
871
-
872
- ```python
873
- # Algorithm:
874
- # INGEST: each chunk → tokenize → embed each token → store in colbert_tokens table
875
- # QUERY: tokenize query → embed each token → MaxSim scoring
876
- # MaxSim(q,d) = Σ max_j(q_i · d_j) for each query token i
877
-
878
- # ⚠️ WARNING shown in UI before selecting:
879
- # "ColBERT embeds every token individually. For a 50-chunk doc,
880
- # expect 500-5000 extra embedding calls. Ingestion will be slow."
881
-
882
- # Steps emitted:
883
- # 🟣 TOKENIZE "Tokenizing query into {n} tokens..."
884
- # 🟢 EMBED_TOK "Embedding {n} query tokens (bge-m3)..."
885
- # 🔴 MAXSIM "MaxSim scoring {n_chunks} × {n_tokens} token vectors..."
886
- # 🟢 DONE "ColBERT scoring → top-{k}"
887
- ```
888
-
889
- ---
890
-
891
- ### Technique 6 — Agentic RAG
892
-
893
- ```python
894
- # Algorithm: Qwen3 agent with 4 tools, max 5 iterations
895
- # Tools:
896
- # search_docs(query, top_k) → pgvector search
897
- # filter_search(filters, query) → metadata-filtered search
898
- # get_page(page_num) → retrieve specific page
899
- # summarize_chunks(chunk_ids) → Qwen3 summarizes chunk set
900
-
901
- # Uses your existing Qwen3 wrapper (llm_service.py)
902
- # Tool call JSON parsed with your extract_json_block() + repair_json_with_module()
903
-
904
- # Steps emitted (one per agent iteration):
905
- # 🟢 AGENT_INIT "Qwen3 agent ready with 4 tools"
906
- # 🟣 PLAN "Agent: '{thought[:80]}...'"
907
- # 🟤 TOOL "Tool call: {tool_name}({args})"
908
- # 🟢 OBSERVE "Tool returned {n} chunks"
909
- # 🟢 FINAL "Answer generated after {n} tool calls"
910
- ```
911
-
912
- ---
913
-
914
- ### Technique 7 — Caching & Incremental Ingestion
915
-
916
- ```python
917
- # SUB-FEATURE A — Redis Query Cache:
918
- # key = SHA-256(user_id + document_id + query + technique)
919
- # hit → return stored QueryResponse instantly
920
- # miss → run pipeline → store in Redis (TTL: 1hr) + Supabase overflow
921
- #
922
- # SUB-FEATURE B — Incremental Ingestion:
923
- # On re-upload: hash each chunk text
924
- # Compare vs stored hashes in Supabase chunks table
925
- # NEW chunks → embed + insert to Supabase
926
- # CHANGED chunks → delete old vectors, re-embed, insert new
927
- # UNCHANGED → skip entirely (0 embedding calls)
928
- # DELETED chunks → remove from Supabase (CASCADE deletes vectors)
929
- # Saves 80-95% of embedding calls on document updates
930
-
931
- # Steps emitted:
932
- # 🟤 CACHE_CHECK "Checking Redis cache..."
933
- # 🟢 CACHE_HIT "Cache hit — returning stored answer (0ms)" OR
934
- # 🟣 CACHE_MISS "Cache miss. Running pipeline..."
935
- # ──── Incremental ────
936
- # 🟤 DIFF "Comparing {n} new chunks vs {m} stored..."
937
- # 🟢 DELTA "{new} new, {changed} changed, {same} unchanged"
938
- # 🟣 EMBED_DELTA "Embedding {n} delta chunks only..."
939
- # 🟢 DONE "Incremental update complete"
940
- ```
941
-
942
- ---
943
-
944
- ### Technique 8 — RAGAs Evaluation
945
-
946
- ```python
947
- # User uploads CSV: question,ground_truth
948
- # For each question:
949
- # 1. Retrieve top-K chunks (standard vector search)
950
- # 2. Generate answer via Qwen3
951
- # 3. Collect dataset: (question, answer, contexts, ground_truth)
952
- # RAGAs metrics (Qwen3 as judge):
953
- # faithfulness, answer_relevancy, context_precision, context_recall
954
- # Results saved to Supabase eval_reports table
955
- # Frontend shows Recharts radar chart + per-question table
956
-
957
- # Steps emitted:
958
- # 🟣 SETUP "RAGAs initialized — {n} test questions"
959
- # 🟢 RETRIEVE "Retrieving context for Q{i}/{n}..."
960
- # 🟣 GENERATE "Qwen3 generating answer {i}/{n}..."
961
- # 🔴 SCORE "Computing RAGAs metrics (Qwen3 as judge)..."
962
- # 🟢 REPORT "Faithfulness:{f:.2f} Relevancy:{r:.2f} ..."
963
- ```
964
-
965
- ---
966
-
967
- ## 10. Multi-User Architecture
968
-
969
- ### User Isolation
970
-
971
- ```
972
- Supabase row-level security (RLS) policies:
973
- All tables have user_id column
974
- RLS enabled: users can only see their own rows
975
- Enforced at DB level — even if API has a bug, data stays isolated
976
-
977
- FAISS → replaced by Supabase pgvector → isolation via user_id column
978
- BM25 index files → ./data/bm25_indexes/{user_id}_{doc_id}.pkl
979
- Upload temp files → ./data/uploads/{user_id}/{filename}
980
- Redis cache keys → cache:{user_id}:{doc_id}:{query_hash}
981
- ```
982
-
983
- ### JWT Auth Flow
984
-
985
- ```
986
- POST /auth/register → username + password → bcrypt hash → Supabase users table
987
- POST /auth/login → verify password → return JWT (24h expiry)
988
- All protected routes → Authorization: Bearer {token}
989
- Frontend → JWT stored in Zustand memory (NOT localStorage — XSS safe)
990
- POST /auth/refresh → return new JWT before expiry
991
- ```
992
-
993
- ### WebSocket Isolation
994
-
995
- ```python
996
- # app/utils/ws_manager.py
997
- # One WebSocket connection per (user_id, job_id)
998
- # Job ownership verified before connecting
999
- # Users only receive their own pipeline events
1000
-
1001
- class WSManager:
1002
- _connections: dict[str, WebSocket] = {} # key = f"{user_id}:{job_id}"
1003
-
1004
- async def connect(self, job_id, websocket, user_id):
1005
- key = f"{user_id}:{job_id}"
1006
- self._connections[key] = websocket
1007
-
1008
- async def emit(self, job_id, user_id, event: dict):
1009
- key = f"{user_id}:{job_id}"
1010
- ws = self._connections.get(key)
1011
- if ws:
1012
- await ws.send_json(event)
1013
- ```
1014
-
1015
- ---
1016
-
1017
- ## 11. API Endpoints
1018
-
1019
- ### Auth
1020
- | Method | Endpoint | Description |
1021
- |--------|----------|-------------|
1022
- | POST | `/auth/register` | Create account |
1023
- | POST | `/auth/login` | Get JWT |
1024
- | POST | `/auth/refresh` | Refresh JWT |
1025
-
1026
- ### Ingestion
1027
- | Method | Endpoint | Description |
1028
- |--------|----------|-------------|
1029
- | POST | `/ingest/upload` | Upload file → background job |
1030
- | GET | `/ingest/status/{job_id}` | Job status + pipeline steps |
1031
- | GET | `/ingest/documents` | User's document list |
1032
- | DELETE | `/ingest/document/{doc_id}` | Delete doc + vectors |
1033
- | POST | `/ingest/reindex/{doc_id}` | Incremental re-ingest |
1034
-
1035
- ### Query
1036
- | Method | Endpoint | Description |
1037
- |--------|----------|-------------|
1038
- | POST | `/query/search` | RAG query with technique |
1039
- | GET | `/query/history/{doc_id}` | Query history |
1040
- | DELETE | `/query/cache/{doc_id}` | Clear Redis cache |
1041
-
1042
- ### Evaluate
1043
- | Method | Endpoint | Description |
1044
- |--------|----------|-------------|
1045
- | POST | `/evaluate/run` | Run RAGAs (CSV upload) |
1046
- | GET | `/evaluate/report/{doc_id}` | Latest report |
1047
-
1048
- ### Stats & Health
1049
- | Method | Endpoint | Description |
1050
- |--------|----------|-------------|
1051
- | GET | `/stats/documents` | Docs with chunk counts |
1052
- | GET | `/health` | Backend + Supabase + Redis status |
1053
-
1054
- ### WebSocket
1055
- | Endpoint | Description |
1056
- |----------|-------------|
1057
- | `WS /ws/pipeline/{job_id}?token={jwt}` | Real-time pipeline steps |
1058
-
1059
- ---
1060
-
1061
- ## 12. Frontend — React Deep Dive
1062
-
1063
- ### Tailwind Green + Violet Theme
1064
-
1065
- ```js
1066
- // tailwind.config.js
1067
- module.exports = {
1068
- theme: {
1069
- extend: {
1070
- colors: {
1071
- primary: { // Green
1072
- 50: '#f0fdf4', 400: '#4ade80',
1073
- 500: '#22c55e', 600: '#16a34a', 700: '#15803d'
1074
- },
1075
- accent: { // Violet
1076
- 50: '#f5f3ff', 400: '#a78bfa',
1077
- 500: '#8b5cf6', 600: '#7c3aed', 700: '#6d28d9'
1078
- },
1079
- surface: { // Dark base for dashboard
1080
- 900: '#0a0f0a', 800: '#111a11', 700: '#1a2b1a'
1081
- }
1082
- },
1083
- boxShadow: {
1084
- 'glow-green': '0 0 20px rgba(34,197,94,0.25)',
1085
- 'glow-violet': '0 0 20px rgba(139,92,246,0.25)',
1086
- }
1087
- }
1088
- }
1089
- }
1090
- ```
1091
-
1092
- ### Step Color Mapping
1093
-
1094
- ```js
1095
- // src/utils/stepColors.js
1096
- export const STEP_COLORS = {
1097
- // Violet — LLM / AI ops
1098
- EMBED: '#8B5CF6', HYDE: '#8B5CF6', EXPAND: '#7C3AED',
1099
- PLAN: '#8B5CF6', GENERATE: '#7C3AED', SCORE: '#6D28D9',
1100
- CACHE_MISS: '#8B5CF6', EMBED_DELTA: '#8B5CF6',
1101
- SETUP: '#8B5CF6', EMBED_TOK: '#8B5CF6', TOKENIZE: '#7C3AED',
1102
- OCR_START: '#8B5CF6', IMAGE_ANALYZE: '#8B5CF6',
1103
-
1104
- // Green — retrieval / data ops
1105
- BM25: '#22C55E', VECTOR: '#16A34A', DONE: '#22C55E',
1106
- CACHE_HIT: '#22C55E', RETRIEVE: '#16A34A', OBSERVE: '#22C55E',
1107
- DELTA: '#22C55E', AGENT_INIT: '#22C55E', OCR_DONE: '#22C55E',
1108
- FINAL: '#22C55E', REPORT: '#22C55E',
1109
-
1110
- // Special
1111
- RERANK: '#EF4444', // red — heavy compute, distinct
1112
- MAXSIM: '#EF4444', // red — heavy compute
1113
- FILTER: '#D97706', // amber — metadata ops
1114
- TOOL: '#D97706', // amber — tool calls
1115
- DIFF: '#6B7280', // gray — neutral checks
1116
- CACHE_CHECK: '#6B7280',
1117
- RRF: '#8B5CF6', // violet — fusion
1118
- ERROR: '#EF4444', // red
1119
- }
1120
- ```
1121
-
1122
- ### Pipeline Page UI Layout
1123
-
1124
- ```
1125
- ┌─────────────────────────────────────────────────────────────┐
1126
- │ 🟢 RAG Pipeline [user ▾] [Logout] │
1127
- ├─────────────────────────────────────────────────────────────┤
1128
- │ 📄 document.pdf 142 chunks ✅ Indexed │
1129
- ├───────────────────────────┬─────────────────────────────────┤
1130
- │ SELECT TECHNIQUE │ CHUNKING CONFIG │
1131
- │ ┌────────┐ ┌────────┐ │ Chunk ──────●────── 512 tok │
1132
- │ │Hybrid │ │ReRank │ │ Overlap ───●──────── 64 tok │
1133
- │ └────────┘ └────────┘ │ Strategy [Fixed ▾] │
1134
- │ ┌────────┐ ┌────────┐ │ Est. chunks: ~148 │
1135
- │ │ HyDE │ │ Meta │ │ [Apply & Re-chunk] │
1136
- │ └────────┘ └────────┘ │ │
1137
- │ ┌────────┐ ┌────────┐ │ │
1138
- │ │ColBERT │ │Agentic │ │ │
1139
- │ └────────┘ └────────┘ │ │
1140
- │ ┌────────┐ ┌────────┐ │ │
1141
- │ │ Cache │ │ RAGAs │ │ │
1142
- │ └────────┘ └────────┘ │ │
1143
- ├───────────────────────────┴─────────────────────────────────┤
1144
- │ QUERY │
1145
- │ ┌──────────────────────────────────────┐ [🔍 Search] │
1146
- │ └──────────────────────────────────────┘ │
1147
- ├─────────────────────────────────────────────────────────────┤
1148
- │ PIPELINE TRACE ● LIVE │
1149
- │ 🟣 EMBED Embedding query (bge-m3)... ✅ 2.1s │
1150
- │ 🟢 BM25 Keyword search → 22 candidates ✅ 0.1s │
1151
- │ 🟢 VECTOR pgvector ANN → top-20 ✅ 0.3s │
1152
- │ 🟣 RRF Reciprocal Rank Fusion... ⏳ │
1153
- ├──────────��──────────────────────────────────────────────────┤
1154
- │ ANSWER │
1155
- │ The contract was signed on April 3rd, 2024... │
1156
- │ SOURCES │
1157
- │ 📄 contract.docx §3 Score: 0.94 ██████████ 94% │
1158
- │ 📄 contract.docx §1 Score: 0.81 ████████── 81% │
1159
- └─────────────────────────────────────────────────────────────┘
1160
- ```
1161
-
1162
- ### Key React Hooks
1163
-
1164
- ```js
1165
- // usePipelineWS.js — WebSocket for real-time steps
1166
- // Connects to: wss://{backend}/ws/pipeline/{job_id}?token={jwt}
1167
- // Each message → add to pipelineStore.steps
1168
- // Auto-reconnects (max 3 attempts)
1169
- // Shows "LIVE" green dot while connected
1170
-
1171
- // useUpload.js — Upload + job polling
1172
- // POST /ingest/upload (multipart form)
1173
- // Polls /ingest/status/{job_id} every 2s until done/failed
1174
-
1175
- // useQuery.js — RAG search
1176
- // POST /query/search → streams answer via WebSocket
1177
- // Updates answerPanel + sourceChunks + pipeline steps simultaneously
1178
- ```
1179
-
1180
- ---
1181
-
1182
- ## 13. Docker Setup
1183
-
1184
- ### `backend/Dockerfile`
1185
-
1186
- ```dockerfile
1187
- FROM python:3.11-slim
1188
-
1189
- WORKDIR /app
1190
-
1191
- # System deps for python-docx, tiktoken
1192
- RUN apt-get update && apt-get install -y \
1193
- build-essential libpq-dev && \
1194
- rm -rf /var/lib/apt/lists/*
1195
-
1196
- COPY requirements.txt .
1197
- RUN pip install --no-cache-dir -r requirements.txt
1198
-
1199
- # Download cross-encoder model at build time (not runtime)
1200
- RUN python -c "from sentence_transformers import CrossEncoder; \
1201
- CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')"
1202
-
1203
- COPY . .
1204
-
1205
- # Create data dirs
1206
- RUN mkdir -p data/uploads data/bm25_indexes data/cache
1207
-
1208
- EXPOSE 8000
1209
-
1210
- CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", \
1211
- "--port", "8000", "--workers", "2"]
1212
- ```
1213
-
1214
- ### `docker-compose.yml` — Local Dev
1215
-
1216
- ```yaml
1217
- version: "3.9"
1218
-
1219
- services:
1220
- backend:
1221
- build: ./backend
1222
- ports:
1223
- - "8000:8000"
1224
- environment:
1225
- - SUPABASE_URL=${SUPABASE_URL}
1226
- - SUPABASE_KEY=${SUPABASE_KEY}
1227
- - REDIS_URL=redis://redis:6379
1228
- - JWT_SECRET_KEY=${JWT_SECRET_KEY}
1229
- - MISTRAL_API_KEY=${MISTRAL_API_KEY}
1230
- # ... other env vars from .env
1231
- env_file:
1232
- - ./backend/.env
1233
- volumes:
1234
- - ./backend/data:/app/data # BM25 indexes + uploads persist locally
1235
- depends_on:
1236
- - redis
1237
- restart: unless-stopped
1238
-
1239
- redis:
1240
- image: redis:7-alpine
1241
- ports:
1242
- - "6379:6379"
1243
- restart: unless-stopped
1244
-
1245
- # Optional: local frontend dev server
1246
- frontend:
1247
- build:
1248
- context: ./frontend
1249
- dockerfile: Dockerfile.dev
1250
- ports:
1251
- - "5173:5173"
1252
- environment:
1253
- - VITE_API_BASE_URL=http://localhost:8000
1254
- - VITE_WS_BASE_URL=ws://localhost:8000
1255
- volumes:
1256
- - ./frontend/src:/app/src # hot reload
1257
- restart: unless-stopped
1258
- ```
1259
-
1260
- ### `backend/requirements.txt`
1261
-
1262
- ```txt
1263
- fastapi==0.111.0
1264
- uvicorn[standard]==0.30.0
1265
- gunicorn==22.0.0
1266
- pydantic-settings==2.3.0
1267
- supabase==2.5.0
1268
- asyncpg==0.29.0
1269
- redis==5.0.6
1270
- python-jose[cryptography]==3.3.0
1271
- passlib[bcrypt]==1.7.4
1272
- python-multipart==0.0.9
1273
- httpx==0.27.0
1274
- gradio_client==0.17.0
1275
- tiktoken==0.7.0
1276
- python-docx==1.1.2
1277
- rank_bm25==0.2.2
1278
- sentence-transformers==3.0.1
1279
- ragas==0.1.14
1280
- json_repair==0.25.2
1281
- loguru==0.7.2
1282
- slowapi==0.1.9
1283
- ```
1284
-
1285
- ---
1286
-
1287
- ## 14. Environment Variables
1288
-
1289
- > All values go into **Render Dashboard → Environment tab**.
1290
- > Never committed to Git. `.env.example` has key names only.
1291
-
1292
- ### Render Backend
1293
-
1294
- ```env
1295
- # Supabase
1296
- SUPABASE_URL = https://xxxx.supabase.co
1297
- SUPABASE_KEY = your_service_role_key
1298
- SUPABASE_DB_URL = postgresql://postgres:pass@db.xxxx.supabase.co:5432/postgres
1299
-
1300
- # Embeddings (HF Space — free)
1301
- EMBED_API_URL = https://lamhieu-lightweight-embeddings.hf.space/
1302
- EMBED_MODEL = bge-m3
1303
- EMBED_DIM = 1024
1304
- EMBED_AUTH_KEY =
1305
- EMBED_MAX_TOKENS = 1000
1306
- EMBED_TIMEOUT = 60
1307
- EMBED_MAX_RETRIES = 3
1308
-
1309
- # LLM — Qwen3 (HF Space — free)
1310
- QWEN3_MODEL_NAME = Qwen/Qwen3-Demo
1311
- QWEN3_THINKING_BUDGET = 38
1312
- LLM_RESPONSE_TIMEOUT = 1080
1313
- MAX_LLM_RETRIES = 5
1314
- MAX_TIMEOUT_RETRIES = 10
1315
-
1316
- # OCR — Mistral (needs API key)
1317
- MISTRAL_OCR_SPACE = tatendachirume/Mistral-OCR
1318
- MISTRAL_API_KEY = your_mistral_api_key
1319
-
1320
- # Image — Ernie Bot (free HF Space)
1321
- ERNIE_SPACE_URL = https://baidu-simple-ernie-bot-demo.hf.space/
1322
-
1323
- # Redis (auto-filled by Render when Redis added)
1324
- REDIS_URL = redis://...
1325
- CACHE_TTL_SECONDS = 3600
1326
-
1327
- # Auth
1328
- JWT_SECRET_KEY = generate_with: openssl rand -hex 32
1329
- JWT_ALGORITHM = HS256
1330
- JWT_EXPIRE_MINUTES = 1440
1331
-
1332
- # Re-ranking
1333
- RERANK_MODEL = cross-encoder/ms-marco-MiniLM-L-6-v2
1334
-
1335
- # Limits
1336
- RATE_LIMIT_PER_MINUTE = 20
1337
- RATE_LIMIT_UPLOAD_PER_DAY = 50
1338
- MAX_FILE_SIZE_MB = 50
1339
-
1340
- # Defaults
1341
- DEFAULT_CHUNK_SIZE = 512
1342
- DEFAULT_OVERLAP = 64
1343
- DEFAULT_TOP_K = 5
1344
-
1345
- # CORS
1346
- CORS_ORIGINS = https://your-app.netlify.app,http://localhost:5173
1347
- ```
1348
-
1349
- ### Netlify Frontend
1350
-
1351
- ```env
1352
- VITE_API_BASE_URL = https://your-backend.onrender.com
1353
- VITE_WS_BASE_URL = wss://your-backend.onrender.com
1354
- ```
1355
-
1356
- ### Local Dev (`backend/.env`)
1357
-
1358
- ```env
1359
- # Same as Render vars above +
1360
- REDIS_URL = redis://localhost:6379
1361
- CORS_ORIGINS = http://localhost:5173
1362
- ```
1363
-
1364
- ---
1365
-
1366
- ## 15. Deployment Guide
1367
-
1368
- ### Step 1 — Supabase Setup (10 min)
1369
-
1370
- ```
1371
- 1. supabase.com → New project (free)
1372
- 2. Settings → Database → Copy connection string → SUPABASE_DB_URL
1373
- 3. Settings → API → Copy URL + service_role key
1374
- 4. SQL Editor → run the schema SQL from Section 4
1375
- 5. SQL Editor → run the match_chunks() function SQL from Section 4
1376
- 6. Authentication → Disable (we handle auth ourselves with JWT)
1377
- 7. Table Editor → Enable RLS on all tables
1378
- ```
1379
-
1380
- ### Step 2 — Render Backend (15 min)
1381
-
1382
- ```
1383
- 1. render.com → New Web Service → Connect GitHub → select backend/
1384
- 2. Runtime: Python / Docker (choose Docker — uses our Dockerfile)
1385
- 3. Build command: (auto from Dockerfile)
1386
- 4. Start command: (auto from Dockerfile CMD)
1387
- 5. Add Redis: New → Redis → Free tier → auto-links REDIS_URL
1388
- 6. Environment tab: add all vars from Section 14
1389
- 7. Deploy → wait ~5 min
1390
- 8. Test: curl https://your-app.onrender.com/health
1391
- ```
1392
-
1393
- ### Step 3 — Netlify Frontend (5 min)
1394
-
1395
- ```
1396
- 1. netlify.com → New site → Import from GitHub → select frontend/
1397
- 2. Build command: npm run build
1398
- 3. Publish dir: dist
1399
- 4. Environment vars: VITE_API_BASE_URL, VITE_WS_BASE_URL
1400
- 5. Deploy
1401
- 6. Copy Netlify URL → update CORS_ORIGINS in Render env
1402
- ```
1403
-
1404
- ### Step 4 — Local Dev
1405
-
1406
- ```bash
1407
- # Clone repo
1408
- git clone https://github.com/you/rag-pipeline.git
1409
- cd rag-pipeline
1410
-
1411
- # Copy env files
1412
- cp backend/.env.example backend/.env
1413
- # Fill in values
1414
-
1415
- # Start with Docker Compose
1416
- docker-compose up --build
1417
-
1418
- # Frontend available: http://localhost:5173
1419
- # Backend available: http://localhost:8000
1420
- # Redis: localhost:6379
1421
- ```
1422
-
1423
- ---
1424
-
1425
- ## 16. Production Additions
1426
-
1427
- Items added beyond what you mentioned — all included in this blueprint:
1428
-
1429
- | # | Item | Why |
1430
- |---|------|-----|
1431
- | 1 | JWT auth + multi-user | You said multi-user needed |
1432
- | 2 | Supabase Row Level Security | Data isolation at DB level |
1433
- | 3 | Rate limiting (slowapi) | Prevent abuse on free Render tier |
1434
- | 4 | Docker + docker-compose | Local dev, portfolio quality, Render deployment |
1435
- | 5 | Cross-encoder model pre-downloaded in Dockerfile | Avoid cold download on first query |
1436
- | 6 | File cleanup after ingestion | Prevent disk fill on Render |
1437
- | 7 | JWT in Zustand memory (not localStorage) | XSS attack prevention |
1438
- | 8 | WebSocket user isolation | Multi-user safety |
1439
- | 9 | ColBERT warning dialog | Prevent accidental slow ingestion |
1440
- | 10 | /health endpoint | Shows Supabase + Redis status to frontend |
1441
- | 11 | BM25 pickle persisted on Render disk | Hybrid search needs it across restarts |
1442
- | 12 | Render cold start note | Free tier sleeps after 15 min — warn interviewer |
1443
-
1444
- ### ⚠️ One Render Free Tier Limitation
1445
-
1446
- Render free tier **sleeps after 15 minutes of inactivity**. First request takes 30-60 seconds to wake up. For an interview demo, either:
1447
- - Upgrade to Starter ($7/mo) — keeps it warm
1448
- - OR ping `/health` from frontend every 5 min to prevent sleep
1449
- - OR just open the app 2 min before the interview
1450
-
1451
- ---
1452
-
1453
- > **Next step:** Confirm this blueprint and tell me which module to code first.
1454
- > Recommended order: `backend/` → `local-bridge removed` → `frontend/`
1455
- > Say **"start backend"** and I will generate every file.