|
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