pathwaycom/llm-app is a catalogue of ten production-shape reference applications that sit on top of the Pathway streaming-dataflow engine. It is not a library, not a single app, and not a framework — it is a curated set of working RAG (Retrieval-Augmented Generation), document-indexing, and alerting services, each one already wired with a Dockerfile, an app.yaml configuration, sample data, and (for most templates) a Streamlit UI. Pick the template that matches your use case, point it at your documents, set your API keys, and run.
This guide walks you through choosing, configuring, and running the templates. For architecture, deployment hardening, and the shared App-class internals, see the Pathway LLM App Templates Administrator & Developer Guide.
1. Quick Start
What this repository ships
- 8 deployable templates under
templates/— each a Pathway pipeline with a REST or MCP server. - 2 Jupyter cookbooks under
cookbooks/self-rag-agents/— interactive tutorials for self-RAG agentic workflows with LangGraph; these are learning notebooks, not deployable apps. - Per-template
Dockerfile,docker-compose.yml,app.yaml, and seed data so each template runs end-to-end in one command. - Streamlit UIs for four of the templates (
question_answering_rag/ui,slides_ai_search/ui,drive_alert/ui,unstructured_to_sql_on_the_fly/ui) so you can chat with your data in a browser.
Key features
- Real-time, always-fresh document indexes — Pathway watches your data sources and updates the vector index as files change.
- Pluggable LLM, embedder, parser, splitter, and retriever — swap any component by editing one YAML block.
- Built-in connectors for the local filesystem, Google Drive, and Microsoft SharePoint (configured via YAML, no code changes).
- Cloud or fully-private deployment — drop
OpenAIChatforLiteLLMChatpointed at Ollama and run with zero outbound calls. - REST API and Model Context Protocol (MCP) servers exposed by the templates' Pathway xpacks.
- Optional persistent caching so a restart re-uses already-computed embeddings.
Prerequisites
Docker 24+ with the Compose plugin (or Python 3.10+ with pip if you prefer to run templates natively). An OpenAI API key for templates that default to GPT — or local Ollama for the private template.
One-command Quick Start (the canonical template)
The reference template — the one whose shape every other App-class template mirrors — is question_answering_rag. Run it like this:
git clone https://github.com/pathwaycom/llm-app.git
cd llm-app/templates/question_answering_rag
# Provide your OpenAI key
echo "OPENAI_API_KEY=sk-..." > .env
# Build and run with the bundled compose file
docker compose up --build
[+] Building 78.4s (12/12) FINISHED
[+] Running 2/2
✔ Container question_answering_rag-app-1 Started
✔ Container question_answering_rag-ui-1 Started
2026-01-07 12:00:00 pathway INFO Indexed 1 documents
2026-01-07 12:00:01 streamlit INFO Running on http://0.0.0.0:8501
Open http://localhost:8501 in a browser. You will see the Streamlit RAG UI with the bundled sample PDF (IdeanomicsInc_..._Content License Agreement.pdf) already indexed. Ask a question; the answer comes back with the source documents that were used as context.
Tip
You can hit the Pathway REST API directly without the Streamlit UI:
curl -X POST http://localhost:8000/v2/answer -H "Content-Type: application/json" -d '{"prompt": "What is this document about?"}'. Every App-class template exposes the same endpoint shape on port 8000 by default.
What to try next
- Pick a different template — see the selector table in section 4 to choose by use case.
- Index your own documents — drop PDFs into
templates/question_answering_rag/data/and the index updates automatically (no restart). - Switch to local LLMs — try
templates/private_rag/to run with Ollama instead of OpenAI. - Read the per-template README — every
templates/<name>/README.mddocuments that specific template in depth.
2. Installation
System requirements
| Component | Minimum | Recommended | Notes |
|---|---|---|---|
| Docker | 24.0 | 26.0+ | Compose plugin required (docker compose, not legacy docker-compose) |
| RAM | 4 GB | 8+ GB | Multimodal/Slides templates push higher; private RAG with Ollama needs more |
| Disk | 2 GB | 10+ GB | For Docker images, document cache, and Pathway persistence directory |
| Python (native run) | 3.10 | 3.11 | Only needed if you skip Docker; templates use pathway[all] |
| OpenAI account | API key | Project key | Default for all templates except private_rag |
Important
The templates default to OpenAI models (
gpt-4.1-mini,gpt-4o,text-embedding-3-small) and will fail to start withoutOPENAI_API_KEYunless you switch the LLM block inapp.yamlto a local provider. Keep the key in.envnext toapp.yaml; never commit it.
Install methods
Docker Compose (recommended)
git clone https://github.com/pathwaycom/llm-app.git
cd llm-app/templates/<template_name>
cp .env.example .env # (when present) — fill in your secrets
docker compose up --build
Native Python
git clone https://github.com/pathwaycom/llm-app.git
cd llm-app/templates/<template_name>
python -m venv .venv && source .venv/bin/activate
pip install pathway[all] python-dotenv
pip install -r requirements.txt # adds template-specific extras
cp .env.example .env # when present
python app.py
Single Dockerfile
cd llm-app/templates/<template_name>
docker build -t pathway-llm-app .
docker run --rm -p 8000:8000 \
-v "$PWD/data:/app/data" \
--env-file .env \
pathway-llm-app
Pathway license key
Each template's app.py calls pw.set_license_key("demo-license-key-with-telemetry") so the demo runs out of the box with anonymous telemetry. To unlock advanced features (multi-machine clustering via --addresses, autoscaling, certain enterprise persistence backends), get a free Scale key from https://pathway.com/features and either edit the line in app.py or set PATHWAY_LICENSE_KEY in .env and replace the call with pw.set_license_key(os.environ["PATHWAY_LICENSE_KEY"]). Comment the call out entirely to use Pathway Community (no telemetry, no advanced features).
Note
The demo license key is real and works — it just enables anonymous usage telemetry. There is no quota or expiration. Switch to a personal Scale key if your environment cannot send telemetry, or remove the call to opt out entirely.
Post-install verification
After docker compose up --build completes for question_answering_rag:
# REST API: ask a question
curl -X POST http://localhost:8000/v2/answer \
-H "Content-Type: application/json" \
-d '{"prompt": "Summarise the document"}'
# REST API: list indexed documents
curl http://localhost:8000/v2/list_documents
# REST API: retrieve top-k chunks for a query (no LLM call)
curl -X POST http://localhost:8000/v1/retrieve \
-H "Content-Type: application/json" \
-d '{"query": "license agreement", "k": 4}'
{"response": "This document is a content license agreement between..."}
If you see a JSON response, the pipeline is live, the documents are indexed, and your API key works.
Install troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
RuntimeError: OpenAI API key not provided |
.env not loaded, file missing, wrong key |
Confirm .env sits next to app.py; key starts with sk-; restart container |
Cannot connect to API on localhost:8000 |
Container not yet ready, port collision | Wait 30-90 s for first index build; check docker ps for port mapping |
pathway.set_license_key: invalid key |
Edited app.py and pasted a malformed key |
Use the demo key string verbatim, or unset and rely on Community mode |
cuda not available (private_rag, document_indexing) |
SentenceTransformer falling back to CPU | Acceptable; embeddings just take longer. Add a GPU-enabled image for production |
pulling image takes forever on first build |
Pathway base image (pathwaycom/pathway:latest) is large |
Normal first-pull; subsequent builds reuse the layer |
Connection refused: ollama:11434 (private_rag) |
Ollama service not started or unreachable | Start ollama serve on the host and use host.docker.internal:11434 from a Mac/Windows container |
No documents found, index empty |
Data directory empty or path mismatch | Drop PDFs into the template's data/ (or files-for-indexing/) folder; Pathway picks them up live |
psycopg.OperationalError: connection refused (unstructured_to_sql_on_the_fly) |
Postgres service not running | This template needs a running Postgres; use the included docker-compose.yml instead of docker run |
KeyError: 'SHAREPOINT_URL' |
Switched on a SharePoint connector without filling its env vars | See section 3.3 for the full SharePoint variable inventory |
Slack 401 unauthorized (drive_alert) |
SLACK_ALERT_TOKEN missing or revoked |
Re-issue the bot token; Slack tokens start with xoxb- |
| Streamlit UI shows "Connection error" | Pathway app container not ready or PATHWAY_HOST/PATHWAY_PORT mismatch |
When using compose, the UI defaults PATHWAY_HOST=app (the service name); native runs need localhost |
3. Configuration
YAML-first, environment-second
Every modern template is configured by exactly one file: app.yaml next to app.py. The YAML uses Pathway's !pw.<class> tag syntax to instantiate Python objects directly. $variables (anchors) let you define an embedder once and reference it from both the parser and the retriever. The two legacy templates (drive_alert and unstructured_to_sql_on_the_fly) instead use procedural Python kwargs and os.environ.get defaults — they predate the App-class refactor.
Annotated minimal config (Question-Answering RAG)
The smallest meaningful app.yaml:
# Where to read documents from
$sources:
- !pw.io.fs.read
path: data
format: binary
with_metadata: true
# The chat model
$llm: !pw.xpacks.llm.llms.OpenAIChat
model: "gpt-4.1-mini"
cache_strategy: !pw.udfs.DefaultCache {}
temperature: 0
capacity: 8
async_mode: "fully_async"
# The embedding model
$embedder: !pw.xpacks.llm.embedders.OpenAIEmbedder
model: "text-embedding-3-small"
cache_strategy: !pw.udfs.DefaultCache {}
# Chunking + parsing
$splitter: !pw.xpacks.llm.splitters.TokenCountSplitter
max_tokens: 400
$parser: !pw.xpacks.llm.parsers.DoclingParser
table_parsing_strategy: "llm"
async_mode: "fully_async"
chunk: false
cache_strategy: !pw.udfs.DefaultCache {}
# Vector index
$retriever_factory: !pw.indexing.UsearchKnnFactory
reserved_space: 1000
embedder: $embedder
metric: !pw.indexing.USearchMetricKind.COS
# The actual document store + question-answerer wired together
$document_store: !pw.xpacks.llm.document_store.DocumentStore
docs: $sources
parser: $parser
splitter: $splitter
retriever_factory: $retriever_factory
question_answerer: !pw.xpacks.llm.question_answering.BaseRAGQuestionAnswerer
llm: $llm
indexer: $document_store
Every YAML field maps directly to a Pydantic field on App in app.py — extra="forbid" is set, so a typo in a key fails fast at startup with a Pydantic validation error.
Tip
The
$prefix is a YAML anchor:$llm:defines,llm: $llmreferences. This lets one component (e.g.$embedder) be reused by both the parser and retriever without duplication. Only fields that are not prefixed with$(top-level:question_answerer,host,port,persistence_mode,terminate_on_error) are read by the App-class itself.
Common option combinations
| Goal | Edit |
|---|---|
| Use a different OpenAI model | Change model: under $llm: (e.g. "gpt-4o" or "gpt-4o-mini") |
| Switch to a local LLM | Replace $llm block with !pw.xpacks.llm.llms.LiteLLMChat and set model: "ollama/<name>" and api_base: "http://host.docker.internal:11434" |
| Add SharePoint as a source | Uncomment the !pw.xpacks.connectors.sharepoint.read block under $sources: and fill in tenant + cert |
| Add Google Drive as a source | Uncomment the !pw.io.gdrive.read block under $sources: and place gdrive_indexer.json next to app.yaml |
| Persist the cache across restarts | Set persistence_mode: !pw.PersistenceMode.PERSISTING at the top of app.yaml |
| Move the cache to a custom path | Add persistence_backend: !pw.persistence.Backend.filesystem with path: "/data/cache" |
| Halt on first error | Set terminate_on_error: true (default false) |
| Change listening port | Set port: 9000 (default 8000 for App-class; 8080 for legacy) |
| Customise the RAG prompt | Add prompt_template: "Given these documents: {context}, please answer the question: {query}" under question_answerer: |
| Cap the number of context docs | Add search_topk: 6 under question_answerer: |
Environment variables you typically set
These are referenced inside app.yaml (with $VARIABLE) or by os.environ.get in legacy templates:
| Variable | Used by | Default | Purpose |
|---|---|---|---|
OPENAI_API_KEY |
All OpenAI-backed templates | — | OpenAI auth |
PATHWAY_LICENSE_KEY |
App, when read at runtime | demo key | Unlock Scale features |
PATHWAY_HOST |
Streamlit UIs | app (compose) / localhost |
Where the UI reaches the Pathway app |
PATHWAY_PORT |
Streamlit UIs | 8000 |
Port for the Pathway REST API |
PATHWAY_REST_CONNECTOR_HOST |
Legacy templates | 0.0.0.0 |
Bind address for legacy REST connector |
PATHWAY_REST_CONNECTOR_PORT |
Legacy templates | 8080 |
Port for legacy REST connector |
FILE_OR_DIRECTORY_ID |
drive_alert |
— | Google Drive folder/file ID to monitor |
GOOGLE_CREDS |
drive_alert |
templates/drive_alert/secrets.json |
Path to GCP service-account JSON |
SLACK_ALERT_CHANNEL_ID |
drive_alert |
— | Slack channel for change alerts |
SLACK_ALERT_TOKEN |
drive_alert |
— | Slack bot token (xoxb-...) |
SHAREPOINT_URL / SHAREPOINT_TENANT / SHAREPOINT_CLIENT_ID / SHAREPOINT_THUMBPRINT / SHAREPOINT_ROOT |
Templates with SharePoint connector enabled | — | Microsoft SharePoint app-only auth |
DRIVE_ID |
Templates with Google Drive connector enabled | — | Google Drive folder ID |
PATHWAY_DATA_DIR |
unstructured_to_sql_on_the_fly |
./data/quarterly_earnings |
Where to read source PDFs |
POSTGRESQL_HOST / POSTGRESQL_PORT / POSTGRESQL_DB / POSTGRESQL_USER / POSTGRESQL_PASSWORD / POSTGRESQL_TABLE |
unstructured_to_sql_on_the_fly |
localhost / 5432 / STRUCTUREDDB / user / password / quarterly_earnings |
Target Postgres for structured output |
FILE_SERVER_URL |
slides_ai_search/ui |
http://localhost:8080/ |
nginx file server inside the slides stack |
Note
Legacy templates (
drive_alert,unstructured_to_sql_on_the_fly) get every parameter fromos.environ.getcalls insideapp.py. Modern App-class templates pull values fromapp.yaml, so the equivalent of "setting an env var" there is editing the YAML.
For deeper config — including persistence-backend tuning, retry strategies, and connector-specific knobs — see the Pathway LLM App Templates Administrator & Developer Guide, section 3.
4. Core Usage
4.1 Pick the template that fits your problem
This is the most important table in the guide. Each template is a complete, self-contained service with its own folder, README, Dockerfile, and YAML. Pick by use case, not by name.
| Template | Best when you need… | Default LLM | Default embedder | Default port | UI? | Connector(s) used |
|---|---|---|---|---|---|---|
question_answering_rag |
A general-purpose RAG over PDFs/PPTX with a Streamlit chat UI — the canonical reference | gpt-4.1-mini |
text-embedding-3-small |
8000 | yes | filesystem (default), SharePoint/GDrive optional |
adaptive_rag |
RAG that retrieves as few documents as possible per question to save tokens (Pathway's adaptive retrieval algorithm) | gpt-4.1-mini |
text-embedding-3-small |
8000 | no | filesystem (default), SharePoint/GDrive optional |
private_rag |
Fully air-gapped RAG — every model runs locally via Ollama + sentence-transformers, no outbound calls | ollama/mistral |
avsolatorio/GIST-small-Embedding-v0 |
8000 | no | filesystem (default), SharePoint/GDrive optional |
multimodal_rag |
RAG over PDFs whose tables and images carry meaning — uses GPT-4o to parse images/tables | gpt-4.1-mini (answers) + gpt-4o (image parsing) |
text-embedding-3-small |
8000 | no | filesystem (default), SharePoint/GDrive optional |
document_indexing |
Just an indexing pipeline + retrieval API — no LLM, no answering, just a vector store you can query yourself | none (no LLM) | mixedbread-ai/mxbai-embed-large-v1 (local) |
8000 | no | filesystem (default) |
document_store_mcp_server |
An MCP server (Model Context Protocol, streamable HTTP) that exposes a live document store as a tool for MCP-aware clients (Claude Desktop, etc.) | none (MCP serves retrieval) | mixedbread-ai/mxbai-embed-large-v1 (local) |
8000 (App) / 8068 (MCP) | no | filesystem (default) |
slides_ai_search |
Searching presentation decks — parses each slide as an image, extracts categories/languages, lets you filter | gpt-4o |
text-embedding-3-small |
8000 (App), 8501 (UI), 8080 (nginx) | yes | filesystem (default), SharePoint/GDrive optional |
drive_alert (legacy) |
RAG over Google Drive that also sends Slack alerts when a doc change materially shifts the answer | gpt-3.5-turbo |
text-embedding-ada-002 |
8080 | yes | Google Drive (required) + Slack (required) |
unstructured_to_sql_on_the_fly (legacy) |
Continuously parse PDFs into a Postgres table, then translate natural-language questions to SQL against that table | gpt-3.5-turbo |
n/a (LLM-only extraction) | 8080 | yes | filesystem in, Postgres out |
Cookbook cookbooks/self-rag-agents/*.ipynb |
Learning self-RAG and agentic patterns interactively with LangGraph — not a deployable app | n/a | n/a | n/a | Jupyter | n/a |
Tip
When in doubt, start with
question_answering_rag. It is the canonical template; every other modern template is a variation on itsApp(BaseModel)shape. Once you understand it, switching toadaptive_ragormultimodal_ragis a YAML diff away.
4.2 The shared shape (every modern template)
Every App-class template follows the same lifecycle:
app.yaml ──► pw.load_yaml(f) ──► App(**config) ──► app.run()
│
├── instantiate xpack server (QASummaryRestServer / DocumentStoreServer / PathwayMcp)
├── resolve persistence_mode + persistence_backend
└── pw.run(persistence_config=…, terminate_on_error=…, monitoring_level=NONE)
The Pydantic shell carries seven public fields (question_answerer or document_store or mcp_http, host, port, with_cache (deprecated), persistence_backend, persistence_mode, terminate_on_error) and one method (run). When you start the container, app.py reads app.yaml, builds the App, and calls pw.run to boot the streaming dataflow.
4.3 Run the canonical RAG (question_answering_rag)
cd templates/question_answering_rag
echo "OPENAI_API_KEY=sk-..." > .env
docker compose up --build
The compose file starts two containers: app (port 8000, the Pathway REST API + index) and ui (port 8501, the Streamlit UI). The UI uses RAGClient(url=f"http://{PATHWAY_HOST}:{PATHWAY_PORT}") — by default app:8000 inside the compose network — to talk to the API.
REST endpoints (canonical RAG and adaptive/multimodal/private RAG)
| Method | Path | Purpose |
|---|---|---|
| POST | /v2/answer |
Ask a question; receive {response, context_docs} |
| POST | /v1/retrieve |
Top-k chunk retrieval; no LLM call |
| POST | /v1/statistics |
Index size, last-update timestamp, document count |
| GET | /v2/list_documents |
List every document the indexer knows about, with _indexing_status per file |
| POST | /v2/summarize |
LLM summary of a list of texts (used by SummaryQuestionAnswerer) |
# Ask a question
curl -X POST http://localhost:8000/v2/answer \
-H "Content-Type: application/json" \
-d '{"prompt": "What are the contract terms?"}'
# Retrieve only — useful for debugging the index
curl -X POST http://localhost:8000/v1/retrieve \
-H "Content-Type: application/json" \
-d '{"query": "license", "k": 4}'
# Health-check the indexer
curl -X POST http://localhost:8000/v1/statistics
Note
The endpoints above belong to Pathway's xpacks (
QASummaryRestServer,DocumentStoreServer). They are not implemented in the templates — the templates configure them. For exhaustive REST-shape details (every JSON field, every status code) consult Pathway's API docs at https://pathway.com/developers/api-docs/pathway-xpacks-llm/.
4.4 Adaptive RAG — same shape, smarter retrieval
adaptive_rag substitutes BaseRAGQuestionAnswerer for AdaptiveRAGQuestionAnswerer, which retrieves documents iteratively: it starts with n_starting_documents: 2, and if the model is not confident enough, it requests factor: 2× more, up to max_iterations: 4. This usually halves token spend on questions where the answer sits in 1-2 chunks while still escalating for hard questions.
question_answerer: !pw.xpacks.llm.question_answering.AdaptiveRAGQuestionAnswerer
llm: $llm
indexer: $document_store
n_starting_documents: 2
factor: 2
max_iterations: 4
Run identically to the canonical template — same endpoints, same UI compatibility.
4.5 Multimodal RAG — image and table understanding
multimodal_rag upgrades the parser to read tables and images by way of a second OpenAI client (gpt-4o) used only inside DoclingParser:
$parsing_llm: !pw.xpacks.llm.llms.OpenAIChat
model: "gpt-4o"
...
$parser: !pw.xpacks.llm.parsers.DoclingParser
multimodal_llm: $parsing_llm
image_parsing_strategy: "llm"
table_parsing_strategy: "llm"
...
The answering LLM stays as gpt-4.1-mini — only parsing is multimodal. Use this for finance reports, scientific papers, slide-derived PDFs, anywhere a chart or a table holds the actual answer.
4.6 Private RAG — fully local
private_rag keeps the same shape but swaps every external service for a local one:
$llm_model: "ollama/mistral"
$llm: !pw.xpacks.llm.llms.LiteLLMChat
model: $llm_model
api_base: "http://localhost:11434"
# api_base: "http://host.docker.internal:11434" # for Docker-on-Mac/Windows
async_mode: "fully_async"
$embedding_model: "avsolatorio/GIST-small-Embedding-v0"
$embedder: !pw.xpacks.llm.embedders.SentenceTransformerEmbedder
model: $embedding_model
Prerequisites:
- Install Ollama on the host.
ollama pull mistral(or any model your$llm_modelreferences).ollama serve(or run the official Ollama Docker image on the host network).- From inside the Docker container,
localhostwon't work — usehost.docker.internalon Mac/Windows, or--network hoston Linux.
ollama pull mistral
ollama serve &
cd templates/private_rag
docker compose up --build
Important
Private RAG uses
AdaptiveRAGQuestionAnswererwithstrict_prompt: true. The strict prompt is more careful about saying "I don't know" when the context isn't sufficient — usually desirable for sensitive data, occasionally surprising if you expect the model to extrapolate.
4.7 Document indexing only — no LLM
document_indexing skips the answerer entirely and exposes a pure document store on REST. Use it when you want Pathway to maintain the index but call your own LLM downstream:
| Method | Path | Purpose |
|---|---|---|
| POST | /v1/retrieve |
Top-k chunk retrieval |
| POST | /v1/statistics |
Index stats |
| GET | /v2/list_documents |
All known documents and their indexing status |
The default embedder is mixedbread-ai/mxbai-embed-large-v1 running locally via SentenceTransformer — no OpenAI key needed. Drop files into templates/document_indexing/files-for-indexing/ and they get indexed live.
4.8 MCP server — feed Pathway to MCP clients
document_store_mcp_server extends the document_indexing shape with an MCP (Model Context Protocol) server that streams over HTTP:
mcp_http: !pw.xpacks.llm.mcp_server.PathwayMcp
name: "Streamable MCP Server"
transport: "streamable-http"
host: "0.0.0.0"
port: 8068
serve:
- $document_store
Point any MCP-aware client (Claude Desktop, an MCP-aware coding agent, etc.) at http://localhost:8068 to access the live, always-current index as a tool.
Tip
The MCP server lives on port 8068, not 8000. The App container itself still binds 8000, but in the MCP template that port carries no useful endpoints — clients should target 8068.
4.9 Slides search — semantic search over presentations
slides_ai_search is the most elaborate stack. It indexes .pdf and .pptx files, parses every slide as an image, classifies each slide along a 15-category taxonomy (Problem Statement, Solution Overview, Pricing and Packaging, …), tags it (e.g. ['price', 'shopping']), detects language, and exposes search filtered by category and language.
The template ships three containers in its docker-compose.yml: the Pathway app (8000), a Streamlit UI (8501), and an nginx file server (8080) that serves the slide images and original PDFs back to the UI for rendering.
cd templates/slides_ai_search
cp .env.example .env # add OPENAI_API_KEY
docker compose up --build
UI at http://localhost:8501 — it shows a query box, two multi-selects (categories and languages), and renders the top-k slides as images you can click through.
4.10 Drive Alert (legacy) — RAG with Slack notifications on document change
drive_alert is one of the two legacy templates — it predates the App-class refactor and uses procedural run(*, ...) kwargs with os.environ.get defaults. It indexes Google Drive, exposes a POST / REST endpoint on port 8080 (not 8000), and on top of that sends a Slack alert whenever a document change makes the answer to a previously-asked alerting query materially different.
Set up:
- Create a GCP service account, download
secrets.json, share the target Drive folder with the service-account email. - Set
FILE_OR_DIRECTORY_IDto the Drive folder/file ID,GOOGLE_CREDStosecrets.json's path. - Create a Slack bot, install it to your workspace, copy the bot token to
SLACK_ALERT_TOKEN, setSLACK_ALERT_CHANNEL_ID. OPENAI_API_KEYfor the LLM.
Ask the assistant Tell me and alert about the start date of the campaign for Magic Cola. The keyword "alert" registers the query for monitoring; the next time a Drive doc change shifts the answer, you receive a Slack notification.
Warning
The legacy templates default to
gpt-3.5-turboandtext-embedding-ada-002, which are older than the App-class defaults. They also bind to port 8080 — if you run them next to a modern template that uses 8000, the ports do not collide, but if you reverse-proxy be aware of the mismatch.
4.11 Unstructured-to-SQL (legacy) — turn PDFs into a Postgres table you can query
unstructured_to_sql_on_the_fly is the second legacy template. Two pipelines run side by side inside one process:
- Pipeline 1 (extraction): Watch
data/quarterly_earnings/, send each PDF to OpenAI with a prompt that extracts the schema below into JSON, write the rows to Postgres and to./data/quarterly_earnings.csv. - Pipeline 2 (query): Expose a REST endpoint on port 8080 that accepts natural-language questions, asks GPT to translate them to a constrained
SELECTagainst thequarterly_earningstable, executes the SQL, returns the result.
The hard-coded extraction schema (you cannot change this without editing app.py):
| Field | Type | Unit |
|---|---|---|
company_symbol |
str | stock ticker |
year |
int | calendar year |
quarter |
str | one of Q1, Q2, Q3, Q4 |
revenue_md |
float | millions of dollars |
eps |
float | dollars per share |
net_income_md |
float | millions of dollars |
Run it with the bundled compose file (which also starts a Postgres container with postgres/init-db.sql):
cd templates/unstructured_to_sql_on_the_fly
docker compose up --build
Browse http://localhost:8501 for the Streamlit UI, or curl -X POST http://localhost:8080/ -d '{"user":"u","query":"What is the average net income in Q2 2023?"}' -H "Content-Type: application/json".
Caution
Pipeline 2 builds and executes SQL produced by an LLM. The prompt constrains the grammar (
SELECTonly, restricted aggregators, no DDL) but you are still trusting an LLM with database access. Run against a read-only role and a database that only contains the extracted financial data — never against your production warehouse.
4.12 Cookbooks — self-RAG agents (Jupyter, learning-only)
cookbooks/self-rag-agents/ ships two notebooks:
pathway_langgraph_agentic_rag.ipynb— building an agentic RAG with LangGraph and Pathway.pathway_deploy_langgraph_agents.ipynb— deploying the same graph as a service.
These are interactive tutorials. They are not deployable (no Dockerfile, no compose, no app.yaml) and they import from LangGraph, which is not in any template's requirements.txt. Treat them as walkthroughs, not products. Open them in Jupyter Lab with pip install pathway[all] langgraph jupyter && jupyter lab.
5. Common Tasks
5.1 Add your own PDFs to a RAG template
cp /path/to/your.pdf templates/question_answering_rag/data/
# Pathway picks it up live; no restart needed
curl -X POST http://localhost:8000/v2/list_documents
5.2 Swap to a cheaper or stronger OpenAI model
# in app.yaml
$llm: !pw.xpacks.llm.llms.OpenAIChat
model: "gpt-4o-mini" # was "gpt-4.1-mini"
Restart the container; the cache will need to rebuild for any prompts that hit the LLM differently.
5.3 Index a Google Drive folder instead of local files
In app.yaml, comment out the filesystem source, uncomment the Google Drive source, fill in $DRIVE_ID, and place gdrive_indexer.json (your service-account JSON) next to app.yaml:
$sources:
- !pw.io.gdrive.read
object_id: $DRIVE_ID
service_user_credentials_file: gdrive_indexer.json
file_name_pattern:
- "*.pdf"
- "*.pptx"
object_size_limit: null
with_metadata: true
refresh_interval: 30
Set DRIVE_ID in .env.
5.4 Index SharePoint instead of local files
$sources:
- !pw.xpacks.connectors.sharepoint.read
url: $SHAREPOINT_URL
tenant: $SHAREPOINT_TENANT
client_id: $SHAREPOINT_CLIENT_ID
cert_path: sharepointcert.pem
thumbprint: $SHAREPOINT_THUMBPRINT
root_path: $SHAREPOINT_ROOT
with_metadata: true
refresh_interval: 30
Place sharepointcert.pem next to app.yaml. Set the five env vars in .env.
5.5 Mix multiple sources in one index
$sources: is a list — repeat as many !pw.io... entries as you like; the document store unions all of them.
$sources:
- !pw.io.fs.read
path: data
format: binary
with_metadata: true
- !pw.io.gdrive.read
object_id: $DRIVE_ID
service_user_credentials_file: gdrive_indexer.json
5.6 Persist the cache between restarts
# At the top level of app.yaml, alongside `host:` and `port:`
persistence_mode: !pw.PersistenceMode.PERSISTING
persistence_backend: !pw.persistence.Backend.filesystem
path: ".Cache"
Mount the path as a Docker volume so it survives container rebuilds.
5.7 Customize the RAG prompt
question_answerer: !pw.xpacks.llm.question_answering.BaseRAGQuestionAnswerer
llm: $llm
indexer: $document_store
prompt_template: |
You are a contracts assistant. Use ONLY the provided context.
Context: {context}
Question: {query}
Answer (cite the document path):
5.8 Run with a different port
host: "0.0.0.0"
port: 9000
Update the compose file's port mapping to match.
5.9 Wire the Streamlit UI to a remote Pathway app
The bundled UIs default PATHWAY_HOST=app (the compose service name). To point a UI at a Pathway service running elsewhere:
PATHWAY_HOST=rag.example.com PATHWAY_PORT=443 streamlit run ui.py
5.10 Halt on first error
For CI or strict production deployments where partial answers are worse than failure:
terminate_on_error: true
5.11 Use a local embedding model (cost / privacy)
Replace OpenAIEmbedder with SentenceTransformerEmbedder:
$embedder: !pw.xpacks.llm.embedders.SentenceTransformerEmbedder
model: "mixedbread-ai/mxbai-embed-large-v1"
call_kwargs:
show_progress_bar: False
The first run downloads the model (~700 MB). After that it is fully local.
5.12 Build a private, end-to-end air-gapped stack
Use private_rag's app.yaml as a starting point. Confirm:
$llmusesLiteLLMChatwith an Ollamaapi_base.$embedderusesSentenceTransformerEmbedder.$parserisDoclingParserwithtable_parsing_strategy: "llm"only if you pointmultimodal_llmat a local model — otherwise drop it to"text".$sources:lists only the local filesystem.- The Docker container runs with
--network host(Linux) so it can reachlocalhost:11434.
5.13 Deploy to a cluster (multi-machine)
Multi-machine deployment uses Pathway's --addresses flag and requires a PATHWAY_LICENSE_KEY (Scale tier). Each machine runs the same app.py binary; the framework coordinates work across them. See the Admin Guide for the full deployment story.
Warning
Multi-machine clustering is gated by the Pathway license. Without a Scale key,
--addressesraises a license error at startup.
6. Troubleshooting
Diagnostic decision tree
- Container exits immediately. Check
docker compose logs app. 90 % of the time it's a missingOPENAI_API_KEYor a YAML typo (extra="forbid"rejects unknown fields). - API responds but answers are empty.
curl http://localhost:8000/v2/list_documents— if the list is empty, Pathway hasn't found your data; check$sources:paths and Docker volume mounts. - Answers are wrong but documents are indexed.
curl -X POST http://localhost:8000/v1/retrieve— if retrieval is wrong, the embedder is the problem (try a stronger model or smaller chunks). If retrieval is right but the answer is wrong, the LLM is the problem (trygpt-4o). - Answers are slow. First-time queries pay full LLM + embedding cost. Cached results are sub-second; cold runs are seconds. If consistently slow, check the OpenAI capacity limit —
capacity: 8in$llmis a per-model rate limit. - Streamlit shows "Connection error". The UI cannot reach the Pathway app. Confirm
PATHWAY_HOSTmatches your topology (compose:app; native:localhost).
Error: pydantic.ValidationError: 1 validation error for App ... extra inputs are not permitted
Cause: Your app.yaml includes a key that the App Pydantic model does not declare. extra="forbid" is set on every modern template.
Solution:
- Compare your
app.yamlagainst the bundled one for that template. - Remove any unknown top-level keys.
- Confirm the spelling:
question_answerer(notquestion_answer),persistence_mode(notpersistance_mode).
Prevention: Start from the shipped app.yaml and edit only what you need.
Error: RuntimeError: OpenAI API key not provided
Cause: OPENAI_API_KEY is missing from the environment when OpenAIChat or OpenAIEmbedder instantiates.
Solution:
- Confirm a
.envfile sits next toapp.py(orapp.yaml) and containsOPENAI_API_KEY=sk-.... - For Docker, the compose file or
--env-file .envflag must explicitly load it. - Restart the container —
dotenv.load_dotenv()runs at process start.
Prevention: Use cp .env.example .env when an .env.example is present and immediately fill in the keys.
Error: KeyError: 'SHAREPOINT_URL'
Cause: You uncommented the SharePoint connector block in app.yaml but did not set the SharePoint env vars.
Solution:
- Set
SHAREPOINT_URL,SHAREPOINT_TENANT,SHAREPOINT_CLIENT_ID,SHAREPOINT_THUMBPRINT,SHAREPOINT_ROOTin.env. - Place the cert file at the path given by
cert_path:(defaultsharepointcert.pemnext toapp.yaml). - Confirm the SharePoint app registration has application (not delegated) permissions for
Sites.Read.All.
Prevention: The relevant .env.example files (when shipped) list every required variable.
Error: psycopg.OperationalError: connection to server on socket failed
Cause: unstructured_to_sql_on_the_fly cannot reach Postgres. Most often you ran docker run instead of the bundled compose file.
Solution: Run docker compose up --build from the template's directory. The compose file starts a Postgres container with postgres/init-db.sql already loaded.
Prevention: This template is the only one that requires a database; do not skip the compose file.
Error: ConnectionRefusedError: [Errno 111] ... ollama:11434
Cause: private_rag's container cannot reach the Ollama service.
Solution:
- On the host:
ollama serve(or run the official Docker image with-p 11434:11434). - On Mac/Windows Docker, edit
app.yaml'sapi_basetohttp://host.docker.internal:11434. - On Linux Docker, run the container with
--network hostand keepapi_base: "http://localhost:11434".
Prevention: Pull the Ollama model and start the daemon before docker compose up.
Error: Streamlit page loads but says "Connection error"
Cause: The UI's RAGClient(url=f"http://{PATHWAY_HOST}:{PATHWAY_PORT}") cannot reach the app.
Solution:
- With compose: ensure
PATHWAY_HOST=appand the app service is namedapp(or update both). - Native run: set
PATHWAY_HOST=localhost. - Confirm the app container actually finished its first index build — the UI hits
list_documentson load and that endpoint isn't ready until indexing completes.
Prevention: Always wait for the app log line "INFO Indexed N documents" before opening the UI.
Error: Slack returns 401 invalid_auth
Cause: drive_alert cannot post to Slack.
Solution:
- Re-issue the bot token from the Slack app settings page.
- Confirm the bot user has been installed to the workspace and invited to the channel.
- Token must start with
xoxb-, notxapp-orxoxp-.
Prevention: Slack rotates tokens on app reinstall — keep .env in sync.
Error: Index is stale, doesn't pick up new files
Cause: Pathway's filesystem connector watches the path you configured in $sources: — if Docker's bind mount points elsewhere or the path is wrong, changes are invisible.
Solution:
- Confirm
$sources:path:matches the in-container path. - Confirm the compose file mounts the host directory to that container path.
- For Google Drive,
refresh_interval: 30controls the poll interval; lower it for faster updates.
Prevention: Always test by adding a small file and watching the app logs for the "Indexed N documents" increment.
Issue: GitHub #124 — Getting Started instructions are fragmented across templates
Cause: Each template has its own README, and there is no single entry-point doc.
Solution: Use this guide's selector table (section 4.1) to pick a template, then the per-template README for deep details. Treat templates/<name>/README.md as the canonical source for that template.
Prevention: Until upstream consolidates, this guide is the index — link or bookmark section 4.1.
Issue: GitHub #46 — OpenAI streaming response support requested
Cause: The shipped templates use OpenAIChat in non-streaming mode; the response is delivered as a single JSON body.
Solution: Streaming is a Pathway xpacks feature, not a template feature. If your downstream client requires SSE or chunked transfer, write a thin proxy in front of the Pathway REST endpoint that re-streams the response. Track the upstream issue on the Pathway core repo.
Issue: GitHub #82 — How to use a local embedding model
Cause: Default templates use OpenAI embeddings; users want fully local.
Solution: Either start from private_rag (local Ollama LLM + SentenceTransformer embedder) or copy its $embedder: block into any other template's app.yaml.
Prevention: Cross-reference the template defaults table (section 4.1) before starting a new project.
7. FAQ
General
Q: Is pathwaycom/llm-app a library?
No. It is a catalogue of ten reference applications (eight templates plus two cookbooks). The library doing the heavy lifting is Pathway, specifically its pathway.xpacks.llm module. The templates here are recipes that wire Pathway's xpacks pieces into deployable services.
Q: Where do I find Pathway's full API?
https://pathway.com/developers/api-docs/pathway-xpacks-llm/. The Admin Guide covers the slice of Pathway exercised by the templates in section 8.
Q: Can I use a model other than OpenAI?
Yes. The $llm: block can be OpenAIChat, LiteLLMChat (Ollama, Anthropic, Bedrock, Vertex, etc.), or any class under pathway.xpacks.llm.llms. private_rag shows the LiteLLM pattern.
Q: Are the templates production-ready?
They are production-shape — real Dockerfiles, real config, real connectors — but they default to demo settings (small capacity:, single-machine, demo license key). For production: rotate to a Scale license, raise capacity, add persistence, and put the API behind a reverse proxy with auth. See the Admin Guide.
Q: What is a cookbook vs a template?
A template is a deployable app (Dockerfile + compose + app.yaml). A cookbook is a Jupyter notebook tutorial. The cookbooks live under cookbooks/self-rag-agents/ and demonstrate LangGraph integration; they are for learning, not deployment.
Configuration
Q: How do I switch from gpt-4.1-mini to gpt-4o?
Edit app.yaml, change model: "gpt-4.1-mini" to model: "gpt-4o" under $llm:. Restart the container. Cached cache entries for the previous model still apply (the cache key includes the prompt, not the model — verify before caring about token cost).
Q: How do I disable caching?
In app.yaml set persistence_mode: null at the top level. Per-component caches (cache_strategy: !pw.udfs.DefaultCache {}) cache LLM and embedder calls in-process for the lifetime of the run regardless.
Q: Where does the cache live?
By default, the container's working directory under ./Cache. Change with persistence_backend: !pw.persistence.Backend.filesystem and path: "/data/cache". Mount that path as a Docker volume to make it persistent.
Q: How do I customize the RAG prompt?
Add prompt_template: under question_answerer:. Use {query} and {context} placeholders. See section 5.7.
Q: What does extra="forbid" mean?
The Pydantic App model rejects YAML keys it doesn't recognise — a typo fails fast at startup with a clear error rather than silently ignoring your override.
Connectors and data sources
Q: How many connectors do the templates use?
Five: filesystem (pw.io.fs.read), Google Drive (pw.io.gdrive.read), SharePoint (pw.xpacks.connectors.sharepoint.read), Postgres (pw.io.postgres.write, output side, unstructured_to_sql_on_the_fly), and Slack (pw.io.slack.send_alerts, output, drive_alert). The parent Pathway library has dozens more — these templates use a subset.
Q: Can I use S3 as a source?
Pathway supports pw.io.s3.read (mentioned in drive_alert's code comments). It is not used in any shipped app.yaml, but you can add it under $sources: the same way the GDrive and SharePoint blocks are added.
Q: Does it support Postgres as a source?
Pathway supports pw.io.postgres.read; none of the templates use it as input. unstructured_to_sql_on_the_fly writes to Postgres, not reads from it.
Q: How fresh is the index?
The filesystem connector polls effectively continuously (Pathway uses a streaming dataflow). Google Drive defaults to refresh_interval: 30 seconds. SharePoint same.
Deployment
Q: Do I need Docker?
No, but it is the recommended path. Native Python works — pip install pathway[all] python-dotenv && python app.py — but you are responsible for matching system libraries (Pathway includes a Rust core that links against system OpenSSL and similar).
Q: Is GPU required?
No. Templates default to CPU-friendly settings. private_rag benefits from a GPU because of the local embedder, but does not require one.
Q: What ports do the templates use?
App-class templates: 8000 for the Pathway REST API. The MCP template adds 8068. Streamlit UIs: 8501. Legacy templates: 8080. The slides template adds an nginx file server on 8080 internally.
Q: How do I deploy on Kubernetes?
The templates do not ship K8s manifests. Build the bundled Dockerfile, push to a registry, write a Deployment + Service per template. For multi-machine clustering, see the Admin Guide on --addresses and the Pathway license.
Cost and limits
Q: How expensive is a default RAG run?
Indexing a small PDF set with text-embedding-3-small is fractions of a cent. Each query at gpt-4.1-mini is roughly the cost of one prompt + ~500-token response. The first answer is most expensive (cold cache); repeated queries hit the cache and cost nothing.
Q: What is the capacity: field on $llm:?
A simple in-process rate limit on concurrent OpenAI calls. The default is 8. Lower it if you hit OpenAI rate limits; raise it for faster indexing on a high-tier account.
Q: Why does the app retry?
retry_strategy: !pw.udfs.ExponentialBackoffRetryStrategy max_retries: 6 is set on every LLM and embedder. Transient OpenAI errors are retried with exponential backoff before the pipeline fails.
Security and privacy
Q: Are documents sent to OpenAI?
By default, yes — both for embedding (so the index can be built) and for question answering. private_rag and document_indexing (with mxbai-embed-large-v1) keep documents local.
Q: Can I run this in a regulated environment?
Yes — start from private_rag and confirm every external service is replaced with a local one. The Admin Guide section 7 enumerates the exact things to audit.
Q: How do I rotate the OpenAI key?
Update .env, then docker compose restart app. The key is read once at process start.
Cookbooks and integrations
Q: Why does cookbooks/self-rag-agents/ import LangGraph?
The cookbooks demonstrate combining Pathway (for the live document store) with LangGraph (for the agent state machine). LangGraph is a dependency only of the notebooks, not of any template's requirements.txt.
Q: Is there an integration with LangChain?
LangChain is not bundled. You can use pw.xpacks.llm.question_answering.RAGClient (used by the Streamlit UIs) from any Python client, including LangChain.