Skip to content
RAG · Event-Driven Backend

DocSage

Production-grade, event-driven RAG service — PDFs are ingested through durable Inngest workflows, chunked page by page, and answered with inline source-and-page citations.

    PythonFastAPIInngestLlamaIndexOpenAIQdrantStreamlitpytestDocker

The Problem

Document collections lose their value when answers can't be trusted: chunk-level RAG loses page context, ingestion pipelines fail silently, and nobody can tell which source an answer came from.

The Solution

An event-driven RAG backend built for reliability. PDFs are ingested through Inngest durable workflows — each stage (load, chunk, embed, upsert) is an independently retriable, observable step with throttling and per-source rate limits. Chunks carry page numbers end to end, so every answer ships with inline [1], [2] citations resolving to source file and page. A Streamlit console handles uploads and questions; queries can be scoped to a single document via payload filtering. Deterministic UUIDv5 ids make re-ingestion idempotent.

How the workflow runs

  1. 01 · Ingest

    Upload fires a docsage/ingest_pdf event; Inngest runs load-and-chunk and embed-and-upsert as retriable steps.

  2. 02 · Guard

    Throttling (2 runs/min) and per-source rate limits (1/4h) prevent accidental embedding storms.

  3. 03 · Store

    Page-tagged chunks land in Qdrant with source payloads; deterministic ids make re-ingestion idempotent.

  4. 04 · Retrieve

    Queries embed and search top-k chunks, optionally hard-filtered to one source document.

  5. 05 · Answer

    GPT-4o-mini answers strictly from numbered context; the response carries citations with source, page, and snippet.

AI layer

OpenAI embeddings
text-embedding-3-large (3072-dim), model and dimensions configurable via env.
Grounded answering
System prompt restricts answers to numbered context with an explicit not-found fallback.
Citation pipeline
Source/page payloads flow from chunker to search to answer — provenance is never lost.

Automation layer

Durable workflows
Inngest steps retry independently and are observable step-by-step in the dashboard.
Idempotent ingestion
UUIDv5 ids derived from source + chunk index — re-uploads overwrite, never duplicate.
One-command infra
Docker Compose ships Qdrant, the Inngest dev server, and the API together.

Technical challenges

  • Naive chunking flattens PDFs and destroys page boundaries, making citations impossible.

    Page-aware chunking: each page is chunked independently and the page number travels in the vector payload end to end.

  • Long ingestion jobs fail midway and re-running duplicates vectors or loses work.

    Durable step functions with deterministic ids — failed steps retry in isolation and completed work is never redone.

  • Vector stores' evolving client APIs (deprecated search methods) break production code.

    Standardised on the modern query_points API with a thin storage class that also exposes in-memory mode for tests.

Outcome

  • Every answer is checkable — inline citations resolve to source file and page in one click.
  • Ingestion is operationally boring: retries, throttling, and idempotency are built into the workflow, not scripts.
  • The test suite runs on in-memory Qdrant with zero API keys, so CI stays fast and free.

Lessons learned

  • Provenance is a data-modelling problem, not a prompting problem — citations work because page numbers live in payloads.
  • Durable execution turns 'hope the batch finishes' into an auditable, retryable pipeline.
  • Storage layers deserve the same abstraction discipline as LLM calls: in-memory mode makes vector logic testable.