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.
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
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
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()andselect(). - Maintain your own
tool_name → source_servermapping outside the curator — you likely already need this for dispatching tool execution, independent of curator integration. Afterselect()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.jsonempty, the LLM receives a visibly smaller tool declaration set per message, and direct keyword queries still resolve correctly. - With
domain_synonyms.jsonpopulated, 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.