Skip to content

Commit 287ed95

Browse files
feat: agentcore tool search
1 parent c311682 commit 287ed95

15 files changed

Lines changed: 1331 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""Gateway Strands plugins."""
2+
3+
from .agentcore_tool_search import AgentCoreToolSearchPlugin
4+
5+
__all__ = ["AgentCoreToolSearchPlugin"]
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# Strands AgentCore Tool Search Plugin
2+
3+
A semantic tool discovery plugin for [Strands Agents](https://github.com/strands-agents/sdk-python) that uses the [Amazon Bedrock AgentCore Gateway](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway-using-mcp-semantic-search.html) `x_amz_bedrock_agentcore_search` tool. This enables agents to dynamically load only the relevant tools for each invocation by deriving user intent from conversation history, even when hundreds of tools are registered on the gateway.
4+
5+
## Features
6+
7+
- **Semantic tool discovery** — uses AgentCore Gateway's built-in search to find relevant tools
8+
- **Intent-based loading** — derives user intent via LLM before searching
9+
- **No list_tools call** — tools are built directly from search results
10+
- **Pluggable intent provider** — swap the default intent provider with your own
11+
- **Agent model reuse** — by default, the intent classifier uses the same model as the parent agent
12+
13+
## Installation
14+
15+
```bash
16+
pip install agentcore-tool-search-plugin
17+
```
18+
19+
## Usage
20+
21+
```python
22+
from mcp_proxy_for_aws.client import aws_iam_streamablehttp_client
23+
from strands import Agent
24+
from strands.tools.mcp import MCPClient
25+
from agentcore_tool_search_plugin import AgentCoreToolSearchPlugin
26+
27+
mcp_client = MCPClient(lambda: aws_iam_streamablehttp_client(
28+
endpoint="https://<gateway-id>.gateway.bedrock-agentcore.<region>.amazonaws.com/mcp",
29+
aws_region="us-east-1",
30+
aws_service="bedrock-agentcore",
31+
))
32+
33+
mcp_client.start()
34+
35+
agent = Agent(plugins=[AgentCoreToolSearchPlugin(mcp_client=mcp_client)])
36+
37+
agent("Find me afternoon flights to New York")
38+
```
39+
40+
Or using a context manager:
41+
42+
```python
43+
with mcp_client:
44+
agent = Agent(plugins=[AgentCoreToolSearchPlugin(mcp_client=mcp_client)])
45+
agent("Find me afternoon flights to New York")
46+
```
47+
48+
## How It Works
49+
50+
![Tool Search Flow](images/agentcore_tool_search_plugin.png)
51+
52+
On each agent invocation:
53+
54+
1. **User query** — The user sends a query to Strands agent.
55+
2. **Hook** — The agent triggers the `AgentCoreToolSearchPlugin` before model invocation
56+
3. **Derive intent** — The `IntentProvider` sends the last N messages from conversation history to the configured LLM to produce a concise intent string
57+
4. **Search gateway** — The intent is passed to AgentCore Gateway's `x_amz_bedrock_agentcore_search` tool to obtain most relevant tools.
58+
5. **Invoke LLM** — The agent invokes the LLM with the user query along with the matched tools from registered MCP targets (Lambda, API Gateway, MCP Server)
59+
60+
Previously loaded tools are cleared before each search, so the agent always has the most relevant tools available.
61+
62+
## Intent Provider
63+
64+
An `IntentProvider` is responsible for analyzing conversation messages and producing a concise intent string that drives tool search. The plugin calls `derive_intent(messages, model)` before each invocation to determine what tools to load.
65+
66+
### Default Intent Provider
67+
68+
`DefaultIntentProvider` uses an LLM to classify the last few conversation messages into a concise intent string. By default it uses the agent's model.
69+
70+
**Basic usage (uses the agent's model automatically):**
71+
72+
```python
73+
from bedrock_agentcore.gateway.integrations.strands.plugins import AgentCoreToolSearchPlugin
74+
75+
agent = Agent(plugins=[
76+
AgentCoreToolSearchPlugin(mcp_client=mcp_client)
77+
])
78+
```
79+
80+
**With a custom model for intent classification:**
81+
82+
```python
83+
from strands.models.bedrock import BedrockModel
84+
from bedrock_agentcore.gateway.integrations.strands.plugins import AgentCoreToolSearchPlugin
85+
from bedrock_agentcore.gateway.integrations.strands.plugins.agentcore_tool_search.intent_providers import DefaultIntentProvider
86+
87+
intent_model = BedrockModel(model_id="us.anthropic.claude-haiku-4-5-20251001-v1:0")
88+
agent = Agent(plugins=[
89+
AgentCoreToolSearchPlugin(
90+
mcp_client=mcp_client,
91+
intent_provider=DefaultIntentProvider(model=intent_model),
92+
)
93+
])
94+
```
95+
96+
### Custom Intent Provider
97+
98+
You can provide your own intent derivation strategy by subclassing `IntentProvider`:
99+
100+
```python
101+
from bedrock_agentcore.gateway.integrations.strands.plugins.agentcore_tool_search.intent_providers import IntentProvider
102+
103+
class MyIntentProvider(IntentProvider):
104+
def derive_intent(self, messages: list[dict], model=None) -> str:
105+
# custom logic to derive intent
106+
return "intent string"
107+
108+
agent = Agent(plugins=[
109+
AgentCoreToolSearchPlugin(
110+
mcp_client=mcp_client,
111+
intent_provider=MyIntentProvider(),
112+
)
113+
])
114+
```
115+
116+
## Prerequisites
117+
118+
- An AgentCore Gateway with **[semantic search](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway-using-mcp-semantic-search.html) enabled**
119+
- Tools registered on the gateway with descriptions
120+
- AWS credentials with access to the gateway
121+
122+
For more details, see the [AgentCore Gateway Documentation](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway-building.html).
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""AgentCore Tool Search plugin for Strands Agents."""
2+
3+
from .intent_providers import DefaultIntentProvider, IntentProvider
4+
from .plugin import AgentCoreToolSearchPlugin
5+
6+
__all__ = ["AgentCoreToolSearchPlugin", "IntentProvider", "DefaultIntentProvider"]
140 KB
Loading
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""Intent provider interfaces and implementations."""
2+
3+
from .default_intent_provider import DefaultIntentProvider
4+
from .intent_provider import IntentProvider
5+
6+
__all__ = ["DefaultIntentProvider", "IntentProvider"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"""Default LLM-based intent provider implementation."""
2+
3+
import logging
4+
5+
from strands import Agent
6+
7+
from .intent_provider import IntentProvider
8+
9+
logger = logging.getLogger(__name__)
10+
11+
INTENT_SYSTEM_PROMPT = (
12+
"You are an intent classifier. Given the recent conversation messages, "
13+
"produce a concise one-sentence description of what the user is trying to accomplish. "
14+
"Focus on the type of task, not the specific details. "
15+
"Reply with ONLY the intent description, nothing else."
16+
)
17+
18+
19+
class DefaultIntentProvider(IntentProvider):
20+
"""LLM-based intent provider that classifies the last N messages."""
21+
22+
def __init__(self, message_window: int = 5, model=None):
23+
"""Initialize DefaultIntentProvider.
24+
25+
Args:
26+
message_window: Number of recent messages to consider.
27+
model: Optional explicit model for intent classification.
28+
"""
29+
self._message_window = message_window
30+
self._explicit_model = model
31+
32+
def derive_intent(self, messages: list[dict], model=None) -> str:
33+
"""Derive intent using an LLM. Falls back to agent's model if no explicit model set."""
34+
try:
35+
recent_messages = messages[-self._message_window :] if messages else []
36+
if not recent_messages:
37+
return ""
38+
39+
kwargs = {"system_prompt": INTENT_SYSTEM_PROMPT, "tools": []}
40+
# Priority: explicit model > agent's model > Strands default
41+
resolved_model = self._explicit_model or model
42+
if resolved_model:
43+
kwargs["model"] = resolved_model
44+
45+
intent_agent = Agent(**kwargs)
46+
response = intent_agent(self._format_messages_for_prompt(recent_messages))
47+
return str(response).strip()
48+
except Exception as e:
49+
logger.error("Failed to derive intent: %s", e)
50+
return ""
51+
52+
def _format_messages_for_prompt(self, messages: list[dict]) -> str:
53+
"""Format user messages into a text prompt for the intent LLM.
54+
55+
Only includes user-role messages to avoid leaking PII or sensitive data
56+
from tool results or assistant responses.
57+
"""
58+
parts = []
59+
for msg in messages:
60+
role = msg.get("role", "")
61+
if role != "user":
62+
continue
63+
content = msg.get("content", [])
64+
text = ""
65+
if isinstance(content, list):
66+
text = " ".join(
67+
block.get("text", "") for block in content if isinstance(block, dict) and "text" in block
68+
)
69+
if text.strip():
70+
parts.append(text.strip())
71+
return "\n".join(parts)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""Intent provider abstract interface."""
2+
3+
from abc import ABC, abstractmethod
4+
5+
6+
class IntentProvider(ABC):
7+
"""Abstract interface for deriving user intent from conversation messages.
8+
9+
Subclasses must implement the `derive_intent` method to analyze conversation
10+
messages and return a concise intent string.
11+
"""
12+
13+
@abstractmethod
14+
def derive_intent(self, messages: list[dict], model=None) -> str:
15+
"""Analyze conversation messages and return a concise intent string.
16+
17+
Args:
18+
messages: List of conversation message dicts in Strands format.
19+
model: Optional model instance from the parent agent. Implementations
20+
can use this for LLM-based intent derivation.
21+
22+
Returns:
23+
A plain text string describing the user's intent.
24+
Returns empty string if intent cannot be determined.
25+
"""
26+
...
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
"""AgentCore tool search plugin for Strands Agents."""
2+
3+
import json
4+
import logging
5+
6+
from mcp.types import Tool as MCPTool
7+
from strands.hooks import BeforeInvocationEvent
8+
from strands.plugins import Plugin, hook
9+
from strands.tools.mcp import MCPClient
10+
from strands.tools.mcp.mcp_agent_tool import MCPAgentTool
11+
12+
from .intent_providers import DefaultIntentProvider, IntentProvider
13+
14+
logger = logging.getLogger(__name__)
15+
16+
17+
class AgentCoreToolSearchPlugin(Plugin):
18+
"""Plugin that dynamically loads tools from AgentCore Gateway based on semantic intent.
19+
20+
Args:
21+
mcp_client: MCPClient connected to an AgentCore Gateway.
22+
intent_provider: Strategy for deriving intent. Defaults to DefaultIntentProvider.
23+
"""
24+
25+
name = "agentcore-tool-search-plugin"
26+
27+
def __init__(
28+
self,
29+
mcp_client: MCPClient,
30+
intent_provider: IntentProvider | None = None,
31+
):
32+
"""Initialize the plugin.
33+
34+
Args:
35+
mcp_client: MCPClient connected to an AgentCore Gateway.
36+
intent_provider: Strategy for deriving intent. Defaults to DefaultIntentProvider.
37+
"""
38+
super().__init__()
39+
self._intent_provider = intent_provider or DefaultIntentProvider()
40+
self._mcp_client = mcp_client
41+
self._loaded_tool_names: set[str] = set()
42+
43+
@property
44+
def tools(self):
45+
"""Return empty list; tools are loaded dynamically via the hook."""
46+
return []
47+
48+
@hook
49+
def on_before_invocation(self, event: BeforeInvocationEvent) -> None:
50+
"""Derive intent, search gateway, and load matching tools."""
51+
messages = event.messages or []
52+
53+
# Pass the agent's model to the intent provider
54+
intent = self._intent_provider.derive_intent(messages, model=event.agent.model)
55+
logger.info("Derived intent: %s", intent)
56+
57+
# Clear all previously loaded dynamic tools
58+
for name in list(self._loaded_tool_names):
59+
event.agent.tool_registry.registry.pop(name, None)
60+
self._loaded_tool_names.clear()
61+
62+
if not intent:
63+
return
64+
65+
try:
66+
result = self._mcp_client.call_tool_sync(
67+
tool_use_id="intent-search",
68+
name="x_amz_bedrock_agentcore_search",
69+
arguments={"query": intent},
70+
)
71+
agent_tools = self._build_tools_from_search_result(result)
72+
except Exception as e:
73+
logger.error("AgentCore Gateway search failed: %s", e)
74+
return
75+
76+
for agent_tool in agent_tools:
77+
try:
78+
# Skip if a non-dynamic tool with this name already exists
79+
if (
80+
agent_tool.tool_name in event.agent.tool_registry.registry
81+
and agent_tool.tool_name not in self._loaded_tool_names
82+
):
83+
logger.debug("Skipping tool %s: already registered as a static tool", agent_tool.tool_name)
84+
continue
85+
event.agent.tool_registry.register_tool(agent_tool)
86+
self._loaded_tool_names.add(agent_tool.tool_name)
87+
except Exception as e:
88+
logger.error("Failed to register tool %s: %s", agent_tool.tool_name, e)
89+
90+
logger.info("Loaded tools: %s", self._loaded_tool_names)
91+
92+
def _build_tools_from_search_result(self, result) -> list[MCPAgentTool]:
93+
"""Build MCPAgentTool objects from the gateway search response."""
94+
tools = []
95+
if not result or not isinstance(result, dict):
96+
return tools
97+
98+
tool_defs = []
99+
structured = result.get("structuredContent")
100+
if isinstance(structured, dict) and "tools" in structured:
101+
tool_defs = structured["tools"]
102+
else:
103+
for block in result.get("content", []):
104+
if isinstance(block, dict) and "text" in block:
105+
try:
106+
data = json.loads(block["text"])
107+
if isinstance(data, dict) and "tools" in data:
108+
tool_defs = data["tools"]
109+
break
110+
except (json.JSONDecodeError, TypeError):
111+
continue
112+
113+
for tool_def in tool_defs:
114+
if not isinstance(tool_def, dict) or "name" not in tool_def:
115+
continue
116+
mcp_tool = MCPTool(
117+
name=tool_def["name"],
118+
description=tool_def.get("description", ""),
119+
inputSchema=tool_def.get("inputSchema", {"type": "object", "properties": {}}),
120+
)
121+
tools.append(MCPAgentTool(mcp_tool=mcp_tool, mcp_client=self._mcp_client))
122+
123+
return tools

tests/bedrock_agentcore/gateway/__init__.py

Whitespace-only changes.

tests/bedrock_agentcore/gateway/integrations/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)