-
Notifications
You must be signed in to change notification settings - Fork 425
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
base: main
Are you sure you want to change the base?
Changes from all commits
ac4ae10
c87fe46
065d80a
1578035
c5caec0
ebfa3c9
675774c
40ae6aa
4f2c8c0
5390d7d
54cb4db
5873914
229decf
5607086
dab85b5
66e53a4
cb507dd
9f33f62
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,4 +11,5 @@ __pycache__* | |
.vscode | ||
dist | ||
repl_state | ||
.kiro | ||
.kiro | ||
uv.lock |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you update the PR description to include the latest information & the goal of the feature since the latest update |
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.process_tools([ToolWithConfigArg(HttpsConnection("localhost"))]) | ||
Unshure marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" | ||
|
||
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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. are these kwargs, invocation_state, both, or something that is now specific to config_to_agent? |
||
"""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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we have a top level agent method |
||
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" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if theres no absolute_path it is from root? |
||
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", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. blocker: can we support mcp_servers, a2a_servers, load_tool_from_directory, system_prompt (not as prompt given below), provider, session id, s3 session support etc here? |
||
"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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we support multi-agents as array of agents instead of just support one agent? |
Unshure marked this conversation as resolved.
Show resolved
Hide resolved
|
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you add an integ test here