generated from amazon-archives/__template_Apache-2.0
-
Notifications
You must be signed in to change notification settings - Fork 445
feat: add experimental AgentConfig with comprehensive tool management #935
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
mr-lee
wants to merge
19
commits into
strands-agents:main
Choose a base branch
from
mr-lee:feature/agent-config-squashed
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
ac4ae10
feat: add experimental AgentConfig with comprehensive tool management
mrlee-amazon c87fe46
fix: remove AgentConfig import from experimental/__init__.py
mrlee-amazon 065d80a
fix: remove strands-agents-tools test dependency
mrlee-amazon 1578035
test: remove test that depends on strands_tools availability
mrlee-amazon c5caec0
test: add back tests with proper mocking for strands_tools
mrlee-amazon ebfa3c9
test: fix Windows compatibility for file prefix test
mrlee-amazon 675774c
refactor: replace AgentConfig class with config_to_agent function
mrlee-amazon 40ae6aa
feat: limit config_to_agent to core configuration keys
mrlee-amazon 4f2c8c0
fix: use native Python typing instead of typing module
mrlee-amazon 5390d7d
test: simplify file prefix test with proper context manager
mrlee-amazon 54cb4db
feat: add JSON schema validation to config_to_agent
mrlee-amazon 5873914
refactor: move JSON schema to separate file
mrlee-amazon 229decf
perf: use pre-compiled JSON schema validator
mrlee-amazon 5607086
feat: add tool validation and clarify limitations
mrlee-amazon dab85b5
fix: improve tool validation error messages and add comprehensive tests
mrlee-amazon 66e53a4
fix: reference module instead of tool in error message
mrlee-amazon cb507dd
revert: change error message back to reference tool
mrlee-amazon 9f33f62
feat: use agent tool loading logic
Unshure 6ca10ce
fix: address pr comments
Unshure File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,4 +11,5 @@ __pycache__* | |
.vscode | ||
dist | ||
repl_state | ||
.kiro | ||
.kiro | ||
uv.lock |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Unshure marked this conversation as resolved.
Show resolved
Hide resolved
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
"""Experimental agent configuration utilities. | ||
|
||
This module provides utilities for creating agents from configuration files or dictionaries. | ||
|
||
Note: Configuration-based agent setup only works for tools that don't require code-based | ||
instantiation. For tools that need constructor arguments or complex setup, use the | ||
programmatic approach after creating the agent: | ||
|
||
agent = config_to_agent("config.json") | ||
# Add tools that need code-based instantiation | ||
agent.tool_registry.process_tools([ToolWithConfigArg(HttpsConnection("localhost"))]) | ||
""" | ||
|
||
import json | ||
from pathlib import Path | ||
from typing import Any | ||
|
||
import jsonschema | ||
from jsonschema import ValidationError | ||
|
||
from ..agent import Agent | ||
|
||
# JSON Schema for agent configuration | ||
AGENT_CONFIG_SCHEMA = { | ||
"$schema": "http://json-schema.org/draft-07/schema#", | ||
"title": "Agent Configuration", | ||
"description": "Configuration schema for creating agents", | ||
"type": "object", | ||
"properties": { | ||
"name": {"description": "Name of the agent", "type": ["string", "null"], "default": None}, | ||
"model": { | ||
"description": "The model ID to use for this agent. If not specified, uses the default model.", | ||
"type": ["string", "null"], | ||
"default": None, | ||
}, | ||
"prompt": { | ||
"description": "The system prompt for the agent. Provides high level context to the agent.", | ||
"type": ["string", "null"], | ||
"default": None, | ||
}, | ||
"tools": { | ||
"description": "List of tools the agent can use. Can be file paths, " | ||
"Python module names, or @tool annotated functions in files.", | ||
"type": "array", | ||
"items": {"type": "string"}, | ||
"default": [], | ||
}, | ||
}, | ||
"additionalProperties": False, | ||
} | ||
|
||
# Pre-compile validator for better performance | ||
_VALIDATOR = jsonschema.Draft7Validator(AGENT_CONFIG_SCHEMA) | ||
|
||
|
||
def config_to_agent(config: str | dict[str, Any], **kwargs: dict[str, Any]) -> Agent: | ||
Unshure marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"""Create an Agent from a configuration file or dictionary. | ||
|
||
This function supports tools that can be loaded declaratively (file paths, module names, | ||
or @tool annotated functions). For tools requiring code-based instantiation with constructor | ||
arguments, add them programmatically after creating the agent: | ||
|
||
agent = config_to_agent("config.json") | ||
Unshure marked this conversation as resolved.
Show resolved
Hide resolved
|
||
agent.process_tools([ToolWithConfigArg(HttpsConnection("localhost"))]) | ||
Unshure marked this conversation as resolved.
Show resolved
Hide resolved
Unshure marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Args: | ||
config: Either a file path (with optional file:// prefix) or a configuration dictionary | ||
**kwargs: Additional keyword arguments to pass to the Agent constructor | ||
|
||
Returns: | ||
Agent: A configured Agent instance | ||
|
||
Raises: | ||
FileNotFoundError: If the configuration file doesn't exist | ||
json.JSONDecodeError: If the configuration file contains invalid JSON | ||
ValueError: If the configuration is invalid or tools cannot be loaded | ||
|
||
Examples: | ||
Create agent from file: | ||
>>> agent = config_to_agent("/path/to/config.json") | ||
|
||
Create agent from file with file:// prefix: | ||
>>> agent = config_to_agent("file:///path/to/config.json") | ||
|
||
Create agent from dictionary: | ||
>>> config = {"model": "anthropic.claude-3-5-sonnet-20241022-v2:0", "tools": ["calculator"]} | ||
>>> agent = config_to_agent(config) | ||
""" | ||
# Parse configuration | ||
if isinstance(config, str): | ||
# Handle file path | ||
file_path = config | ||
|
||
# Remove file:// prefix if present | ||
if file_path.startswith("file://"): | ||
file_path = file_path[7:] | ||
|
||
# Load JSON from file | ||
config_path = Path(file_path) | ||
if not config_path.exists(): | ||
raise FileNotFoundError(f"Configuration file not found: {file_path}") | ||
|
||
with open(config_path, "r") as f: | ||
config_dict = json.load(f) | ||
elif isinstance(config, dict): | ||
config_dict = config.copy() | ||
else: | ||
raise ValueError("Config must be a file path string or dictionary") | ||
|
||
# Validate configuration against schema | ||
try: | ||
_VALIDATOR.validate(config_dict) | ||
except ValidationError as e: | ||
# Provide more detailed error message | ||
error_path = " -> ".join(str(p) for p in e.absolute_path) if e.absolute_path else "root" | ||
Unshure marked this conversation as resolved.
Show resolved
Hide resolved
|
||
raise ValueError(f"Configuration validation error at {error_path}: {e.message}") from e | ||
Unshure marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
# Prepare Agent constructor arguments | ||
agent_kwargs = {} | ||
|
||
# Map configuration keys to Agent constructor parameters | ||
config_mapping = { | ||
"model": "model", | ||
Unshure marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"prompt": "system_prompt", | ||
"tools": "tools", | ||
"name": "name", | ||
} | ||
|
||
# Only include non-None values from config | ||
for config_key, agent_param in config_mapping.items(): | ||
if config_key in config_dict and config_dict[config_key] is not None: | ||
agent_kwargs[agent_param] = config_dict[config_key] | ||
|
||
# Override with any additional kwargs provided | ||
agent_kwargs.update(kwargs) | ||
|
||
# Create and return Agent | ||
return Agent(**agent_kwargs) | ||
Unshure marked this conversation as resolved.
Show resolved
Hide resolved
|
Unshure marked this conversation as resolved.
Show resolved
Hide resolved
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
"""Tests for experimental config_to_agent function.""" | ||
|
||
import json | ||
import os | ||
import tempfile | ||
|
||
import pytest | ||
|
||
from strands.experimental import config_to_agent | ||
|
||
|
||
def test_config_to_agent_with_dict(): | ||
"""Test config_to_agent can be created with dict config.""" | ||
config = {"model": "test-model"} | ||
agent = config_to_agent(config) | ||
assert agent.model.config["model_id"] == "test-model" | ||
|
||
|
||
def test_config_to_agent_with_system_prompt(): | ||
"""Test config_to_agent handles system prompt correctly.""" | ||
config = {"model": "test-model", "prompt": "Test prompt"} | ||
agent = config_to_agent(config) | ||
assert agent.system_prompt == "Test prompt" | ||
|
||
|
||
def test_config_to_agent_with_tools_list(): | ||
"""Test config_to_agent handles tools list without failing.""" | ||
# Use a simple test that doesn't require actual tool loading | ||
config = {"model": "test-model", "tools": []} | ||
agent = config_to_agent(config) | ||
assert agent.model.config["model_id"] == "test-model" | ||
|
||
|
||
def test_config_to_agent_with_kwargs_override(): | ||
"""Test that kwargs can override config values.""" | ||
config = {"model": "test-model", "prompt": "Config prompt"} | ||
agent = config_to_agent(config, system_prompt="Override prompt") | ||
assert agent.system_prompt == "Override prompt" | ||
|
||
|
||
def test_config_to_agent_file_prefix_required(): | ||
"""Test that file paths without file:// prefix work.""" | ||
import json | ||
import tempfile | ||
|
||
config_data = {"model": "test-model"} | ||
temp_path = "" | ||
|
||
# We need to create files like this for windows compatibility | ||
try: | ||
with tempfile.NamedTemporaryFile(mode="w+", suffix=".json", delete=False) as f: | ||
json.dump(config_data, f) | ||
f.flush() | ||
temp_path = f.name | ||
|
||
agent = config_to_agent(temp_path) | ||
assert agent.model.config["model_id"] == "test-model" | ||
finally: | ||
# Clean up the temporary file | ||
if os.path.exists(temp_path): | ||
os.remove(temp_path) | ||
|
||
|
||
def test_config_to_agent_file_prefix_valid(): | ||
"""Test that file:// prefix is properly handled.""" | ||
config_data = {"model": "test-model", "prompt": "Test prompt"} | ||
temp_path = "" | ||
|
||
try: | ||
with tempfile.NamedTemporaryFile(mode="w+", suffix=".json", delete=False) as f: | ||
json.dump(config_data, f) | ||
f.flush() | ||
temp_path = f.name | ||
|
||
agent = config_to_agent(f"file://{temp_path}") | ||
assert agent.model.config["model_id"] == "test-model" | ||
assert agent.system_prompt == "Test prompt" | ||
finally: | ||
# Clean up the temporary file | ||
if os.path.exists(temp_path): | ||
os.remove(temp_path) | ||
|
||
|
||
def test_config_to_agent_file_not_found(): | ||
"""Test that FileNotFoundError is raised for missing files.""" | ||
with pytest.raises(FileNotFoundError, match="Configuration file not found"): | ||
config_to_agent("/nonexistent/path/config.json") | ||
|
||
|
||
def test_config_to_agent_invalid_json(): | ||
"""Test that JSONDecodeError is raised for invalid JSON.""" | ||
try: | ||
with tempfile.NamedTemporaryFile(mode="w+", suffix=".json", delete=False) as f: | ||
f.write("invalid json content") | ||
temp_path = f.name | ||
|
||
with pytest.raises(json.JSONDecodeError): | ||
config_to_agent(temp_path) | ||
finally: | ||
# Clean up the temporary file | ||
if os.path.exists(temp_path): | ||
os.remove(temp_path) | ||
|
||
|
||
def test_config_to_agent_invalid_config_type(): | ||
"""Test that ValueError is raised for invalid config types.""" | ||
with pytest.raises(ValueError, match="Config must be a file path string or dictionary"): | ||
config_to_agent(123) | ||
|
||
|
||
def test_config_to_agent_with_name(): | ||
"""Test config_to_agent handles agent name.""" | ||
config = {"model": "test-model", "name": "TestAgent"} | ||
agent = config_to_agent(config) | ||
assert agent.name == "TestAgent" | ||
|
||
|
||
def test_config_to_agent_ignores_none_values(): | ||
"""Test that None values in config are ignored.""" | ||
config = {"model": "test-model", "prompt": None, "name": None} | ||
agent = config_to_agent(config) | ||
assert agent.model.config["model_id"] == "test-model" | ||
# Agent should use its defaults for None values | ||
|
||
|
||
def test_config_to_agent_validation_error_invalid_field(): | ||
"""Test that invalid fields raise validation errors.""" | ||
config = {"model": "test-model", "invalid_field": "value"} | ||
with pytest.raises(ValueError, match="Configuration validation error"): | ||
config_to_agent(config) | ||
|
||
|
||
def test_config_to_agent_validation_error_wrong_type(): | ||
"""Test that wrong field types raise validation errors.""" | ||
config = {"model": "test-model", "tools": "not-a-list"} | ||
with pytest.raises(ValueError, match="Configuration validation error"): | ||
config_to_agent(config) | ||
|
||
|
||
def test_config_to_agent_validation_error_invalid_tool_item(): | ||
"""Test that invalid tool items raise validation errors.""" | ||
config = {"model": "test-model", "tools": ["valid-tool", 123]} | ||
with pytest.raises(ValueError, match="Configuration validation error"): | ||
config_to_agent(config) | ||
|
||
|
||
def test_config_to_agent_validation_error_invalid_tool(): | ||
"""Test that invalid tools raise helpful error messages.""" | ||
config = {"model": "test-model", "tools": ["nonexistent_tool"]} | ||
with pytest.raises(ValueError, match="Failed to load tool nonexistent_tool"): | ||
config_to_agent(config) | ||
|
||
|
||
def test_config_to_agent_validation_error_missing_module(): | ||
"""Test that missing modules raise helpful error messages.""" | ||
config = {"model": "test-model", "tools": ["nonexistent.module.tool"]} | ||
with pytest.raises(ValueError, match="Failed to load tool nonexistent.module.tool"): | ||
config_to_agent(config) | ||
|
||
|
||
def test_config_to_agent_validation_error_missing_function(): | ||
"""Test that missing functions in existing modules raise helpful error messages.""" | ||
config = {"model": "test-model", "tools": ["json.nonexistent_function"]} | ||
with pytest.raises(ValueError, match="Failed to load tool json.nonexistent_function"): | ||
config_to_agent(config) | ||
|
||
|
||
def test_config_to_agent_with_tool(): | ||
"""Test that missing functions in existing modules raise helpful error messages.""" | ||
config = {"model": "test-model", "tools": ["tests.fixtures.say_tool:say"]} | ||
agent = config_to_agent(config) | ||
assert "say" in agent.tool_names |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
from strands import tool | ||
|
||
|
||
@tool | ||
def say(input: str) -> str: | ||
"""Say the input""" | ||
return f"Said: {input}" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"model": "global.anthropic.claude-sonnet-4-5-20250929-v1:0", | ||
"tools": ["tests_integ.fixtures.say_tool:say"], | ||
"prompt": "You use the say tool to communicate", | ||
"name": "Sayer" | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
from strands.experimental import config_to_agent | ||
|
||
|
||
def test_load_agent_from_config(): | ||
agent = config_to_agent("file://tests_integ/fixtures/test_agent.json") | ||
|
||
result = agent("Say hello") | ||
|
||
assert "Sayer" == agent.name | ||
assert "You use the say tool to communicate" == agent.system_prompt | ||
assert agent.tool_names[0] == "say" | ||
assert agent.model.get_config().get("model_id") == "global.anthropic.claude-sonnet-4-5-20250929-v1:0" | ||
assert "hello" in str(result).lower() |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.