Skip to main content

Adaptive Tool Routing - Dynamic tool selection for AI agents

Project description

ATR - Adaptive Tool Routing

PyPI version License: MIT Python 3.10+

Dynamic tool selection for AI agents. ATR filters tools based on user queries before they reach the agent's system prompt, reducing context tokens by ~90% and improving tool selection accuracy.

The Problem

When agents have access to many tools (50+ MCP tools), two things happen:

  1. Context explosion - Tool definitions consume 10,000-15,000 tokens before the conversation starts
  2. Tool selection degradation - Research shows 7-85% accuracy drops with large tool catalogs

The Solution

ATR intercepts the agent's tool resolution flow and filters tools before they reach the system prompt, using a lightweight LLM (e.g., Claude Haiku, GPT-4o-mini) to select only relevant tools.

Before ATR: 50 tools × 250 tokens = 12,500 tokens
After ATR:   5 tools × 250 tokens =  1,250 tokens
Savings: 90%

How It Works

User Query ──> ATR Router ──> Lightweight LLM ──> Filtered Tool List ──> Agent
                  │                                      │
                  └── Full tool catalog (50+)             └── Only relevant tools (3-5)
  1. Register your tools with ATR (from any framework - MCP, LangChain, Agno, OpenAI, etc.)
  2. Before each agent call, pass the user query through ATR's router
  3. ATR uses a cheap, fast LLM to pick only the relevant tools
  4. Pass the filtered tools to your agent - smaller context, better accuracy

Installation

# Core package (zero dependencies)
pip install adaptive-tools

# With LLM provider
pip install adaptive-tools[openrouter]  # Recommended - access to many models
pip install adaptive-tools[openai]
pip install adaptive-tools[anthropic]

# With framework integration
pip install adaptive-tools[langgraph]
pip install adaptive-tools[agno]
pip install adaptive-tools[openai-agents]
pip install adaptive-tools[litellm]

# Everything
pip install adaptive-tools[all]

Quick Start

from atr import ToolRouter, ToolSpec
from atr.llm import OpenRouterLLM

# Create router with LLM
router = ToolRouter(llm=OpenRouterLLM())

# Add tools
router.add_tools([
    ToolSpec(name="get_stock_price", description="Get current stock price"),
    ToolSpec(name="get_company_news", description="Get company news articles"),
    ToolSpec(name="get_weather", description="Get weather for a location"),
    ToolSpec(name="send_email", description="Send an email"),
    ToolSpec(name="create_calendar_event", description="Create a calendar event"),
])

# Route query to filter tools
filtered = router.route("What is AAPL's stock price?")
print(filtered.names)  # {'get_stock_price'}

Framework Integrations

LangGraph

from langchain_mcp_adapters.client import MultiServerMCPClient
from atr import ToolRouter
from atr.adapters import LangChainAdapter
from atr.adapters.langchain import filter_tools
from atr.llm import OpenRouterLLM

async with MultiServerMCPClient(server_configs) as client:
    all_tools = await client.get_tools()

    # Create router from LangChain tools
    router = ToolRouter(llm=OpenRouterLLM())
    router.add_tools(LangChainAdapter.to_specs(all_tools))

    # Route and filter
    filtered_specs = router.route("Read the README")
    filtered_tools = filter_tools(all_tools, filtered_specs)

    # Use filtered tools with your agent
    agent = create_react_agent(model, filtered_tools)

For LangGraph graphs, use the built-in node creators to add routing as a graph node:

from atr.adapters.langchain import create_async_router_node

# Create a LangGraph-compatible node that routes tools
route_node = create_async_router_node(router, all_tools)

# Add to your graph
graph = StateGraph(AgentState)
graph.add_node("route_tools", route_node)
graph.add_node("agent", agent_node)
graph.add_edge("route_tools", "agent")

Agno

from agno.agent import Agent
from agno.models.openai import OpenAIChat
from agno.tools.mcp import MCPTools
from atr import ToolRouter
from atr.adapters import AgnoAdapter
from atr.adapters.agno import filter_tools
from atr.llm import OpenRouterLLM

async with MCPTools(command="npx", args=["-y", "@anthropic/mcp-server-filesystem", "/tmp"]) as mcp:
    # Convert toolkit to specs and create router
    router = ToolRouter(llm=OpenRouterLLM(), max_tools=5)
    router.add_tools(AgnoAdapter.to_specs([mcp]))

    # Route and filter
    filtered_specs = await router.aroute("List files")
    filtered_funcs = filter_tools([mcp], filtered_specs)

    # Create agent with filtered tools
    agent = Agent(model=OpenAIChat(id="gpt-4o"), tools=filtered_funcs)

OpenAI Agents SDK

from atr import ToolRouter
from atr.adapters import OpenAIAdapter
from atr.adapters.openai import filter_tools
from atr.llm import OpenRouterLLM

# Create router from OpenAI function definitions
router = ToolRouter(llm=OpenRouterLLM())
router.add_tools(OpenAIAdapter.to_specs(openai_tools))

# Route and filter
filtered_specs = router.route("What's the weather?")
filtered_tools = filter_tools(openai_tools, filtered_specs)

LiteLLM

ATR integrates with LiteLLM as a custom hook for automatic tool routing - no manual filtering needed:

import litellm
from atr.adapters.litellm import create_hook

# Create and register the hook
hook = create_hook(
    llm_provider="openrouter",
    llm_model="anthropic/claude-3-haiku",
    max_tools=5,
)
litellm.callbacks = [hook]

# Tools are now automatically filtered before reaching the model
response = await litellm.acompletion(
    model="gpt-4",
    messages=[{"role": "user", "content": "What's the weather?"}],
    tools=all_tools,  # ATR filters these automatically
)

For LiteLLM proxy, configure in proxy_config.yaml:

litellm_settings:
  callbacks:
    - atr.adapters.litellm.ATRToolRoutingHook
  atr_config:
    enabled: true
    max_tools: 10
    llm_provider: openrouter
    llm_model: anthropic/claude-3-haiku

MCP (Direct)

from mcp import ClientSession
from atr import ToolRouter
from atr.adapters import MCPAdapter
from atr.llm import OpenRouterLLM

# Get tools from MCP session
mcp_tools = await session.list_tools()

# Convert and route
router = ToolRouter(llm=OpenRouterLLM())
router.add_tools(MCPAdapter.to_specs(mcp_tools.tools))

filtered_specs = router.route("Read the README")
filtered_tools = MCPAdapter.filter_tools(mcp_tools.tools, filtered_specs)

API Reference

Core Classes

ToolSpec

Framework-agnostic tool specification.

ToolSpec(
    name="get_weather",
    description="Get weather for a location",
    parameters={"type": "object", ...},  # JSON Schema (optional)
    source="mcp:weather",                # Origin identifier (optional)
    metadata={},                         # Framework-specific data (optional)
)

ToolCollection

Returned from routing operations. Provides convenient access to filtered tools.

collection = ToolCollection(tools=[...])
collection.names                           # Set of tool names
collection.filter_by_names(["tool1"])      # Filter by name
collection.to_summaries()                  # For routing prompts
len(collection)                            # Number of tools
"tool_name" in collection                  # Membership check
collection[0]                              # Index access
collection["tool_name"]                    # Name-based access

ToolRouter

Main router class. Routes queries to select relevant tools.

router = ToolRouter(
    llm=OpenRouterLLM(),  # Or any RoutingLLM
    max_tools=10,         # Max tools to return (default: 10)
)
router.add_tools([...])
router.add_tool(single_spec)
router.clear_tools()

filtered = router.route("query")        # Sync
filtered = await router.aroute("query") # Async

LLM Providers

All providers use lazy client initialization and support both sync and async.

from atr.llm import OpenRouterLLM, OpenAILLM, AnthropicLLM

# OpenRouter (recommended - access to many models via single API key)
llm = OpenRouterLLM(model="anthropic/claude-3-haiku")  # default model

# OpenAI
llm = OpenAILLM(model="gpt-4o-mini")

# Anthropic
llm = AnthropicLLM(model="claude-3-haiku-20240307")

Adapters

Every adapter follows the same pattern: to_specs() to convert, filter_tools() to filter back.

from atr.adapters import MCPAdapter, LangChainAdapter, AgnoAdapter, OpenAIAdapter, LiteLLMAdapter

# Convert framework tools to ToolSpecs
specs = MCPAdapter.to_specs(mcp_tools)
specs = LangChainAdapter.to_specs(langchain_tools)
specs = AgnoAdapter.to_specs([toolkit_or_function, ...])
specs = OpenAIAdapter.to_specs(openai_tool_defs)
specs = LiteLLMAdapter.to_specs(litellm_tool_defs)

# After routing, filter original tools by the routing result
filtered = MCPAdapter.filter_tools(mcp_tools, filtered_collection)
filtered = LangChainAdapter.filter_tools(langchain_tools, filtered_collection)
filtered = AgnoAdapter.filter_tools([toolkit_or_function, ...], filtered_collection)
filtered = OpenAIAdapter.filter_tools(openai_tool_defs, filtered_collection)
filtered = LiteLLMAdapter.filter_tools(litellm_tool_defs, filtered_collection)

Each adapter module also exports a standalone filter_tools() convenience function:

from atr.adapters.langchain import filter_tools
from atr.adapters.agno import filter_tools
from atr.adapters.openai import filter_tools
from atr.adapters.litellm import filter_tools

Custom Filter Strategies

ATR uses a pluggable strategy pattern. The default LLMFilterStrategy uses an LLM, but you can implement your own:

from atr import ToolRouter, ToolCollection, BaseFilterStrategy

class MyCustomStrategy(BaseFilterStrategy):
    def filter(self, query: str, tools: ToolCollection) -> ToolCollection:
        # Your custom filtering logic (embeddings, keyword matching, etc.)
        return tools.filter_by_names(["relevant_tool"])

    async def afilter(self, query: str, tools: ToolCollection) -> ToolCollection:
        return self.filter(query, tools)

router = ToolRouter(strategy=MyCustomStrategy())

Built-in strategies:

  • LLMFilterStrategy - Uses an LLM to select relevant tools (default when llm is provided)
  • PassthroughStrategy - Returns all tools unfiltered (default when no llm or strategy is provided)

Design

  • Zero core dependencies - Optional extras only for the frameworks you use
  • Fail-open - If routing fails, all original tools are returned
  • Protocol-based - FilterStrategy and RoutingLLM are Protocols, not base classes - duck typing works
  • Lazy imports - Optional dependencies are loaded only when their adapter is accessed
  • Typed - Full type hints with py.typed marker, strict mypy config

Environment Variables

Variable Description
OPENROUTER_API_KEY OpenRouter API key
OPENAI_API_KEY OpenAI API key
ANTHROPIC_API_KEY Anthropic API key

Examples

See the examples/ directory for complete working examples:

  • basic_usage.py - Core functionality without frameworks
  • langgraph_example.py - LangGraph with MCP tools
  • agno_example.py - Agno with MCP and toolkits
  • openai_agents_example.py - OpenAI Agents SDK
  • litellm_example.py - LiteLLM with automatic hook-based routing

Development

git clone https://github.com/yess-ai/atr.git
cd atr

# Install with dev dependencies using uv
uv sync --all-extras

# Run tests
uv run pytest

# Format & lint
uv run ruff format
uv run ruff check --fix

License

MIT License - see LICENSE for details.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

adaptive_tools-0.1.1.tar.gz (43.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

adaptive_tools-0.1.1-py3-none-any.whl (27.1 kB view details)

Uploaded Python 3

File details

Details for the file adaptive_tools-0.1.1.tar.gz.

File metadata

  • Download URL: adaptive_tools-0.1.1.tar.gz
  • Upload date:
  • Size: 43.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for adaptive_tools-0.1.1.tar.gz
Algorithm Hash digest
SHA256 41ea83490308b1ef494f1bd1b64d2bd685bcbd41539a12a7541f0ebbcbbf42ef
MD5 a43ff81b1daa441356b76fae0fa5a030
BLAKE2b-256 db7860aa0c722e96a81c8711b01cfa16507fd6373d9659afb897f160b5a515d4

See more details on using hashes here.

Provenance

The following attestation bundles were made for adaptive_tools-0.1.1.tar.gz:

Publisher: publish.yml on yess-ai/atr

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file adaptive_tools-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: adaptive_tools-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 27.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for adaptive_tools-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 5e56eb481c5d0aa1bd125308db0d15216ce322d3a9631bfc92a9f92caafc6e69
MD5 6e26f71a9797230775f4a3cb4eb21f06
BLAKE2b-256 266a8c87a36c33192b32e9d1b8a368e40202ae816e46f21878010d114dc6975f

See more details on using hashes here.

Provenance

The following attestation bundles were made for adaptive_tools-0.1.1-py3-none-any.whl:

Publisher: publish.yml on yess-ai/atr

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page