Skip to content

Commit de4196e

Browse files
mrlee-amazonUnshure
authored andcommitted
feat: implement experimental AgentConfig and ToolBox with comprehensive API
- Add AgentConfig class for JSON/dict-based agent configuration - Add ToolBox class for structured tool management and selection - Support file:// prefix for JSON config files with validation - Implement to_agent() method for seamless Agent instantiation - Add comprehensive tool validation with configurable error handling - Support both strands_tools integration and custom ToolBox instances - Include extensive test coverage for all functionality - Add documentation examples for both dictionary and JSON config formats - Rename get_tools to list_tools for API consistency - Clean up import handling with proper TYPE_CHECKING separation - Consolidate test utilities to reduce code duplication - Rename all pool variables to toolbox for consistent terminology - Improve temp file handling in tests with proper cleanup 🤖 Assisted by Amazon Q Developer
1 parent fb73e0b commit de4196e

File tree

10 files changed

+241
-4347
lines changed

10 files changed

+241
-4347
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ __pycache__*
1111
.vscode
1212
dist
1313
repl_state
14-
.kiro
14+
.kirou
15+
uv.lock

pyproject.toml

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -255,10 +255,3 @@ style = [
255255
["text", ""],
256256
["disabled", "fg:#858585 italic"]
257257
]
258-
259-
[dependency-groups]
260-
dev = [
261-
"moto>=5.1.13",
262-
"pytest>=8.4.2",
263-
"pytest-asyncio>=1.1.1",
264-
]

src/strands/experimental/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@
44
"""
55

66
from .agent_config import AgentConfig
7-
from .tool_pool import ToolPool
7+
from .tool_box import ToolBox
88

9-
__all__ = ["AgentConfig", "ToolPool"]
9+
__all__ = ["AgentConfig", "ToolBox"]

src/strands/experimental/agent_config.py

Lines changed: 59 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1-
# ABOUTME: Experimental agent configuration with toAgent() method for creating Agent instances
2-
# ABOUTME: Extends core AgentConfig with experimental instantiation patterns using ToolPool
31
"""Experimental agent configuration with enhanced instantiation patterns."""
42

53
import json
64
import importlib
5+
from typing import TYPE_CHECKING
76

8-
from .tool_pool import ToolPool
7+
if TYPE_CHECKING:
8+
# Import here to avoid circular imports:
9+
# experimental/agent_config.py -> agent.agent -> event_loop.event_loop ->
10+
# experimental.hooks -> experimental.__init__.py -> AgentConfig
11+
from ..agent.agent import Agent
12+
13+
from .tool_box import ToolBox
914

1015
# File prefix for configuration file paths
1116
FILE_PREFIX = "file://"
@@ -16,15 +21,34 @@
1621

1722

1823
class AgentConfig:
19-
"""Agent configuration with toAgent() method and ToolPool integration."""
24+
"""Agent configuration with to_agent() method and ToolBox integration.
25+
26+
Example config.json:
27+
{
28+
"model": "anthropic.claude-3-5-sonnet-20241022-v2:0",
29+
"prompt": "You are a helpful assistant",
30+
"tools": ["file_read", "editor"]
31+
}
32+
"""
2033

21-
def __init__(self, config_source: str | dict[str, any], tool_pool: ToolPool | None = None, raise_exception_on_missing_tool: bool = True):
34+
def __init__(self, config_source: str | dict[str, any], tool_box: ToolBox | None = None, raise_exception_on_missing_tool: bool = True):
2235
"""Initialize AgentConfig from file path or dictionary.
2336
2437
Args:
2538
config_source: Path to JSON config file (must start with 'file://') or config dictionary
26-
tool_pool: Optional ToolPool to select tools from when 'tools' is specified in config
39+
tool_box: Optional ToolBox to select tools from when 'tools' is specified in config
2740
raise_exception_on_missing_tool: If False, skip missing tools instead of raising ImportError
41+
42+
Example:
43+
# Dictionary config
44+
config = AgentConfig({
45+
"model": "anthropic.claude-3-5-sonnet-20241022-v2:0",
46+
"prompt": "You are a helpful assistant",
47+
"tools": ["file_read", "editor"]
48+
})
49+
50+
# File config
51+
config = AgentConfig("file://config.json")
2852
"""
2953
if isinstance(config_source, str):
3054
# Require file:// prefix for file paths
@@ -45,40 +69,38 @@ def __init__(self, config_source: str | dict[str, any], tool_pool: ToolPool | No
4569

4670
# Process tools configuration if provided
4771
config_tools = config_data.get('tools')
48-
if config_tools is not None and tool_pool is None:
49-
raise ValueError("Tool names specified in config but no ToolPool provided")
72+
if config_tools is not None and tool_box is None:
73+
raise ValueError("Tool names specified in config but no ToolBox provided")
5074

51-
# Handle tool selection from ToolPool
52-
if tool_pool is not None:
53-
self._tool_pool = tool_pool
75+
# Handle tool selection from ToolBox
76+
if tool_box is not None:
77+
self._toolbox = tool_box
5478
else:
55-
# Create default ToolPool with strands_tools
56-
self._tool_pool = self._create_default_tool_pool()
79+
# Create default ToolBox with strands_tools
80+
self._toolbox = self._create_default_toolbox()
5781

5882
# Track configured tools separately from full tool pool
5983
self._configured_tools = []
6084

6185
# Apply tool selection if specified
6286
if config_tools is not None:
63-
# Validate all tool names exist in the ToolPool
64-
available_tools = self._tool_pool.list_tool_names()
65-
for tool_name in config_tools:
66-
if tool_name not in available_tools:
67-
if self._raise_exception_on_missing_tool:
68-
raise ValueError(f"Tool '{tool_name}' not found in ToolPool. Available tools: {available_tools}")
69-
# Skip missing tools when flag is False
70-
continue
87+
# Validate all tool names exist in the ToolBox
88+
available_tools = self._toolbox.list_tool_names()
89+
90+
if any(tool_name not in available_tools for tool_name in config_tools) and self._raise_exception_on_missing_tool:
91+
missing_tool = next(tool_name for tool_name in config_tools if tool_name not in available_tools)
92+
raise ValueError(f"Tool '{missing_tool}' not found in ToolBox. Available tools: {available_tools}")
7193

72-
# Store selected tools from the ToolPool (only ones that exist)
73-
all_tools = self._tool_pool.get_tools()
94+
# Store selected tools from the ToolBox (only ones that exist)
95+
all_tools = self._toolbox.list_tools()
7496
for tool in all_tools:
7597
if tool.tool_name in config_tools:
7698
self._configured_tools.append(tool)
7799
# If no tools specified in config, use no tools (empty list)
78100

79-
def _create_default_tool_pool(self) -> ToolPool:
80-
"""Create default ToolPool with strands_tools."""
81-
pool = ToolPool()
101+
def _create_default_toolbox(self) -> ToolBox:
102+
"""Create default ToolBox with strands_tools."""
103+
pool = ToolBox()
82104

83105
for tool in DEFAULT_TOOLS:
84106
try:
@@ -88,23 +110,23 @@ def _create_default_tool_pool(self) -> ToolPool:
88110
except ImportError:
89111
if self._raise_exception_on_missing_tool:
90112
raise ImportError(
91-
f"strands_tools is not available and no ToolPool was specified. "
113+
f"strands_tools is not available and no ToolBox was specified. "
92114
f"Either install strands_tools with 'pip install strands-agents-tools' "
93-
f"or provide your own ToolPool with your own tools."
115+
f"or provide your own ToolBox with your own tools."
94116
)
95117
# Skip missing tools when flag is False
96118
continue
97119

98120
return pool
99121

100122
@property
101-
def tool_pool(self) -> ToolPool:
102-
"""Get the full ToolPool (superset of all available tools).
123+
def toolbox(self) -> ToolBox:
124+
"""Get the full ToolBox (superset of all available tools).
103125
104126
Returns:
105-
ToolPool instance containing all available tools
127+
ToolBox instance containing all available tools
106128
"""
107-
return self._tool_pool
129+
return self._toolbox
108130

109131
@property
110132
def configured_tools(self) -> list:
@@ -115,7 +137,7 @@ def configured_tools(self) -> list:
115137
"""
116138
return self._configured_tools
117139

118-
def to_agent(self, **kwargs: any):
140+
def to_agent(self, **kwargs: any) -> "Agent":
119141
"""Create an Agent instance from this configuration.
120142
121143
Args:
@@ -135,24 +157,22 @@ def to_agent(self, **kwargs: any):
135157
agent = config.to_agent()
136158
response = agent("Read the contents of README.md")
137159
138-
# Using custom ToolPool
160+
# Using custom ToolBox
139161
from strands import tool
140162
141163
@tool
142164
def custom_tool(input: str) -> str:
143165
return f"Custom: {input}"
144166
145-
custom_pool = ToolPool([custom_tool])
167+
custom_toolbox = ToolBox([custom_tool])
146168
config = AgentConfig({
147169
"model": "anthropic.claude-3-5-sonnet-20241022-v2:0",
148170
"prompt": "You are a custom assistant",
149171
"tools": ["custom_tool"]
150-
}, tool_pool=custom_pool)
172+
}, tool_box=custom_toolbox)
151173
agent = config.to_agent()
152174
"""
153-
# Import here to avoid circular imports:
154-
# experimental/agent_config.py -> agent.agent -> event_loop.event_loop ->
155-
# experimental.hooks -> experimental.__init__.py -> AgentConfig
175+
# Import at runtime since TYPE_CHECKING import is not available during execution
156176
from ..agent.agent import Agent
157177

158178
# Start with config values
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"""Experimental tool box system for structured tool management."""
2+
3+
from ..types.tools import AgentTool
4+
from ..tools.tools import PythonAgentTool
5+
from ..tools.decorator import DecoratedFunctionTool
6+
7+
8+
class ToolBox:
9+
"""Box of available tools for agent selection using existing tool infrastructure."""
10+
11+
def __init__(self, tools: "list[AgentTool] | None" = None):
12+
"""Initialize tool box.
13+
14+
Args:
15+
tools: List of AgentTool instances
16+
"""
17+
self._tools: dict[str, AgentTool] = {}
18+
if tools:
19+
for tool in tools:
20+
self.add_tool(tool)
21+
22+
def add_tool(self, tool: AgentTool) -> None:
23+
"""Add existing AgentTool instance to the pool.
24+
25+
Args:
26+
tool: AgentTool instance to add
27+
"""
28+
self._tools[tool.tool_name] = tool
29+
30+
def add_tools_from_module(self, module: any) -> None:
31+
"""Add all @tool decorated functions from a Python module.
32+
33+
Args:
34+
module: Python module containing @tool decorated functions
35+
"""
36+
import inspect
37+
38+
for name, obj in inspect.getmembers(module):
39+
if inspect.isfunction(obj) and hasattr(obj, '_strands_tool_spec'):
40+
# Create DecoratedFunctionTool directly
41+
tool_spec = obj._strands_tool_spec
42+
tool_name = tool_spec.get('name', obj.__name__)
43+
decorated_tool = DecoratedFunctionTool(
44+
tool_name=tool_name,
45+
tool_spec=tool_spec,
46+
tool_func=obj,
47+
metadata={}
48+
)
49+
self.add_tool(decorated_tool)
50+
51+
@classmethod
52+
def from_module(cls, module: any) -> "ToolBox":
53+
"""Create ToolBox from all @tool functions in a module.
54+
55+
Args:
56+
module: Python module containing @tool decorated functions
57+
58+
Returns:
59+
ToolBox with all tools from the module
60+
"""
61+
pool = cls()
62+
pool.add_tools_from_module(module)
63+
return pool
64+
65+
def get_tool(self, name: str) -> AgentTool | None:
66+
"""Get tool by name.
67+
68+
Args:
69+
name: Tool name
70+
71+
Returns:
72+
AgentTool if found, None otherwise
73+
"""
74+
return self._tools.get(name)
75+
76+
def list_tool_names(self) -> list[str]:
77+
"""List available tool names.
78+
79+
Returns:
80+
List of tool names
81+
"""
82+
return list(self._tools.keys())
83+
84+
def list_tools(self) -> list[AgentTool]:
85+
"""List all tools as AgentTool instances.
86+
87+
Returns:
88+
List of AgentTool instances in the box
89+
"""
90+
return list(self._tools.values())

0 commit comments

Comments
 (0)