Jesse A. Green / notebook

Pathway LLM App Templates Administrator & Developer Guide

Generated April 2026 from pathwaycom/llm-app at 5fa97b7

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

pathwaycom/llm-app is a curated catalogue of ten reference applications — eight deployable templates plus two Jupyter cookbooks — each built on the Pathway streaming-dataflow engine and its pathway.xpacks.llm toolkit. This guide documents the architecture, deployment model, configuration surface, security posture, and developer internals for the catalogue. For end-user task walkthroughs, see the Pathway LLM App Templates User Guide.

The repository is small (126 files, ~62 KB of analyzed source). Most of the substance lives upstream in the Pathway library; the templates are configuration recipes and Pydantic glue that wire the library's RAG, indexing, and MCP primitives into deployable services. Read this guide together with the Pathway core docs at https://pathway.com/developers/ when you need to understand a class or method that the templates merely reference.

1. Architecture Overview

What the catalogue actually is

Each template under templates/ is a self-contained Pathway program with the following on-disk shape:

templates/<template_name>/
  app.py              ← the Pydantic App shell + entrypoint
  app.yaml            ← declarative pipeline config (App-class templates only)
  Dockerfile          ← image build: pathwaycom/pathway base + requirements
  docker-compose.yml  ← multi-container stack (app, ui, optional services)
  requirements.txt    ← typically just python-dotenv (pathway is in the base image)
  .env.example        ← env-var template (some templates only)
  README.md           ← per-template operational doc
  data/               ← seed documents (or files-for-indexing/)
  ui/                 ← Streamlit UI (some templates only)

Modern templates use a Pydantic App(BaseModel) class whose fields are parsed from app.yaml via pw.load_yaml. Legacy templates (drive_alert, unstructured_to_sql_on_the_fly) instead expose a procedural run(*, ...) function with os.environ.get defaults.

High-level system diagram

                         ┌────────────────────────────────────────────────┐
                         │      pathwaycom/llm-app TEMPLATE CATALOGUE     │
                         │  (this repository — Pydantic shells + YAML)    │
                         └──────────────────────┬─────────────────────────┘
                                                │
                                  ┌─────────────┴───────────────┐
                                  ▼                             ▼
        ┌──────────────────────────────────┐    ┌─────────────────────────────┐
        │ App(BaseModel) Pydantic shell    │    │ Legacy procedural run()     │
        │ extra="forbid"                   │    │ os.environ.get defaults     │
        │  ├── question_answerer | doc_    │    │  ├── drive_alert            │
        │  │   store | mcp_http (one of)   │    │  └── unstructured_to_sql_   │
        │  ├── host, port                  │    │      on_the_fly             │
        │  ├── persistence_mode/_backend   │    └─────────────────────────────┘
        │  ├── terminate_on_error          │                 │
        │  └── run() → pw.run(...)         │                 │
        └────────┬─────────────────────────┘                 │
                 │                                           │
                 └──────────────────┬────────────────────────┘
                                    ▼
        ┌────────────────────────────────────────────────────────────────┐
        │       pathway.xpacks.llm  (UPSTREAM LIBRARY — pathway[all])    │
        │                                                                │
        │  llms.OpenAIChat / LiteLLMChat                                 │
        │  embedders.OpenAIEmbedder / SentenceTransformerEmbedder        │
        │  parsers.DoclingParser / SlideParser / UnstructuredParser      │
        │  splitters.TokenCountSplitter                                  │
        │  document_store.DocumentStore / SlidesDocumentStore            │
        │  question_answering.{Base,Adaptive}RAGQuestionAnswerer         │
        │  question_answering.SummaryQuestionAnswerer                    │
        │  servers.{QASummary,DocumentStore}RestServer                   │
        │  mcp_server.PathwayMcp                                         │
        └────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
        ┌────────────────────────────────────────────────────────────────┐
        │     pathway core (pathway[all]) — Python+Rust dataflow         │
        │                                                                │
        │  pw.io.{fs,gdrive,postgres,slack,...}                          │
        │  pw.xpacks.connectors.sharepoint                               │
        │  pw.persistence.{Backend,Config}, pw.PersistenceMode           │
        │  pw.indexing.{UsearchKnnFactory,USearchMetricKind}             │
        │  pw.run(persistence_config=…, terminate_on_error=…)            │
        └────────────────────────────────────────────────────────────────┘

Note

The templates own almost no logic. The Pydantic App is ~50 lines per template; everything substantive — the streaming dataflow, the connectors, the LLM orchestration, the persistence layer — lives in the Pathway library. Audit and hardening work on the templates is therefore mostly about (a) configuration choices, (b) container/network posture, and (c) the legacy procedural code in drive_alert and unstructured_to_sql_on_the_fly.

Component inventory

Component Path Role
App Pydantic shell templates/<name>/app.py Loads YAML → instantiates xpack server → calls pw.run
app.yaml declarative config templates/<name>/app.yaml Source of truth for pipeline composition
Dockerfile templates/<name>/Dockerfile Image build (pathwaycom/pathway base + reqs)
docker-compose templates/<name>/docker-compose.yml Local multi-container orchestration (where present)
Streamlit UI templates/<name>/ui/ui.py (or server.py for legacy) Browser-facing chat/search front-end
Legacy drive_alert procedural run templates/drive_alert/app.py Google-Drive RAG with Slack alerts (port 8080)
Legacy unstructured_to_sql_on_the_fly templates/unstructured_to_sql_on_the_fly/app.py PDF→Postgres extractor + NL2SQL on top
Cookbooks cookbooks/self-rag-agents/*.ipynb Jupyter tutorials (LangGraph + Pathway), not deployable
nginx file server templates/slides_ai_search/nginx/ Serves slide images and PDFs back to the slides UI
Postgres init SQL templates/unstructured_to_sql_on_the_fly/postgres/init-db.sql Creates the target table for the legacy SQL pipeline

Technology stack

Layer Technology Used by
Streaming dataflow runtime Pathway (Python entrypoint, Rust core via pw.run) Every template
LLM client OpenAI Python (via pathway.xpacks.llm.llms.OpenAIChat) All except private_rag
Local LLM client LiteLLM (via LiteLLMChat) → Ollama private_rag
Embeddings OpenAI text-embedding-3-small / SentenceTransformer (mxbai-embed-large-v1, GIST-small-Embedding-v0) per-template
Parser Docling (pathway.xpacks.llm.parsers.DoclingParser) RAG templates
Slide parser SlideParser (LLM-based, image-aware) slides_ai_search
Unstructured parser UnstructuredParser Legacy templates
Vector index usearch (UsearchKnnFactory, cosine metric) RAG/indexing templates
Legacy index pathway.stdlib.ml.index.KNNIndex drive_alert
REST API QASummaryRestServer / DocumentStoreServer (xpacks) App-class templates
MCP pathway.xpacks.llm.mcp_server.PathwayMcp (streamable-http) document_store_mcp_server
UI Streamlit 4 templates
Reverse-proxy / file server nginx slides_ai_search
Database (output) Postgres unstructured_to_sql_on_the_fly
Alerting (output) Slack (pw.io.slack.send_alerts) drive_alert
Container runtime Docker + Compose All templates
Config YAML + Pydantic v2 (extra="forbid") App-class templates
Auth (LLM) API keys via .env / os.environ All templates

Data flow — canonical RAG (question_answering_rag)

[ source files (filesystem / GDrive / SharePoint) ]
                        │
                        ▼ (Pathway streaming connector watches changes)
              pw.io.fs.read(path=data, format=binary, with_metadata=true)
                        │
                        ▼  (table of binary files)
                DoclingParser  (LLM-aware table parsing)
                        │
                        ▼  (table of text strings)
              TokenCountSplitter(max_tokens=400)
                        │
                        ▼  (table of chunks)
              OpenAIEmbedder(text-embedding-3-small)
                        │
                        ▼  (table of (chunk, vector))
              UsearchKnnFactory(metric=COS) → DocumentStore
                        │
                        ▼  (live, always-current vector index)
   ┌────────────────────┴─────────────────────────┐
   │                                              │
   ▼                                              ▼
BaseRAGQuestionAnswerer.answer()           DocumentStore.list_documents()
(top-k retrieval → prompt build →           (returns metadata for UI)
OpenAIChat → response)
   │
   ▼
QASummaryRestServer (HTTP, port 8000)
   │
   ▼
[ /v2/answer | /v1/retrieve | /v1/statistics | /v2/list_documents | /v2/summarize ]

Data flow — MCP server (document_store_mcp_server)

[ files-for-indexing/ ] ──► pw.io.fs.read ──► DoclingParser ──► TokenCountSplitter
        ──► SentenceTransformerEmbedder(mxbai-embed-large-v1) ──► UsearchKnn
        ──► DocumentStore ──► PathwayMcp(streamable-http, port 8068)
                              │
                              ▼
              [ MCP-aware client: Claude Desktop, agent, etc. ]

Data flow — drive_alert (legacy)

[ Google Drive folder ]
        │ pw.io.gdrive.read(refresh_interval=30s)
        ▼
   UnstructuredParser ──► TokenCountSplitter(40-120 tokens)
        │
        ▼
   OpenAIEmbedder(text-embedding-ada-002) ──► KNNIndex
        │
        │     ┌──────────────── HTTP POST / on port 8080 (REST connector) ──┐
        │     ▼                                                             │
        │  QueryInputSchema(query, user)                                    │
        │     │                                                             │
        │     ▼  prompt: "Is this an alerting query?" → OpenAIChat          │
        │     ▼  yields (alert_enabled: bool, query: str)                   │
        ▼     ▼
     KNNIndex.get_nearest_items(k=3)
        │
        ▼  build_prompt(documents_list, query) → OpenAIChat → response
        │
        ▼  HTTP response_writer(output)
        │
        ▼  filter(alert_enabled) → pw.stateful.deduplicate(acceptor=LLM)
        │
        ▼  pw.io.slack.send_alerts(message, channel_id, token)

Data flow — unstructured_to_sql_on_the_fly (legacy)

[ ./data/quarterly_earnings/*.pdf ]
        │ pw.io.fs.read(format=binary)
        ▼
   UnstructuredParser ──► strip_metadata
        │
        ▼  build_prompt_structure (insert FinancialStatementSchema typehints)
        ▼  OpenAIChat → JSON response
        ▼  parse_str_to_list → unpack_col → typed columns
        ▼  pw.io.postgres.write(structured_table, settings, table=quarterly_earnings)
        ▼  pw.io.csv.write(structured_table, "./data/quarterly_earnings.csv")

──── alongside ────

[ HTTP POST / on port 8080 ] ──► NLQuerySchema(query, user)
        │
        ▼  build_prompt_query(table, query) → OpenAIChat → SQL string
        ▼  psycopg cursor.execute(sql_query) → fetchall()
        ▼  pw.make_tuple(sql_query, result) → response_writer

Note

Both legacy templates use pw.io.http.rest_connector (port 8080), not the modern QASummaryRestServer. They predate the App-class refactor and therefore expose a different REST shape (single POST / rather than the /v2/answer-family). Keep this in mind when fronting them with a reverse proxy.

2. System Requirements

Runtime requirements

Component Required Notes
Docker Engine 24.0+ Compose plugin (docker compose, not docker-compose)
Linux kernel 5.15+ Pathway's Rust core links against modern glibc
RAM 4 GB minimum / 8+ GB recommended Multimodal/Slides templates push higher
CPU 2 cores minimum / 4+ cores recommended Pathway parallelism scales with cores
Disk 10 GB Docker images, Pathway persistence, document cache
Network egress to OpenAI / GDrive / SharePoint Skip with private_rag

Native (non-Docker) runtime

Component Required Notes
Python 3.10 - 3.12 pathway[all] published wheels
pip 23+ For pip install pathway[all]
OpenSSL system Pathway's Rust core links against it
Postgres 14+ Only for unstructured_to_sql_on_the_fly (compose ships its own)
Ollama 0.1.30+ Only for private_rag; install on the host

External services

Service Required by Auth
OpenAI Chat Completions Most templates OPENAI_API_KEY
OpenAI Embeddings Templates with OpenAIEmbedder same key
Google Drive API drive_alert and any template configured with the GDrive connector service-account JSON
Microsoft Graph (SharePoint) Templates configured with the SharePoint connector client cert + thumbprint
Slack Web API (chat.postMessage) drive_alert bot token
Postgres unstructured_to_sql_on_the_fly user/password
Ollama private_rag none (local)

Pathway license

Templates ship with pw.set_license_key("demo-license-key-with-telemetry"). Operational implications:

License mode What works What doesn't Telemetry
Demo (default) All template features as shown none — full features for evaluation anonymous usage telemetry sent to Pathway
Pathway Community (key removed) Single-machine streaming, all xpacks --addresses clustering, autoscaling, certain enterprise persistence backends none
Scale (paid key) Everything, including --addresses and autoscaling — none

Important

The templates do not read PATHWAY_LICENSE_KEY from the environment by default — the license string is hard-coded in app.py. To use a personal Scale key, edit app.py to call pw.set_license_key(os.environ["PATHWAY_LICENSE_KEY"]) and put the key in .env. This is a small but important hardening step for production deployments.

3. Configuration Reference

Configuration files (per template)

File Format Location Purpose
app.yaml YAML w/ Pathway tags templates/<name>/ (App-class only) Declarative pipeline composition
app.py Python templates/<name>/ Entrypoint + Pydantic shell (modern) or procedural run() (legacy)
.env dotenv templates/<name>/ Secrets (gitignored)
.env.example dotenv templates/<name>/ (when present) Template for .env
Dockerfile Dockerfile templates/<name>/ Image build
docker-compose.yml YAML templates/<name>/ (most) Container orchestration
requirements.txt pip templates/<name>/ Extra Python deps beyond the Pathway base image
pyproject.toml TOML repo root Linter/formatter config only (package-mode = false — not an installable package)
setup.cfg INI repo root Linter (flake8/black) config

Note

The repo root has pyproject.toml and setup.cfg, but they declare package-mode = false and only configure linters. There is no PyPI-installable llm-app package. Each template is its own freestanding application; cloning the repo is the supported install method.

App Pydantic model — the shared shape

Every modern template's App(BaseModel) exposes a near-identical surface (the variable field is the domain object: question_answerer, document_store, or mcp_http):

Field Type Default Required Notes
question_answerer or document_store or mcp_http InstanceOf[<class>] — yes The domain object the App wraps. Exactly one is present per template.
host str "0.0.0.0" no REST/MCP bind address
port int 8000 no REST/MCP port
with_cache bool | None None no Deprecated — emits DeprecationWarning if set; use persistence_mode instead
persistence_backend pw.persistence.Backend | None None (resolved to Backend.filesystem("./Cache") when persistence active) no Where Pathway stores cached/persisted state
persistence_mode pw.PersistenceMode | None pw.PersistenceMode.UDF_CACHING no One of UDF_CACHING, PERSISTING, or None
terminate_on_error bool False no If True, pw.run aborts on first runtime error

model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True) is set on every App. Forbid is the load-bearing setting: it turns YAML typos into Pydantic validation errors at startup rather than silently ignored fields.

run() resolves persistence as follows:

  1. If persistence_mode is None and the deprecated with_cache is True: warn, use UDF_CACHING.
  2. If persistence_mode is None and with_cache is not True: persistence disabled.
  3. Else: use the explicit persistence_mode.
  4. If persistence is active and persistence_backend is None: default to Backend.filesystem("./Cache").
  5. Build pw.persistence.Config and pass it to pw.run.
  6. Always call pw.run(persistence_config=…, terminate_on_error=…, monitoring_level=pw.MonitoringLevel.NONE).

Slides AI Search App — extra fields

templates/slides_ai_search/app.py's App has additional fields tied to its slide-specific pipeline:

Field Type Default Notes
sources list[InstanceOf[pw.Table]] — Top-level (no $ prefix) — list of source connectors
llm InstanceOf[pw.UDF] — Top-level — used for both classification and parsing
retriever_factory InstanceOf[pw.indexing.AbstractRetrieverFactory] — Top-level — usearch by default
search_topk int 6 Per-query result count
details_schema FilePath | dict[str, Any] | None None The slide-classification schema (categories, tags, language, etc.)

Note that sources, llm, retriever_factory are top-level (read by App) in this template, whereas in the canonical RAG they are $-prefixed YAML anchors consumed by $document_store and question_answerer.

YAML configuration grammar (per-template differences)

Block question_answering_rag adaptive_rag multimodal_rag private_rag document_indexing document_store_mcp_server slides_ai_search
$llm OpenAIChat gpt-4.1-mini same same + $parsing_llm: gpt-4o LiteLLMChat ollama/mistral none none top-level llm: gpt-4o
$embedder OpenAIEmbedder text-embedding-3-small same same SentenceTransformer GIST-small-Embedding-v0 SentenceTransformer mxbai-embed-large-v1 same as document_indexing OpenAIEmbedder text-embedding-3-small
$splitter TokenCountSplitter max_tokens=400 same same same same same none (slide-level chunks)
$parser DoclingParser table_parsing_strategy=llm DoclingParser (no table strategy) DoclingParser + multimodal_llm + image_parsing_strategy=llm DoclingParser table_parsing_strategy=llm DoclingParser (no table strategy) same as document_indexing SlideParser (built in App.run)
$retriever_factory UsearchKnnFactory metric=COS same same same same same top-level retriever_factory:
$document_store DocumentStore same same same top-level document_store: $document_store: n/a (uses SlidesDocumentStore internally)
Domain object question_answerer: BaseRAGQuestionAnswerer question_answerer: AdaptiveRAGQuestionAnswerer (n_starting_documents=2, factor=2, max_iterations=4) question_answerer: BaseRAGQuestionAnswerer question_answerer: AdaptiveRAGQuestionAnswerer (… strict_prompt: true) document_store: (DocumentStore directly) mcp_http: PathwayMcp transport=streamable-http port=8068 top-level details_schema:
Default port 8000 8000 8000 8000 8000 8000 (App) / 8068 (MCP) 8000 (App) + 8501 UI + 8080 nginx

BaseRAGQuestionAnswerer knobs (recognised under question_answerer:)

Key Type Default Purpose
llm pw.UDF — Required — the chat model
indexer DocumentStore — Required — the vector index
search_topk int 6 Top-k chunks fetched per query
prompt_template str (Pathway internal default) Custom Jinja-style template; placeholders {query}, {context}
summarize_template str (default) For the summarize endpoint

AdaptiveRAGQuestionAnswerer knobs

Key Type Default in adaptive_rag Default in private_rag
n_starting_documents int 2 2
factor int 2 2
max_iterations int 4 4
strict_prompt bool not set true

Connector configuration

Every template's app.yaml includes commented examples for SharePoint and Google Drive sources. Live examples in shipped configs:

Filesystem (default, every template)

- !pw.io.fs.read
  path: data                # or files-for-indexing for indexing/MCP templates
  format: binary
  with_metadata: true

Google Drive (commented in every template)

- !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

SharePoint (commented in every template)

- !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

Important

SharePoint uses app-only authentication via certificate + thumbprint, not username/password. The cert file (sharepointcert.pem) must be next to app.yaml (or wherever cert_path: points) and the matching thumbprint registered with the Azure AD app registration. The app registration must have application (not delegated) permissions for Sites.Read.All.

Environment variable reference

Variable Type Default Required Used by Description
OPENAI_API_KEY secret — conditionally OpenAI-backed templates Bearer token for OpenAI Chat + Embeddings
PATHWAY_LICENSE_KEY secret — optional (manual wiring) Scale-tier features; not auto-read by templates
PATHWAY_HOST string app (compose) / localhost optional Streamlit UIs UI's target Pathway service hostname
PATHWAY_PORT int 8000 optional Streamlit UIs UI's target Pathway service port
PATHWAY_REST_CONNECTOR_HOST string 0.0.0.0 optional drive_alert, unstructured_to_sql_on_the_fly Bind address for legacy REST connector
PATHWAY_REST_CONNECTOR_PORT int 8080 optional legacy templates Port for legacy REST connector
FILE_OR_DIRECTORY_ID string — yes (for drive_alert) drive_alert Google Drive folder/file ID
GOOGLE_CREDS path templates/drive_alert/secrets.json yes (for drive_alert) drive_alert Service-account JSON path
SLACK_ALERT_CHANNEL_ID string — yes (for drive_alert) drive_alert Slack channel to post alerts to
SLACK_ALERT_TOKEN secret — yes (for drive_alert) drive_alert Slack bot token (xoxb-…)
SHAREPOINT_URL URL — yes (if SharePoint enabled) RAG templates with SharePoint SharePoint site URL
SHAREPOINT_TENANT string — yes (if SharePoint enabled) RAG templates with SharePoint Azure AD tenant
SHAREPOINT_CLIENT_ID string — yes (if SharePoint enabled) RAG templates with SharePoint Azure AD app registration client ID
SHAREPOINT_THUMBPRINT string — yes (if SharePoint enabled) RAG templates with SharePoint Cert thumbprint registered to the app
SHAREPOINT_ROOT string — yes (if SharePoint enabled) RAG templates with SharePoint Root path in SharePoint to start indexing
DRIVE_ID string — yes (if GDrive enabled) RAG templates with GDrive Google Drive folder ID
PATHWAY_DATA_DIR path ./data/quarterly_earnings optional unstructured_to_sql_on_the_fly Where to read source PDFs
POSTGRESQL_HOST string localhost optional unstructured_to_sql_on_the_fly Postgres host
POSTGRESQL_PORT int 5432 optional unstructured_to_sql_on_the_fly Postgres port
POSTGRESQL_DB string STRUCTUREDDB optional unstructured_to_sql_on_the_fly Postgres database name
POSTGRESQL_USER string user optional unstructured_to_sql_on_the_fly Postgres user
POSTGRESQL_PASSWORD secret password optional unstructured_to_sql_on_the_fly Postgres password
POSTGRESQL_TABLE string quarterly_earnings optional unstructured_to_sql_on_the_fly Target table
FILE_SERVER_URL URL http://localhost:8080/ optional slides_ai_search/ui nginx-served slide images/PDFs

Important

Several variables (SHAREPOINT_*, DRIVE_ID) are referenced only inside app.yaml via $VAR interpolation, so they will not appear in any AST scan of os.environ.get — they are still required at runtime when the corresponding connector block is uncommented. Keep this matrix as the canonical inventory; the Python source alone is incomplete.

Annotated walkthrough — production-hardened app.yaml

# Persistence: keep the cache between restarts so we don't re-embed on deploy
persistence_mode: !pw.PersistenceMode.PERSISTING
persistence_backend: !pw.persistence.Backend.filesystem
  path: "/var/lib/pathway/cache"

# Halt on errors so a partial pipeline doesn't silently serve bad answers
terminate_on_error: true

# Listen only on the in-cluster network; reverse-proxy will terminate TLS
host: "0.0.0.0"
port: 8000

# Sources: SharePoint as primary, with file-system fallback (mounted volume)
$sources:
  - !pw.xpacks.connectors.sharepoint.read
    url: $SHAREPOINT_URL
    tenant: $SHAREPOINT_TENANT
    client_id: $SHAREPOINT_CLIENT_ID
    cert_path: /etc/secrets/sharepointcert.pem
    thumbprint: $SHAREPOINT_THUMBPRINT
    root_path: $SHAREPOINT_ROOT
    with_metadata: true
    refresh_interval: 60
  - !pw.io.fs.read
    path: /data/local
    format: binary
    with_metadata: true

# Production model + capacity tuned to OpenAI tier-3 limits
$llm: !pw.xpacks.llm.llms.OpenAIChat
  model: "gpt-4o"
  retry_strategy: !pw.udfs.ExponentialBackoffRetryStrategy
    max_retries: 8
  cache_strategy: !pw.udfs.DefaultCache {}
  temperature: 0
  capacity: 32
  async_mode: "fully_async"

$embedder: !pw.xpacks.llm.embedders.OpenAIEmbedder
  model: "text-embedding-3-small"
  cache_strategy: !pw.udfs.DefaultCache {}
  retry_strategy: !pw.udfs.ExponentialBackoffRetryStrategy {}

$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 {}

$retriever_factory: !pw.indexing.UsearchKnnFactory
  reserved_space: 100000   # tune to expected doc count
  embedder: $embedder
  metric: !pw.indexing.USearchMetricKind.COS

$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
  search_topk: 8
  prompt_template: |
    You are a customer-support assistant for ACME Corp.
    Use ONLY the provided context. If the answer isn't in the context,
    say "I don't have that information."
    Context: {context}
    Question: {query}
    Answer:

4. Deployment & Scaling

Single-machine Docker Compose (the shipped path)

Each template ships a docker-compose.yml (most do — multimodal_rag, private_rag, adaptive_rag, slides_ai_search, question_answering_rag, document_indexing, document_store_mcp_server, unstructured_to_sql_on_the_fly, drive_alert all do). For the canonical RAG:

cd templates/question_answering_rag
docker compose up --build

Compose typically starts:

Service Image source Ports Role
app template's Dockerfile (Pathway base) 8000:8000 Pathway REST API
ui template's ui/Dockerfile 8501:8501 Streamlit

Slides adds an nginx service (8080); unstructured-to-SQL adds a postgres service.

Single Dockerfile (no UI)

cd templates/<template>
docker build -t myorg/llm-app-<template>:5fa97b7 .
docker run -d \
  -p 8000:8000 \
  -v "$PWD/data:/app/data" \
  -v "/var/lib/pathway/cache:/app/Cache" \
  --env-file .env \
  --restart unless-stopped \
  myorg/llm-app-<template>:5fa97b7

Kubernetes (manifest sketch — not bundled)

The repository does not ship Kubernetes manifests. A typical Deployment looks like:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: pathway-rag
spec:
  replicas: 1            # see "scaling" below before raising
  selector:
    matchLabels: {app: pathway-rag}
  template:
    metadata:
      labels: {app: pathway-rag}
    spec:
      containers:
        - name: app
          image: myorg/llm-app-question_answering_rag:5fa97b7
          ports: [{containerPort: 8000}]
          envFrom:
            - secretRef: {name: pathway-rag-secrets}
          volumeMounts:
            - {name: data, mountPath: /app/data}
            - {name: cache, mountPath: /app/Cache}
          resources:
            requests: {cpu: 1, memory: 4Gi}
            limits:   {cpu: 4, memory: 8Gi}
      volumes:
        - {name: data,  persistentVolumeClaim: {claimName: pathway-data}}
        - {name: cache, persistentVolumeClaim: {claimName: pathway-cache}}
---
apiVersion: v1
kind: Service
metadata: {name: pathway-rag}
spec:
  selector: {app: pathway-rag}
  ports: [{port: 80, targetPort: 8000}]

Multi-machine clustering (Pathway Scale)

Pathway supports multi-machine workers via the --addresses runtime flag. The catalogue's app.py files do not invoke --addresses themselves — you would either:

  1. Modify app.py to call pw.run with the processes and nodes arguments (Pathway core API), or
  2. Run multiple instances of the container with appropriate environment variables (Pathway's docs on multi-machine cover the exact incantation).

Both require PATHWAY_LICENSE_KEY (Scale tier) — without it pw.run raises a license error when clustering is requested.

Warning

Do not raise the Kubernetes replicas count above 1 unless you have wired up Pathway's multi-machine clustering. Two instances of the same App without coordination will both try to be authoritative for the same index and produce duplicated work, not horizontal scaling.

Persistence and state

pw.PersistenceMode controls what the cache holds:

Mode Behaviour
None No persistence. Restart re-runs everything from scratch.
UDF_CACHING (default) Cache LLM and embedder calls (memoise expensive UDFs). Restart loses streaming state but skips already-computed embeddings.
PERSISTING Full streaming-state persistence. Restart resumes the pipeline where it left off.

pw.persistence.Backend.filesystem(path) is the only backend used in the shipped templates. Pathway Scale supports additional backends (S3, Azure Blob) — those require a Scale license and app.py changes.

Restart and reload behaviour

  • Config change (app.yaml): edit, docker compose restart app. Pathway re-reads YAML at process start.
  • Code change (app.py): rebuild image with docker compose up --build.
  • New documents: Pathway picks them up live; no restart.
  • Document deletion: Pathway notices the file disappear; the corresponding chunks leave the index.
  • OPENAI_API_KEY rotation: restart; key is read once at process start.

5. Operations

Health checks

Every App-class template exposes the pw.io.http.PathwayWebserver-backed endpoints. A simple liveness probe:

curl -f -X POST http://localhost:8000/v1/statistics

Returns 200 with index stats when the pipeline is alive. For Kubernetes:

livenessProbe:
  httpGet: {path: /v1/statistics, port: 8000, scheme: HTTP}
  initialDelaySeconds: 60
  periodSeconds: 30
readinessProbe:
  httpGet: {path: /v1/statistics, port: 8000, scheme: HTTP}
  initialDelaySeconds: 10
  periodSeconds: 5

Note

Initial delay matters — first index build can take a minute on a large corpus. The pipeline will return 503 (or refuse connections) until then.

Logging

Every template configures Python's logging to INFO level with a fixed format:

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(name)s %(levelname)s %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)

Pathway emits log lines like Indexed N documents, OpenAI request retried, and Document store updated. For production, redirect Docker logs to your log aggregator (the included compose files do not configure log drivers — add logging: blocks).

Streamlit UI logging

The UIs log structured events for analytics:

logger.info({"_type": "search_request_event", "query": question})
logger.info({"_type": "search_response_event", "query": question, "response": type(response)})
logger.info({"_type": "rate_event", "rating": event, "rank": idx, ...})

Slides UI also logs _type: create_adjacent_image_urls. These are JSON-shaped log lines — filter on the _type field.

Monitoring

Pathway emits internal monitoring at monitoring_level=pw.MonitoringLevel.NONE (the catalogue's default). Higher levels (IN_OUT, ALL) write to stdout; configure them by editing pw.run(monitoring_level=…) in app.py. For production, integrate with Prometheus by scraping the Pathway metrics endpoint (see Pathway core docs).

Scaling guidance

Situation Action
OpenAI rate limit hit Lower capacity: on $llm:, raise max_retries:
Slow first index build Acceptable — cache warms on subsequent runs; mount the cache volume
Need multi-machine clustering Scale-tier license + --addresses configuration
Need horizontal replicas (no clustering) Front N independent instances with a load balancer; each maintains its own index
Memory grows Tune reserved_space: on UsearchKnnFactory to your expected doc count; very large indexes may need a different retriever
Slow embedding Switch from OpenAI to SentenceTransformerEmbedder on a GPU node

Backup and disaster recovery

Asset Backup strategy
Source documents Source of truth lives outside Pathway (filesystem mount, GDrive, SharePoint). Back up at the source.
Pathway cache (./Cache) Treat as ephemeral; rebuilds in minutes. Optional volume backup.
Postgres (legacy) Standard pg_dump; the data lives in Postgres, not the catalogue.
app.yaml and .env Source-controlled (yaml) and secret-managed (env).

Ops checklist for go-live

  • [ ] Replace demo license key with personal key or remove entirely.
  • [ ] Move OPENAI_API_KEY (and Slack/SharePoint secrets) to a secret manager (Kubernetes Secrets, AWS Secrets Manager).
  • [ ] Set terminate_on_error: true so partial-pipeline answers don't reach customers.
  • [ ] Mount /app/Cache to a persistent volume.
  • [ ] Configure persistence_mode: !pw.PersistenceMode.PERSISTING if you need restart-resumption.
  • [ ] Place the API behind a reverse proxy with TLS (Caddy, nginx, Traefik).
  • [ ] Add an authentication layer in front (the templates do not authenticate REST calls).
  • [ ] Set log shipping (the templates write to stdout — wire to your log aggregator).
  • [ ] Add liveness/readiness probes.
  • [ ] Pin OpenAI model versions explicitly (avoid floating gpt-4o-class names that update silently).
  • [ ] Test failure modes: kill OpenAI access, kill Postgres (legacy), kill Drive access, confirm the pipeline degrades sensibly.

Tip

Keep one Streamlit UI deployed even if your end users hit the API directly — it's the fastest way to debug the index and confirm a deployment is healthy without writing curl commands.

6. Performance

Where the time and money go

For a typical RAG query on the canonical template:

Phase Cost driver Typical cost (per query)
Embed query OpenAI embeddings <1 ms (cached) / ~50 ms cold
Retrieve top-k usearch in-memory <1 ms
Build prompt string concat <1 ms
LLM completion OpenAI Chat ~1-5 s (cold) / 0 ms (cache hit)
Serialise response JSON encode <1 ms

Indexing is the heavy phase: parsing PDFs (especially with DoclingParser table_parsing_strategy: "llm"), chunking, and embedding. A 100-page PDF with table-LLM parsing can take 30-60 s and ~10 cents in OpenAI calls. Subsequent runs hit DefaultCache and are nearly free.

Tuning levers

Lever Effect Recommendation
$llm.capacity: Concurrent LLM calls 8 default; lower if rate-limited; raise if your tier permits
$llm.retry_strategy.max_retries: Retries on transient failure 6 default; raise to 8-10 in production
$llm.async_mode: "fully_async" (default in modern templates) lets Pathway parallelise Keep on
$splitter.max_tokens: Chunk size 400 default; smaller = more chunks, more retrieval precision, more embedding cost; larger = fewer chunks, more LLM context per chunk
$retriever_factory.reserved_space: usearch initial capacity 1000 default; tune up to 10x expected docs to avoid resize
search_topk: (under question_answerer) Context document count 6 default; 4-8 is typical; higher costs more LLM tokens
Adaptive RAG n_starting_documents Iterative retrieval seed 2 default; lower = cheaper average query, more risk of escalation
persistence_mode UDF_CACHING skips repeated embedding/LLM calls Always on in production

Benchmarks (rough orders of magnitude)

Workload Indexing Per-query latency Per-query cost
10 small PDFs, text-embedding-3-small, gpt-4.1-mini, DoclingParser no LLM tables ~30 s 1-3 s ~$0.001-0.005
100 PDFs, multimodal, gpt-4o parsing ~10-20 min 2-5 s ~$0.01-0.05
10 PDFs, private_rag (CPU Mistral via Ollama) ~5 min 5-30 s $0 (local)
Slides AI search, 50 decks, gpt-4o parsing ~30-60 min 1-3 s ~$0.005 (search only)

These are not from a benchmark suite — they are typical orders of magnitude for the shipped configurations on commodity hardware.

7. Security

Threat model

Threat Surface Mitigation in shipped templates Production hardening
Stolen LLM API key .env, environment None (key sits in .env) Secret manager, key rotation, narrow OpenAI project key with low budget
Indexed documents leaking via LLM OpenAI receives every retrieved chunk Demo data uses public PDFs Use private_rag for sensitive data; review OpenAI's data-retention terms
Unauthenticated REST API /v2/answer, /v1/retrieve, /v2/list_documents exposed on 0.0.0.0:8000 None (no auth in templates) Reverse-proxy with auth (mTLS, OAuth2, API keys)
Prompt injection in source docs A malicious PDF can include "ignore previous instructions" content None LLM-side guardrails, document source vetting, content moderation on responses
LLM-generated SQL injection unstructured_to_sql_on_the_fly executes LLM-produced SQL Constrained prompt grammar (SELECT only) Read-only DB role, strictly siloed database, allow-list the column set
GitHub issue #75 — security issue (closed) Reported and addressed in upstream commits — Track upstream advisories on pathwaycom/pathway
Slack token leak SLACK_ALERT_TOKEN in env None Rotate on incident; least-privilege bot scope
Service-account JSON theft secrets.json in template directory .gitignore for the file Do not bake into images; mount as a Kubernetes Secret
Network-level interception All LLM calls over HTTPS by default OK TLS-terminate at the proxy; pin certificates if your environment requires
Exposed nginx file server slides_ai_search/nginx serves slide images and PDFs None Bind only to the cluster network; require auth for the file server

Warning

The shipped templates have no authentication on the REST or MCP endpoints. They are designed for evaluation and internal deployments behind a reverse proxy. Putting a template directly on the public internet without a fronting authn/authz layer is unsafe — anyone can query your index, read list_documents, and incur LLM cost.

Secret handling

The templates rely on python-dotenv's load_dotenv() plus os.environ.get (legacy) or YAML $VAR interpolation (modern):

Secret Where it sits Rotation
OPENAI_API_KEY .env next to app.py restart
SLACK_ALERT_TOKEN .env restart
Service-account JSON (secrets.json) next to app.py restart
SharePoint cert (sharepointcert.pem) next to app.yaml restart
POSTGRESQL_PASSWORD .env restart
PATHWAY_LICENSE_KEY hard-coded in app.py (manual edit) restart

For production, replace .env with environment variables sourced from your secret manager. Do not bake secrets into images.

Auditing

Pathway logs every retry and connector poll. The Streamlit UIs log structured search_request_event / search_response_event lines that can feed audit trails. For full audit coverage of "who asked what":

  1. Front the API with a proxy that logs (Caddy, Traefik, ALB).
  2. Pass user identity through to the request body or a header that ends up in the prompt.
  3. Capture the JSON-shaped UI log lines.

Caution

Pathway's pw.io.slack.send_alerts posts to Slack with the bot token in plaintext at request time. Anyone with read access to the running container's environment or to outgoing-traffic mirrors can recover the token. Treat the container as a security boundary equivalent to the Slack bot itself.

License compliance

Component License
pathwaycom/llm-app (this repo) MIT (LICENSE file at repo root)
Pathway core (Python+Rust) BSL 1.1 → Apache 2.0 after 4 years
Pathway xpacks (pathway.xpacks.llm) BSL 1.1 → Apache 2.0 after 4 years
OpenAI API (terms separate) OpenAI ToS
LiteLLM MIT
Sentence-Transformers embeddings Apache 2.0 (most models)
Ollama MIT
Streamlit Apache 2.0

The catalogue is MIT and you can use, modify, and redistribute the templates freely. The Pathway engine you depend on is under BSL 1.1 — for commercial production use of advanced features you need a Pathway Scale license (which also unlocks the demo telemetry-free mode). Read the BSL terms carefully if you are forking Pathway itself.

8. Developer Internals

Repository structure

pathwaycom/llm-app/
├── README.md                          ← marketing landing + selector table
├── CONTRIBUTING.md
├── CODE_OF_CONDUCT.md
├── LICENSE                            ← MIT
├── pyproject.toml                     ← linter config; package-mode = false
├── setup.cfg                          ← flake8/black config
├── .gitignore
├── assets/                            ← logos, marketing images
├── cookbooks/
│   └── self-rag-agents/
│       ├── pathway_langgraph_agentic_rag.ipynb
│       └── pathway_deploy_langgraph_agents.ipynb
└── templates/
    ├── adaptive_rag/
    │   ├── app.py                     ← App(BaseModel), question_answerer
    │   ├── app.yaml
    │   ├── Dockerfile
    │   ├── requirements.txt           ← python-dotenv~=1.0
    │   ├── .env.example
    │   ├── README.md
    │   └── data/                      ← seed PDF
    ├── document_indexing/              ← App(BaseModel), document_store
    │   └── … (similar shape)
    ├── document_store_mcp_server/      ← App(BaseModel), mcp_http (PathwayMcp)
    │   └── …
    ├── drive_alert/                    ← LEGACY — procedural run(*, ...)
    │   ├── app.py
    │   ├── __init__.py                 ← re-exports run
    │   ├── ui/server.py
    │   └── …
    ├── multimodal_rag/                 ← App(BaseModel), question_answerer + $parsing_llm
    │   └── …
    ├── private_rag/                    ← App(BaseModel), AdaptiveRAG + LiteLLM
    │   └── …
    ├── question_answering_rag/         ← canonical App(BaseModel), question_answerer
    │   ├── app.py
    │   ├── app.yaml
    │   ├── docker-compose.yml
    │   ├── ui/                         ← Streamlit RAGClient UI
    │   │   ├── ui.py
    │   │   ├── Dockerfile
    │   │   ├── requirements.txt        ← streamlit==1.37.0, load_dotenv
    │   │   └── static/pathway-logo-black.png
    │   └── data/
    ├── slides_ai_search/               ← App(BaseModel) + SlidesDocumentStore + nginx
    │   ├── app.py
    │   ├── app.yaml
    │   ├── nginx/                      ← file-server stack
    │   │   ├── Dockerfile
    │   │   └── nginx.conf
    │   ├── pathway_slides_ai_search/   ← per-template helper module
    │   │   └── __init__.py
    │   └── ui/                         ← Streamlit UI (categories, languages, slide cards)
    │       ├── ui.py
    │       └── requirements.txt        ← includes openai==1.35.10, beautifulsoup4
    └── unstructured_to_sql_on_the_fly/ ← LEGACY — procedural run(*, ...)
        ├── app.py
        ├── postgres/init-db.sql
        ├── ui/server.py
        └── data/quarterly_earnings/    ← seed PDFs

The shared App-class lifecycle (deep dive)

Every modern template's app.py follows this exact recipe:

import logging
from warnings import warn
import pathway as pw
from dotenv import load_dotenv
# template-specific xpack imports (one of):
from pathway.xpacks.llm.question_answering import SummaryQuestionAnswerer
from pathway.xpacks.llm.servers import QASummaryRestServer
# or DocumentStore + DocumentStoreServer
# or PathwayMcp
from pydantic import BaseModel, ConfigDict, InstanceOf

pw.set_license_key("demo-license-key-with-telemetry")

logging.basicConfig(level=logging.INFO, format="…", datefmt="…")
load_dotenv()


class App(BaseModel):
    # exactly one domain object: question_answerer / document_store / mcp_http
    question_answerer: InstanceOf[SummaryQuestionAnswerer]
    host: str = "0.0.0.0"
    port: int = 8000
    with_cache: bool | None = None  # deprecated
    persistence_backend: pw.persistence.Backend | None = None
    persistence_mode: pw.PersistenceMode | None = pw.PersistenceMode.UDF_CACHING
    terminate_on_error: bool = False

    def run(self) -> None:
        server = QASummaryRestServer(self.host, self.port, self.question_answerer)
        # … persistence resolution …
        pw.run(persistence_config=…, terminate_on_error=…,
               monitoring_level=pw.MonitoringLevel.NONE)

    model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)


if __name__ == "__main__":
    with open("app.yaml") as f:
        config = pw.load_yaml(f)
    app = App(**config)
    app.run()

The five blocks that vary across modern templates:

  1. The xpack import — which class is used as the domain object.
  2. The single Pydantic field — question_answerer, document_store, or mcp_http.
  3. The server instantiation in run() — which xpack server (or none, for MCP) is built.
  4. Whether with_cache exists — the MCP template omits the deprecated field.
  5. base_dir resolution — slides_ai_search reads the YAML relative to __file__ because its compose mounts a non-default working directory.

Legacy template internals

drive_alert/app.py

A 200-line procedural run(*, ...) that builds a four-stage Pathway dataflow:

  1. Index build: pw.io.gdrive.read → UnstructuredParser → TokenCountSplitter(min_tokens=40, max_tokens=120) → OpenAIEmbedder → KNNIndex.
  2. Query intake + intent detection: pw.io.http.rest_connector (port 8080, schema QueryInputSchema(query, user)), build_prompt_check_for_alert_request_and_extract_query UDF, OpenAIChat, split_answer UDF that yields (alert_enabled: bool, cleaned_query: str).
  3. Query answer: for each query, fetch top-3 nearest chunks, build prompt, call OpenAIChat, send back via response_writer.
  4. Alert loop: filter alert_enabled=True queries, run pw.stateful.deduplicate with an LLM-driven acceptor(new, old) -> bool that asks the LLM whether two answers materially differ. When the dedupe accepts a new answer, pw.io.slack.send_alerts posts a notification.

Key UDFs:

UDF Purpose
build_prompt(documents, query) RAG prompt construction
build_prompt_check_for_alert_request_and_extract_query(query) Alert-intent classifier prompt
split_answer(answer) -> tuple[bool, str] Parse "Yes/No. <query>" into (alert_enabled, query)
build_prompt_compare_answers(new, old) Dedupe-acceptor prompt
make_query_id(user, query) Stable hash for deduplication instance
construct_notification_message(query, response) Slack message body
construct_message(response, alert_flag, metainfo=None) UI-side response with bell emoji when alerting
decision_to_bool(decision) LLM "yes/no" → Python bool

Schemas:

class DocumentInputSchema(pw.Schema):
    doc: str

class QueryInputSchema(pw.Schema):
    query: str
    user: str

Defaults that matter:

Parameter Default
embedder_locator text-embedding-ada-002
embedding_dimension 1536
model_locator gpt-3.5-turbo
max_tokens (response) 400
temperature 0.0
refresh_interval (GDrive) 30 seconds
splitter min/max tokens 40/120
KNN k 3
delete_completed_queries False
autocommit_duration_ms 50

unstructured_to_sql_on_the_fly/app.py

A 240-line procedural run(*, ...) that builds two pipelines in one process:

Pipeline 1 — extraction:

files = pw.io.fs.read(data_dir, format="binary")
parser = UnstructuredParser()
unstructured_documents = files.select(texts=parser(pw.this.data)).select(
    texts=strip_metadata(pw.this.texts)
)
structured_table = structure_on_the_fly(
    unstructured_documents, api_key, model_locator, max_tokens, temperature
)
pw.io.postgres.write(structured_table, postgreSQL_settings, postresql_table)
pw.io.csv.write(structured_table, "./data/quarterly_earnings.csv")

structure_on_the_fly builds a token-aware prompt, calls OpenAIChat, parses JSON, unpacks columns, types them. Schema:

class FinancialStatementSchema(pw.Schema):
    company_symbol: str
    year: int
    quarter: str
    revenue_md: float
    eps: float
    net_income_md: float

Pipeline 2 — NL→SQL query:

query, response_writer = pw.io.http.rest_connector(
    host=host, port=port, schema=NLQuerySchema,
    autocommit_duration_ms=50, delete_completed_queries=True,
)
query += query.select(prompt=build_prompt_query(postresql_table, pw.this.query))
query += query.select(sql_query=model(prompt_chat_single_qa(pw.this.prompt)))

@pw.udf
def execute_sql_query(sql_query):
    cursor.execute(sql_query); answer = cursor.fetchall(); conn.commit(); return answer

query = query.select(query, sql_query, result=execute_sql_query(pw.this.sql_query))
answers = query.select(result=pw.make_tuple(pw.this.sql_query, pw.this.result))
response_writer(answers)

The prompt grammar pinned by build_prompt_query:

  • SELECT clause specifies columns or aggregators (SUM, COUNT, MIN, MAX, AVG).
  • WHERE uses standard binary operators with AND/OR.
  • GROUP BY includes any WHERE columns to avoid psycopg2.errors.GroupingError.
  • If no aggregator, always include company_symbol, year, quarter.

Caution

The prompt is the only SQL guard. An LLM can be tricked into producing malicious SQL with sufficient prompt injection. Run against a read-only Postgres role; never share a connection with privileged tables.

Streamlit UIs

Every UI is a single ui.py (or server.py) that:

  1. Loads .env, reads PATHWAY_HOST and PATHWAY_PORT.
  2. Constructs RAGClient(url=f"http://{PATHWAY_HOST}:{PATHWAY_PORT}").
  3. Renders a Streamlit page with a query input, a sidebar listing indexed docs (from conn.list_documents(keys=[])), and a result region.
  4. On submit, calls conn.answer(question, ...) (or for slides, a filtered variant) and renders.

question_answering_rag/ui/ui.py (5408 chars) is the simplest. slides_ai_search/ui/ui.py (11886 chars) adds:

  • Image rendering via the nginx file server (FILE_SERVER_URL).
  • Filename → slide-ID parsing (parse_slide_id_components, create_slide_url, get_adjacent_image_urls).
  • Category and language multi-selects → JMESPath filter strings (combine_filters).
  • BeautifulSoup scrape of the nginx index page to surface "Indexing in Progress" files.

drive_alert/ui/server.py and unstructured_to_sql_on_the_fly/ui/server.py are smaller (2047 and 2502 chars) — they use the legacy requests.post against port 8080 rather than RAGClient.

Module reference

Module Path Purpose
templates/question_answering_rag/app.py canonical RAG App Pydantic shell, BaseRAGQuestionAnswerer, QASummaryRestServer
templates/adaptive_rag/app.py adaptive RAG App identical shell + AdaptiveRAGQuestionAnswerer
templates/multimodal_rag/app.py multimodal RAG App identical shell + multimodal parser config in YAML
templates/private_rag/app.py private RAG App identical shell + LiteLLMChat + SentenceTransformerEmbedder
templates/document_indexing/app.py indexing-only App DocumentStore + DocumentStoreServer, no LLM
templates/document_store_mcp_server/app.py MCP server App PathwayMcp (streamable-http) on port 8068
templates/slides_ai_search/app.py slides App SlidesDocumentStore + SlideParser; helper module pathway_slides_ai_search
templates/drive_alert/app.py legacy GDrive+Slack procedural run(), KNNIndex, REST on 8080, Slack output
templates/unstructured_to_sql_on_the_fly/app.py legacy PDF→SQL procedural run(), two pipelines, Postgres output, NL2SQL on REST
templates/question_answering_rag/ui/ui.py RAG Streamlit UI RAGClient + sidebar list of indexed files
templates/slides_ai_search/ui/ui.py Slides Streamlit UI RAGClient + image rendering + category/language filters
templates/drive_alert/ui/server.py legacy GDrive UI Streamlit hitting port 8080, displays response
templates/unstructured_to_sql_on_the_fly/ui/server.py legacy SQL UI Streamlit hitting port 8080, parses (sql_query, rows) tuple result
templates/slides_ai_search/pathway_slides_ai_search/__init__.py slides helpers DeckRetrieverWithFileSave, add_slide_id, get_model (per-template helpers)
cookbooks/self-rag-agents/pathway_langgraph_agentic_rag.ipynb tutorial notebook LangGraph + Pathway agentic RAG
cookbooks/self-rag-agents/pathway_deploy_langgraph_agents.ipynb tutorial notebook deploy the same graph

Code patterns and conventions

Pydantic-with-extra-forbid is the hard contract. Every modern template sets extra="forbid". This shifts every config error from runtime mystery to startup-time validation error with a clear message. Keep this when forking.

YAML anchors over inheritance. $llm, $embedder, $splitter, $parser, $retriever_factory are conventionally $-prefixed; question_answerer, host, port, persistence_*, terminate_on_error are top-level. The $-prefix doesn't change Pathway behaviour — it's a project convention indicating "this is referenced elsewhere in the YAML, not consumed by App". Keep the convention.

pw.set_license_key at module-import time. Every app.py calls pw.set_license_key("demo-…") at the top, before App is even defined. Do not move this — Pathway needs the license set before the dataflow is described.

load_dotenv() once, early. Same pattern in every template.

monitoring_level=pw.MonitoringLevel.NONE. The default for the catalogue. Raise to IN_OUT or ALL for production.

Procedural legacy is not deprecated as a Pathway pattern. Pathway still supports building dataflows procedurally (in fact, app.py is a procedural Pathway program — it just happens to be pre-wired by Pydantic). The legacy templates are "legacy" only in the sense that they predate the App-class catalogue convention; the underlying API is current.

Testing strategy

The repository ships no test suite — there are no tests/ or pytest.ini files. The catalogue's correctness story is:

  1. Each template has a working seed dataset (PDFs in data/ or files-for-indexing/).
  2. docker compose up --build is the smoke test: it must produce indexed documents and answer a curl query.
  3. Upstream Pathway has its own test suite that covers the xpacks the templates use.

For your fork, recommended tests:

  • Build smoke test — docker compose up --build reaches INFO Indexed N documents.
  • Endpoint smoke test — curl /v2/answer returns a non-empty JSON response.
  • YAML schema test — python -c "import yaml; from app import App; cfg = yaml.safe_load(open('app.yaml')); App(**cfg)" passes Pydantic validation.
  • Connector test — point at a tiny S3 bucket / SharePoint folder / Drive folder and confirm indexing.

Potential improvements (tech debt)

  • Legacy templates lack the App-class shell. drive_alert and unstructured_to_sql_on_the_fly could be refactored to expose configuration via app.yaml like the modern templates. They predate the convention; refactoring is straightforward but breaks API surface.
  • No central README with the selector table. The root README is marketing; users discover templates by ls'ing templates/. Adding a selector table to the README would close GitHub issue #124. (This guide's section 4.1 is the de-facto fix.)
  • PATHWAY_LICENSE_KEY should be read from env by default. Hard-coding "demo-license-key-with-telemetry" in every app.py is a footgun for production deployments.
  • Streaming response support (issue #46) — OpenAIChat doesn't expose stream=True through Pathway xpacks. Would require upstream library changes.
  • Templates do not expose Pathway monitoring metrics. A metrics endpoint and a tiny Prometheus scrape would help operators.
  • No authentication on REST/MCP endpoints. A simple API-key middleware is a 50-line fix, currently absent.
  • No rate-limiting on REST/MCP endpoints. Any client can hammer the LLM and run up the OpenAI bill.
  • Per-template pyproject.toml would help dependency pinning. Current requirements.txt files are sparse — they rely on the Pathway base image for everything substantive.
  • Tests — see above.

Contributing

The repository is open-source under MIT (see LICENSE). CONTRIBUTING.md exists at the root with the contribution workflow. Pull requests typically:

  • Add a new template (mirror the App-class shape — Pydantic shell + app.yaml + Dockerfile + compose + README).
  • Update an existing template to a newer Pathway xpack version.
  • Refactor a legacy template to App-class.
  • Fix a connector configuration.

For substantial work (e.g., wiring multi-machine clustering into the templates), open an issue first.

9. Integration Points

Inbound integrations (data sources)

Integration Class Auth Used by templates Notes
Local filesystem pw.io.fs.read none every template Default; watches the path live
Google Drive pw.io.gdrive.read service-account JSON drive_alert (live), every RAG template (commented example) refresh_interval: 30 default; can filter by file_name_pattern:
Microsoft SharePoint pw.xpacks.connectors.sharepoint.read app-only cert + thumbprint every RAG template (commented example) Requires Azure AD app registration with Sites.Read.All
Amazon S3 pw.io.s3.read AWS credentials none directly (mentioned in drive_alert comments) Drop into $sources:
HTTP REST pw.io.http.rest_connector none drive_alert, unstructured_to_sql_on_the_fly Also acts as input — this is how queries arrive

Outbound integrations (sinks and services)

Integration Class Auth Used by Notes
Slack pw.io.slack.send_alerts bot token drive_alert Posts to a single channel
Postgres pw.io.postgres.write user/password unstructured_to_sql_on_the_fly Writes typed columns from the schema
CSV pw.io.csv.write none unstructured_to_sql_on_the_fly Mirrors output to disk
OpenAI Chat Completions OpenAIChat (xpacks) API key most templates Configurable retry + cache
LiteLLM (Ollama, Anthropic, Bedrock, Vertex, …) LiteLLMChat varies by provider private_rag One class, dozens of providers
OpenAI Embeddings OpenAIEmbedder API key most templates text-embedding-3-small default
SentenceTransformer SentenceTransformerEmbedder none (local) private_rag, document_indexing, document_store_mcp_server First run downloads the model
MCP-over-HTTP PathwayMcp none in template document_store_mcp_server Streamable HTTP transport

Client-side integration

The Streamlit UIs use pathway.xpacks.llm.question_answering.RAGClient — a Python client pointed at the Pathway REST API. You can use the same client from any Python application:

from pathway.xpacks.llm.question_answering import RAGClient

conn = RAGClient(url="http://your-pathway-host:8000")
response = conn.answer("Your question here", return_context_docs=True)
print(response["response"])
print(response["context_docs"])

# Other methods used by the shipped UIs
docs = conn.list_documents(keys=[])  # for a sidebar/admin view

For non-Python clients, hit the REST endpoints directly (see section 4.3 of the User Guide).

MCP integration

document_store_mcp_server exposes the live Pathway document store to MCP-aware clients via streamable HTTP. Configuration in app.yaml:

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

To consume from Claude Desktop or another MCP client, register http://your-host:8068 as an MCP server. The server exposes the document store as MCP tools (retrieve, list, etc.) — your client sees them as native tool calls.

LangGraph integration (cookbook only)

cookbooks/self-rag-agents/ demonstrates wiring Pathway's live DocumentStore into a LangGraph state machine. The notebooks pin LangGraph and pathway[all] but the cookbooks are not deployable — they are tutorials. To productionise, port the LangGraph state machine into a templates/<your_name>/app.py that spins up its own Pathway document store, exposes a REST endpoint, and runs the graph as a UDF over the query stream.

Tip

The cookbook pattern is the bridge for adding agentic behaviour to a Pathway-powered RAG: keep the live document store on Pathway, run the agent reasoning in LangGraph, expose the combined service as REST or MCP. The catalogue does not yet ship this combined shape as a deployable template — building it is a natural extension if you need multi-step agentic RAG.