touchdesigner-manual
# 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.
TDQS
Scored across 3 tools
The three tools are mostly distinct: browse_manual for orientation/discovery, search_manual for finding passages, and read_page for reading full content. The browse_manual and search_manual could potentially overlap when searching for a specific topic (both filter by family and query), but the descriptions clearly differentiate them by cost tier and use case, so an agent can usually tell them apart.
The tool names follow a consistent verb_noun pattern (browse_manual, search_manual, read_page), all snake_case. Slight inconsistency in that browse_manual and search_manual both target 'manual' while read_page targets 'page', but the pattern is predictable and readable overall.
Three tools is a reasonable count for a documentation/manual retrieval server. It's on the lean side but each tool serves a distinct retrieval stage (discover, search, read). Given the narrow scope of a documentation lookup server, three well-chosen tools feel appropriate, though it borders on thin.
The core browse-search-read workflow is covered and represents the essential operations for a wiki documentation server. However, there are minor gaps: no way to list all pages in a family without a query, no direct tool for navigating to a specific page by title from scratch (read_page requires knowing the exact title), and no distinction between getting a section vs. full page could be improved. These are workable gaps but notable.