TUCAN RAG Tools — Tool Discovery and Filtering
Audiences: developer, internal
Tool-RAG is human-discovery infrastructure — a Qdrant collection of tool embeddings used by the studio / inventory tooling to find a tool by intent — NOT a per-turn semantic filter at LLM call time. Production agents receive a static, category-filtered tool list at
new Agent({...})construction time.
Business Purpose
There are ~350 createTucanTool definitions across ~35 categories in
ddx-api. A single agent (e.g. the clinical specialist) is bound to
dozens of them. Two engineering problems fall out:
- For humans authoring agents and writing prompts: "is there
already a tool for X?" must be answerable in seconds. Tool-RAG
provides a
searchRelevantTools(prompt, opts)API that returns the top-K most semantically relevant tool IDs, with scores, against a plain-language query. - For per-agent prompt-shaping: even with a static category
allow-list, the prompt that tells the LLM "here are your tools and
when to use each" needs to be ordered by descending relevance to the
current intent. The
tool-rag-prompt.builderconsumes Tool-RAG search results to rank the bound tools in the agent system prompt.
Tool-RAG is not the dispatcher's tool selector. The dispatcher's
selector is deterministic: it reads agentType from the session,
fetches the registry's category allow-list, and binds Mastra tools at
construction time. Re-running a vector search per turn would add latency
without changing the bound set — see Operational Notes for the dormant
"SkillSearchProcessor" pitfall.
Audiences
- Investor: Tool-RAG keeps the catalog scalable. Doubling the catalog does not double the LLM prompt size — irrelevant tools are filtered out by category at agent construction and ranked by embedding similarity inside the bound set.
- Developer / partner: Adding a tool with good
ragDescriptionandragKeywordsmakes it discoverable to TUCAN's authoring + studio tooling automatically — there is no manual "register me in the prompt" step. The Qdrant collection lives attucan_tool_rag_<sha-prefix>and is rebuilt incrementally when tool metadata changes. - Internal (ops / support): A regression test asserts the embedding
text composition is
{id}\n{category}\n{ragDescription}\nKeywords:{keywords}([[ddx-mastra-specialist_memory.md]] commit f6eef8a18). Any change there is a breaking-bit — caches and embeddings must be invalidated.
Architecture
┌──────────────────────────────────────────────────────────────┐
│ Tool authoring │
│ createTucanTool({ │
│ metadata: { │
│ ragDescription: 'USE WHEN ... DO NOT USE WHEN ...', │
│ ragKeywords: ['en kw1', 'de kw1', ...], │
│ } │
│ }) │
└──────────────────────────────────────────────────────────────┘
│
▼ (onModuleInit)
┌──────────────────────────────────────────────────────────────┐
│ ToolRagService │
│ · onModuleInit → ensureCollection (3 retries: 500/1500/4500ms)│
│ · indexToolsIfChanged(tools[]) — content-hash skip │
│ · for each changed tool: │
│ text = buildToolEmbeddingText(tool) │
│ vec = EmbeddingService.generateEmbedding(text) │
│ pid = toolIdToPointId(slug) // uuid v5 │
│ upsertPoint(collection, pid, vec, payload) │
└──────────────────────────────────────────────────────────────┘
│
▼ (consumer calls)
┌──────────────────────────────────────────────────────────────┐
│ searchRelevantTools(prompt, { limit, scoreThreshold, ... }) │
│ · embed(prompt) → query vector │
│ · qdrantRepo.search(collection, queryVec, opts) │
│ · returns [{ toolId, score }, ...] sorted desc │
└──────────────────────────────────────────────────────────────┘
│
▼ (build-time, NOT per-turn)
┌──────────────────────────────────────────────────────────────┐
│ tool-rag-prompt.builder.ts │
│ · re-orders the agent's BOUND tool list by tool-RAG score │
│ · clinical agent stays clinical — category filter wins │
│ · ordering is for prompt readability, not access control │
└──────────────────────────────────────────────────────────────┘
buildToolEmbeddingText is the contract surface that downstream
embeddings depend on. It is snapshot-tested
(tool-rag/__tests__/) and must not change without a re-index.
Tech Stack & Choices
| Component | Choice | Why |
|---|---|---|
| Vector store | Qdrant at port 15333 (shared with project assets) | Already an infrastructure dependency; HTTP API; cosine + payload filter. |
| Embedding model | dudoxx-embed via https://embedder.home.dudoxx.com/v1/embeddings, dim 2560 | Same production CUDA endpoint that powers the DDX RAG sidecar; one embedding space across docs, code, tools. |
| Point ID | uuidv5(toolSlug, TOOL_RAG_NAMESPACE) (tool-rag.service.ts:30) | Idempotent upserts — re-indexing the same slug overwrites, never produces orphans. |
| Collection name | tucan_tool_rag_<sha> (versioned by embedding text hash) | Schema breaking changes get a new collection; rollback = config flip. |
| Change detection | content-hash on the composed embedding text | "indexToolsIfChanged" is a no-op when nothing changed; cheap restart. |
| Module init | OnModuleInit triggers ensureCollection with retry | Don't fail boot if Qdrant is briefly unavailable; defer to first index call. |
| Sibling-disambiguation | anti-synonym keywords ("NOT encounter", "NOT fhir-encounter") + canonical TOOL_ID suffix | Pitfall f6eef8a18 — RAG was surfacing Visit tools for FHIR Encounter queries until anti-synonyms were added. |
| Static per-agent binding | AgentRegistryService binds tools by category, not by tool-RAG | Removes the per-turn embedding call from the hot path; tool access control is deterministic, not statistical. |
| Multi-entity intake heuristic | MULTI_ENTITY_PATTERNS regex set + detectMultiEntityIntake | When the prompt mentions ≥2 entities ("patient and visit and intake"), Tool-RAG widens its return set so the planner can fan out. |
Alternatives rejected:
- Per-turn semantic tool reduction (the "SkillSearchProcessor"
pattern) — wired but DORMANT in the registry config
([[ddx-mastra-specialist_memory.md]]). Switching it on would mean a
Qdrant round-trip per turn for marginal context savings on a
category-filtered set of ~30 tools, and the LLM cost saving rounds
to zero against
dudoxx-gemmainternal pricing. - Local FAISS index in-process — adds a deploy artifact and a refresh story. Qdrant is already deployed.
- No embedding at all (keyword search) — multi-lingual
(German-first) clinical vocabulary is sibling-rich; keyword search
picks
VisitforEncounterqueries unless we maintain a giant synonym table.
Data Flow
Business outcome: A developer types "search for a tool to calculate BMI" into the TUCAN studio. The studio returns
utility/calculate-bmiat the top, scored 0.84.
Technical mechanism:
- Studio calls
ToolRagService.searchRelevantTools('search for a tool to calculate BMI', { limit: 5 })(ddx-api/src/ai/tucan/tool-rag/tool-rag.service.ts:458).- Service calls
EmbeddingService.generateEmbedding(prompt)→ HTTPS toembedder.home.dudoxx.com/v1/embeddings→dudoxx-embedmodel, dim 2560.- Service calls
QdrantRepository.search(collection, queryVector, { limit, scoreThreshold })(ddx-api/src/ai/tucan/tool-rag/qdrant.repository.ts:183).- Qdrant returns ordered
[{id, score, payload}, ...].- Service converts point IDs back to tool slugs via the
payload.toolIdfield.- Studio renders the ranked list with rag-description preview.
Build-time rerank for the clinical agent:
tool-rag-prompt.builderis called with the bound tool list (clinical agent: medications + prescriptions + clinical-notes + fhir + ui).- For each tool the builder calls
searchRelevantTools(tool's intent label)and orders the prompt section by descending score.- The result is part of the agent's system prompt — produced once at agent registration, not per turn.
Implicated Code
ddx-api/src/ai/tucan/tool-rag/tool-rag.service.ts:26—TOOL_RAG_NAMESPACEUUID v5 namespace for stable point IDs.ddx-api/src/ai/tucan/tool-rag/tool-rag.service.ts:30—toolIdToPointId(toolSlug)— uuid v5 mapping.ddx-api/src/ai/tucan/tool-rag/tool-rag.service.ts:80—MULTI_ENTITY_PATTERNSregex set for fan-out heuristic.ddx-api/src/ai/tucan/tool-rag/tool-rag.service.ts:100—detectMultiEntityIntake(prompt)returns count.ddx-api/src/ai/tucan/tool-rag/tool-rag.service.ts:124—buildToolEmbeddingText(tool)— frozen contract (snapshot test).ddx-api/src/ai/tucan/tool-rag/tool-rag.service.ts:146— classToolRagService implements OnModuleInit.ddx-api/src/ai/tucan/tool-rag/tool-rag.service.ts:193—onModuleInit()with 3 retries (500/1500/4500ms).ddx-api/src/ai/tucan/tool-rag/tool-rag.service.ts:243—indexToolsIfChanged(tools, opts)— content-hash skip path.ddx-api/src/ai/tucan/tool-rag/tool-rag.service.ts:276—raw = buildToolEmbeddingText(t)— composition site.ddx-api/src/ai/tucan/tool-rag/tool-rag.service.ts:408—qdrantRepo.upsertPoints(collectionName, points)— batch upsert.ddx-api/src/ai/tucan/tool-rag/tool-rag.service.ts:458—searchRelevantTools(prompt, options).ddx-api/src/ai/tucan/tool-rag/tool-rag.service.ts:537— multi-entity widening branch.ddx-api/src/ai/tucan/tool-rag/qdrant.repository.ts:44— classQdrantRepository implements IVectorStore.ddx-api/src/ai/tucan/tool-rag/qdrant.repository.ts:111—ensureCollection(name, vectorSize).ddx-api/src/ai/tucan/tool-rag/qdrant.repository.ts:164—upsertPoints(...).ddx-api/src/ai/tucan/tool-rag/qdrant.repository.ts:183—search(name, vector, options).ddx-api/src/ai/tucan/tool-rag/embedding.service.ts:35—EmbeddingService implements IEmbeddingService.ddx-api/src/ai/tucan/tool-rag/embedding.service.ts:68—generateEmbeddings(texts: string[]).ddx-api/src/ai/tucan/tool-rag/embedding.service.ts:103—generateEmbedding(text: string).ddx-api/src/ai/tucan/registry/tool-rag-prompt.builder.ts— build-time per-agent reranking consumer.
Operational Notes
- Tool-RAG is build-time, NOT per-turn. Project memory invariant ([[ddx-mastra-specialist_memory.md]], commit f6eef8a18): "agents receive static category-filtered lists at Agent() construction — NO per-turn semantic reduction." Do not wire Tool-RAG into the dispatch hot path.
- SkillSearchProcessor / ToolSearchProcessor is NOT wired (memory shard, f6eef8a18). The config exists but is dormant; do not re-document it as "active" without re-verifying.
- Embedding text composition is frozen.
buildToolEmbeddingTextreturns{id}\n{category}\n{ragDescription}\nKeywords:{keywords}. Any change requires a full re-index — there is a snapshot test that will fail loudly. - Sibling disambiguation needs anti-synonyms. Two tools with
near-identical
ragDescription+ragKeywordsproduce near-identical embeddings; Tool-RAG picks the wrong one. Add "NOT encounter" / "NOT fhir-encounter" + a canonical TOOL_ID suffix. ensureCollectionretries 3× then no-ops. If Qdrant is down at boot, the service logs a warning and continues — the collection will be created on the firstindexToolsIfChangedcall. Do not treat a startup log line as fatal.uuidv5(slug, NAMESPACE)is the only legitimate point ID generator. Inserting points with random UUIDs creates orphans on re-index.- Multi-entity intake widens the return set. A prompt mentioning
≥2 entities ("patient and intake form") triggers
detectMultiEntityIntake(prompt) >= 2(tool-rag.service.ts:537), used by the orchestrator's planner to fan out across more tools. - Per-tool authoring rule (from mastra review checklist): every
tool MUST have
metadata.ragDescriptionANDmetadata.ragKeywordsin EN + DE minimum. Tools missing these are surfaced as Critical by the review agent and degrade Tool-RAG quality. - Document/knowledge RAG is a different service. Tool-RAG embeds
tool metadata; the user-facing document RAG (clinic uploads,
patient documents, knowledge base) lives in
ddx-api/src/ai/tucan/tools/rag/+tools/knowledge/+tools/deep-search/and is covered by [[rag-system-indexation]].
Related Topics
- [[tucan-assistant]] — dispatcher orchestration entry.
- [[ai-medical-tools]] — the tool catalog Tool-RAG indexes.
- [[tucan-intent-filtering]] — the intent classifier that runs BEFORE per-agent binding (and is the production filter for agent + tool scope, not Tool-RAG).
- [[tucan-dispatcher]] — confirms the no-per-turn-RAG contract.
- [[agent-templates]] — agent factory whose system prompt consumes
tool-rag-prompt.builder. - [[rag-system-indexation]] — the user-facing document/knowledge RAG pipeline (different Qdrant collections, different lifecycle).
- [[sse-event-engine]] (W1) — emits
metrics:toolragevents that surface tool-rag activity in the SSE stream. docs/claude-context/CLAUDE_TUCAN_API_INJECTION.md— tool-RAG filtering pattern reference.