77import pydantic
88from agentstack .exceptions import ValidationError
99from agentstack .utils import get_package_path , open_json_file , term_color , snake_to_camel
10+ from agentstack import conf , log
1011
1112
1213TOOLS_DIR : Path = get_package_path () / '_tools' # NOTE: if you change this dir, also update MANIFEST.in
1314TOOLS_CONFIG_FILENAME : str = 'config.json'
1415
1516
17+ def _get_custom_tool_path (name : str ) -> Path :
18+ """Get the path to a custom tool."""
19+ return conf .PATH / 'src/tools' / name / TOOLS_CONFIG_FILENAME
20+
21+
22+ def _get_builtin_tool_path (name : str ) -> Path :
23+ """Get the path to a builtin tool."""
24+ return TOOLS_DIR / name / TOOLS_CONFIG_FILENAME
25+
26+
1627class ToolConfig (pydantic .BaseModel ):
1728 """
1829 This represents the configuration data for a tool.
@@ -32,8 +43,14 @@ class ToolConfig(pydantic.BaseModel):
3243
3344 @classmethod
3445 def from_tool_name (cls , name : str ) -> 'ToolConfig' :
35- path = TOOLS_DIR / name / TOOLS_CONFIG_FILENAME
36- if not os .path .exists (path ):
46+ # First check in the user's project directory for custom tools
47+ custom_path = _get_custom_tool_path (name )
48+ if custom_path .exists ():
49+ return cls .from_json (custom_path )
50+
51+ # Then check in the package's tools directory
52+ path = _get_builtin_tool_path (name )
53+ if not path .exists ():
3754 raise ValidationError (f'No known agentstack tool: { name } ' )
3855 return cls .from_json (path )
3956
@@ -48,6 +65,14 @@ def from_json(cls, path: Path) -> 'ToolConfig':
4865 error_str += f"{ ' ' .join ([str (loc ) for loc in error ['loc' ]])} : { error ['msg' ]} \n "
4966 raise ValidationError (f"Error loading tool from { path } .\n { error_str } " )
5067
68+ def write_to_file (self , filename : Path ):
69+ """Write the tool config to a json file."""
70+ if not filename .suffix == '.json' :
71+ raise ValidationError (f"Filename must end with .json: { filename } " )
72+
73+ with open (filename , 'w' ) as f :
74+ f .write (self .model_dump_json ())
75+
5176 @property
5277 def type (self ) -> type :
5378 """
@@ -74,6 +99,12 @@ def not_implemented(*args, **kwargs):
7499 @property
75100 def module_name (self ) -> str :
76101 """Module name for the tool module."""
102+ # Check if this is a custom tool in the user's project
103+ custom_path = _get_custom_tool_path (self .name )
104+ if custom_path .exists ():
105+ return f"src.tools.{ self .name } "
106+
107+ # Otherwise, it's a package tool
77108 return f"agentstack._tools.{ self .name } "
78109
79110 @property
@@ -105,19 +136,36 @@ def get_all_tool_paths() -> list[Path]:
105136 Get all the paths to the tool configuration files.
106137 ie. agentstack/_tools/<tool_name>/
107138 Tools are identified by having a `config.json` file inside the _tools/<tool_name> directory.
139+ Also checks the user's project directory for custom tools.
108140 """
109141 paths = []
142+
143+ # Get package tools
110144 for tool_dir in TOOLS_DIR .iterdir ():
111145 if tool_dir .is_dir ():
112146 config_path = tool_dir / TOOLS_CONFIG_FILENAME
113147 if config_path .exists ():
114148 paths .append (tool_dir )
149+
150+ # Get custom tools from user's project if in a project directory
151+ if conf .PATH :
152+ custom_tools_dir = conf .PATH / 'src/tools'
153+ if custom_tools_dir .exists ():
154+ for tool_dir in custom_tools_dir .iterdir ():
155+ if tool_dir .is_dir ():
156+ config_path = tool_dir / TOOLS_CONFIG_FILENAME
157+ if config_path .exists ():
158+ paths .append (tool_dir )
159+
115160 return paths
116161
117162
118163def get_all_tool_names () -> list [str ]:
119- return [path .stem for path in get_all_tool_paths ()]
164+ """Get names of all available tools, including custom tools."""
165+ return [path .name for path in get_all_tool_paths ()]
120166
121167
122168def get_all_tools () -> list [ToolConfig ]:
123- return [ToolConfig .from_tool_name (path ) for path in get_all_tool_names ()]
169+ """Get all tool configs, including custom tools."""
170+ tool_names = get_all_tool_names ()
171+ return [ToolConfig .from_tool_name (name ) for name in tool_names ]
0 commit comments