|
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"] |
| 1 | +"""Experimental agent configuration utilities. |
21 | 2 |
|
| 3 | +This module provides utilities for creating agents from configuration files or dictionaries. |
| 4 | +""" |
22 | 5 |
|
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 | | - } |
| 6 | +import json |
| 7 | +from pathlib import Path |
| 8 | +from typing import Any, Dict, Union |
| 9 | + |
| 10 | +from ..agent import Agent |
| 11 | + |
| 12 | + |
| 13 | +def config_to_agent(config: Union[str, Dict[str, Any]], **kwargs) -> Agent: |
| 14 | + """Create an Agent from a configuration file or dictionary. |
| 15 | + |
| 16 | + Args: |
| 17 | + config: Either a file path (with optional file:// prefix) or a configuration dictionary |
| 18 | + **kwargs: Additional keyword arguments to pass to the Agent constructor |
| 19 | + |
| 20 | + Returns: |
| 21 | + Agent: A configured Agent instance |
| 22 | + |
| 23 | + Raises: |
| 24 | + FileNotFoundError: If the configuration file doesn't exist |
| 25 | + json.JSONDecodeError: If the configuration file contains invalid JSON |
| 26 | + ValueError: If the configuration is invalid |
| 27 | + |
| 28 | + Examples: |
| 29 | + Create agent from file: |
| 30 | + >>> agent = config_to_agent("/path/to/config.json") |
| 31 | + |
| 32 | + Create agent from file with file:// prefix: |
| 33 | + >>> agent = config_to_agent("file:///path/to/config.json") |
| 34 | + |
| 35 | + Create agent from dictionary: |
| 36 | + >>> config = {"model": "anthropic.claude-3-5-sonnet-20241022-v2:0", "tools": ["calculator"]} |
| 37 | + >>> agent = config_to_agent(config) |
32 | 38 | """ |
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) |
| 39 | + # Parse configuration |
| 40 | + if isinstance(config, str): |
| 41 | + # Handle file path |
| 42 | + file_path = config |
| 43 | + |
| 44 | + # Remove file:// prefix if present |
| 45 | + if file_path.startswith("file://"): |
| 46 | + file_path = file_path[7:] |
| 47 | + |
| 48 | + # Load JSON from file |
| 49 | + config_path = Path(file_path) |
| 50 | + if not config_path.exists(): |
| 51 | + raise FileNotFoundError(f"Configuration file not found: {file_path}") |
| 52 | + |
| 53 | + with open(config_path, 'r') as f: |
| 54 | + config_dict = json.load(f) |
| 55 | + elif isinstance(config, dict): |
| 56 | + config_dict = config.copy() |
| 57 | + else: |
| 58 | + raise ValueError("Config must be a file path string or dictionary") |
| 59 | + |
| 60 | + # Prepare Agent constructor arguments |
| 61 | + agent_kwargs = {} |
| 62 | + |
| 63 | + # Map configuration keys to Agent constructor parameters |
| 64 | + config_mapping = { |
| 65 | + "model": "model", |
| 66 | + "prompt": "system_prompt", |
| 67 | + "tools": "tools", |
| 68 | + "name": "name", |
| 69 | + "agent_id": "agent_id", |
| 70 | + "session_manager": "session_manager", |
| 71 | + "conversation_manager": "conversation_manager", |
| 72 | + "hooks": "hooks", |
| 73 | + "callback_handler": "callback_handler", |
| 74 | + "state": "state", |
| 75 | + "trace_attributes": "trace_attributes", |
| 76 | + } |
| 77 | + |
| 78 | + # Only include non-None values from config |
| 79 | + for config_key, agent_param in config_mapping.items(): |
| 80 | + if config_key in config_dict and config_dict[config_key] is not None: |
| 81 | + agent_kwargs[agent_param] = config_dict[config_key] |
| 82 | + |
| 83 | + # Override with any additional kwargs provided |
| 84 | + agent_kwargs.update(kwargs) |
| 85 | + |
| 86 | + # Create and return Agent |
| 87 | + return Agent(**agent_kwargs) |
0 commit comments