Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
ac4ae10
feat: add experimental AgentConfig with comprehensive tool management
mrlee-amazon Sep 26, 2025
c87fe46
fix: remove AgentConfig import from experimental/__init__.py
mrlee-amazon Sep 26, 2025
065d80a
fix: remove strands-agents-tools test dependency
mrlee-amazon Sep 26, 2025
1578035
test: remove test that depends on strands_tools availability
mrlee-amazon Sep 26, 2025
c5caec0
test: add back tests with proper mocking for strands_tools
mrlee-amazon Sep 26, 2025
ebfa3c9
test: fix Windows compatibility for file prefix test
mrlee-amazon Sep 26, 2025
675774c
refactor: replace AgentConfig class with config_to_agent function
mrlee-amazon Sep 26, 2025
40ae6aa
feat: limit config_to_agent to core configuration keys
mrlee-amazon Sep 26, 2025
4f2c8c0
fix: use native Python typing instead of typing module
mrlee-amazon Sep 26, 2025
5390d7d
test: simplify file prefix test with proper context manager
mrlee-amazon Sep 26, 2025
54cb4db
feat: add JSON schema validation to config_to_agent
mrlee-amazon Sep 26, 2025
5873914
refactor: move JSON schema to separate file
mrlee-amazon Sep 26, 2025
229decf
perf: use pre-compiled JSON schema validator
mrlee-amazon Sep 26, 2025
5607086
feat: add tool validation and clarify limitations
mrlee-amazon Sep 27, 2025
dab85b5
fix: improve tool validation error messages and add comprehensive tests
mrlee-amazon Sep 27, 2025
66e53a4
fix: reference module instead of tool in error message
mrlee-amazon Sep 27, 2025
cb507dd
revert: change error message back to reference tool
mrlee-amazon Sep 27, 2025
9f33f62
feat: use agent tool loading logic
Unshure Oct 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ __pycache__*
.vscode
dist
repl_state
.kiro
.kiro
uv.lock
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ dependencies = [
"boto3>=1.26.0,<2.0.0",
"botocore>=1.29.0,<2.0.0",
"docstring_parser>=0.15,<1.0",
"jsonschema>=4.0.0,<5.0.0",
"mcp>=1.11.0,<2.0.0",
"pydantic>=2.4.0,<3.0.0",
"typing-extensions>=4.13.2,<5.0.0",
Expand Down
4 changes: 4 additions & 0 deletions src/strands/experimental/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@

Copy link
Member

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

This module implements experimental features that are subject to change in future revisions without notice.
"""

from .agent_config import config_to_agent

__all__ = ["config_to_agent"]
138 changes: 138 additions & 0 deletions src/strands/experimental/agent_config.py
Copy link
Member

Choose a reason for hiding this comment

The 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"))])
"""

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:
Copy link
Member

Choose a reason for hiding this comment

The 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")
agent.process_tools([ToolWithConfigArg(HttpsConnection("localhost"))])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have a top level agent method process_tools?

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"
Copy link
Member

Choose a reason for hiding this comment

The 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

# Prepare Agent constructor arguments
agent_kwargs = {}

# Map configuration keys to Agent constructor parameters
config_mapping = {
"model": "model",
Copy link
Member

Choose a reason for hiding this comment

The 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)
Copy link
Member

Choose a reason for hiding this comment

The 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?

172 changes: 172 additions & 0 deletions tests/strands/experimental/test_agent_config.py
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