Skip to content
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

Create custom tools #236

Merged
merged 14 commits into from
Feb 14, 2025
60 changes: 51 additions & 9 deletions agentstack/_tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@
import pydantic
from agentstack.exceptions import ValidationError
from agentstack.utils import get_package_path, open_json_file, term_color, snake_to_camel
from agentstack import conf
import logging


TOOLS_DIR: Path = get_package_path() / '_tools' # NOTE: if you change this dir, also update MANIFEST.in
TOOLS_CONFIG_FILENAME: str = 'config.json'

log = logging.getLogger(__name__)


class ToolConfig(pydantic.BaseModel):
"""
Expand All @@ -31,12 +35,26 @@ class ToolConfig(pydantic.BaseModel):
post_remove: Optional[str] = None

@classmethod
def from_tool_name(cls, name: str) -> 'ToolConfig':
def from_tool_name(cls, name: str) -> Optional['ToolConfig']:
# First check in the user's project directory for custom tools
if conf.PATH:
custom_path = conf.PATH / 'src/tools' / name / TOOLS_CONFIG_FILENAME
if custom_path.exists():
try:
return cls.from_json(custom_path)
except Exception as e:
log.debug(f"Failed to load custom tool {name}: {e}")
return None

# Then check in the package's tools directory
path = TOOLS_DIR / name / TOOLS_CONFIG_FILENAME
if not os.path.exists(path): # TODO raise exceptions and handle message/exit in cli
print(term_color(f'No known agentstack tool: {name}', 'red'))
sys.exit(1)
return cls.from_json(path)
if not os.path.exists(path):
return None
try:
return cls.from_json(path)
except Exception as e:
log.debug(f"Failed to load tool {name}: {e}")
return None

@classmethod
def from_json(cls, path: Path) -> 'ToolConfig':
Expand Down Expand Up @@ -76,6 +94,13 @@ def not_implemented(*args, **kwargs):
@property
def module_name(self) -> str:
"""Module name for the tool module."""
# Check if this is a custom tool in the user's project
if conf.PATH:
custom_path = conf.PATH / 'src/tools' / self.name / TOOLS_CONFIG_FILENAME
if custom_path.exists():
return f"src.tools.{self.name}"

# Otherwise, it's a package tool
return f"agentstack._tools.{self.name}"

@property
Expand Down Expand Up @@ -106,20 +131,37 @@ def get_all_tool_paths() -> list[Path]:
"""
Get all the paths to the tool configuration files.
ie. agentstack/_tools/<tool_name>/
Tools are identified by having a `config.json` file inside the _tools/<tool_name> directory.
Tools are identified by having a `config.json` file inside the _tools/<tool_name> directory.
Also checks the user's project directory for custom tools.
"""
paths = []

# Get package tools
for tool_dir in TOOLS_DIR.iterdir():
if tool_dir.is_dir():
config_path = tool_dir / TOOLS_CONFIG_FILENAME
if config_path.exists():
paths.append(tool_dir)

# Get custom tools from user's project if in a project directory
if conf.PATH:
custom_tools_dir = conf.PATH / 'src/tools'
if custom_tools_dir.exists():
for tool_dir in custom_tools_dir.iterdir():
if tool_dir.is_dir():
config_path = tool_dir / TOOLS_CONFIG_FILENAME
if config_path.exists():
paths.append(tool_dir)

return paths


def get_all_tool_names() -> list[str]:
return [path.stem for path in get_all_tool_paths()]
"""Get names of all available tools, including custom tools."""
return [path.name for path in get_all_tool_paths()]


def get_all_tools() -> list[ToolConfig]:
return [ToolConfig.from_tool_name(path) for path in get_all_tool_names()]
def get_all_tools() -> list[Optional[ToolConfig]]:
"""Get all tool configs, including custom tools."""
tool_names = get_all_tool_names()
return [ToolConfig.from_tool_name(name) for name in tool_names]
Binary file removed agentstack/_tools/py_sql/test.db
Binary file not shown.
66 changes: 0 additions & 66 deletions agentstack/_tools/py_sql/test.py

This file was deleted.

4 changes: 2 additions & 2 deletions agentstack/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .cli import init_project_builder, configure_default_model, export_template, welcome_message
from .init import init_project
from .tools import list_tools, add_tool
from .run import run_project
from .tools import list_tools, add_tool, create_tool
from .run import run_project
36 changes: 36 additions & 0 deletions agentstack/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,42 @@ def ask_tools() -> list:

return tools_to_add

def create_tool(tool_name: str):
user_tools_dir = Path('src/tools').resolve()
tool_path = user_tools_dir / tool_name
if tool_path.exists():
print(term_color(f"Tool '{tool_name}' already exists.", 'yellow'))
sys.exit(1)

# Create tool directory
tool_path.mkdir(parents=True, exist_ok=False)

# Create __init__.py with CrewAI tool decorator
init_file = tool_path / '__init__.py'
init_content = f"""# {tool_name} tool module

def main():
print("This is the {tool_name} tool. Implement your functionality here.")
"""
init_file.write_text(init_content)

# Create config.json with placeholders
config = {
"name": tool_name,
"category": "general", # default category
"tools": ["main"], # default tool method
"url": "",
"cta": "",
"env": {},
"dependencies": [],
"post_install": "",
"post_remove": ""
}
config_file = tool_path / 'config.json'
config_file.write_text(json.dumps(config, indent=4))

print(term_color(f"Tool '{tool_name}' has been created successfully in {user_tools_dir}.", 'green'))


def ask_project_details(slug_name: Optional[str] = None) -> dict:
name = inquirer.text(message="What's the name of your project (snake_case)", default=slug_name or '')
Expand Down
74 changes: 69 additions & 5 deletions agentstack/cli/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,27 @@
from agentstack import generation
from agentstack._tools import get_all_tools
from agentstack.agents import get_all_agents
from pathlib import Path
import sys
import json


def list_tools():
"""
List all available tools by category.
"""
tools = get_all_tools()
tools = [t for t in get_all_tools() if t is not None] # Filter out None values
categories = {}
custom_tools = []

# Group tools by category
for tool in tools:
if tool.category not in categories:
categories[tool.category] = []
categories[tool.category].append(tool)
if tool.category == 'custom':
custom_tools.append(tool)
else:
if tool.category not in categories:
categories[tool.category] = []
categories[tool.category].append(tool)

print("\n\nAvailable AgentStack Tools:")
# Display tools by category
Expand All @@ -29,7 +36,16 @@ def list_tools():
print(term_color(f"{tool.name}", 'blue'), end='')
print(f": {tool.url if tool.url else 'AgentStack default tool'}")

# Display custom tools if any exist
if custom_tools:
print("\nCustom Tools:")
for tool in custom_tools:
print(" - ", end='')
print(term_color(f"{tool.name}", 'blue'), end='')
print(": Custom tool")

print("\n\n✨ Add a tool with: agentstack tools add <tool_name>")
print(" Create a custom tool with: agentstack tools create <tool_name>")
print(" https://docs.agentstack.sh/tools/core")


Expand All @@ -44,12 +60,16 @@ def add_tool(tool_name: Optional[str], agents=Optional[list[str]]):
- add the tool to the specified agents or all agents if none are specified
"""
if not tool_name:
# Get all available tools including custom ones
available_tools = [t for t in get_all_tools() if t is not None]
tool_names = [t.name for t in available_tools]

# ask the user for the tool name
tools_list = [
inquirer.List(
"tool_name",
message="Select a tool to add to your project",
choices=[tool.name for tool in get_all_tools()],
choices=tool_names,
)
]
try:
Expand All @@ -72,3 +92,47 @@ def add_tool(tool_name: Optional[str], agents=Optional[list[str]]):

assert tool_name # appease type checker
generation.add_tool(tool_name, agents=agents)


def create_tool(tool_name: str):
"""Create a new custom tool.

Args:
tool_name: Name of the tool to create (must be snake_case)
"""
# Check if tool already exists
user_tools_dir = Path('src/tools').resolve()
tool_path = user_tools_dir / tool_name
if tool_path.exists():
print(term_color(f"Tool '{tool_name}' already exists.", 'yellow'))
sys.exit(1)

# Create tool directory
tool_path.mkdir(parents=True, exist_ok=False)

# Create __init__.py with basic function template
init_file = tool_path / '__init__.py'
init_content = f'''def define_your_tool():
"""
Define your tool's functionality here.
"""
pass
'''
init_file.write_text(init_content)

# Create config.json with basic structure
config = {
"name": tool_name,
"category": "custom",
"tools": ["define_your_tool"],
"url": "",
"cta": "",
"env": {},
"dependencies": [],
"post_install": "",
"post_remove": ""
}
config_file = tool_path / 'config.json'
config_file.write_text(json.dumps(config, indent=4))

print(term_color(f"Tool '{tool_name}' has been created successfully in {user_tools_dir}.", 'green'))
16 changes: 16 additions & 0 deletions agentstack/frameworks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ def validate_project(self) -> None:
"""
...

def create_tool(self, tool_name: str) -> None:
"""
Create a new custom tool in the user's project.
Args:
tool_name: Name of the tool to create (must be snake_case)
"""
...

def get_tool_names(self) -> list[str]:
"""
Get a list of tool names in the user's project.
Expand Down Expand Up @@ -169,3 +177,11 @@ def get_task_names() -> list[str]:
Get a list of task names in the user's project.
"""
return get_framework_module(get_framework()).get_task_names()


def create_tool(tool_name: str):
"""
Create a new custom tool in the user's project.
The tool will be created with a basic structure and configuration.
"""
return get_framework_module(get_framework()).create_tool(tool_name)
Loading