A small, modular pipeline that reads a research-paper PDF, extracts structured metadata (title, authors, abstract, section headings, and more), optionally generates a structured LLM summary using an Ollama cloud model, and saves the result as clean JSON.
ResearchPaperLens extracts structured metadata from research-paper PDFs, saves the result as JSON, optionally generates an LLM summary, and supports semantic retrieval plus RAG Q&A over indexed paper chunks.
Web deployment is not implemented yet.
- Extracts text from research-paper PDFs
- Counts pages and words
- Guesses title and authors
- Extracts abstract and keywords when present
- Detects section headings
- Word-based chunking for long papers (
src/chunking.py), with configurable overlap between chunks - Optional LLM summarization: fills a structured
summary(tldr, problem, approach, key results/metrics, contributions, limitations, key insights) via an Ollama cloud model--summarize— single-pass summary (first ~12k characters of the paper)--summarize-full— section-aware hybrid summarization: split by section, word-chunk long sections, merge section summaries into a final summary (recommended for long papers)- Model selectable with
--model
- Semantic retrieval: hybrid-chunk embeddings via Ollama (
nomic-embed-text), per-paper chunk JSON stores, in-memory cosine search, CLI--index/--search - RAG Q&A: retrieve top-k chunks, answer questions with Ollama cloud (
--ask), with section citations and source chunk ids - Saves structured results as JSON
- Includes unit and integration tests
The pipeline is split into focused, independently testable modules:
.
├── src/
│ ├── paper.py # ResearchPaper data model (dataclass)
│ ├── pdf_reader.py # PDF I/O: extract full text + page count
│ ├── analyzer.py # Pure analysis: text -> ResearchPaper fields
│ ├── chunking.py # Word-based text chunking with overlap
│ ├── ollama_client.py # Shared Ollama cloud client (API key)
│ ├── embeddings.py # Ollama cloud text embeddings
│ ├── chunk_store.py # Indexed chunks + JSON persistence
│ ├── vector_index.py # In-memory cosine similarity search
│ ├── retrieval.py # index_paper() and search() orchestration
│ ├── rag.py # RAG Q&A over retrieved chunks
│ ├── summarizer.py # LLM summarizer (Ollama cloud): summary/key_insights
│ └── storage.py # Serialize ResearchPaper <-> JSON
├── data/ # Input PDFs
├── outputs/ # Generated JSON output (git-ignored)
├── tests/ # Unit tests + tests/integration/ end-to-end test
├── main.py # CLI entry point that wires the pipeline together
├── requirements.txt # Python dependencies
└── README.md
Data flows in one direction:
PDF --(pdf_reader)--> text + page count --(analyzer)--> ResearchPaper --(storage)--> JSON
|
optional --(summarizer)--> summary
|
--summarize-full--> sections --> per-section summaries --> merge
analyzer.py is deliberately decoupled from PDF reading: it only accepts
already-extracted text, which keeps the analysis logic easy to test and reusable
for any text source.
For --summarize-full, chunking.py splits the paper into top-level sections
(Abstract, Preamble, numbered sections). Sections longer than 1,200 words are
subdivided with fixed-word chunks (with overlap). The summarizer summarizes each
section (merging sub-chunks when needed), then merges section summaries into the
final structured summary using paper metadata as anchors.
Requires Python 3.10+.
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtLLM summarization uses Ollama cloud models via
the cloud API. To use --summarize or --summarize-full, create an API key at
ollama.com/settings/keys and provide it as
the OLLAMA_API_KEY environment variable.
The recommended way is a local .env file (auto-loaded via python-dotenv and
git-ignored, so your key is never committed). Copy the template and fill it in:
cp .env.example .env
# then edit .env and set OLLAMA_API_KEY=your_api_keyAlternatively, export it in your shell:
export OLLAMA_API_KEY=your_api_keyThe key is required for --summarize, --summarize-full, and --ask (chat
model). Embedding for --index, --search, and --ask uses Ollama cloud when
authorized, otherwise a local Ollama daemon (ollama pull nomic-embed-text).
Basic PDF analysis and --search/--ask against an existing index work without
re-indexing the paper.
Run the pipeline on a PDF:
# Use the bundled sample paper -> outputs/AttentionIsAllYouNeed.json
python main.py
# Analyze your own PDF (output defaults to outputs/<pdf-stem>.json)
python main.py path/to/paper.pdf
# Choose an explicit output file
python main.py path/to/paper.pdf -o results/paper.json
# Summarize with an Ollama cloud model (requires OLLAMA_API_KEY)
python main.py path/to/paper.pdf --summarize
# Full-paper summarization via chunking + merge (recommended for long papers)
python main.py path/to/paper.pdf --summarize-full
# Switch the cloud model used for summarization
python main.py path/to/paper.pdf --summarize-full --model gpt-oss:20b
# Build a semantic chunk index (embed + save outputs/<pdf-stem>.chunks.json)
python main.py path/to/paper.pdf --index
# Search an existing chunk index
python main.py --search "What BLEU scores were reported?" \
--from outputs/AttentionIsAllYouNeed.chunks.json
# Show more search hits
python main.py --search "multi-head attention" \
--from outputs/AttentionIsAllYouNeed.chunks.json --top-k 10
# Ask a grounded question (retrieve + Ollama cloud answer)
python main.py --ask "How does multi-head attention work?" \
--from outputs/AttentionIsAllYouNeed.chunks.jsonSee all options with python main.py --help.
Note:
--summarizeand--summarize-fullare mutually exclusive. Both requireOLLAMA_API_KEY. The model defaults togpt-oss:120band can be changed with--model; other options includegpt-oss:20b,qwen3-coder:480b, anddeepseek-v3.1:671b(see Ollama cloud models).
--summarize— one API call; sends only the first ~12k characters of the paper (fast, but may miss later sections on long papers).--summarize-full— hierarchical hybrid summarization (default strategy): split by section, word-chunk sections over 1,200 words, summarize each section, merge into a final summary. Multiple API calls (more than--summarize, but covers the full paper with better structure).--index— embeds hybrid section chunks and writes a chunk index JSON. Tries Ollama cloud first; if/api/embedis unauthorized, falls back to a local Ollama daemon (runollama pull nomic-embed-text).--search— loads a chunk index from--fromand prints ranked matches (section, score, text snippet). Does not require a PDF argument.--ask— RAG Q&A: retrieves top-k chunks, sends them as context to the Ollama cloud model, and prints an answer with section citations and source chunk ids. RequiresOLLAMA_API_KEYfor the chat model and a working embed endpoint (same as--index). The answer appears right after the question line;CitationsandSourcesare metadata at the bottom. This is not the same structured summary card as--summarize.
Indexing is not automatic. Only --index (re)builds the chunk file; other
commands load the saved index from --from.
# 1. Index once (slow — embeds all chunks, overwrites .chunks.json)
python main.py data/AttentionIsAllYouNeed.pdf --index
# 2. Search or ask many times (fast — only embeds your query)
python main.py --search "What BLEU scores were reported?" \
--from outputs/AttentionIsAllYouNeed.chunks.json
python main.py --ask "How does multi-head attention work?" \
--from outputs/AttentionIsAllYouNeed.chunks.jsonFor indexing, hybrid chunking uses Abstract + top-level numbered sections (no Preamble). Sections ≤ 1,200 words stay whole; longer sections are split into 1,200-word chunks with 150-word overlap.
| Command | What you get |
|---|---|
--summarize / --summarize-full |
Structured summary card (TL;DR, Problem, Approach, Key results, …) |
--search |
Ranked chunk snippets with scores — no synthesized answer |
--ask |
Plain-English answer to your question + Citations + Sources |
Example --ask terminal layout:
Question: "What BLEU scores were reported?"
The Transformer achieved 28.4 BLEU on English-German WMT 2014. ← answer here
Citations:
- 6 Results
Sources:
- AttentionIsAllYouNeed:6-results:0
You can also use the pieces directly in Python:
from src.pdf_reader import read_pdf
from src.analyzer import analyze
from src.storage import save_paper, load_paper
from src.summarizer import summarize, summarize_full
full_text, page_count = read_pdf("data/AttentionIsAllYouNeed.pdf")
paper = analyze(full_text=full_text, page_count=page_count,
source_path="data/AttentionIsAllYouNeed.pdf")
# Optional: single-pass or full-paper summarization (requires OLLAMA_API_KEY)
# paper = summarize(paper)
# paper = summarize_full(paper)
save_paper(paper, "outputs/paper.json")
paper = load_paper("outputs/paper.json") # round-trips back into a ResearchPaperIndex hybrid chunks (Abstract + numbered sections), search them, or ask
grounded questions. Chunk indexes are saved as outputs/<pdf-stem>.chunks.json.
python main.py data/AttentionIsAllYouNeed.pdf --index
python main.py --search "What BLEU scores were reported?" \
--from outputs/AttentionIsAllYouNeed.chunks.json
python main.py --ask "What BLEU scores were reported?" \
--from outputs/AttentionIsAllYouNeed.chunks.jsonOr from Python:
from src.retrieval import index_paper, search
from src.embeddings import resolve_embed_client
from src.chunk_store import save_chunk_store, default_chunk_store_path
from src.rag import ask, format_rag_answer
client = resolve_embed_client()
store = index_paper(paper, client=client)
save_chunk_store(store, default_chunk_store_path(paper))
results = search(store, "What BLEU scores were reported?", client=client, top_k=3)
answer = ask(store, "What BLEU scores were reported?", embed_client=client, top_k=5)
print(format_rag_answer(answer))The output JSON mirrors the ResearchPaper dataclass. Example (abridged):
{
"title": "Attention Is All You Need",
"authors": ["Ashish Vaswani", "Noam Shazeer", "..."],
"abstract": "The dominant sequence transduction models are based on ...",
"keywords": [],
"section_headings": ["1 Introduction", "2 Background", "3 Model Architecture", "..."],
"page_count": 15,
"word_count": 6166,
"full_text": "...",
"references": [],
"doi": "",
"publication_year": 0,
"venue": "",
"source_path": "data/AttentionIsAllYouNeed.pdf",
"summary": {
"tldr": "",
"problem": "",
"approach": "",
"key_results": [],
"contributions": [],
"limitations": [],
"key_insights": []
}
}Currently derived: title, authors, abstract, keywords, section_headings,
page_count, word_count, full_text, source_path. The remaining fields
(references, doi, publication_year, venue) are reserved and left at their
defaults for now. summary is a structured object (a "summary card") populated
by the LLM summarizer when --summarize or --summarize-full is passed;
otherwise its fields stay empty. Its fields are:
tldr- a 1-2 sentence plain-English overviewproblem- the problem or motivation the paper addressesapproach- the method or technique usedkey_results- main findings, including quantitative metrics when statedcontributions- the paper's novel contributionslimitations- caveats or weaknesseskey_insights- the most important standalone takeaways
- Extraction is heuristic and tuned for common single-column paper layouts, so results may vary on other formats (e.g. multi-column or scanned PDFs).
- Field detection relies on structural cues (capitalization, numbered headings, superscript author markers), not semantic understanding, so it can miss or misidentify fields in papers that deviate from those conventions.
- Keywords are only extracted when the paper has an explicit
Keywords/Index Termsline; otherwise the field is left empty. references,doi,publication_year, andvenueare not yet extracted.--summarizesends only the first ~12k characters of the paper to the model, so very long papers may miss content from later sections. Use--summarize-fullfor full-paper coverage (at the cost of more API calls).--summarize-fullmakes one API call per chunk plus a final merge call; long papers take longer and cost more than--summarize.--askanswers from retrieved excerpts only; quality depends on chunking and retrieval. Use--top-kto retrieve more chunks if answers miss key sections.
# Run everything
python -m pytest
# Unit tests only / integration only
python -m pytest tests --ignore=tests/integration
python -m pytest tests/integrationUnit tests are fully offline: the summarizer tests patch the Ollama network
boundary, so no API key or network access is needed. Live API integration tests
call a real Ollama cloud model and are skipped automatically unless
OLLAMA_API_KEY is set (e.g. via .env):
tests/integration/test_summarizer_live_api.py— single-pass--summarizetests/integration/test_summarizer_full_live_api.py— chunked--summarize-fulltests/integration/test_retrieval_live_api.py— hybrid-chunk indexing + semantic search (cloud embed when authorized, otherwise localnomic-embed-text)tests/integration/test_rag_live_api.py— RAG Q&A over indexed chunks