open-source · fully local · no cloud APIs

PDF + Query
Styled PPTX

An agentic RAG pipeline that reads any PDF, reasons about the right presentation structure, and generates a polished .pptx file — entirely on your machine.

Local-first Python 3.11 LangGraph Ollama · qwen2.5:14b Milvus Lite

01 Demo

Two commands — one to ingest, one to generate.

bash
# Step 1 — ingest a PDF (run once per document)
$ python main.py offline path/to/paper.pdf

# Step 2 — generate a presentation
$ python main.py online \
  "make a 10 slide technical presentation on transformer architecture" \
  Attention_is_all_you_Need

✓ Presentation saved → ./outputs/presentation.pptx

Output is a styled .pptx with titles, bullets, speaker notes, and a takeaway bar per slide. Built during an internship at Unicloud as a production-grade document intelligence system.

02 Architecture

Two separate paths keep ingestion and generation cleanly isolated.

OFFLINE — once per PDF
PDF File S1: ingest.py S3a: chunker.py S3b: embedder.py Milvus Lite
ONLINE — per query · LangGraph StateGraph
Raw Query S0: QueryCompiler S3c: Retrieval S4: Strategy S4c: grade (det.) S5: Planner S5c: grade (det.) S6: ContentGen S7: Compress S9: Style S11: Render .pptx
LLM stage    Deterministic stage

The planner and strategy nodes each run inside a conditional retry loop — if the critic detects a schema violation, the agent re-retrieves missing evidence and replans. Critics are deterministic Python, so no extra LLM calls at validation time.

pdf-agent architecture diagram
pdf_PR_pptx — Full pipeline architecture (Offline + Online phases)

Typed artifact chain

Every stage boundary is a validated Pydantic model — no raw strings passed between stages:

str (raw query)
   PresentationRequest
   EvidencePack
   Strategy
   SlidePlan
   list[SlideContent]
   StyleConfig
   .pptx

03 Stack

Everything runs locally. No OpenAI, no Anthropic, no external APIs.

📄
PyMuPDF (fitz)
PDF parsing → DocTree
✂️
LangChain
RecursiveCharacterTextSplitter
🧠
sentence-transformers
all-MiniLM-L6-v2 · 384-dim embeddings
🗄️
Milvus Lite
Local vector store
🦙
Ollama · qwen2.5:14b
Local LLM inference
🕸️
LangGraph
StateGraph agent orchestration
📊
python-pptx
PPTX generation + rendering
⚙️
YAML + Pydantic
Config + typed artifact models

04 Project Structure

pdf-agent/
├── main.py # CLI entry point (offline + online modes)
├── config.yaml # model names, chunk params, retrieval settings
├── requirements.txt
└── src/
    ├── models.py # all Pydantic artifact definitions
    ├── utils.py # shared LLM caller, JSON parser, logger
    ├── ingest.py # S1 — PDF → DocTree
    ├── chunker.py # S3a — DocTree → list[Chunk]
    ├── embedder.py # S3b — list[Chunk] → Milvus
    ├── retrieval.py # S3c — query → EvidencePack
    ├── query_compiler.py # S0 — raw string → PresentationRequest
    ├── strategy.py # S4 — PresentationStrategy (LLM)
    ├── planner.py # S5 — SlidePlan (LLM)
    ├── content.py # S6 — SlideContent (LLM)
    ├── compression.py # S7 — trim bullets for slide format
    ├── visual.py # S8 — VisualSpec per slide
    ├── style.py # S9 — StyleConfig from description
    ├── layout.py # S10 — LayoutSpec (deterministic)
    ├── renderer.py # S11 — python-pptx → .pptx
    └── agent.py # LangGraph StateGraph wiring

05 Setup

Requires Python 3.11, Conda (recommended), and Ollama running locally.

Install

bash
# Create environment
$ conda create -n pdf-agent python=3.11 -y
$ conda activate pdf-agent

# Install dependencies
$ pip install -r requirements.txt

# Fix OMP conflict on macOS Apple Silicon
$ conda env config vars set KMP_DUPLICATE_LIB_OK=TRUE -n pdf-agent
$ conda deactivate && conda activate pdf-agent

# Pull the LLM
$ ollama pull qwen2.5:14b

Verify

bash
# Test Ollama connection
$ python -c "import requests; r = requests.post('http://localhost:11434/api/chat', json={'model':'qwen2.5:14b','messages':[{'role':'user','content':'ping'}],'stream':False}); print(r.json()['message']['content'])"

# Test Milvus
$ python -c "from pymilvus import MilvusClient; print('Milvus ok')"

06 Usage

Ingest a PDF

bash
$ python main.py offline /path/to/document.pdf

Parses, chunks, embeds, and stores the document in ./data/milvus.db. Run once per document. The doc_id is the filename stem.

Generate a presentation

bash
# Technical overview
$ python main.py online "make a 10 slide technical presentation on the transformer architecture" Attention_is_all_you_Need

# Executive summary
$ python main.py online "create a 5 slide executive summary of the attention mechanism" Attention_is_all_you_Need

# Beginner tutorial with dark theme
$ python main.py online "build a beginner tutorial on self-attention dark theme" Attention_is_all_you_Need

Style keywords

dark, minimal
Dark background · blue accent
corporate, formal
White · navy accent
colorful, creative
Light grey · pink accent
(default)
White · blue accent

07 Configuration

All tuneable parameters live in config.yaml.

llm:
  model: qwen2.5:14b
  base_url: http://localhost:11434/api/chat
  timeout: 120

embedding:
  model: all-MiniLM-L6-v2
  device: mps # cpu if not on Apple Silicon

retrieval:
  top_k: 15
  min_evidence: 3

chunking:
  chunk_size: 512
  overlap: 64

agent:
  max_plan_retries: 3
  max_content_retries: 2

08 Roadmap

Phase 1
MVP — offline ingestion + online generation + PPTX render
Phase 2
LangGraph agent loop with plan critic
Phase 3
Semantic DocTree (heading-level chunking, table/figure extraction)
BGE-M3 embeddings + hybrid search (dense + BM25)
S4 strategy stage + strategy critic
S7 compression agent
Phase 4
Gradio UI — PDF upload, query input, style selector, download
FastAPI endpoint
Phase 5
Evaluation harness + open-source release

09 Design Principles

Typed artifacts at every boundary

Every stage produces a Pydantic model. No raw strings between stages. Enables caching, partial regeneration, and model swapping.

Constraints propagate as data

Slide count, audience, and style are extracted at S0 and enforced mechanically downstream — not passed as vague prompt instructions.

Offline / online separation

Ingestion happens once per document. Query-time work starts at retrieval. These paths never mix.

Critics are cheap

The plan critic is deterministic Python. LLM critic calls happen only when schema validation fails, keeping latency low.