Skip to content

Commit 5bc1cd3

Browse files
committed
feat: implement experimental AgentConfig and ToolPool with natural API
- Add experimental AgentConfig with toAgent() method for creating Agent instances - Implement ToolPool for managing collections of tools with clean constructor API - Support direct tool function passing: ToolPool([calculator, current_time]) - Add ToolPool.from_module() for importing entire tool modules - Resolve circular import issues with runtime imports (no TYPE_CHECKING needed) - Add comprehensive test coverage for experimental features - Clean up obsolete code and maintain backward compatibility - Remove hacky path manipulation from tests, follow standard import patterns - Use native Python typing (A | B, list[T], any) instead of typing module - Require file:// prefix for file paths to maintain standard interface - Dictionary config only accepts 'prompt' key, not 'system_prompt' - Rename ToolPool methods: to_agent_tools() -> get_tools(), list_tools() -> list_tool_names() - Move imports to top of test files following Python conventions - Add ToolPool integration with tool validation and selection from registry - Implement static FILE_PREFIX and DEFAULT_TOOLS with proper error handling - Require either strands_tools installation or custom ToolPool with your own tools 🤖 Assisted by Amazon Q Developer
1 parent 16e6b24 commit 5bc1cd3

File tree

12 files changed

+4644
-342
lines changed

12 files changed

+4644
-342
lines changed

pyproject.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,3 +255,10 @@ 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/agent/agent.py

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@
5757
from ..types.tools import ToolResult, ToolUse
5858
from ..types.traces import AttributeValue
5959
from .agent_result import AgentResult
60-
from .config import AgentConfig
6160
from .conversation_manager import (
6261
ConversationManager,
6362
SlidingWindowConversationManager,
@@ -219,7 +218,6 @@ def __init__(
219218
load_tools_from_directory: bool = False,
220219
trace_attributes: Optional[Mapping[str, AttributeValue]] = None,
221220
*,
222-
config: Optional[Union[str, dict[str, Any]]] = None,
223221
agent_id: Optional[str] = None,
224222
name: Optional[str] = None,
225223
description: Optional[str] = None,
@@ -257,9 +255,6 @@ def __init__(
257255
load_tools_from_directory: Whether to load and automatically reload tools in the `./tools/` directory.
258256
Defaults to False.
259257
trace_attributes: Custom trace attributes to apply to the agent's trace span.
260-
config: Path to agent configuration file (JSON) or configuration dictionary.
261-
Supports agent-format.md specification with fields: tools, model, prompt.
262-
Constructor parameters override config file values when both are provided.
263258
agent_id: Optional ID for the agent, useful for session management and multi-agent scenarios.
264259
Defaults to "default".
265260
name: name of the Agent
@@ -277,24 +272,6 @@ def __init__(
277272
Raises:
278273
ValueError: If agent id contains path separators.
279274
"""
280-
# Load configuration if provided and merge with constructor parameters
281-
# Constructor parameters take precedence over config file values
282-
if config is not None:
283-
try:
284-
agent_config = AgentConfig(config)
285-
286-
# Apply config values only if constructor parameters are None
287-
if model is None:
288-
model = agent_config.model
289-
if tools is None:
290-
config_tools = agent_config.tools
291-
if config_tools is not None:
292-
tools = list(config_tools) # Cast List[str] to list[Union[str, dict[str, str], Any]]
293-
if system_prompt is None:
294-
system_prompt = agent_config.system_prompt
295-
except (FileNotFoundError, json.JSONDecodeError, ValueError) as e:
296-
raise ValueError(f"Failed to load agent configuration: {e}") from e
297-
298275
self.model = BedrockModel() if not model else BedrockModel(model_id=model) if isinstance(model, str) else model
299276
self.messages = messages if messages is not None else []
300277

src/strands/agent/config.py

Lines changed: 0 additions & 64 deletions
This file was deleted.

src/strands/experimental/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,8 @@
22
33
This module implements experimental features that are subject to change in future revisions without notice.
44
"""
5+
6+
from .agent_config import AgentConfig
7+
from .tool_pool import ToolPool
8+
9+
__all__ = ["AgentConfig", "ToolPool"]
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# ABOUTME: Experimental agent configuration with toAgent() method for creating Agent instances
2+
# ABOUTME: Extends core AgentConfig with experimental instantiation patterns using ToolPool
3+
"""Experimental agent configuration with enhanced instantiation patterns."""
4+
5+
import json
6+
import importlib
7+
8+
from .tool_pool import ToolPool
9+
10+
# Minimum viable list of tools to enable agent building
11+
# This list is experimental and will be revisited as tools evolve
12+
DEFAULT_TOOLS = ["file_read", "editor", "http_request", "shell", "use_agent"]
13+
14+
15+
class AgentConfig:
16+
"""Agent configuration with toAgent() method and ToolPool integration."""
17+
18+
FILE_PREFIX = "file://"
19+
20+
def __init__(self, config_source: str | dict[str, any], tool_pool: "ToolPool | None" = None):
21+
"""Initialize AgentConfig from file path or dictionary.
22+
23+
Args:
24+
config_source: Path to JSON config file (must start with 'file://') or config dictionary
25+
tool_pool: Optional ToolPool to select tools from when 'tools' is specified in config
26+
"""
27+
if isinstance(config_source, str):
28+
# Require file:// prefix for file paths
29+
if not config_source.startswith(self.FILE_PREFIX):
30+
raise ValueError(f"File paths must be prefixed with '{self.FILE_PREFIX}'")
31+
32+
# Remove file:// prefix and load from file
33+
file_path = config_source.removeprefix(self.FILE_PREFIX)
34+
with open(file_path, 'r') as f:
35+
config_data = json.load(f)
36+
else:
37+
# Use dictionary directly
38+
config_data = config_source
39+
40+
self.model = config_data.get('model')
41+
self.system_prompt = config_data.get('prompt') # Only accept 'prompt' key
42+
43+
# Process tools configuration if provided
44+
config_tools = config_data.get('tools')
45+
if config_tools is not None and tool_pool is None:
46+
raise ValueError("Tool names specified in config but no ToolPool provided")
47+
48+
# Handle tool selection from ToolPool
49+
if tool_pool is not None:
50+
self._tool_pool = tool_pool
51+
else:
52+
# Create default ToolPool with strands_tools
53+
self._tool_pool = self._create_default_tool_pool()
54+
55+
# Apply tool selection if specified
56+
if config_tools is not None:
57+
# Validate all tool names exist in the ToolPool
58+
available_tools = tool_pool.list_tool_names()
59+
for tool_name in config_tools:
60+
if tool_name not in available_tools:
61+
raise ValueError(f"Tool '{tool_name}' not found in ToolPool. Available tools: {available_tools}")
62+
63+
# Create new ToolPool with only selected tools
64+
selected_pool = ToolPool()
65+
all_tools = tool_pool.get_tools()
66+
for tool in all_tools:
67+
if tool.tool_name in config_tools:
68+
selected_pool.add_tool(tool)
69+
70+
self._tool_pool = selected_pool
71+
72+
def _create_default_tool_pool(self) -> ToolPool:
73+
"""Create default ToolPool with strands_tools."""
74+
pool = ToolPool()
75+
76+
for tool in DEFAULT_TOOLS:
77+
try:
78+
module_name = f"strands_tools.{tool}"
79+
tool_module = importlib.import_module(module_name)
80+
pool.add_tools_from_module(tool_module)
81+
except ImportError:
82+
raise ImportError(
83+
f"strands_tools is not available and no ToolPool was specified. "
84+
f"Either install strands_tools with 'pip install strands-agents-tools' "
85+
f"or provide your own ToolPool with your own tools."
86+
)
87+
88+
return pool
89+
90+
@property
91+
def tool_pool(self) -> "ToolPool":
92+
"""Get the tool pool for this configuration.
93+
94+
Returns:
95+
ToolPool instance
96+
"""
97+
return self._tool_pool
98+
99+
def toAgent(self, tools: "ToolPool | None" = None, **kwargs: any):
100+
"""Create an Agent instance from this configuration.
101+
102+
Args:
103+
tools: ToolPool to use (overrides default empty pool)
104+
**kwargs: Additional parameters to override config values.
105+
Supports all Agent constructor parameters.
106+
107+
Returns:
108+
Configured Agent instance
109+
110+
Example:
111+
config = AgentConfig({"model": "anthropic.claude-3-5-sonnet-20241022-v2:0", "prompt": "You are helpful"})
112+
pool = ToolPool()
113+
pool.add_tool_function(calculator)
114+
agent = config.toAgent(tools=pool)
115+
"""
116+
# Import here to avoid circular imports:
117+
# experimental/agent_config.py -> agent.agent -> event_loop.event_loop ->
118+
# experimental.hooks -> experimental.__init__.py -> AgentConfig
119+
from ..agent.agent import Agent
120+
121+
# Start with config values
122+
agent_params = {}
123+
124+
if self.model is not None:
125+
agent_params['model'] = self.model
126+
if self.system_prompt is not None:
127+
agent_params['system_prompt'] = self.system_prompt
128+
129+
# Use provided ToolPool or default empty one
130+
tool_pool = tools if tools is not None else self._tool_pool
131+
agent_params['tools'] = tool_pool.get_tools()
132+
133+
# Override with any other provided kwargs
134+
agent_params.update(kwargs)
135+
136+
return Agent(**agent_params)
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# ABOUTME: Tool pool system using existing AgentTool base class for tool selection and management
2+
# ABOUTME: Integrates with existing tool infrastructure and @tool decorator pattern
3+
"""Experimental tool pool system for structured tool management."""
4+
5+
from collections.abc import Callable
6+
7+
from ..types.tools import AgentTool
8+
from ..tools.tools import PythonAgentTool
9+
from ..tools.decorator import DecoratedFunctionTool
10+
11+
12+
class ToolPool:
13+
"""Pool of available tools for agent selection using existing tool infrastructure."""
14+
15+
def __init__(self, tools: "list[AgentTool | Callable] | None" = None):
16+
"""Initialize tool pool.
17+
18+
Args:
19+
tools: List of AgentTool instances or @tool decorated functions
20+
"""
21+
self._tools: dict[str, AgentTool] = {}
22+
if tools:
23+
for tool in tools:
24+
if isinstance(tool, AgentTool):
25+
self.add_tool(tool)
26+
elif callable(tool):
27+
self.add_tool_function(tool)
28+
else:
29+
raise ValueError(f"Tool must be AgentTool instance or callable, got {type(tool)}")
30+
31+
def add_tool(self, tool: AgentTool) -> None:
32+
"""Add existing AgentTool instance to the pool.
33+
34+
Args:
35+
tool: AgentTool instance to add
36+
"""
37+
self._tools[tool.tool_name] = tool
38+
39+
def add_tool_function(self, tool_func: Callable) -> None:
40+
"""Add @tool decorated function to the pool.
41+
42+
Args:
43+
tool_func: Function decorated with @tool
44+
"""
45+
if hasattr(tool_func, '_strands_tool_spec'):
46+
# This is a decorated function tool
47+
tool_spec = tool_func._strands_tool_spec
48+
tool_name = tool_spec.get('name', tool_func.__name__)
49+
decorated_tool = DecoratedFunctionTool(
50+
tool_name=tool_name,
51+
tool_spec=tool_spec,
52+
tool_func=tool_func,
53+
metadata={}
54+
)
55+
self.add_tool(decorated_tool)
56+
else:
57+
raise ValueError(f"Function {tool_func.__name__} is not decorated with @tool")
58+
59+
def add_tools_from_module(self, module: any) -> None:
60+
"""Add all @tool decorated functions from a Python module.
61+
62+
Args:
63+
module: Python module containing @tool decorated functions
64+
"""
65+
import inspect
66+
67+
for name, obj in inspect.getmembers(module):
68+
if inspect.isfunction(obj) and hasattr(obj, '_strands_tool_spec'):
69+
self.add_tool_function(obj)
70+
71+
@classmethod
72+
def from_module(cls, module: any) -> "ToolPool":
73+
"""Create ToolPool from all @tool functions in a module.
74+
75+
Args:
76+
module: Python module containing @tool decorated functions
77+
78+
Returns:
79+
ToolPool with all tools from the module
80+
"""
81+
pool = cls()
82+
pool.add_tools_from_module(module)
83+
return pool
84+
85+
def get_tool(self, name: str) -> AgentTool | None:
86+
"""Get tool by name.
87+
88+
Args:
89+
name: Tool name
90+
91+
Returns:
92+
AgentTool if found, None otherwise
93+
"""
94+
return self._tools.get(name)
95+
96+
def list_tool_names(self) -> list[str]:
97+
"""List available tool names.
98+
99+
Returns:
100+
List of tool names
101+
"""
102+
return list(self._tools.keys())
103+
104+
def get_tools(self) -> list[AgentTool]:
105+
"""Get all tools as AgentTool instances.
106+
107+
Returns:
108+
List of AgentTool instances
109+
"""
110+
return list(self._tools.values())

0 commit comments

Comments
 (0)