🧠 RAG stack¶
Papers RAG loads PDFs with pypdf, embeds locally, persists Chroma per backend, and generates answers with OpenAI plus citations. LlamaIndex and LangChain are in-process backends over the same corpus; each has its own Chroma directory.
Overlap chunking, a prose filter, and catalog routing (library-wide and authorship questions skip vector search) sit on top of retrieve-then-generate.
Leaf |
Path |
Role |
|---|---|---|
|
|
Config, loaders, backends, |
|
|
CLI: PDF → Chroma + |
|
|
Streamlit chat |
Docs: rag-core, papers-rag, papers-ingest. Langflow is a separate studio: Langflow.
flowchart TB
PDFs["assets/pdf/papers/*.pdf"] --> Load["pypdf + is_prose_text"]
Load --> Catalog["catalog.json"]
Load --> Dispatch["papers-ingest --strategy"]
Dispatch --> LI[".data/indexes/llamaindex/"]
Dispatch --> LC[".data/indexes/langchain/"]
LI --> App["papers-rag"]
LC --> App
Catalog --> Ask{"ask()"}
App --> Ask
Ask -->|corpus / author| Synth["OpenAI on catalog"]
Ask -->|otherwise| Retrieve["overfetch → prose filter → top_k"]
Retrieve --> GPT["OpenAI generation"]
Synth --> UI["answer + citations"]
GPT --> UI
sequenceDiagram
participant Dev as Developer
participant Ingest as papers-ingest
participant Embed as bge-small
participant Chroma as Chroma
participant App as papers-rag
participant GPT as OpenAI
Dev->>Ingest: poe ingest-papers --strategy llamaindex
Ingest->>Embed: embed chunks
Embed-->>Ingest: vectors
Ingest->>Chroma: persist + catalog.json
Dev->>App: poe run-papers-rag
App->>App: classify question
alt corpus or author
App->>GPT: catalog plus question
else paper
App->>Chroma: retrieve top_k × 3
Chroma-->>App: chunks
App->>App: drop non-prose
App->>GPT: labeled context plus question
end
GPT-->>App: answer
App-->>Dev: answer plus citations
🎛️ Strategies¶
Strategy |
Ingest |
Query |
Persist |
|---|---|---|---|
|
|
Query engine + |
|
|
|
Retriever → |
|
poe ingest-papers --strategy llamaindex
poe ingest-papers --strategy langchain
poe ingest-papers --strategy all
--strategy all cannot take --chroma-dir. CHROMA_DIR overrides persist for
one strategy. RAG_STRATEGY defaults to llamaindex; the Streamlit sidebar
overrides it for the session.
📦 Code map (rag.core)¶
Module |
Responsibility |
|---|---|
|
|
|
|
|
Non-recursive |
|
|
|
Author-question cues, acknowledgement / SPIE filters, page labels |
|
|
|
Dispatch + LlamaIndex chunk → embed → Chroma |
|
Open persistent LlamaIndex collection; |
|
|
|
|
|
|
Public API: RagConfig, RagStrategy, ingest_papers, ask,
build_query_engine, Citation, QueryResult, IndexMissingError.
📄 PDF loading¶
Shared loader (rag.core.loaders):
Directory:
PAPERS_DIRorassets/pdf/papers.Non-recursive
*.pdf, sorted by path.Per-page pypdf extraction.
Pages that fail
is_prose_textare skipped (empty pages, dedications, figure dumps, pyLDAvis chrome, PDF/uni00encodings).Metadata on kept pages:
file_name,file_path,page(1-based),page_label.Image-only scans raise
FileNotFoundError.
Rebuild with poe ingest-papers after loader or filter changes, then restart
Streamlit (file watching is off).
🧹 Prose filter¶
is_prose_text runs at ingest and again at query. A page or chunk is kept when
all of these hold:
Check |
Threshold |
|---|---|
Collapsed length |
≥ 120 characters |
Real words ( |
≥ 8 |
PDF |
rejected |
Letter stutter ( |
rejected |
Tokens mixing letters with unusual symbols |
< 10% of tokens |
Glued TitleCase / camelCase |
< 4% of tokens |
Snake_case tokens |
< 3% of tokens |
Single-letter tokens |
reject if ≥ 7% and fewer than 80 real words |
Query retrieves similarity_top_k × 3 (RETRIEVAL_OVERFETCH = 3), then keeps
at most similarity_top_k prose chunks.
✂️ Chunking¶
Backend |
Splitter |
Defaults |
|---|---|---|
LlamaIndex |
|
|
LangChain |
|
same sizes |
Override with CHUNK_SIZE / CHUNK_OVERLAP. Rebuild the index after changing
these.
🧮 Embeddings¶
LlamaIndex and LangChain both use the local model BAAI/bge-small-en-v1.5
(LlamaIndex: HuggingFaceEmbedding; LangChain: HuggingFaceEmbeddings). First
ingest downloads weights from HuggingFace Hub. Override with EMBED_MODEL.
Query-time embed must match ingest.
🗄️ Vector store¶
Chroma PersistentClient under .data/indexes/{strategy} (gitignored).
Collection name defaults to papers (CHROMA_COLLECTION). Ingest defaults to
--rebuild (delete and recreate). --no-rebuild appends.
Each ingest writes catalog.json next to that strategy’s Chroma dir:
{
"papers": [
{"file_name": "example.pdf", "title": "opening-page snippet…"}
]
}
Titles are the first extractable page, collapsed, truncated to 220 characters.
If catalog.json is missing or invalid, ask() uses filenames from
papers_dir.
🧭 Query routing¶
ask(question=…, config=…) classifies before vector search:
flowchart TD
Q[question] --> A{author cues?}
A -->|yes| Cat[catalog.json → AUTHOR_SYNTHESIS_PROMPT]
A -->|no| C{corpus cues?}
C -->|yes| Corp[catalog.json → CORPUS_SYNTHESIS_PROMPT]
C -->|no| R[vector retrieve → generate]
Corpus questions¶
all papers,all the papers,every paperentire corpus,the corpus,among all,across allcommon topic among,main topic among,common theme,unifying themebody of work,what do these papers,these papers have in commontheme across,overall research
When files span domains but share ML methods, the prompt names applied machine learning as the umbrella, then lists domains from filenames.
Paper questions¶
Similarity search. The QA prompt treats excerpts as a retrieved subset: do not say “all papers” or “both papers” unless every filename used is listed.
🔎 Retrieval¶
Embed the question with the same local model.
Retrieve
top_k × 3nearest chunks from that strategy’s collection.Drop non-prose chunks.
For leftover authorship-shaped questions: drop acknowledgement text and SPIE boilerplate (
edited by,proc. of spie,ccc code,spiedigitallibrary,terms of use); sort remaining chunks by earlier page first.Keep at most
top_k(sidebar 1–12; env default 5).LlamaIndex uses
text_qa_template+refine_template. LangChain labels chunks as[file.pdf, p.N].Both inject
QA_GROUNDING_RULES: manuscript thanks are not authors; SPIE “edited by” names are volume editors; prefer title-page names; do not pick one main author for the library from a retrieval subset.
Empty collection raises IndexMissingError.
💬 LLM¶
Generation uses OpenAI (gpt-4o-mini by default, OPENAI_MODEL).
OPENAI_API_KEY is required. Streamlit also reads Cloud secrets.toml.
Process env wins over .env (override=False). Streamlit does not source
.zshrc; load_repo_dotenv() loads the repo-root .env even when cwd is the
app script directory.
📚 Citations¶
Citation objects render in the Streamlit Sources expander (collapsed):
Field |
Retrieval |
Catalog (corpus / author) |
|---|---|---|
|
chunk metadata |
catalog entry |
|
1-based when present |
|
|
retriever score when present |
|
|
≤ 280 chars of chunk text |
opening-page title snippet |
Markdown: 1. **file.pdf**, p.3 (score=0.812) — snippet…
💬 Streamlit¶
poe run-papers-rag runs Streamlit with --server.fileWatcherType none so
HuggingFace vision-module probes do not spam ModuleNotFoundError: torchvision.
Restart after code changes.
Control |
Meaning |
|---|---|
Strategy |
LlamaIndex or LangChain; switching clears chat |
Similarity top-k |
Chunks kept after overfetch + filter (1–12) |
Clear chat |
Reset session messages |
Sources |
Citations expander, collapsed |
Sidebar shows LLM id, embed model, Chroma path, and whether the index is ready.
⚙️ Environment¶
Copy .env.example to .env. Do not commit real keys.
Variable |
Default |
Meaning |
|---|---|---|
|
(required) |
Chat completions |
|
|
Chat model id |
|
|
Local HuggingFace embed |
|
|
Default backend |
|
|
PDF directory |
|
|
Persist dir (single strategy) |
|
|
Collection name |
|
|
Splitter chunk size |
|
|
Splitter overlap |
|
|
Chunks after filter |
CLI: papers-ingest --papers-dir / --chroma-dir / --strategy / --rebuild.
🆓 Cost¶
Layer |
Choice |
Cost |
|---|---|---|
Orchestration |
LlamaIndex or LangChain |
OSS |
Embeddings |
|
Free |
Vector DB |
Chroma (persistent dir) |
Free |
LLM chat |
OpenAI API |
Your key |
Docs hosting |
GitHub Pages |
Free |
🧯 Troubleshooting¶
Symptom |
Fix |
|---|---|
Empty index / |
|
Missing |
Repo-root |
|
Use |
Chart axes / pyLDAvis / |
Rebuild after the prose filter; restart Streamlit |
Acknowledgements or SPIE editors named as authors |
Author routing uses |
One-paper answer to an “all papers” question |
Corpus cues + catalog; re-ingest to refresh |
Garbled UTF-8 snippets |
Rebuild the index |