diff --git a/README.md b/README.md index faa94e38..90ed8aeb 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ A list of all tools can be found [on our docs](https://docs.agentstack.sh/tools/ Adding tools is as simple as ```bash -agentstack tools add +agentstack tools add ``` ## Running Your Agent diff --git a/agentstack/cli/__init__.py b/agentstack/cli/__init__.py index 94f53ebf..32c08ec3 100644 --- a/agentstack/cli/__init__.py +++ b/agentstack/cli/__init__.py @@ -1,2 +1,3 @@ -from .cli import init_project_builder, list_tools, configure_default_model, export_template +from .cli import init_project_builder, configure_default_model, export_template +from .tools import list_tools, add_tool from .run import run_project \ No newline at end of file diff --git a/agentstack/cli/cli.py b/agentstack/cli/cli.py index 99a6a393..0c085d5d 100644 --- a/agentstack/cli/cli.py +++ b/agentstack/cli/cli.py @@ -6,8 +6,6 @@ import json import shutil -import itertools - from art import text2art import inquirer from cookiecutter.main import cookiecutter @@ -22,7 +20,6 @@ from agentstack import conf from agentstack.conf import ConfigFile from agentstack.utils import get_package_path -from agentstack.tools import get_all_tools from agentstack.generation.files import ProjectFile from agentstack import frameworks from agentstack import generation @@ -440,25 +437,6 @@ def insert_template( ) -def list_tools(): - # Display the tools - tools = get_all_tools() - curr_category = None - - print("\n\nAvailable AgentStack Tools:") - for category, tools in itertools.groupby(tools, lambda x: x.category): - if curr_category != category: - print(f"\n{category}:") - curr_category = category - for tool in tools: - print(" - ", end='') - print(term_color(f"{tool.name}", 'blue'), end='') - print(f": {tool.url if tool.url else 'AgentStack default tool'}") - - print("\n\n✨ Add a tool with: agentstack tools add ") - print(" https://docs.agentstack.sh/tools/core") - - def export_template(output_filename: str): """ Export the current project as a template. diff --git a/agentstack/cli/tools.py b/agentstack/cli/tools.py new file mode 100644 index 00000000..bee8a537 --- /dev/null +++ b/agentstack/cli/tools.py @@ -0,0 +1,69 @@ +from typing import Optional +import itertools +import inquirer +from agentstack.utils import term_color +from agentstack import generation +from agentstack.tools import get_all_tools +from agentstack.agents import get_all_agents + + +def list_tools(): + """ + List all available tools by category. + """ + tools = get_all_tools() + curr_category = None + + print("\n\nAvailable AgentStack Tools:") + for category, tools in itertools.groupby(tools, lambda x: x.category): + if curr_category != category: + print(f"\n{category}:") + curr_category = category + for tool in tools: + print(" - ", end='') + print(term_color(f"{tool.name}", 'blue'), end='') + print(f": {tool.url if tool.url else 'AgentStack default tool'}") + + print("\n\n✨ Add a tool with: agentstack tools add ") + print(" https://docs.agentstack.sh/tools/core") + + +def add_tool(tool_name: Optional[str], agents=Optional[list[str]]): + """ + Add a tool to the user's project. + If no tool name is provided: + - prompt the user to select a tool + - prompt the user to select one or more agents + If a tool name is provided: + - add the tool to the user's project + - add the tool to the specified agents or all agents if none are specified + """ + if not tool_name: + # 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()], + ) + ] + try: + tool_name = inquirer.prompt(tools_list)['tool_name'] + except TypeError: + return # user cancelled the prompt + + # ask the user for the agents to add the tool to + agents_list = [ + inquirer.Checkbox( + "agents", + message="Select which agents to make the tool available to", + choices=[agent.name for agent in get_all_agents()], + ) + ] + try: + agents = inquirer.prompt(agents_list)['agents'] + except TypeError: + return # user cancelled the prompt + + assert tool_name # appease type checker + generation.add_tool(tool_name, agents=agents) diff --git a/agentstack/frameworks/crewai.py b/agentstack/frameworks/crewai.py index a88d4cff..69d21fb7 100644 --- a/agentstack/frameworks/crewai.py +++ b/agentstack/frameworks/crewai.py @@ -152,17 +152,27 @@ def add_agent_tools(self, agent_name: str, tool: ToolConfig): if method is None: raise ValidationError(f"`@agent` method `{agent_name}` does not exist in {ENTRYPOINT}") - new_tool_nodes: set[ast.expr] = set() - for tool_name in tool.tools: - # This prefixes the tool name with the 'tools' module - node: ast.expr = asttools.create_attribute('tools', tool_name) - if tool.tools_bundled: # Splat the variable if it's bundled - node = ast.Starred(value=node, ctx=ast.Load()) - new_tool_nodes.add(node) - existing_node: ast.List = self.get_agent_tools(agent_name) - elts: set[ast.expr] = set(existing_node.elts) | new_tool_nodes - new_node = ast.List(elts=list(elts), ctx=ast.Load()) + existing_elts: list[ast.expr] = existing_node.elts + + new_tool_nodes: list[ast.expr] = [] + for tool_name in tool.tools: + # TODO there is definitely a better way to do this. We can't use + # a `set` becasue the ast nodes are unique objects. + _found = False + for elt in existing_elts: + if str(asttools.get_node_value(elt)) == tool_name: + _found = True + break # skip if the tool is already in the list + + if not _found: + # This prefixes the tool name with the 'tools' module + node: ast.expr = asttools.create_attribute('tools', tool_name) + if tool.tools_bundled: # Splat the variable if it's bundled + node = ast.Starred(value=node, ctx=ast.Load()) + existing_elts.append(node) + + new_node = ast.List(elts=existing_elts, ctx=ast.Load()) start, end = self.get_node_range(existing_node) self.edit_node_range(start, end, new_node) diff --git a/agentstack/generation/tool_generation.py b/agentstack/generation/tool_generation.py index 0490e844..314ff481 100644 --- a/agentstack/generation/tool_generation.py +++ b/agentstack/generation/tool_generation.py @@ -80,46 +80,46 @@ def remove_import_for_tool(self, tool: ToolConfig, framework: str): def add_tool(tool_name: str, agents: Optional[list[str]] = []): agentstack_config = ConfigFile() + tool = ToolConfig.from_tool_name(tool_name) if tool_name in agentstack_config.tools: - print(term_color(f'Tool {tool_name} is already installed', 'red')) - sys.exit(1) + print(term_color(f'Tool {tool_name} is already installed', 'blue')) + else: # handle install + tool_file_path = tool.get_impl_file_path(agentstack_config.framework) - tool = ToolConfig.from_tool_name(tool_name) - tool_file_path = tool.get_impl_file_path(agentstack_config.framework) + if tool.packages: + packaging.install(' '.join(tool.packages)) - if tool.packages: - packaging.install(' '.join(tool.packages)) + # Move tool from package to project + shutil.copy(tool_file_path, conf.PATH / f'src/tools/{tool.module_name}.py') - # Move tool from package to project - shutil.copy(tool_file_path, conf.PATH / f'src/tools/{tool.module_name}.py') + try: # Edit the user's project tool init file to include the tool + with ToolsInitFile(conf.PATH / TOOLS_INIT_FILENAME) as tools_init: + tools_init.add_import_for_tool(tool, agentstack_config.framework) + except ValidationError as e: + print(term_color(f"Error adding tool:\n{e}", 'red')) - try: # Edit the user's project tool init file to include the tool - with ToolsInitFile(conf.PATH / TOOLS_INIT_FILENAME) as tools_init: - tools_init.add_import_for_tool(tool, agentstack_config.framework) - except ValidationError as e: - print(term_color(f"Error adding tool:\n{e}", 'red')) + if tool.env: # add environment variables which don't exist + with EnvFile() as env: + for var, value in tool.env.items(): + env.append_if_new(var, value) + with EnvFile(".env.example") as env: + for var, value in tool.env.items(): + env.append_if_new(var, value) + + if tool.post_install: + os.system(tool.post_install) + + with agentstack_config as config: + config.tools.append(tool.name) # Edit the framework entrypoint file to include the tool in the agent definition if not agents: # If no agents are specified, add the tool to all agents agents = frameworks.get_agent_names() for agent_name in agents: + print(f'Adding tool {tool.name} to agent {agent_name}') frameworks.add_tool(tool, agent_name) - if tool.env: # add environment variables which don't exist - with EnvFile() as env: - for var, value in tool.env.items(): - env.append_if_new(var, value) - with EnvFile(".env.example") as env: - for var, value in tool.env.items(): - env.append_if_new(var, value) - - if tool.post_install: - os.system(tool.post_install) - - with agentstack_config as config: - config.tools.append(tool.name) - print(term_color(f'🔨 Tool {tool.name} added to agentstack project successfully', 'green')) if tool.cta: print(term_color(f'🪩 {tool.cta}', 'blue')) diff --git a/agentstack/main.py b/agentstack/main.py index 8044e339..eac6482a 100644 --- a/agentstack/main.py +++ b/agentstack/main.py @@ -5,6 +5,7 @@ from agentstack import conf from agentstack.cli import ( init_project_builder, + add_tool, list_tools, configure_default_model, run_project, @@ -120,7 +121,7 @@ def main(): tools_add_parser = tools_subparsers.add_parser( "add", aliases=["a"], help="Add a new tool", parents=[global_parser] ) - tools_add_parser.add_argument("name", help="Name of the tool to add") + tools_add_parser.add_argument("name", help="Name of the tool to add", nargs="?") tools_add_parser.add_argument( "--agents", "-a", help="Name of agents to add this tool to, comma separated" ) @@ -179,7 +180,7 @@ def main(): elif args.tools_command in ["add", "a"]: agents = [args.agent] if args.agent else None agents = args.agents.split(",") if args.agents else agents - generation.add_tool(args.name, agents=agents) + add_tool(args.name, agents) elif args.tools_command in ["remove", "r"]: generation.remove_tool(args.name) else: diff --git a/agentstack/templates/crewai/{{cookiecutter.project_metadata.project_slug}}/README.md b/agentstack/templates/crewai/{{cookiecutter.project_metadata.project_slug}}/README.md index 514e0088..889ea879 100644 --- a/agentstack/templates/crewai/{{cookiecutter.project_metadata.project_slug}}/README.md +++ b/agentstack/templates/crewai/{{cookiecutter.project_metadata.project_slug}}/README.md @@ -15,7 +15,7 @@ This will automatically create a new agent in the `agents.yaml` config as well a Similarly, tasks can be created with `agentstack g t ` -Add tools with `agentstack tools add ` and view tools available with `agentstack tools list` +Add tools with `agentstack tools add` and view tools available with `agentstack tools list` ## How to use your Crew In this directory, run `poetry install` diff --git a/docs/tools/tools.mdx b/docs/tools/tools.mdx index cfa723b8..35f67142 100644 --- a/docs/tools/tools.mdx +++ b/docs/tools/tools.mdx @@ -7,5 +7,10 @@ description: 'Giving your agents tools should be easy' Once you find the right tool for your use-case, install it with simply ```bash -agentstack tools add +agentstack tools add +``` + +You can also specify a tool, and one or more agents to install it to: +```bash +agentstack tools add --agents=, ``` \ No newline at end of file