Content
# touchdesigner-wiki-rag
Search the entire TouchDesigner documentation from your AI agent — with real
citations.
This tool builds a local search index over the official TouchDesigner wiki
([docs.derivative.ca](https://docs.derivative.ca/), ~2,270 articles) and
exposes it to AI agents over [MCP](https://modelcontextprotocol.io/) (Claude
Code, Claude Desktop, Cursor, and any other MCP client). Every answer carries
a `Page § Section` citation with a clickable URL, so you can verify what the
agent tells you instead of trusting a model's memory of TouchDesigner — which
is exactly how you end up with hallucinated parameter names.
**You build the index yourself, on your machine, from the live wiki.** This
repo ships no documentation content — the docs belong to
[Derivative](https://derivative.ca/) and are fetched by each user directly
from docs.derivative.ca. The build takes ~45 minutes of unattended fetching
(politely rate-limited) plus a few minutes of indexing, and can run entirely
free with no API keys.
## Why not just let the agent browse the wiki?
Three reasons this beats live browsing: hybrid retrieval (exact-term BM25 +
semantic vectors, fused and reranked) finds sections a keyword search misses;
the index knows TouchDesigner's operator-family trap — **Noise TOP, Noise
CHOP, Noise SOP, and Noise POP are four different operators**, and results
warn when same-named siblings exist in other families; and answers are
grounded in retrieved text with citations rather than model memory.
## Requirements
- Python 3.12+ and [uv](https://docs.astral.sh/uv/)
- ~1.5 GB disk for the index
- **No API keys required** for the default fully-local setup
- Optional (better retrieval): an [OpenAI](https://platform.openai.com/) key
for stronger embeddings (about $1–2 one-time build cost) and HyDE query
expansion; a [Cohere](https://dashboard.cohere.com/) key for stronger
reranking (about $2 per 1,000 searches; free trial keys work)
## Quickstart
```bash
git clone https://github.com/johnnyvincentvitale/touchdesigner-wiki-rag
cd touchdesigner-wiki-rag
uv sync
cp .env.example .env # optional: add API keys; works fine empty
uv run python fetch.py # ~45 min, fetches the wiki via its public API
uv run python ingest.py --rebuild # chunks + embeds + indexes
# try it from the terminal
uv run python query.py "how do I add noise to a texture"
```
The embedding backend is chosen at build time — local
[bge-base](https://huggingface.co/BAAI/bge-base-en-v1.5) (free, no account)
by default, OpenAI `text-embedding-3-large` if `OPENAI_API_KEY` is set — and
stamped into the index so queries always use the matching model. See
`.env.example` for the details.
### Already have the docs? Skip the fetch
TouchDesigner installs bundle an **Offline Help** mirror of the wiki. If you
have it, import it instead of fetching (seconds instead of ~45 minutes):
```bash
# Windows (default install location):
uv run python fetch.py --offline-help "C:/Program Files/Derivative/TouchDesigner/Samples/Learn/OfflineHelp/https.docs.derivative.ca"
# then optionally pull only the pages edited since your mirror was generated:
uv run python fetch.py --update
uv run python ingest.py --rebuild
```
Notes: the mirror snapshots the wiki at your TouchDesigner release date —
`--update` tops it up from the live API. Recent **macOS** builds ship the
Offline Help folder empty (a known TouchDesigner issue), so Mac users should
use the plain API fetch.
## Configuration — what goes in `.env`
**Nothing is required.** With an empty (or absent) `.env`, everything runs on
free local models. Keys only upgrade individual stages:
| Variable | What it enables | Without it |
|---|---|---|
| `OPENAI_API_KEY` | `text-embedding-3-large` embeddings (stronger dense retrieval) + HyDE query expansion | local bge embeddings; HyDE disabled |
| `COHERE_API_KEY` | Cohere Rerank scoring | local ms-marco cross-encoder reranks |
| `EMBED_BACKEND` | force `local` or `openai` embeddings explicitly | inferred: `openai` if a key is set, else `local` |
| `EMBED_MODEL` | different OpenAI embedding model (e.g. `-3-small`, cheaper/weaker) | `text-embedding-3-large` |
| `COHERE_RERANK_MODEL` | different Cohere rerank model | `rerank-v3.5` |
| `HYDE_MODEL` | different HyDE model | `gpt-4o-mini` |
| `OPENAI_BASE_URL` | route OpenAI-client calls to any compatible server (see below) | api.openai.com |
Two behaviors worth knowing: the embedding backend is **stamped into the
index at build time** and queries auto-detect it, so you can't accidentally
query with the wrong model — but switching backends means rebuilding
(`ingest.py --rebuild`). And every API stage **degrades visibly, never
silently**: no Cohere key falls back to the local reranker, an unreachable
embedding API drops that search to keyword-only and says so in the response.
### Going fully local (including HyDE)
The default keyless setup is already local except HyDE, which simply disables
itself. To run HyDE on a local model, point the OpenAI client at any
OpenAI-compatible server — [Ollama](https://ollama.com/) or LM Studio:
```bash
# .env
OPENAI_API_KEY=anything-nonempty # satisfies the client; never sent anywhere real
OPENAI_BASE_URL=http://localhost:11434/v1
HYDE_MODEL=llama3.2 # any model you've pulled locally
EMBED_BACKEND=local # keeps embeddings on bge — required!
```
`EMBED_BACKEND=local` is load-bearing here: without it, setting
`OPENAI_API_KEY` flips embeddings to the OpenAI backend, which would then be
aimed at your local server expecting a model it doesn't serve. A small local
HyDE model is rougher than gpt-4o-mini, but HyDE only fires on low-scoring
searches and only keeps its result when it scores better — so the downside is
bounded. (This recipe follows the OpenAI SDK's documented `OPENAI_BASE_URL`
behavior but hasn't been exercised against a live Ollama by the author —
issue reports welcome.)
"Local" models still download once from Hugging Face on first use (no
account needed) and are cached after that; the wiki fetch itself needs the
network once. After the index is built, the keyless setup searches fully
offline.
## Hook it up to your agent
**Claude Code:**
```bash
claude mcp add --scope user touchdesigner-manual -- uv run --directory /ABSOLUTE/PATH/TO/touchdesigner-wiki-rag python server.py
```
**Any other MCP client** — stdio server config:
```json
{
"mcpServers": {
"touchdesigner-manual": {
"command": "uv",
"args": ["run", "--directory", "/ABSOLUTE/PATH/TO/touchdesigner-wiki-rag", "python", "server.py"]
}
}
}
```
### What the agent gets
| Tier | Tool | Returns |
|---|---|---|
| L0 | `browse_manual(query, family, limit)` | section map — cheap orientation, discovers the wiki's own vocabulary |
| L1 | `search_manual(query, family, limit)` | reranked passages with `Page § Section` + URL citations |
| L2 | `read_page(title, section)` | full page as markdown |
Plus resources: `manual://index` (corpus stats and freshness) and
`manual://operators/{family}` (operator roster per family).
`family` accepts TOP, CHOP, SOP, DAT, MAT, COMP, or POP to scope a search to
one operator family.
## How retrieval works
1. **BM25** over SQLite FTS5, Porter-stemmed — exact operator and parameter
names
2. **Dense vector search** over Chroma — paraphrased and conceptual questions
3. **Reciprocal Rank Fusion** combines both rankings
4. **Rerank** — Cohere Rerank if a key is configured, local ms-marco
cross-encoder otherwise (automatic fallback, works offline)
5. **Adaptive HyDE** — if the top rerank score is low (a reliable "retrieval
missed" signal), an LLM writes a hypothetical answer in the wiki's
vocabulary and retrieval re-runs on that. Fires only when needed, only
with an OpenAI key, and only keeps the result if it scores better.
Two guardrails ship in every response: a **family warning** when a result's
operator has same-named siblings in other families, and a **low-confidence
warning** when scores suggest retrieval missed — so an agent reports "not
found" instead of inventing an answer, and never concludes a feature doesn't
exist just because retrieval came back empty.
## Keeping the index fresh
The wiki changes continuously. Responses warn when pages have been edited
since your index was built (checked against the wiki's `recentchanges` feed,
cached hourly, skipped silently when offline). To update:
```bash
uv run python fetch.py --update # re-fetches only pages edited since last fetch
uv run python ingest.py --rebuild # rebuilds the index
```
## Known issues
- Every operator page carries an identical "Common Operator Info Channels"
boilerplate section; those chunks can pollute results on hard queries.
- The `--offline-help` importer is tested against synthetic MediaWiki static
pages, not yet against a real TouchDesigner install's mirror (the author's
macOS build ships it empty). If the import misses pages on your install,
please open an issue with a sample HTML file.
- Four title-variant pages (e.g. `Palette:webRTC Ext` vs `Palette:WebRTC
Ext`) collide in the page cache on case-insensitive filesystems; their
partner pages are indexed, so coverage impact is negligible.
- Rerank/HyDE score thresholds were calibrated on a small held-out query set;
they're adjustable in `retrieval.py` (`THRESHOLDS`).
- `ingest.py` is full-rebuild-only; with OpenAI embeddings that re-costs
~$1–2 per rebuild. The local backend rebuilds for free.
## Content ownership
All documentation content belongs to Derivative. This repository contains no
wiki content — only code that fetches it from the public API into a private,
local index, the same way a browser or TouchDesigner's built-in help fetches
it. Please keep the polite rate limits in `fetch.py` intact.
## License
MIT — see [LICENSE](LICENSE). Applies to the code in this repository only,
not to TouchDesigner or its documentation.