# Hybrid Tool Search with Suggestions

Demonstrate the new hybrid tool search and automatic suggestion system.

**Components**:

- [`tool_search`](../agentchat/tools/tool_search/tool_search.py): Semantic search (BM25 + vector) for discovery
- [`tool_search_regex`](../agentchat/tools/tool_search/tool_search.py): Exact pattern matching for known names
- [`SuggestMiddleware`](../agentchat/middleware/suggest.py): Automatic suggestions in system prompt


## Setup



```python
from dotenv import load_dotenv
from langchain_core.tools import BaseTool, tool

load_dotenv()
```




    True



## Define Tools



```python
@tool
def get_weather(city: str) -> str:
    """Get current weather for a city."""
    return f"Weather in {city}: Sunny, 22°C"


@tool
def get_forecast(city: str, days: int = 3) -> str:
    """Get weather forecast for a city."""
    return f"{days}-day forecast for {city}: Sunny → Cloudy → Rain"


@tool
def send_email(to: str, subject: str, body: str) -> str:
    """Send an email to a recipient."""
    return f"Email sent to {to}: {subject}"


@tool
def search_contacts(query: str) -> str:
    """Search for contacts by name."""
    return f"Found contacts matching '{query}': Alice, Bob"


@tool
def create_reminder(text: str, time: str) -> str:
    """Create a reminder."""
    return f"Reminder set: '{text}' at {time}"


TOOL_REGISTRY: dict[str, BaseTool] = {
    t.name: t for t in [get_weather, get_forecast, send_email, search_contacts, create_reminder]
}

print("Tools:", list(TOOL_REGISTRY.keys()))
```

    Tools: ['get_weather', 'get_forecast', 'send_email', 'search_contacts', 'create_reminder']


## Build Tool Index

The [`ToolIndex`](../agentchat/tools/tool_search/index.py) uses LanceDB for hybrid search (BM25 + vector embeddings).



```python
from agentchat.tools.tool_search import ToolIndex

index = ToolIndex()
index.build_index(TOOL_REGISTRY)

print("Index built with", len(TOOL_REGISTRY), "tools")
```

    Index built with 5 tools


## Semantic Search

Search with natural language queries. The hybrid approach combines:

- BM25 for keyword matching
- Vector embeddings for semantic similarity



```python
# Natural language query
results = await index.search("What's the temperature outside?", top_k=3)
for r in results:
    print(f"  {r['name']}: {r['description'][:50]}... (score: {r['score']:.3f})")
```

      get_weather: Get current weather for a city.... (score: 0.016)
      get_forecast: Get weather forecast for a city.... (score: 0.016)
      send_email: Send an email to a recipient.... (score: 0.016)



```python
# Another query
results = await index.search("send a message to someone", top_k=3)
for r in results:
    print(f"  {r['name']}: {r['description'][:50]}... (score: {r['score']:.3f})")
```

      send_email: Send an email to a recipient.... (score: 0.033)
      create_reminder: Create a reminder.... (score: 0.016)
      search_contacts: Search for contacts by name.... (score: 0.016)


## tool_search vs tool_search_regex

Two search tools with different use cases ([source](../agentchat/tools/tool_search/tool_search.py)):

| Tool                | Use Case                              | Example                              |
| ------------------- | ------------------------------------- | ------------------------------------ |
| `tool_search`       | Unknown tool name, semantic discovery | `tool_search("check temperature")`   |
| `tool_search_regex` | Known name, exact match               | `tool_search_regex("^get_weather$")` |



```python
from agentchat.tools.registry import TOOL_REGISTRY as REAL_REGISTRY
from agentchat.tools.tool_search import get_tool_index

# Clear and register our test tools
REAL_REGISTRY.clear()
REAL_REGISTRY.update(TOOL_REGISTRY)

# Initialize the global tool index with our registry
get_tool_index(REAL_REGISTRY)
```




    <agentchat.tools.tool_search.index.ToolIndex at 0x112d6ae90>




```python
from agentchat.tools.tool_search import tool_search, tool_search_regex

# Semantic search - finds related tools
print("tool_search('weather forecast'):")
result = await tool_search.ainvoke({"query": "weather forecast", "top_k": 3})
for t in result["tools"]:
    print(f"  - {t['name']}")
```

    tool_search('weather forecast'):
      - get_forecast
      - get_weather
      - create_reminder



```python
# Regex search - exact match
print("tool_search_regex('^get_weather$'):")
result = tool_search_regex.invoke({"pattern": "^get_weather$"})
for t in result["tools"]:
    print(f"  - {t['name']}")
```

    tool_search_regex('^get_weather$'):
      - get_weather



```python
# Regex search - pattern match
print("tool_search_regex('get_.*'):")
result = tool_search_regex.invoke({"pattern": "get_.*"})
for t in result["tools"]:
    print(f"  - {t['name']}")
```

    tool_search_regex('get_.*'):
      - get_weather
      - get_forecast


## SuggestMiddleware

[`SuggestMiddleware`](../agentchat/middleware/suggest.py) automatically suggests items from multiple indexes based on user message and injects into system prompt.



```python
from agentchat.middleware import IndexConfig, SuggestMiddleware
from agentchat.tools.tool_search import get_tool_index

# Get tool index (lazy loaded on first search, needs registry on first call)
tool_index = get_tool_index(REAL_REGISTRY)

middleware = SuggestMiddleware(
    indexes=[
        IndexConfig(
            index=tool_index,
            label="tool",
            usage_hint="Use tool_search_regex('^name$') to enable tools.",
        ),
    ],
    top_k=3,
)

# Simulate what the middleware does
user_msg = "What's the weather like in Tokyo?"
tools = await tool_index.search(user_msg, top_k=3)

print(f"User: {user_msg}")
print("\nSuggested tools:")
for t in tools:
    print(f"  - {t['name']}: {t['description'][:50]}...")
```

    User: What's the weather like in Tokyo?
    
    Suggested tools:
      - get_weather: Get current weather for a city....
      - get_forecast: Get weather forecast for a city....
      - create_reminder: Create a reminder....



```python
# Show the suggestion text that gets injected
config = middleware.indexes[0]
items = [(config, t) for t in tools]
suggestion = middleware._build_suggestion_text(items)
print("Injected into system prompt:")
print(suggestion)
```

    Injected into system prompt:
    [SUGGESTIONS]
    Suggested items based on user's request:
    - [tool] get_weather: Get current weather for a city.
    - [tool] get_forecast: Get weather forecast for a city.
    - [tool] create_reminder: Create a reminder.
    
    Use tool_search_regex('^name$') to enable tools.
    [/SUGGESTIONS]


## Full Agent Demo

Create an agent with both `SuggestMiddleware` and `ToolSearchFilterMiddleware`.



```python
from typing import Any

import rich
from langchain.agents import create_agent
from langchain_anthropic import ChatAnthropic
from langgraph.graph.state import CompiledStateGraph

from agentchat.middleware import IndexConfig, SuggestMiddleware, ToolSearchFilterMiddleware
from agentchat.tools.tool_search import get_tool_index, tool_search, tool_search_regex

# Create middlewares
filter_middleware = ToolSearchFilterMiddleware(REAL_REGISTRY)

suggest_middleware = SuggestMiddleware(
    indexes=[
        IndexConfig(
            index=get_tool_index(REAL_REGISTRY),  # Pass registry for lazy loading
            label="tool",
            usage_hint="Use tool_search_regex('^name$') to enable tools.",
        ),
    ],
    top_k=3,
)

# Create agent
model = ChatAnthropic(model="claude-sonnet-4-5-20250929")
all_tools: list[BaseTool] = [tool_search, tool_search_regex, *REAL_REGISTRY.values()]

agent: CompiledStateGraph[Any] = create_agent(
    model=model,
    tools=all_tools,
    system_prompt="You are a helpful assistant. Use tool_search_regex to enable suggested tools.",
    middleware=[filter_middleware, suggest_middleware],
)

print(f"Agent created with {len(all_tools)} tools")
```

    Agent created with 7 tools



```python
# Ask about weather - agent should see suggestions and use tool_search_regex
async def run() -> None:
    async for chunk in agent.astream(
        {"messages": [{"role": "user", "content": "What's the weather in Tokyo?"}]},
    ):
        rich.print("chunk =", chunk)


await run()
```


    Visible tools: ['tool_search', 'tool_search_regex']




    Suggestions: [(' get_weather', '0.03'), (' get_forecast', '0.03'), (' search_contacts', '0.02')]




    chunk =
    {
        'model': {
            'messages': [
                AIMessage(
                    content=[
                        {
                            'text': "I'll help you get the weather information for Tokyo. Let me search for the weather
    tool.",
                            'type': 'text'
                        },
                        {
                            'id': 'toolu_01JQ54sqg9hpeRuecr2feGgS',
                            'input': {'pattern': '^get_weather$'},
                            'name': 'tool_search_regex',
                            'type': 'tool_use'
                        }
                    ],
                    additional_kwargs={},
                    response_metadata={
                        'id': 'msg_01JEreaknDrRwn892TbHeUzX',
                        'model': 'claude-sonnet-4-5-20250929',
                        'stop_reason': 'tool_use',
                        'stop_sequence': None,
                        'usage': {
                            'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0},
                            'cache_creation_input_tokens': 0,
                            'cache_read_input_tokens': 0,
                            'input_tokens': 1093,
                            'output_tokens': 79,
                            'server_tool_use': None,
                            'service_tier': 'standard'
                        },
                        'model_name': 'claude-sonnet-4-5-20250929',
                        'model_provider': 'anthropic'
                    },
                    id='lc_run--6cd39370-87be-42f1-9354-8e94e13047c4-0',
                    tool_calls=[
                        {
                            'name': 'tool_search_regex',
                            'args': {'pattern': '^get_weather$'},
                            'id': 'toolu_01JQ54sqg9hpeRuecr2feGgS',
                            'type': 'tool_call'
                        }
                    ],
                    usage_metadata={
                        'input_tokens': 1093,
                        'output_tokens': 79,
                        'total_tokens': 1172,
                        'input_token_details': {
                            'cache_read': 0,
                            'cache_creation': 0,
                            'ephemeral_5m_input_tokens': 0,
                            'ephemeral_1h_input_tokens': 0
                        }
                    }
                )
            ]
        }
    }




    Discovered: get_weather




    chunk =
    {
        'tools': {
            'messages': [
                ToolMessage(
                    content='{"tools": [{"description": "Get current weather for a city.", "properties": {"city": 
    {"title": "City", "type": "string"}}, "required": ["city"], "title": "get_weather", "type": "object", "name": 
    "get_weather"}], "message": "Found 1 tool(s).", "page": 1, "total": 1}',
                    name='tool_search_regex',
                    id='d60b9121-887f-4961-87b1-410fb2301b3e',
                    tool_call_id='toolu_01JQ54sqg9hpeRuecr2feGgS'
                )
            ]
        }
    }




    Visible tools: ['tool_search', 'tool_search_regex', 'get_weather']




    chunk =
    {
        'model': {
            'messages': [
                AIMessage(
                    content=[
                        {'text': 'Now let me get the weather for Tokyo:', 'type': 'text'},
                        {
                            'id': 'toolu_01MRRNNkFEbaHpGzwGK4Svif',
                            'input': {'city': 'Tokyo'},
                            'name': 'get_weather',
                            'type': 'tool_use'
                        }
                    ],
                    additional_kwargs={},
                    response_metadata={
                        'id': 'msg_011TafZyV5SabDpKEvE4LH7v',
                        'model': 'claude-sonnet-4-5-20250929',
                        'stop_reason': 'tool_use',
                        'stop_sequence': None,
                        'usage': {
                            'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0},
                            'cache_creation_input_tokens': 0,
                            'cache_read_input_tokens': 0,
                            'input_tokens': 1241,
                            'output_tokens': 63,
                            'server_tool_use': None,
                            'service_tier': 'standard'
                        },
                        'model_name': 'claude-sonnet-4-5-20250929',
                        'model_provider': 'anthropic'
                    },
                    id='lc_run--45b540cc-3cda-4f1b-b488-0256ceb36055-0',
                    tool_calls=[
                        {
                            'name': 'get_weather',
                            'args': {'city': 'Tokyo'},
                            'id': 'toolu_01MRRNNkFEbaHpGzwGK4Svif',
                            'type': 'tool_call'
                        }
                    ],
                    usage_metadata={
                        'input_tokens': 1241,
                        'output_tokens': 63,
                        'total_tokens': 1304,
                        'input_token_details': {
                            'cache_read': 0,
                            'cache_creation': 0,
                            'ephemeral_5m_input_tokens': 0,
                            'ephemeral_1h_input_tokens': 0
                        }
                    }
                )
            ]
        }
    }




    chunk =
    {
        'tools': {
            'messages': [
                ToolMessage(
                    content='Weather in Tokyo: Sunny, 22°C',
                    name='get_weather',
                    id='6086a559-e0e5-4aa6-bcde-80ab387029c6',
                    tool_call_id='toolu_01MRRNNkFEbaHpGzwGK4Svif'
                )
            ]
        }
    }




    Visible tools: ['tool_search', 'tool_search_regex', 'get_weather']




    chunk =
    {
        'model': {
            'messages': [
                AIMessage(
                    content='The current weather in Tokyo is **Sunny** with a temperature of **22°C** (approximately 
    72°F).',
                    additional_kwargs={},
                    response_metadata={
                        'id': 'msg_019PDBMnq6eRTB7yQSYzapgA',
                        'model': 'claude-sonnet-4-5-20250929',
                        'stop_reason': 'end_turn',
                        'stop_sequence': None,
                        'usage': {
                            'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0},
                            'cache_creation_input_tokens': 0,
                            'cache_read_input_tokens': 0,
                            'input_tokens': 1327,
                            'output_tokens': 29,
                            'server_tool_use': None,
                            'service_tier': 'standard'
                        },
                        'model_name': 'claude-sonnet-4-5-20250929',
                        'model_provider': 'anthropic'
                    },
                    id='lc_run--1086af94-8fa2-48a1-a3aa-5ef0297a6b01-0',
                    usage_metadata={
                        'input_tokens': 1327,
                        'output_tokens': 29,
                        'total_tokens': 1356,
                        'input_token_details': {
                            'cache_read': 0,
                            'cache_creation': 0,
                            'ephemeral_5m_input_tokens': 0,
                            'ephemeral_1h_input_tokens': 0
                        }
                    }
                )
            ]
        }
    }



## Summary

- [`tool_search`](../agentchat/tools/tool_search/tool_search.py): Semantic search for discovery when tool name is unknown
- [`tool_search_regex`](../agentchat/tools/tool_search/tool_search.py): Exact match for known tool names (faster, precise)
- [`SuggestMiddleware`](../agentchat/middleware/suggest.py): Suggests items from multiple indexes, guides LLM to use appropriate tools
- [`ToolSearchFilterMiddleware`](../agentchat/middleware/tool_filter.py): Hides undiscovered tools to save tokens

