Skip to content

Commit 763092a

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 🤖 Assisted by Amazon Q Developer
1 parent 16e6b24 commit 763092a

File tree

12 files changed

+4511
-342
lines changed

12 files changed

+4511
-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: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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+
7+
8+
class AgentConfig:
9+
"""Agent configuration with toAgent() method and ToolPool integration."""
10+
11+
def __init__(self, config_source: str | dict[str, any]):
12+
"""Initialize AgentConfig from file path or dictionary.
13+
14+
Args:
15+
config_source: Path to JSON config file (must start with 'file://') or config dictionary
16+
"""
17+
if isinstance(config_source, str):
18+
# Require file:// prefix for file paths
19+
if not config_source.startswith('file://'):
20+
raise ValueError("File paths must be prefixed with 'file://'")
21+
22+
# Remove file:// prefix and load from file
23+
file_path = config_source[7:] # Remove 'file://' prefix
24+
with open(file_path, 'r') as f:
25+
config_data = json.load(f)
26+
else:
27+
# Use dictionary directly
28+
config_data = config_source
29+
30+
self.model = config_data.get('model')
31+
self.tools = config_data.get('tools')
32+
self.system_prompt = config_data.get('prompt') # Only accept 'prompt' key
33+
34+
# Create empty default ToolPool
35+
from .tool_pool import ToolPool
36+
self._tool_pool = ToolPool()
37+
38+
@property
39+
def tool_pool(self) -> "ToolPool":
40+
"""Get the tool pool for this configuration.
41+
42+
Returns:
43+
ToolPool instance
44+
"""
45+
return self._tool_pool
46+
47+
def toAgent(self, tools: "ToolPool | None" = None, **kwargs: any):
48+
"""Create an Agent instance from this configuration.
49+
50+
Args:
51+
tools: ToolPool to use (overrides default empty pool)
52+
**kwargs: Additional parameters to override config values.
53+
Supports all Agent constructor parameters.
54+
55+
Returns:
56+
Configured Agent instance
57+
58+
Example:
59+
config = AgentConfig({"model": "anthropic.claude-3-5-sonnet-20241022-v2:0", "prompt": "You are helpful"})
60+
pool = ToolPool()
61+
pool.add_tool_function(calculator)
62+
agent = config.toAgent(tools=pool)
63+
"""
64+
# Import here to avoid circular imports:
65+
# experimental/agent_config.py -> agent.agent -> event_loop.event_loop ->
66+
# experimental.hooks -> experimental.__init__.py -> AgentConfig
67+
from ..agent.agent import Agent
68+
69+
# Start with config values
70+
agent_params = {}
71+
72+
if self.model is not None:
73+
agent_params['model'] = self.model
74+
if self.system_prompt is not None:
75+
agent_params['system_prompt'] = self.system_prompt
76+
77+
# Use provided ToolPool or default empty one
78+
tool_pool = tools if tools is not None else self._tool_pool
79+
agent_params['tools'] = tool_pool.get_tools()
80+
81+
# Override with any other provided kwargs
82+
agent_params.update(kwargs)
83+
84+
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())

src/strands/tools/registry.py

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -55,23 +55,8 @@ def process_tools(self, tools: List[Any]) -> List[str]:
5555
tool_names = []
5656

5757
def add_tool(tool: Any) -> None:
58-
# Case 1: String - could be file path or tool name from strands_tools
58+
# Case 1: String file path
5959
if isinstance(tool, str):
60-
# First try to import from strands_tools.{tool_name}
61-
try:
62-
import importlib
63-
64-
module_name = f"strands_tools.{tool}"
65-
tool_module = importlib.import_module(module_name)
66-
if hasattr(tool_module, tool):
67-
tool_obj = getattr(tool_module, tool)
68-
# Recursively process the imported tool object
69-
add_tool(tool_obj)
70-
return
71-
except (ImportError, AttributeError):
72-
pass
73-
74-
# If not found in strands_tools, treat as file path
7560
# Extract tool name from path
7661
tool_name = os.path.basename(tool).split(".")[0]
7762
self.load_tool_from_filepath(tool_name=tool_name, tool_path=tool)

0 commit comments

Comments
 (0)