-
Notifications
You must be signed in to change notification settings - Fork 241
refactor: update tool registry interface #468
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?
refactor: update tool registry interface #468
Conversation
src/strands/tools/registry.py
Outdated
except Exception as e: | ||
logger.warning("tool_name=<%s> | failed to create function tool | %s", name, e) | ||
|
||
return tools | ||
|
||
def _update_tool_config(self, tool_config: Dict[str, Any], new_tool: NewToolDict) -> None: |
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.
Why do we need NewToolDict
here? Can we just pass in ToolSpec
which is already defined?
src/strands/tools/registry.py
Outdated
providing a clean single interface for all tool registration needs. This replaces | ||
the previous multi-step pattern for better separation of concerns. |
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.
Lets not mention what this replaces
src/strands/tools/registry.py
Outdated
- Dictionaries with name/path keys | ||
- Instance of an AgentTool | ||
load_tools_from_directory: Whether to automatically discover and load tools | ||
from the ./tools/ directory. |
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.
nit: Indents seem a little off here
src/strands/tools/registry.py
Outdated
logger.debug("tool_modules=<%s> | discovered", list(tool_modules.keys())) | ||
return tool_modules | ||
|
||
def _initialize_tools(self, load_tools_from_directory: bool = False) -> None: |
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 we rename this to something like _load_tools_from_directory
? And then instead of passing in the boolean, we conditionally call the method?
src/strands/tools/registry.py
Outdated
self.registry: Dict[str, AgentTool] = {} | ||
self.dynamic_tools: Dict[str, AgentTool] = {} | ||
self.tool_config: Optional[Dict[str, Any]] = None | ||
|
||
def register_tools( |
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.
What methods in this class are meant to be public or private? If the only public method is register_tools
, can we put an _
before the method names of the others?
src/strands/tools/registry.py
Outdated
self.registry: Dict[str, AgentTool] = {} | ||
self.dynamic_tools: Dict[str, AgentTool] = {} | ||
self.tool_config: Optional[Dict[str, Any]] = None |
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.
Are these attributes public or private? If private, and we put an _
prefix on them?
src/strands/tools/registry.py
Outdated
except Exception: | ||
# Restore original state on any failure (atomic operation) | ||
self.registry = original_registry | ||
self.dynamic_tools = original_dynamic_tools | ||
raise |
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.
Do we need to restore the tools here? If there is an unknown exception, it wont really matter if the tools are restored or not right? This should also simplify the logic in this function.
d523d6e
to
bfbd518
Compare
bfbd518
to
2033bbd
Compare
src/strands/agent/agent.py
Outdated
@@ -285,15 +284,18 @@ def __init__( | |||
self.record_direct_tool_call = record_direct_tool_call | |||
self.load_tools_from_directory = load_tools_from_directory |
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.
It looks like we don't use this outside of init anymore so I think we can go ahead and remove it as a member variable.
else: | ||
logger.warning("tool=<%s> | unrecognized tool specification", tool) | ||
# Process the tool - this will validate and register the tool | ||
created_tool_name = self._process_tool(tool, validate_unique=True) |
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.
It looks like we are checking if tool name is unique multiple times. We should consolidate this logic.
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.
_process_tool
is extracting the name with similar logic to _extract_tool_name
. We should try to remove the duplication.
src/strands/tools/registry.py
Outdated
# Update tool configuration | ||
if created_tool_name in self.agent_tools: | ||
tool_spec = self.agent_tools[created_tool_name].tool_spec | ||
self._update_tool_config(self._tool_config, tool_spec) |
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.
What is the purpose of _tool_config
? I only see us adding to it and deleting from it. I'm not seeing where we actually use it.
src/strands/tools/registry.py
Outdated
spec = normalize_tool_spec(spec) | ||
self._validate_tool_spec(spec) |
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.
Do we need to normalize and validate the tool spec on every read? For efficiency, is this something that can be done upon creating or updating the tool?
src/strands/tools/registry.py
Outdated
return tool_name | ||
except Exception as e: | ||
# Restore the original tool if the update fails | ||
if original_tool: |
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.
Nit: We shouldn't need this condition. original_tool
should be guaranteed to exist.
Raises: | ||
ValueError: If the specified tool doesn't exist. | ||
""" | ||
if tool_name is None: |
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.
Nit: We shouldn't need this condition since we are using type hints.
src/strands/tools/registry.py
Outdated
|
||
# Make a backup of the tool in case we need to restore it | ||
original_tool = self.agent_tools.get(tool_name) | ||
original_hot_reload = self._hot_reload_tools.get(tool_name) |
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.
What is the difference between original_tool and original_hot_reload? Also, how is _hot_reload_tools
used? I only see us adding to it and deleting from it. I don't see us using it though.
src/strands/tools/registry.py
Outdated
spec = normalize_tool_spec(spec) | ||
self._validate_tool_spec(spec) |
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.
Same question as in read_tool, do we need to normalize and validate here?
Do you think there is a way to simplify the tool registry? I see we are adding 200 more lines of code and I am worried it's going to become a bit more challenging to maintain. Just looking at some of the method names, it seems like their might be some redundancy we can eliminate. For example, I see we have the following:
|
src/strands/agent/agent.py
Outdated
@@ -285,15 +284,18 @@ def __init__( | |||
self.record_direct_tool_call = record_direct_tool_call | |||
self.load_tools_from_directory = load_tools_from_directory | |||
|
|||
self.tool_registry = ToolRegistry() | |||
# Initialize the tool registry with the directory loading option | |||
self.tool_registry = ToolRegistry(load_tools_from_directory=self.load_tools_from_directory) |
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.
Since we're changing this interface can we also allow the directories to be passed for watching ^^
Idea is we're only allowing ./tools
directory to be loaded but customers can pass their directories like:
Agent(tool_directories=["./tools", -> default, ...]
if tools is not None: | ||
self.tool_registry.process_tools(tools) | ||
# Initialize individual tools if provided | ||
for tool in tools or []: |
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.
nit: for tool in tools or []:
do we need or []
here ^^ 🤔
src/strands/tools/registry.py
Outdated
This module provides the central registry for all tools available to the agent, including discovery, validation, and | ||
invocation capabilities. | ||
This module provides a central registry for managing agent tools, including discovery, | ||
validation, registration, and invocation capabilities. |
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 we add detailed docstrings here for agents, so they can immediately understand how to use the registry 🤔 💭
src/strands/tools/registry.py
Outdated
updated_tools.append(tool_name) | ||
except Exception as e: | ||
logger.warning("tool_name=<%s> | failed to update from directory | %s", tool_name, e) | ||
# Fail atomically - if any tool fails to update, the entire operation fails |
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.
I believe if one tool failed to update we can skip that tool to be loaded instead of raising a ValueError ^^
A list of Path objects for current working directory's "./tools/". | ||
""" | ||
# Current working directory's tools directory | ||
cwd_tools_dir = Path.cwd() / "tools" |
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 we allow people to set the tools directories from Agent interface?
src/strands/tools/registry.py
Outdated
@@ -24,244 +24,318 @@ | |||
|
|||
|
|||
class ToolRegistry: | |||
"""Central registry for all tools available to the agent. | |||
"""Agent Tool CRUD Interface. |
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.
I think the "Central registry for all tools available to the agent." hits closer to the mark TBH. As someone reading through the code, the next sentence tells me the same thing so I'd rather see something like:
Contains the collection of tools available to the agent.
if load_tools_from_directory: | ||
self._load_directory_tools() | ||
|
||
def create_tool(self, tool: Union[str, dict[str, str], Any]) -> str: |
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.
I'm not sure that create
makes sense here. In some cases, it's wrapping an instance of an AgentTool, in other cases it's adding it directly. IMHO, here it makes more sense that this would be called add_tool
or simply add
if load_tools_from_directory: | ||
self._load_directory_tools() | ||
|
||
def create_tool(self, tool: Union[str, dict[str, str], Any]) -> str: |
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.
Any reason not to return the wrapped tool instance AgentTool
vs the name of the tool?
|
||
return created_tool_name | ||
|
||
def read_tool(self, tool_name: str) -> ToolSpec: |
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.
Per the description, this seems more like read_tool_spec
instead of read_tool
try: | ||
# Update the tool | ||
self._update_single_tool(tool, validate_unique=False) | ||
return tool_name |
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.
I'd rather see us returning the AgentTool
which already has the name attached AFAIK
logger.warning("tool_name=<%s> | failed to update tool | %s", tool_name, e) | ||
raise ValueError(f"Failed to update tool '{tool_name}': {str(e)}") from e | ||
|
||
def delete_tool(self, tool_name: str) -> str: |
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.
Given that this class is named ToolRegistry
perhaps we can name this as just delete
? Same with the other {verb}_{noun}
names?
# Register in dynamic tools if applicable | ||
if tool.is_dynamic: | ||
self.dynamic_tools[tool.tool_name] = tool | ||
def list_tools(self) -> Dict[str, ToolSpec]: |
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.
It's a little weird that a list
method is returning a dictionary. Would this be better named as get_tool_specs
or something?
2033bbd
to
30a4d42
Compare
Description
[DO NOT MERGE SINCE THIS IS A BACKWARDS INCOMPATIBLE CHANGE]
New CRUDL interface for Tool Registry for customers to directly use in their codebase
create_tool(tool)
read_tool(tool_name)
update_tool(tool)
delete_tool(tool_name)
list_tools()
Related Issues
https://github.com/strands-agents/private-sdk-python-staging/issues/26
Documentation PR
N/A
Type of Change
Breaking change to the ToolRegistry interface
Testing
hatch run prepare
Checklist
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.