--- title: Hybrid Vision RAG PDF Processor emoji: πŸ“„ colorFrom: indigo colorTo: blue sdk: gradio sdk_version: "5.20.0" app_file: main.py pinned: false license: mit --- # Hybrid Vision RAG PDF Processor A demo-ready Python project that extracts PDF content, analyzes charts/graphs via a Vision LLM, and answers questions with a Hybrid RAG system (Vector + Knowledge Graph + BM25). ## Tech stack - **Docling** β€” PDF content extraction (text, pages, figures) - **OpenRouter** β€” Vision LLM for chart/graph analysis - **Groq** β€” LLM powering the RAG pipeline - **LlamaIndex** β€” Vector + Knowledge Graph + BM25 retrieval with RRF fusion - **Gradio** β€” demo UI ## What's new - βœ… **Clean layered architecture** β€” `config`, `models`, `services`, `retrieval`, `session`, `web` - βœ… **Own session architecture** β€” `Session` entity + pluggable `SessionStore` + `SessionManager` with janitor - βœ… **Pydantic configuration** β€” all settings loaded from `.env` with validation - βœ… **Structured logging** β€” no more raw `print()` statements - βœ… **Polished Gradio demo UI** β€” live progress, status badge, commands, and session controls - βœ… **Pre-flight API validation** β€” OpenRouter + Groq are live-checked before the pipeline starts; failures pop a warning and block processing (see the **API Status** panel) - βœ… **Unit tests** for session lifecycle and BM25 retrieval - βœ… **Unified CLI** β€” `extract`, `preprocess`, `app` ## Quick start ### 1. Install dependencies ```bash uv sync # or pip install -e . ``` ### 2. Configure API keys Copy `.env.example` to `.env` and add your keys: ```bash cp .env.example .env ``` ```env OPENROUTER_API_KEY=sk-or-v1-... GROQ_API_KEY=gsk_... ``` ### 3. Launch the demo ```bash app # or python -m docling_pdf_processor ``` Open `http://localhost:7860` in your browser. --- ## Docker Run the demo in a container (secrets are injected at runtime β€” never baked into the image): ```bash # 1. Put real keys in .env (the image does NOT see your host's OS env vars) # OPENROUTER_API_KEY=sk-or-v1-... # GROQ_API_KEY=gsk_... # 2. Build & run docker compose up --build ``` Then open `http://localhost:8010`. Without compose: ```bash docker build -t docling-pdf-processor . docker run -p 8010:8010 --env-file .env docling-pdf-processor ``` The compose file mounts a named volume at `/app/.cache/huggingface` so Docling's model weights download once and persist across restarts. > ⚠️ **Key source caveat for containers:** on your host, `GROQ_API_KEY` may live in an OS environment variable that overrides the `.env` placeholder (the API Status panel shows `[source: OS env $GROQ_API_KEY]`). A container does **not** inherit host OS env vars β€” only what you pass via `--env-file` / `env_file`. So you must put the **real** Groq key in `.env` before `docker compose up`, or the pre-flight validation will block processing with `401 Invalid API key`. --- ## CLI usage ```bash # Extract images + markdown from a PDF extract --pdf path/to/doc.pdf --output ./extracted_images # Preprocess extracted images with a Vision LLM preprocess --quarters ./extracted_images --output graphs_description.pkl # Launch the Gradio demo app ``` --- ## Project architecture ``` docling_pdf_processor/ β”œβ”€β”€ config.py # Pydantic Settings from .env β”œβ”€β”€ models.py # Shared dataclasses β”œβ”€β”€ exceptions.py # Domain errors β”œβ”€β”€ logging_config.py # Structured logging setup β”œβ”€β”€ cli.py # Unified CLI β”œβ”€β”€ pipeline.py # Session-aware PipelineOrchestrator β”œβ”€β”€ services/ β”‚ β”œβ”€β”€ extractor.py # Docling PDF extraction β”‚ β”œβ”€β”€ vision.py # OpenRouter Vision-LLM β”‚ └── rag.py # Hybrid RAG builder + query β”œβ”€β”€ retrieval/ β”‚ β”œβ”€β”€ bm25.py # BM25 keyword retriever β”‚ └── hybrid_rrf.py # RRF fusion retriever β”œβ”€β”€ session/ β”‚ β”œβ”€β”€ models.py # Session + SessionStatus β”‚ β”œβ”€β”€ store.py # SessionStore protocol + implementations β”‚ └── manager.py # Lifecycle + janitor └── web/ β”œβ”€β”€ gradio_app.py # Demo UI └── components.py # UI helpers ``` ### Data flow ``` User uploads PDF β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ SessionManager β”‚ Creates UUID workspace in temp dir β”‚ (own session arch) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ PipelineOrchestratorβ”‚ Streams progress via Queue β”‚ β€’ ExtractorService β”‚ Docling β†’ text, pages, figures β”‚ β€’ VisionService β”‚ OpenRouter β†’ graph descriptions β”‚ β€’ RAGService β”‚ Vector + KG + BM25 indexes β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Gradio Chat UI β”‚ Session-scoped memory β”‚ !reasoning β”‚ Inspect retriever contributions β”‚ !compare β”‚ Side-by-side retriever comparison β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` --- ## Session architecture Every upload creates a `Session` with: - A unique UUID - An isolated workspace under `tempdir/docling_sessions/{uuid}/` - A `SessionStatus` lifecycle: `created β†’ extracting β†’ analyzing β†’ indexing β†’ ready` - A heartbeat timestamp for stale-session cleanup The `SessionStore` protocol has two built-in implementations: - `InMemorySessionStore` β€” default for single-process demos - `FileSystemSessionStore` β€” persists session metadata to disk A background janitor cleans sessions idle longer than `SESSION_MAX_AGE_SECONDS` (default 30 min). --- ## Chat commands - `!reasoning ` β€” show how Vector / KG / BM25 contribute to the final answer - `!compare ` β€” compare Vector, KG, BM25, and Hybrid answers side-by-side --- ## API validation Before the pipeline runs, the app live-checks the two external APIs it depends on: | API | Check | Endpoint | |-----|-------|----------| | OpenRouter (Vision LLM) | key + credits | `GET https://openrouter.ai/api/v1/key` | | Groq (RAG LLM) | key + model **actually generate** (1-token chat completion) | `POST https://api.groq.com/openai/v1/chat/completions` | - The **API Status** panel in the *Process PDF* tab shows each API as βœ… Valid / ❌ Invalid, with a **masked key prefix and the key's source** (`UI field` / `OS env $VAR` / `.env`) so it's clear which credential is being tested. - Click **Validate APIs** to re-run the checks on demand; editing any key/model field live-refreshes the panel. - When you click **Process PDF**, validation runs first. If any API fails (bad key β†’ 401, wrong model β†’ 404, no credits β†’ 402), a popup warning shows which API and why, and the pipeline **does not start**. > **Note on key sources:** pydantic-settings loads OS environment variables *with higher priority than* `.env`. So if a key is set in your shell/system environment, it overrides a placeholder in `.env`. The source tag in the panel makes this visible β€” e.g. `OS env $GROQ_API_KEY` instead of `.env`. Validation calls are cheap and run in parallel, typically returning in ~1s. --- ## Production notes - **Concurrency** β€” Gradio runs with `default_concurrency_limit=1` so only one pipeline processes at a time per process instance. - **Multi-tenant scaling** β€” For true multi-user production, run each session in a separate worker or process. - **Vector store** β€” Uses LlamaIndex's in-memory `SimpleVectorStore` (the previous DeepLake store is abandoned and breaks on NumPy 2 / Python 3.13). Indexes are rebuilt from the extracted files each session, which is the existing behavior, so nothing is lost β€” but for very large corpora consider swapping in a persisted store (FAISS, Chroma, pgvector). - **API keys** β€” Never commit `.env`. It is already ignored by `.gitignore`. - **Large PDFs** β€” PDFs > 100 MB trigger a warning but are not blocked. --- ## Development ### Run tests ```bash pytest ``` ### Run a specific stage from the CLI ```bash # Extraction only extract --pdf sample.pdf --output ./out # Vision analysis only preprocess --quarters ./out --output ./out/graphs.pkl --api-key $OPENROUTER_API_KEY ``` --- ## License MIT