RAG Tool Curator docs v0.1.0
Guides

Integration Guide

Wiring both plug-ins into an existing tool-calling system. Written generically — neither package depends on a transport, orchestration framework, or LLM provider.

The bundled example/orchestrator/agent_orchestrator.py (FastMCP over stdio, Gemini) is one concrete instance of the pattern below; the same pattern applies unchanged to any other stack.

Orchestration layer, tool curating layer, and execution layer of the reference deployment
The reference deployment's layering. Predates the normalizer — in a non-English-aware deployment, a translation stage sits between the Orchestrator and the Tool Curating Layer.

The pattern

Once per session (when the tool pool for a user/persona becomes known):
    1. Apply your existing access-control / entitlement filtering
    2. curator.build_index(filtered_tools, extra_context=synonyms)
    3. [optional] instantiate a normalizer if non-English input is expected

Once per message (before the LLM is called):
    0. [optional] english_query, language = normalizer.normalize(user_message)
                  — pure-ASCII English input returns immediately at zero cost
    1. selected = curator.select(english_query, filtered_tools)
    2. [optional] selected = apply_dependency_rules(selected, filtered_tools, rules)
    3. Build LLM tool declarations from `selected`, not the full pool
    4. Proceed with the LLM call exactly as before

Everything downstream of step 3 — tool execution, result handling, streaming, persistence — is unaffected and needs no changes. Neither plug-in has any role in actually invoking tools.

Step 1 — install

pip install -e ".[normalizer]"    # from the repo root, or once published: pip install "rag-tool-curator[normalizer]"

Or copy tool_curator/ and/or multilingual_normalizer/ directly into your project — see Installation. Neither folder depends on anything else in this repository; example/, accuracy_boosters/, and this docs site are not required at runtime.

Step 2 — session-scoped index construction

Locate the point in your existing code where the tool pool for a user/persona becomes known — typically right after your existing tool-listing step and after any role/entitlement filtering. Build the curator’s index there, and keep the instance for the session’s lifetime:

from tool_curator import get_curator, load_tool_context

curator = get_curator(mode="rag", top_k=20, index_dir="/var/cache/tool_curator")

tools_with_desc = [(tool.name, tool.description or "") for tool in authorized_tools]
synonyms = load_tool_context("/path/to/domain_synonyms.json")
curator.build_index(tools_with_desc, extra_context=synonyms)

session.curator          = curator
session.authorized_tools = authorized_tools    # full tool objects, for dispatch
session.tools_with_desc  = tools_with_desc     # (name, description) pairs, for select()

authorized_tools must already reflect whatever access control your system enforces — the curator performs no authorization of its own and will never select a tool outside the pool it’s given.

If your existing code currently builds the full set of LLM tool declarations at this same point, move that step to Step 3 — the declaration set now needs rebuilding per message, not once per session.

Step 3 — per-message selection

Sequence diagram: user query, role filter, curator select, Gemini call, tool call routed to execution
One query's path through role filtering and curation. Insert the normalizer's normalize() call between "submit query" and select(query, role_visible_tools) for non-English support.
# Stage 0 (optional): non-English input first.
english_query, user_language = session.normalizer.normalize(user_message)

# Stage 1: narrow the tool pool for this query.
selected_pairs = session.curator.select(english_query, session.tools_with_desc)

# Stage 2 (optional): force-include structural companions.
selected_pairs = apply_dependency_rules(selected_pairs, session.tools_with_desc, session.dependency_rules)

selected_names    = {name for name, _ in selected_pairs}
small_tool_set    = [t for t in session.authorized_tools if t.name in selected_names]
tool_declarations = build_llm_tool_declarations(small_tool_set)
# proceed with the existing LLM call, using `tool_declarations`

build_llm_tool_declarations represents whatever function already converts tool objects into your LLM provider’s format — no change to its internals should be necessary, beyond confirming it has no implicit assumption that its input is always the full, fixed-size tool pool (e.g. a cache keyed on total tool count), since the input size now varies per message.

Step 4 — optional hardening

Sequence diagram: name guard validation, action confirmation gate, then tool execution
Tool-name correction and the action-confirmation gate, sitting between tool selection and actual execution.

Two further layers exist in the reference implementation, recommended once the core integration above is verified working, though neither is required for either plug-in to function:

Dependency expansion — see Configuration & Domain Synonyms and dependency_rules.json.

Tool-name correction — LLMs occasionally call a tool with a name that has minor drift from its registered name. example/orchestrator/hallucination_guard.py implements a similarity-based correction step, applied immediately before tool execution, using only the Python standard library — no dependency on either plug-in, adaptable directly into any dispatch path. A related, separate concern — confirming state-mutating tool calls before execution — is handled by example/orchestrator/mutation_guard.py.

Multi-server and multi-transport systems

Both plug-ins operate exclusively on flat (name, description) / plain-string inputs and have no awareness of how tools were discovered — a single local process, multiple processes over stdio, or multiple remote services over HTTP are all equivalent from their perspective. When tools are aggregated from several independent servers:

  • Collect the union of all servers’ authorized tools into one flat list before calling build_index() and select().
  • Maintain your own tool_name → source_server mapping outside the curator — you likely already need this for dispatching tool execution, independent of curator integration. After select() narrows the candidate set, this mapping is what routes execution correctly for whichever tools remain.

Verification checklist

  • With curator integration present but inactive (mode="none"), behavior is identical to the pre-integration baseline.
  • With the curator active and domain_synonyms.json empty, the LLM receives a visibly smaller tool declaration set per message, and direct keyword queries still resolve correctly.
  • With domain_synonyms.json populated, paraphrased queries sharing no exact keywords with the intended tool resolve correctly.
  • A query containing two distinct intents resolves tools for both, not only the first.
  • Non-English queries resolve to the same tools as their English equivalents, and the LLM’s response comes back in the user’s original language.
  • A quantified evaluation run (see Configuration & Domain Synonyms) exists and reports recall figures, rather than relying on manual spot checks alone.
  • Per-message token usage, measured through whatever tracking your system already has, decreases relative to the full-tool-set baseline.