Produce a reusable template repo that adds a **local Wikipedia search source** to the LangGraph “Open Deep Research” stack and ships runnable Kubernetes resources. Generate files with concrete content, parameterized via `{{variables}}`. Follow the spec exactly.
## Goal
Create a minimal, reproducible template a downstream user can clone, set a few vars, and deploy:
- Runs `open_deep_research` API with a Wikipedia search backend.
- Hosts a local Kiwix mirror of Wikipedia ZIM files.
- Performs semantic search with `txtai` and returns Markdown excerpts with citations to local Kiwix URLs.
- Selects the Wikipedia tool via `search_api: WIKIPEDIA` without changing graph logic.
## Output format
Do not add commentary outside files. Include all files listed below.
## Repository layout
```
open-deep-research-wiki-template/
README.md
Makefile
k8s/
resources.yaml
kiwix.yaml
docker/
Dockerfile
entrypoint.sh
run.sh
run_fastapi.sh
pyproject.toml
uv.lock # generate a minimal lock; pin txtai + torch cpu
langgraph.json # pass-through if needed
src/open_deep_research/
__init__.py
deep_researcher.py # integrates unchanged graph, no business-logic changes
utils.py # adds wiki_search + get_search_tool wiring
wikipedia.py # local Kiwix + txtai retrieval
configuration.py # SearchAPI enum includes WIKIPEDIA
prompts.py # unchanged
state.py # unchanged
configuration_example.env
```
## Parameterization
Use Jinja-style placeholders. Do not hardcode values.
- `{{NAMESPACE}}` default: `open-deep-research`
- `{{DOMAIN}}` default: `<domain>`
- `{{WIKI_HOST}}` default: `wiki.{{DOMAIN}}`
- `{{KIWIX_BASE}}` default: `https://{{WIKI_HOST}}`
- `{{ZIM_NAME}}` default: `wikipedia_en_all_maxi_2025-08`
- `{{API_IMAGE}}` default: `localhost:32000/mm_open_deep_research:latest`
- `{{TLS_SECRET}}` default: `<domain>-wildcard-tls`
- `{{REPLICAS}}` default: `1`
- `{{API_PORT}}` default: `8123`
- `{{CPU_LIMIT}}` default: `4`
- `{{MEM_LIMIT}}` default: `56Gi`
- `{{ZIM_HOSTPATH}}` default: `/mnt/data/zim`
## File requirements
### 1) `k8s/resources.yaml`
- Deployment + Service for API.
- Env for threading control: `FAISS_NUM_THREADS=1`, `OMP_NUM_THREADS=1`, `OPENBLAS_NUM_THREADS=1`, `MKL_NUM_THREADS=1`, `PYTHONFAULTHANDLER=1`.
- `SERVER_MODE=fastapi`, `PORT={{API_PORT}}`.
- Resource requests: cpu `100m`, mem `2Gi`. Limits from params.
- Readiness/Liveness GET `/`.
- Service on port 80 -> `{{API_PORT}}`.
### 2) `k8s/kiwix.yaml`
- Deployment, Service, Ingress for `kiwix-serve`.
- Container `ghcr.io/kiwix/kiwix-serve:latest` with args `kiwix-serve -p 8080 /data/zim/*.zim`.
- `hostPath` volume at `{{ZIM_HOSTPATH}}` read-only.
- Ingress `{{WIKI_HOST}}` with TLS `{{TLS_SECRET}}`.
### 3) `docker/Dockerfile`
- Base: `python:3.11-slim`.
- Install build deps: `ca-certificates curl git build-essential libgomp1`.
- Install `uv` via official script.
- Set caches to `/opt/cache` (HF, transformers, sentence-transformers, torch, pip).
- Copy `pyproject.toml`, `uv.lock`, `src`, `server` (if present), scripts.
- `uv sync` then export venv to PATH.
- Set `XDG_CACHE_HOME` and related envs.
- Prewarm by invoking `/app/src/open_deep_research/wikipedia.py` to trigger model pulls.
- `ENTRYPOINT` to `entrypoint.sh`.
### 4) `docker/entrypoint.sh`
- `SERVER_MODE` switch: `fastapi` -> `run_fastapi.sh`, `cli` -> `run.sh`.
### 5) `pyproject.toml` and `uv.lock`
- Dependencies include:
- `langgraph`, `langchain-*`, `openai`, `exa-py`, `tavily-python`, `pandas`, `fastapi`, `uvicorn`, `langserve`, `rich`, `httpx`, `beautifulsoup4==4.13.3`, `txtai==9.0.0`, `torch>=2.5,<3`.
- Configure CPU-only torch source:
```
[tool.uv.sources]
torch = [{ index = "pytorch-cpu" }]
[[tool.uv.index]]
name = "pytorch-cpu"
url = "https://download.pytorch.org/whl/cpu"
explicit = true
```
- Ensure `txtai` pulls `neuml/txtai-wikipedia` at runtime.
### 6) `src/open_deep_research/wikipedia.py`
Implement:
- `KIWIX_BASE=os.getenv("KIWIX_BASE","{{KIWIX_BASE}}")`
- `ZIM_NAME=os.getenv("ZIM_NAME","{{ZIM_NAME}}")`
- Force CPU: `os.environ["FAISS_DISABLE_GPU"]="1"`.
- `Embeddings().load(provider="huggingface-hub", container="neuml/txtai-wikipedia")` behind an `@lru_cache` and `threading.RLock`.
- `wiki_query(query, percentile_threshold=0.99, max_articles=5)` that:
- Executes `txtai` SQL `similar('{query}')` with percentile filter.
- For each hit: resolve `title`, fetch HTML from Kiwix (`/raw/...` then XML search fallback), strip to Markdown, drop references, collapse whitespace.
- Return dicts: `{title,id,score,percentile,kiwix_url,markdown}`.
### 7) `src/open_deep_research/utils.py`
Add:
- `wiki_search` tool with signature `queries: List[str]` that calls `wiki_search_async`, flattens results, and formats `Search results:\n\n--- SOURCE i: <title> ---\nURL: <kiwix_url>\n\nSUMMARY:\n<markdown>\n\n---------`.
- `get_search_tool` to map `SearchAPI.WIKIPEDIA` to `wiki_search` and expose it as `name="web_search"` so the rest of the graph is unchanged.
### 8) `src/open_deep_research/deep_researcher.py`
- Keep the supervisor/researcher/compression logic as-is.
- Ensure `get_all_tools(config)` path picks up `SearchAPI.WIKIPEDIA` via `utils.get_search_tool`.
- No other semantic changes.
### 9) `src/open_deep_research/configuration.py`
- Ensure `SearchAPI` enum includes `WIKIPEDIA`.
- Default remains selectable via `config.configurable.search_api`.
### 10) `Makefile`
Targets:
- `build`: build API image tag `{{API_IMAGE}}`.
- `push`: push image.
- `deploy-kiwix`: apply `k8s/kiwix.yaml`.
- `deploy-api`: apply `k8s/resources.yaml`.
- `undeploy`: delete both.
- `test-wiki`: curl `https://{{WIKI_HOST}}/` readiness.
- `invoke`: sample JSON call with `search_api: wikipedia`.
### 11) `src/open_deep_research/configuration_example.env`
Document:
```
KIWIX_BASE={{KIWIX_BASE}}
ZIM_NAME={{ZIM_NAME}}
FAISS_DISABLE_GPU=1
SERVER_MODE=fastapi
PORT={{API_PORT}}
```
## Quality gates
Implement and satisfy these checks:
1. **Static**: repository `pip check` inside the built image must pass.
2. **Runtime**: container starts and `GET /` responds 200.
3. **Kiwix**: readiness on `/` returns 200 after ZIM mount.
4. **Search**: calling the graph with `search_api=wikipedia` returns at least one `SOURCE` block with `URL: https://{{WIKI_HOST}}/content/{{ZIM_NAME}}/A/...`.
5. **Isolation**: no external HTTP calls are required during research if Kiwix and ZIM exist.
6. **Idempotency**: `kubectl apply` twice yields no changes.
## Constraints
- Use only the libraries listed in `pyproject.toml`.
- Keep `txtai` CPU-only; do not enable GPU.
- Keep tool name exposed to the graph as `web_search` to avoid changing planner logic.
- Keep code Python 3.11 compatible.
## Done when
- All files are emitted with full contents using the delimiter.
- Placeholders are present where specified.
- A user can fill variables, apply manifests, build image, and run a local Wikipedia-backed deep-research workflow end to end.