Skip to content

Commit 17ae352

Browse files
Adding AgentCoreToolSearch plugin
1 parent c311682 commit 17ae352

8 files changed

Lines changed: 327 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: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .intent_providers import IntentProvider, DefaultIntentProvider
2+
3+
__all__ = ["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,58 @@
1+
import logging
2+
3+
from strands import Agent
4+
5+
from .intent_provider import IntentProvider
6+
7+
logger = logging.getLogger(__name__)
8+
9+
INTENT_SYSTEM_PROMPT = (
10+
"You are an intent classifier. Given the recent conversation messages, "
11+
"produce a concise one-sentence description of what the user is trying to accomplish. "
12+
"Focus on the type of task, not the specific details. "
13+
"Reply with ONLY the intent description, nothing else."
14+
)
15+
16+
17+
class DefaultIntentProvider(IntentProvider):
18+
"""LLM-based intent provider that classifies the last N messages."""
19+
20+
def __init__(self, message_window: int = 5, model=None):
21+
self._message_window = message_window
22+
self._explicit_model = model
23+
24+
def derive_intent(self, messages: list[dict], model=None) -> str:
25+
"""Derive intent using an LLM. Falls back to agent's model if no explicit model set."""
26+
try:
27+
recent_messages = messages[-self._message_window:] if messages else []
28+
if not recent_messages:
29+
return ""
30+
31+
kwargs = {"system_prompt": INTENT_SYSTEM_PROMPT, "tools": []}
32+
# Priority: explicit model > agent's model > Strands default
33+
resolved_model = self._explicit_model or model
34+
if resolved_model:
35+
kwargs["model"] = resolved_model
36+
37+
intent_agent = Agent(**kwargs)
38+
response = intent_agent(self._format_messages_for_prompt(recent_messages))
39+
return str(response).strip()
40+
except Exception as e:
41+
logger.error("Failed to derive intent: %s", e)
42+
return ""
43+
44+
def _format_messages_for_prompt(self, messages: list[dict]) -> str:
45+
"""Format messages into a text prompt for the intent LLM."""
46+
parts = []
47+
for msg in messages:
48+
role = msg.get("role", "unknown")
49+
content = msg.get("content", [])
50+
text = ""
51+
if isinstance(content, list):
52+
text = " ".join(
53+
block.get("text", "")
54+
for block in content
55+
if isinstance(block, dict) and "text" in block
56+
)
57+
parts.append(f"{role}: {text}")
58+
return "\n".join(parts)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from abc import ABC, abstractmethod
2+
3+
4+
class IntentProvider(ABC):
5+
"""Abstract interface for deriving user intent from conversation messages.
6+
7+
Subclasses must implement the `derive_intent` method to analyze conversation
8+
messages and return a concise intent string.
9+
"""
10+
11+
@abstractmethod
12+
def derive_intent(self, messages: list[dict], model=None) -> str:
13+
"""Analyze conversation messages and return a concise intent string.
14+
15+
Args:
16+
messages: List of conversation message dicts in Strands format.
17+
model: Optional model instance from the parent agent. Implementations
18+
can use this for LLM-based intent derivation.
19+
20+
Returns:
21+
A plain text string describing the user's intent.
22+
Returns empty string if intent cannot be determined.
23+
"""
24+
...
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
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+
super().__init__()
33+
self._intent_provider = intent_provider or DefaultIntentProvider()
34+
self._mcp_client = mcp_client
35+
self._loaded_tool_names: set[str] = set()
36+
37+
@property
38+
def tools(self):
39+
return []
40+
41+
@hook
42+
def on_before_invocation(self, event: BeforeInvocationEvent) -> None:
43+
"""Derive intent, search gateway, and load matching tools."""
44+
messages = event.messages or []
45+
46+
# Pass the agent's model to the intent provider
47+
intent = self._intent_provider.derive_intent(messages, model=event.agent.model)
48+
logger.info("Derived intent: %s", intent)
49+
50+
# Clear all previously loaded conditional tools
51+
for name in list(self._loaded_tool_names):
52+
event.agent.tool_registry.registry.pop(name, None)
53+
self._loaded_tool_names.clear()
54+
55+
if not intent:
56+
return
57+
58+
try:
59+
result = self._mcp_client.call_tool_sync(
60+
tool_use_id="intent-search",
61+
name="x_amz_bedrock_agentcore_search",
62+
arguments={"query": intent},
63+
)
64+
agent_tools = self._build_tools_from_search_result(result)
65+
except Exception as e:
66+
logger.error("AgentCore Gateway search failed: %s", e)
67+
return
68+
69+
for agent_tool in agent_tools:
70+
try:
71+
event.agent.tool_registry.register_tool(agent_tool)
72+
self._loaded_tool_names.add(agent_tool.tool_name)
73+
except Exception as e:
74+
logger.error("Failed to register tool %s: %s", agent_tool.tool_name, e)
75+
76+
logger.info("Loaded tools: %s", self._loaded_tool_names)
77+
78+
def _build_tools_from_search_result(self, result) -> list[MCPAgentTool]:
79+
"""Build MCPAgentTool objects from the gateway search response."""
80+
tools = []
81+
if not result or not isinstance(result, dict):
82+
return tools
83+
84+
tool_defs = []
85+
structured = result.get("structuredContent")
86+
if isinstance(structured, dict) and "tools" in structured:
87+
tool_defs = structured["tools"]
88+
else:
89+
for block in result.get("content", []):
90+
if isinstance(block, dict) and "text" in block:
91+
try:
92+
data = json.loads(block["text"])
93+
if isinstance(data, dict) and "tools" in data:
94+
tool_defs = data["tools"]
95+
break
96+
except (json.JSONDecodeError, TypeError):
97+
continue
98+
99+
for tool_def in tool_defs:
100+
if not isinstance(tool_def, dict) or "name" not in tool_def:
101+
continue
102+
mcp_tool = MCPTool(
103+
name=tool_def["name"],
104+
description=tool_def.get("description", ""),
105+
inputSchema=tool_def.get("inputSchema", {"type": "object", "properties": {}}),
106+
)
107+
tools.append(MCPAgentTool(mcp_tool=mcp_tool, mcp_client=self._mcp_client))
108+
109+
return tools

0 commit comments

Comments
 (0)