Skip to content

Commit 6bbe313

Browse files
committed
feat: add experimental AgentConfig with comprehensive tool management
- Add AgentConfig class for declarative agent configuration via JSON/dict - Support file:// prefix for loading configurations from JSON files - Implement ToolRegistry integration with automatic default tool loading - Add raise_exception_on_missing_tool parameter for flexible error handling - Support tool selection from registry via tool names in config - Add comprehensive test coverage for all configuration scenarios - Move hook events from experimental to production with updated names - Add OpenAI model provider enhancements and Gemini model improvements - Update event loop and tool executors to use production hook events 🤖 Assisted by Amazon Q Developer
1 parent eef11cc commit 6bbe313

File tree

5 files changed

+408
-1
lines changed

5 files changed

+408
-1
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+
.kiro
15+
uv.lock

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ dependencies = [
134134
"pytest-asyncio>=1.0.0,<1.3.0",
135135
"pytest-xdist>=3.0.0,<4.0.0",
136136
"moto>=5.1.0,<6.0.0",
137+
"strands-agents-tools>=0.2.0,<1.0.0",
137138
]
138139

139140
[[tool.hatch.envs.hatch-test.matrix]]

src/strands/experimental/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,7 @@
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+
8+
__all__ = ["AgentConfig"]
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
"""Experimental agent configuration with enhanced instantiation patterns."""
2+
3+
import importlib
4+
import json
5+
from typing import TYPE_CHECKING, Any
6+
7+
from ..tools.registry import ToolRegistry
8+
9+
if TYPE_CHECKING:
10+
# Import here to avoid circular imports:
11+
# experimental/agent_config.py -> agent.agent -> event_loop.event_loop ->
12+
# experimental.hooks -> experimental.__init__.py -> AgentConfig
13+
from ..agent.agent import Agent
14+
15+
# File prefix for configuration file paths
16+
FILE_PREFIX = "file://"
17+
18+
# Minimum viable list of tools to enable agent building
19+
# This list is experimental and will be revisited as tools evolve
20+
DEFAULT_TOOLS = ["file_read", "editor", "http_request", "shell", "use_agent"]
21+
22+
23+
class AgentConfig:
24+
"""Agent configuration with to_agent() method and ToolRegistry 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+
"""
33+
34+
def __init__(
35+
self,
36+
config_source: str | dict[str, Any],
37+
tool_registry: ToolRegistry | None = None,
38+
raise_exception_on_missing_tool: bool = True,
39+
):
40+
"""Initialize AgentConfig from file path or dictionary.
41+
42+
Args:
43+
config_source: Path to JSON config file (must start with 'file://') or config dictionary
44+
tool_registry: Optional ToolRegistry to select tools from when 'tools' is specified in config
45+
raise_exception_on_missing_tool: If False, skip missing tools instead of raising ImportError
46+
47+
Example:
48+
# Dictionary config
49+
config = AgentConfig({
50+
"model": "anthropic.claude-3-5-sonnet-20241022-v2:0",
51+
"prompt": "You are a helpful assistant",
52+
"tools": ["file_read", "editor"]
53+
})
54+
55+
# File config
56+
config = AgentConfig("file://config.json")
57+
"""
58+
if isinstance(config_source, str):
59+
# Require file:// prefix for file paths
60+
if not config_source.startswith(FILE_PREFIX):
61+
raise ValueError(f"File paths must be prefixed with '{FILE_PREFIX}'")
62+
63+
# Remove file:// prefix and load from file
64+
file_path = config_source.removeprefix(FILE_PREFIX)
65+
with open(file_path, "r") as f:
66+
config_data = json.load(f)
67+
else:
68+
# Use dictionary directly
69+
config_data = config_source
70+
71+
self.model = config_data.get("model")
72+
self.system_prompt = config_data.get("prompt") # Only accept 'prompt' key
73+
self._raise_exception_on_missing_tool = raise_exception_on_missing_tool
74+
75+
# Handle tool selection from ToolRegistry
76+
if tool_registry is not None:
77+
self._tool_registry = tool_registry
78+
else:
79+
# Create default ToolRegistry with strands_tools
80+
self._tool_registry = self._create_default_tool_registry()
81+
82+
# Process tools configuration if provided
83+
config_tools = config_data.get("tools")
84+
85+
# Track configured tools separately from full tool pool
86+
self._configured_tools = []
87+
88+
# Apply tool selection if specified
89+
if config_tools is not None:
90+
# Validate all tool names exist in the ToolRegistry
91+
available_tools = self._tool_registry.registry.keys()
92+
93+
missing_tools = set(config_tools).difference(available_tools)
94+
if missing_tools and self._raise_exception_on_missing_tool:
95+
raise ValueError(
96+
f"Tool(s) '{missing_tools}' not found in ToolRegistry. Available tools: {available_tools}"
97+
)
98+
99+
for tool_name in config_tools:
100+
if tool_name in self._tool_registry.registry:
101+
tool = self._tool_registry.registry[tool_name]
102+
self._configured_tools.append(tool)
103+
# If no tools specified in config, use no tools (empty list)
104+
105+
def _create_default_tool_registry(self) -> ToolRegistry:
106+
"""Create default ToolRegistry with strands_tools."""
107+
tool_registry = ToolRegistry()
108+
109+
try:
110+
tool_modules = [importlib.import_module(f"strands_tools.{tool}") for tool in DEFAULT_TOOLS]
111+
tool_registry.process_tools(tool_modules)
112+
except ImportError as e:
113+
if self._raise_exception_on_missing_tool:
114+
raise ImportError(
115+
"strands_tools is not available and no ToolRegistry was specified. "
116+
"Either install strands_tools with 'pip install strands-agents-tools' "
117+
"or provide your own ToolRegistry with your own tools."
118+
) from e
119+
120+
return tool_registry
121+
122+
@property
123+
def tool_registry(self) -> ToolRegistry:
124+
"""Get the full ToolRegistry (superset of all available tools).
125+
126+
Returns:
127+
ToolRegistry instance containing all available tools
128+
"""
129+
return self._tool_registry
130+
131+
@property
132+
def configured_tools(self) -> list:
133+
"""Get the configured tools (subset selected for this agent).
134+
135+
Returns:
136+
List of tools configured for this agent
137+
"""
138+
return self._configured_tools
139+
140+
def to_agent(self, **kwargs: Any) -> "Agent":
141+
"""Create an Agent instance from this configuration.
142+
143+
Args:
144+
**kwargs: Additional parameters to override config values.
145+
Supports all Agent constructor parameters.
146+
147+
Returns:
148+
Configured Agent instance
149+
150+
Example:
151+
# Using default tools from strands_tools
152+
config = AgentConfig({
153+
"model": "anthropic.claude-3-5-sonnet-20241022-v2:0",
154+
"prompt": "You are a helpful assistant",
155+
"tools": ["file_read"]
156+
})
157+
agent = config.to_agent()
158+
response = agent("Read the contents of README.md")
159+
160+
# Using custom ToolRegistry
161+
from strands import tool
162+
163+
@tool
164+
def custom_tool(input: str) -> str:
165+
return f"Custom: {input}"
166+
167+
custom_tool_registry = ToolRegistry()
168+
custom_tool_registry.process_tools([custom_tool])
169+
config = AgentConfig({
170+
"model": "anthropic.claude-3-5-sonnet-20241022-v2:0",
171+
"prompt": "You are a custom assistant",
172+
"tools": ["custom_tool"]
173+
}, tool_registry=custom_tool_registry)
174+
agent = config.to_agent()
175+
"""
176+
# Import at runtime since TYPE_CHECKING import is not available during execution
177+
from ..agent.agent import Agent
178+
179+
# Start with config values
180+
agent_params = {}
181+
182+
if self.model is not None:
183+
agent_params["model"] = self.model
184+
if self.system_prompt is not None:
185+
agent_params["system_prompt"] = self.system_prompt
186+
187+
# Use configured tools (subset of tool pool)
188+
agent_params["tools"] = self._configured_tools
189+
190+
# Override with any other provided kwargs
191+
agent_params.update(kwargs)
192+
193+
return Agent(**agent_params)

0 commit comments

Comments
 (0)