RAGU is a local-first RAG application focused on question answering over structural biology PDFs. The project favors practical self-hosted dependencies and a compact Python codebase over deep abstraction.
This document explains the implemented architecture, the main technical decisions, and the coding/operational constraints contributors should preserve.
flowchart TD
PDF[PDF upload] --> Loader[PyMuPDFLoader]
Loader --> Splitter[RecursiveCharacterTextSplitter]
Splitter --> Embed[Ollama embeddings]
Embed --> Chroma[Chroma service]
Prompt[Question] --> Retrieve[Chroma retrieval]
Retrieve --> Rerank[Cross-encoder reranking]
Rerank --> Chat[Ollama chat]
Chat --> Stream[Streamlit streamed answer]
Stream --> PDB[PDB extraction]
PDB --> MolStar[Mol* viewer]
Stream --> TTS[Piper TTS when available]
The majority of product logic still lives in app.py.
Why:
- the project is still small enough to keep iteration speed high
- deployment and debugging remain straightforward
- most changes still touch one vertical slice of the app
Tradeoff:
- concerns are mixed
- modular reuse and testing are harder than they should be
Ollama is used for:
- chat generation
- embedding generation
Why:
- keeps the app local/self-hosted
- avoids cloud inference dependencies
- fits privacy-sensitive local workflows
Tradeoff:
- heavier startup and runtime resource usage
- model selection materially affects usability on low-end hardware
The Docker deployment no longer uses embedded Chroma persistence inside the app container. The app talks to a dedicated Chroma service via chromadb.HttpClient(...).
Why:
- avoids embedded SQLite permission issues inside the app container
- aligns with Chroma’s supported client/server deployment model
- separates vector storage concerns from the app runtime
Tradeoff:
- adds another service to the stack
- local development becomes more operationally involved
Retrieved chunks are reranked with cross-encoder/ms-marco-MiniLM-L-6-v2.
Why:
- improves answer relevance over plain nearest-neighbor retrieval
- keeps the retrieval pipeline simple while improving precision
Tradeoff:
- extra CPU and model-loading overhead
Piper is treated as an optional enhancement.
Why:
- improves usability when audio playback is desired
- remains fully optional so the app still works in text-only environments
Current behavior:
- the app checks whether Piper is available
- if Piper is unavailable, the app shows a visible status message and remains text-only
- if Piper synthesis succeeds, Streamlit renders an audio player in the browser
Tradeoff:
- architecture-specific install behavior
- extra build-time and image-size cost
The stack has two distinct monitoring/observability paths:
- Chroma -> OTEL Collector -> Zipkin (+ Prometheus scrape of collector metrics)
- Ollama -> community exporter -> Prometheus -> Grafana
Why:
- Chroma already documents an OTEL-based observability path
- Ollama does not expose a first-party monitoring stack, so exporter-based monitoring is the practical option
Tradeoff:
- more containers
- one monitoring dependency is community-maintained, not official
Implemented in Streamlit inside app.py.
Responsibilities:
- PDF upload
- answer prompt input
- streamed answer rendering
- PDB visualization
- optional TTS playback
- retrieval debugging visibility
Implemented with:
PyMuPDFLoaderRecursiveCharacterTextSplitter
Responsibilities:
- temporary file handling
- PDF parsing
- chunk generation
Implemented through Chroma.
Responsibilities:
- embedding-backed storage
- retrieval
- collection lifecycle
Implemented through Ollama.
Responsibilities:
- embedding inference
- chat inference
- streamed response delivery
Implemented through sentence_transformers.CrossEncoder.
Responsibilities:
- rerank retrieved chunks
- improve context selection before generation
Implemented with Mol* in a Streamlit HTML embed.
Responsibilities:
- detect PDB code
- render structure when possible
Implemented through:
- OTEL Collector
- Zipkin
- community Ollama exporter
- Prometheus
- Grafana
- cAdvisor
flowchart LR
App[app]
Chroma[chroma]
Ollama[ollama]
Redis[redis]
OTel[otel-collector]
Zipkin[zipkin]
Exporter[ollama-exporter]
Prom[prometheus]
Grafana[grafana]
CAdvisor[cadvisor]
App --> Chroma
App --> Ollama
App -. future .-> Redis
Chroma --> OTel
OTel --> Zipkin
OTel --> Prom
Ollama --> Exporter
Exporter --> Prom
CAdvisor --> Prom
Prom --> Grafana
streamlit: fast local UIollama: self-hosted LLM/embedding inferencechromadb: vector store client
langchain_communitylangchain_corelangchain_text_splittersPyMuPDF
sentence_transformerstorchvision
- Docker Compose
- Chroma server
- OTEL Collector
- Zipkin
- Prometheus
- Grafana
- cAdvisor
- Redis
- Piper binary and voice model installed in the app image when supported
- Use env-driven configuration instead of hardcoded hostnames, models, file paths, or binary paths.
- Keep optional integrations optional. Piper and Redis should never block the core Q&A path.
- Prefer graceful degradation over hard failure for non-core features.
- Avoid “hidden” server-side UX behavior. If a feature is unavailable, tell the user.
- Keep the Chroma HTTP mode working as the default Docker path.
- Do not reintroduce direct
ollama.chat(...)calls that bypassOLLAMA_BASE_URL.
- Create a Python virtual environment.
- Install
requirements.txt. - Start Ollama locally and pull the required models.
- Optionally configure local Piper.
- Run
streamlit run app.py.
- Review
.env. - Build the app image.
- Start the stack with
docker compose up --build. - Wait for Ollama model bootstrap on first start.
- Use Grafana/Zipkin if you need monitoring or tracing.
To run against an Ollama service on the Docker host, use the host override instead of the bundled ollama service:
docker compose -f docker-compose.yml -f docker-compose.host-ollama.yml --profile minimal up --build- Keep changes simple.
- Prefer root-cause fixes over workaround patches.
- Minimize blast radius.
- Keep docs synchronized with code and Compose.
- If you add or remove services, update the architecture diagrams.
- If you change model defaults, update
.envandREADME.md. - If you change TTS behavior, document the fallback mode explicitly.
app.pyis still monolithic- no automated tests yet
- Redis not yet integrated in the Python runtime
- frontend styling is still mostly Streamlit-default
- monitoring dashboard is intentionally conservative and could be improved with richer exporter-specific panels