Skip to main content
Glama
johnnyvincentvitale

touchdesigner-manual

README.md
# 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

A4.4/5.0

Scored across 3 tools

Disambiguation4/5

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.

Naming Consistency4/5

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.

Tool Count4/5

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.

Completeness3/5

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.

Maintenance

ActivityStale
ResponsivenessNo issues