PrakashPalsaniya/Docu-Mind

0

stars

16

commits

JavaScript

primary language

Aug 22, 2026

updated

docu-mind-alpha-topaz.vercel.app

README

๐Ÿง  DocuMind โ€” Agentic PDF Intelligence

Upload a PDF and chat with it through an agentic Retrieval-Augmented Generation (RAG) pipeline. DocuMind grounds every answer in your document with citations, falls back to web search when the PDF doesn't have the answer, self-corrects when retrieval is weak, and caches by meaning so repeat questions return instantly.

LangGraph ยท Qdrant ยท local embeddings (no embedding API key) ยท OpenRouter ยท Langfuse


โœจ Features

  • Agentic RAG (LangGraph state machine) โ€” query rewriting โ†’ hybrid retrieval โ†’ relevance grading โ†’ self-reflective retry โ†’ grounded generation with citations.
  • Hybrid retrieval + reranking โ€” dense vector search + BM25 keyword search fused with Reciprocal Rank Fusion, then re-ordered by a local cross-encoder re-ranker.
  • Automatic web-search fallback โ€” if the document can't answer after retries, the agent searches the web and cites sources (answers badged "From the web" vs "From the document").
  • Semantic answer cache โ€” embeds each question and reuses a past answer when a meaning-equivalent question was already asked (cosine โ‰ฅ threshold), skipping the entire pipeline. Hits are badged โšก Cached.
  • Per-user document isolation โ€” every chunk is tagged userId + pdfId; all retrieval, caching, and history reads are filtered so users only touch their own PDFs.
  • Live agent trace (streaming) โ€” the UI streams reasoning steps and answer tokens in real time over SSE, with stop-generation.
  • Conversation memory โ€” the last 6 turns are fed back so follow-ups resolve references ("what about its pricing?").
  • Background ingestion โ€” chunking + embeddings run in a BullMQ worker; status (PROCESSING โ†’ READY / FAILED) tracked in Postgres with a failure reason.
  • Auth โ€” Clerk protects UI and API (Bearer token verified server-side), with an Svix-verified webhook syncing users to Postgres.
  • Distributed rate limiting โ€” atomic Redis/Valkey Lua fixed-window counter, shared across instances, fails open.
  • Observability โ€” every agent run traced to Langfuse (one span per reasoning step, latency, source type). No-ops without keys.
  • Eval harness โ€” LLM-as-judge scoring for faithfulness & relevance.

๐Ÿ› ๏ธ Tech Stack

LayerTechnology
FrontendReact 18 + Vite 5, Tailwind CSS, Clerk, react-markdown, sonner
BackendNode.js (ESM) + Express 5
AgentLangGraph StateGraph + LangChain
LLMOpenRouter (default meta-llama/llama-3.1-8b-instruct, configurable)
EmbeddingsLocal on-device Xenova/all-MiniLM-L6-v2 โ€” 384-dim, no API key
RerankerLocal cross-encoder Xenova/ms-marco-MiniLM-L-6-v2
RetrievalQdrant (dense) + in-process BM25 (sparse) + RRF + cross-encoder rerank
Web searchTavily if TAVILY_API_KEY is set, else keyless DuckDuckGo
CacheSemantic cache in Qdrant (qa_cache_local)
QueueBullMQ + Valkey/Redis
DatabasePostgreSQL + Prisma
File storageAWS S3 (required)
ObservabilityLangfuse
InfraDocker Compose

๐Ÿงฉ Architecture

Ingestion pipeline

Client โ†’ POST /upload/pdf (auth, 5/min, โ‰ค15MB, PDF-only)
       โ†’ upload buffer to S3  (uploads/<userId>/<uuid>-<name>.pdf)
       โ†’ create Pdf(status=PROCESSING) in Postgres
       โ†’ enqueue job on "file-upload-queue" (BullMQ / Valkey)
       โ†“
Worker (concurrency 5)
       โ†’ download from S3 to temp โ†’ PDFLoader โ†’ split (1000 chars / 200 overlap)
       โ†’ reject scanned/image-only PDFs (no OCR yet) โ†’ status=FAILED + reason
       โ†’ tag every chunk {userId, pdfId} โ†’ embed locally (MiniLM)
       โ†’ upsert to Qdrant in batches of 50 โ†’ status=READY โ†’ delete temp file

Query pipeline (Agentic RAG)

Client โ†’ POST /chat/stream {query, pdfId} (auth, 20/min)  โ”€โ”€ streams SSE โ”€โ”€โ–บ

  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Semantic Cache โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
  โ”‚ embed(query) โ†’ search qa_cache_local (filtered userId+pdfId) โ”‚
  โ”‚   score โ‰ฅ 0.85 & within TTL โ†’ return cached answer โšก        โ”‚
  โ”‚   miss / error (fails open)  โ†’ run the agent โ–ผ               โ”‚
  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

  LangGraph agent:
    rewrite โ”€โ–บ retrieve (dense + BM25 โ†’ RRF โ†’ rerank, top 4)
            โ”€โ–บ grade relevance (LLM yes/no)
                 โ”œโ”€ relevant           โ”€โ–บ generate (cited)        [document]
                 โ”œโ”€ weak & tries < 2   โ”€โ–บ rewrite & retry โ†บ
                 โ””โ”€ still weak         โ”€โ–บ webSearch โ”€โ–บ generate   [web]
                                            โ””โ”€ no results โ”€โ–บ noAnswer
    โ†’ cache the answer (skipped when noAnswer)
    โ†’ persist USER + AI turns to Postgres
    โ†’ trace the run to Langfuse

Retrieval internals

  1. Dense โ€” Qdrant cosine similarity over pdf_embeddings_local, filtered by metadata.userId + metadata.pdfId (payload indexes created on demand โ€” Qdrant Cloud requires them to filter). Pulls a 10-doc candidate pool.
  2. Sparse โ€” Okapi BM25 (k1=1.5, b=0.75) computed in-process over all chunks of that PDF, scrolled from Qdrant in pages of 128.
  3. Fusion โ€” Reciprocal Rank Fusion (k=60) merges both ranked lists, deduping on a content prefix. Falls back to the dense list if fusion yields nothing.
  4. Rerank โ€” local cross-encoder scores each candidate against the query and returns the top 4. If the model fails to load, it degrades gracefully to input order.

Document relevance shown in the UI is the rerank logit squashed through a sigmoid.

Streaming contract (SSE)

POST /chat/stream emits named events:

EventPayloadMeaning
trace{step, detail}one agent reasoning step
token{token}answer token from the generate node
final{answer, sources, sourceType, cached}complete result
done{}stream finished
error{error}failure mid-stream

Turns are persisted once (idempotent saved guard) โ€” including when the client disconnects mid-generation or the stream errors, so a stopped generation is never lost.

Data model (Prisma)

User  1โ”€โ”€โ”€n  Pdf  1โ”€โ”€โ”€n  Chat
                  โ””โ”€ status: PROCESSING | READY | FAILED  (+ statusMessage)
      โ””โ”€โ”€โ”€n  Chat    role: USER | AI, sources Json?

All relations cascade on delete. Deleting a PDF also removes its Qdrant vectors and the S3 object.


๐Ÿš€ Getting Started

1. Infrastructure (Postgres, Qdrant, Valkey, Adminer)

docker compose up -d
ServicePort
Postgres5432 (admin / secret / pdf_rag)
Qdrant6333
Valkey (Redis)6379
Adminer (DB UI)8080

2. Backend

cd server
cp .env.example .env      # see configuration below โ€” S3 + Clerk + OpenRouter are required
npm install
npm run prisma:generate
npm run prisma:migrate

npm start                 # API on :8000
npm run start:worker      # ingestion worker (separate terminal)
# or run both together:
npm run start:all

The API throws at boot if S3 isn't configured, and the worker throws if OPENROUTER_API_KEY, QDRANT_URL, or DATABASE_URL are missing โ€” fail fast instead of failing mid-upload.

First run downloads the embedding + reranker models (~120MB) to a local cache; startup is slower once.

3. Frontend

cd frontend
cp .env.example .env      # VITE_CLERK_PUBLISHABLE_KEY + VITE_API_URL
npm install
npm run dev               # app on :5173

4. Tests & eval (optional)

cd server
npm test                  # Jest (ESM) โ€” rate limiter + request validation
npm run eval              # LLM-as-judge faithfulness / relevance

For npm run eval, set EVAL_USER_ID and EVAL_PDF_ID in .env to an already-ingested document.


โš™๏ธ Configuration (server/.env)

Required

VariablePurpose
OPENROUTER_API_KEYLLM access
DATABASE_URLPostgreSQL connection string
QDRANT_URLVector DB URL (http://localhost:6333 locally)
CLERK_SECRET_KEYVerifies API Bearer tokens
CLERK_WEBHOOK_SECRETVerifies the Svix-signed user-sync webhook
AWS_REGION, S3_BUCKET, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEYPDF storage (all four needed)

Optional

VariablePurposeDefault
OPENROUTER_MODELChat modelmeta-llama/llama-3.1-8b-instruct
QDRANT_API_KEYFor Qdrant Cloudโ€”
REDIS_HOST, REDIS_PORTQueue + rate-limit backendlocalhost, 6379
REDIS_USERNAME, REDIS_PASSWORDRedis auth (managed Redis / Valkey)โ€”
TAVILY_API_KEYBetter web search (else DuckDuckGo)โ€”
CORS_ORIGINSComma-separated allowlisthttp://localhost:5173,http://127.0.0.1:5173
PORTAPI port8000
SEMANTIC_CACHE_ENABLEDToggle the semantic cachetrue
SEMANTIC_CACHE_THRESHOLDMin cosine similarity for a hit0.85
SEMANTIC_CACHE_TTL_HOURSIgnore entries older than this168
LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY, LANGFUSE_BASEURLObservabilitydisabled

๐Ÿ“ก API

All routes except / and /webhook/clerk require Authorization: Bearer <clerk-token>.

MethodRouteDescriptionLimit
GET/Health checkโ€”
POST/upload/pdfUpload a PDF (multipart pdf, โ‰ค15MB), queues ingestion5/min
GET/pdfsList the current user's PDFs120/min
GET/pdfs/:pdfId/fileStream the PDF back for in-app viewing120/min
DELETE/pdfs/:pdfIdDelete PDF + its vectors + its S3 object5/min
POST/chatAsk {query, pdfId} (non-streaming)20/min
POST/chat/streamAsk {query, pdfId}, stream trace + tokens (SSE)20/min
GET/chat/:pdfIdFull chat history for a PDF120/min
POST/webhook/clerkClerk user-sync webhook (Svix-verified raw body)โ€”

Chat returns 409 while a PDF is still PROCESSING, and 404 if the PDF isn't yours.


๐Ÿ“ Project layout

server/
  index.js               Express app, routes, CORS, rate limits, error handling
  worker.js              BullMQ ingestion worker
  start-all.js           runs API + worker in one process
  controllers/           pdf, chat (incl. SSE), clerk webhook
  lib/
    agent.js             LangGraph state machine (the agentic RAG loop)
    ai.js                embeddings, chat model, hybrid retrieval, BM25, RRF
    rerank.js            local cross-encoder reranker
    semanticCache.js     meaning-based answer cache in Qdrant
    webSearch.js         Tavily โ†’ DuckDuckGo fallback
    rateLimit.js         Redis Lua fixed-window limiter
    observability.js     Langfuse tracing
    auth.js  db.js  redis.js  s3.js  validate.js
  prisma/                schema + migrations
  eval/evaluate.js       LLM-as-judge eval harness
  tests/                 Jest tests

frontend/src/
  App.jsx                layout, PDF list, upload, auth gate
  components/            ChatArea (SSE + trace UI), FileUpload, PdfViewer, โ€ฆ
  lib/api.js             axios client + Clerk token injection

๐Ÿงช Verify the interesting bits quickly

  • Semantic cache โ€” ask a question, then ask a paraphrase. The second answer returns near-instantly with a โšก Cached badge, and the backend logs โšก CACHE HIT (similarity 0.9xx) โ€” skipped RAG+LLM pipeline.
  • Web fallback โ€” ask something the PDF doesn't cover. The answer gets a "From the web" badge with linked sources.
  • Self-correction โ€” watch the live trace: on weak retrieval it rewrites the query and retries (up to 2 attempts) before touching the web.
  • Isolation โ€” sign in as another user and request the first user's pdfId โ†’ 404, never a leaked chunk.
  • Resilience โ€” stop the generation mid-stream; reload the page. The partial turn is still in history.

๐Ÿงญ Known limits

  • No OCR โ€” scanned / image-only PDFs are rejected at ingestion with a clear FAILED reason.
  • BM25 is in-process โ€” all chunks of the queried PDF are scrolled from Qdrant per request. Fine for single documents; a server-side sparse index is the upgrade path for large corpora.
  • Fixed-window rate limiting โ€” allows a burst at window edges; a sliding window is the upgrade if that matters.

Contributors

PrakashPalsaniya/Docu-Mind

0

stars

16

commits

JavaScript

primary language

Aug 22, 2026

updated

docu-mind-alpha-topaz.vercel.app

README

๐Ÿง  DocuMind โ€” Agentic PDF Intelligence

Upload a PDF and chat with it through an agentic Retrieval-Augmented Generation (RAG) pipeline. DocuMind grounds every answer in your document with citations, falls back to web search when the PDF doesn't have the answer, self-corrects when retrieval is weak, and caches by meaning so repeat questions return instantly.

LangGraph ยท Qdrant ยท local embeddings (no embedding API key) ยท OpenRouter ยท Langfuse


โœจ Features

  • Agentic RAG (LangGraph state machine) โ€” query rewriting โ†’ hybrid retrieval โ†’ relevance grading โ†’ self-reflective retry โ†’ grounded generation with citations.
  • Hybrid retrieval + reranking โ€” dense vector search + BM25 keyword search fused with Reciprocal Rank Fusion, then re-ordered by a local cross-encoder re-ranker.
  • Automatic web-search fallback โ€” if the document can't answer after retries, the agent searches the web and cites sources (answers badged "From the web" vs "From the document").
  • Semantic answer cache โ€” embeds each question and reuses a past answer when a meaning-equivalent question was already asked (cosine โ‰ฅ threshold), skipping the entire pipeline. Hits are badged โšก Cached.
  • Per-user document isolation โ€” every chunk is tagged userId + pdfId; all retrieval, caching, and history reads are filtered so users only touch their own PDFs.
  • Live agent trace (streaming) โ€” the UI streams reasoning steps and answer tokens in real time over SSE, with stop-generation.
  • Conversation memory โ€” the last 6 turns are fed back so follow-ups resolve references ("what about its pricing?").
  • Background ingestion โ€” chunking + embeddings run in a BullMQ worker; status (PROCESSING โ†’ READY / FAILED) tracked in Postgres with a failure reason.
  • Auth โ€” Clerk protects UI and API (Bearer token verified server-side), with an Svix-verified webhook syncing users to Postgres.
  • Distributed rate limiting โ€” atomic Redis/Valkey Lua fixed-window counter, shared across instances, fails open.
  • Observability โ€” every agent run traced to Langfuse (one span per reasoning step, latency, source type). No-ops without keys.
  • Eval harness โ€” LLM-as-judge scoring for faithfulness & relevance.

๐Ÿ› ๏ธ Tech Stack

LayerTechnology
FrontendReact 18 + Vite 5, Tailwind CSS, Clerk, react-markdown, sonner
BackendNode.js (ESM) + Express 5
AgentLangGraph StateGraph + LangChain
LLMOpenRouter (default meta-llama/llama-3.1-8b-instruct, configurable)
EmbeddingsLocal on-device Xenova/all-MiniLM-L6-v2 โ€” 384-dim, no API key
RerankerLocal cross-encoder Xenova/ms-marco-MiniLM-L-6-v2
RetrievalQdrant (dense) + in-process BM25 (sparse) + RRF + cross-encoder rerank
Web searchTavily if TAVILY_API_KEY is set, else keyless DuckDuckGo
CacheSemantic cache in Qdrant (qa_cache_local)
QueueBullMQ + Valkey/Redis
DatabasePostgreSQL + Prisma
File storageAWS S3 (required)
ObservabilityLangfuse
InfraDocker Compose

๐Ÿงฉ Architecture

Ingestion pipeline

Client โ†’ POST /upload/pdf (auth, 5/min, โ‰ค15MB, PDF-only)
       โ†’ upload buffer to S3  (uploads/<userId>/<uuid>-<name>.pdf)
       โ†’ create Pdf(status=PROCESSING) in Postgres
       โ†’ enqueue job on "file-upload-queue" (BullMQ / Valkey)
       โ†“
Worker (concurrency 5)
       โ†’ download from S3 to temp โ†’ PDFLoader โ†’ split (1000 chars / 200 overlap)
       โ†’ reject scanned/image-only PDFs (no OCR yet) โ†’ status=FAILED + reason
       โ†’ tag every chunk {userId, pdfId} โ†’ embed locally (MiniLM)
       โ†’ upsert to Qdrant in batches of 50 โ†’ status=READY โ†’ delete temp file

Query pipeline (Agentic RAG)

Client โ†’ POST /chat/stream {query, pdfId} (auth, 20/min)  โ”€โ”€ streams SSE โ”€โ”€โ–บ

  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Semantic Cache โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
  โ”‚ embed(query) โ†’ search qa_cache_local (filtered userId+pdfId) โ”‚
  โ”‚   score โ‰ฅ 0.85 & within TTL โ†’ return cached answer โšก        โ”‚
  โ”‚   miss / error (fails open)  โ†’ run the agent โ–ผ               โ”‚
  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

  LangGraph agent:
    rewrite โ”€โ–บ retrieve (dense + BM25 โ†’ RRF โ†’ rerank, top 4)
            โ”€โ–บ grade relevance (LLM yes/no)
                 โ”œโ”€ relevant           โ”€โ–บ generate (cited)        [document]
                 โ”œโ”€ weak & tries < 2   โ”€โ–บ rewrite & retry โ†บ
                 โ””โ”€ still weak         โ”€โ–บ webSearch โ”€โ–บ generate   [web]
                                            โ””โ”€ no results โ”€โ–บ noAnswer
    โ†’ cache the answer (skipped when noAnswer)
    โ†’ persist USER + AI turns to Postgres
    โ†’ trace the run to Langfuse

Retrieval internals

  1. Dense โ€” Qdrant cosine similarity over pdf_embeddings_local, filtered by metadata.userId + metadata.pdfId (payload indexes created on demand โ€” Qdrant Cloud requires them to filter). Pulls a 10-doc candidate pool.
  2. Sparse โ€” Okapi BM25 (k1=1.5, b=0.75) computed in-process over all chunks of that PDF, scrolled from Qdrant in pages of 128.
  3. Fusion โ€” Reciprocal Rank Fusion (k=60) merges both ranked lists, deduping on a content prefix. Falls back to the dense list if fusion yields nothing.
  4. Rerank โ€” local cross-encoder scores each candidate against the query and returns the top 4. If the model fails to load, it degrades gracefully to input order.

Document relevance shown in the UI is the rerank logit squashed through a sigmoid.

Streaming contract (SSE)

POST /chat/stream emits named events:

EventPayloadMeaning
trace{step, detail}one agent reasoning step
token{token}answer token from the generate node
final{answer, sources, sourceType, cached}complete result
done{}stream finished
error{error}failure mid-stream

Turns are persisted once (idempotent saved guard) โ€” including when the client disconnects mid-generation or the stream errors, so a stopped generation is never lost.

Data model (Prisma)

User  1โ”€โ”€โ”€n  Pdf  1โ”€โ”€โ”€n  Chat
                  โ””โ”€ status: PROCESSING | READY | FAILED  (+ statusMessage)
      โ””โ”€โ”€โ”€n  Chat    role: USER | AI, sources Json?

All relations cascade on delete. Deleting a PDF also removes its Qdrant vectors and the S3 object.


๐Ÿš€ Getting Started

1. Infrastructure (Postgres, Qdrant, Valkey, Adminer)

docker compose up -d
ServicePort
Postgres5432 (admin / secret / pdf_rag)
Qdrant6333
Valkey (Redis)6379
Adminer (DB UI)8080

2. Backend

cd server
cp .env.example .env      # see configuration below โ€” S3 + Clerk + OpenRouter are required
npm install
npm run prisma:generate
npm run prisma:migrate

npm start                 # API on :8000
npm run start:worker      # ingestion worker (separate terminal)
# or run both together:
npm run start:all

The API throws at boot if S3 isn't configured, and the worker throws if OPENROUTER_API_KEY, QDRANT_URL, or DATABASE_URL are missing โ€” fail fast instead of failing mid-upload.

First run downloads the embedding + reranker models (~120MB) to a local cache; startup is slower once.

3. Frontend

cd frontend
cp .env.example .env      # VITE_CLERK_PUBLISHABLE_KEY + VITE_API_URL
npm install
npm run dev               # app on :5173

4. Tests & eval (optional)

cd server
npm test                  # Jest (ESM) โ€” rate limiter + request validation
npm run eval              # LLM-as-judge faithfulness / relevance

For npm run eval, set EVAL_USER_ID and EVAL_PDF_ID in .env to an already-ingested document.


โš™๏ธ Configuration (server/.env)

Required

VariablePurpose
OPENROUTER_API_KEYLLM access
DATABASE_URLPostgreSQL connection string
QDRANT_URLVector DB URL (http://localhost:6333 locally)
CLERK_SECRET_KEYVerifies API Bearer tokens
CLERK_WEBHOOK_SECRETVerifies the Svix-signed user-sync webhook
AWS_REGION, S3_BUCKET, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEYPDF storage (all four needed)

Optional

VariablePurposeDefault
OPENROUTER_MODELChat modelmeta-llama/llama-3.1-8b-instruct
QDRANT_API_KEYFor Qdrant Cloudโ€”
REDIS_HOST, REDIS_PORTQueue + rate-limit backendlocalhost, 6379
REDIS_USERNAME, REDIS_PASSWORDRedis auth (managed Redis / Valkey)โ€”
TAVILY_API_KEYBetter web search (else DuckDuckGo)โ€”
CORS_ORIGINSComma-separated allowlisthttp://localhost:5173,http://127.0.0.1:5173
PORTAPI port8000
SEMANTIC_CACHE_ENABLEDToggle the semantic cachetrue
SEMANTIC_CACHE_THRESHOLDMin cosine similarity for a hit0.85
SEMANTIC_CACHE_TTL_HOURSIgnore entries older than this168
LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY, LANGFUSE_BASEURLObservabilitydisabled

๐Ÿ“ก API

All routes except / and /webhook/clerk require Authorization: Bearer <clerk-token>.

MethodRouteDescriptionLimit
GET/Health checkโ€”
POST/upload/pdfUpload a PDF (multipart pdf, โ‰ค15MB), queues ingestion5/min
GET/pdfsList the current user's PDFs120/min
GET/pdfs/:pdfId/fileStream the PDF back for in-app viewing120/min
DELETE/pdfs/:pdfIdDelete PDF + its vectors + its S3 object5/min
POST/chatAsk {query, pdfId} (non-streaming)20/min
POST/chat/streamAsk {query, pdfId}, stream trace + tokens (SSE)20/min
GET/chat/:pdfIdFull chat history for a PDF120/min
POST/webhook/clerkClerk user-sync webhook (Svix-verified raw body)โ€”

Chat returns 409 while a PDF is still PROCESSING, and 404 if the PDF isn't yours.


๐Ÿ“ Project layout

server/
  index.js               Express app, routes, CORS, rate limits, error handling
  worker.js              BullMQ ingestion worker
  start-all.js           runs API + worker in one process
  controllers/           pdf, chat (incl. SSE), clerk webhook
  lib/
    agent.js             LangGraph state machine (the agentic RAG loop)
    ai.js                embeddings, chat model, hybrid retrieval, BM25, RRF
    rerank.js            local cross-encoder reranker
    semanticCache.js     meaning-based answer cache in Qdrant
    webSearch.js         Tavily โ†’ DuckDuckGo fallback
    rateLimit.js         Redis Lua fixed-window limiter
    observability.js     Langfuse tracing
    auth.js  db.js  redis.js  s3.js  validate.js
  prisma/                schema + migrations
  eval/evaluate.js       LLM-as-judge eval harness
  tests/                 Jest tests

frontend/src/
  App.jsx                layout, PDF list, upload, auth gate
  components/            ChatArea (SSE + trace UI), FileUpload, PdfViewer, โ€ฆ
  lib/api.js             axios client + Clerk token injection

๐Ÿงช Verify the interesting bits quickly

  • Semantic cache โ€” ask a question, then ask a paraphrase. The second answer returns near-instantly with a โšก Cached badge, and the backend logs โšก CACHE HIT (similarity 0.9xx) โ€” skipped RAG+LLM pipeline.
  • Web fallback โ€” ask something the PDF doesn't cover. The answer gets a "From the web" badge with linked sources.
  • Self-correction โ€” watch the live trace: on weak retrieval it rewrites the query and retries (up to 2 attempts) before touching the web.
  • Isolation โ€” sign in as another user and request the first user's pdfId โ†’ 404, never a leaked chunk.
  • Resilience โ€” stop the generation mid-stream; reload the page. The partial turn is still in history.

๐Ÿงญ Known limits

  • No OCR โ€” scanned / image-only PDFs are rejected at ingestion with a clear FAILED reason.
  • BM25 is in-process โ€” all chunks of the queried PDF are scrolled from Qdrant per request. Fine for single documents; a server-side sparse index is the upgrade path for large corpora.
  • Fixed-window rate limiting โ€” allows a burst at window edges; a sliding window is the upgrade if that matters.

Contributors

Languages

JavaScript

97.5%

CSS

1.4%

HTML

1.1%