Skip to content

Commit 1f96437

Browse files
committed
Interactively add tools with agentstack tools add if no tool_name is passed.
Repeatedly add tools to different agents without reinstalling all supporting tools. Bugfix: Prevent duplicate tools from being added to the same agent.
1 parent e41b92a commit 1f96437

File tree

9 files changed

+129
-65
lines changed

9 files changed

+129
-65
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ A list of all tools can be found [on our docs](https://docs.agentstack.sh/tools/
8181
Adding tools is as simple as
8282

8383
```bash
84-
agentstack tools add <tool_name>
84+
agentstack tools add
8585
```
8686

8787
## Running Your Agent

agentstack/cli/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1-
from .cli import init_project_builder, list_tools, configure_default_model, export_template
1+
from .cli import init_project_builder, configure_default_model, export_template
2+
from .tools import list_tools, add_tool
23
from .run import run_project

agentstack/cli/cli.py

-22
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66

77
import json
88
import shutil
9-
import itertools
10-
119
from art import text2art
1210
import inquirer
1311
from cookiecutter.main import cookiecutter
@@ -22,7 +20,6 @@
2220
from agentstack import conf
2321
from agentstack.conf import ConfigFile
2422
from agentstack.utils import get_package_path
25-
from agentstack.tools import get_all_tools
2623
from agentstack.generation.files import ProjectFile
2724
from agentstack import frameworks
2825
from agentstack import generation
@@ -440,25 +437,6 @@ def insert_template(
440437
)
441438

442439

443-
def list_tools():
444-
# Display the tools
445-
tools = get_all_tools()
446-
curr_category = None
447-
448-
print("\n\nAvailable AgentStack Tools:")
449-
for category, tools in itertools.groupby(tools, lambda x: x.category):
450-
if curr_category != category:
451-
print(f"\n{category}:")
452-
curr_category = category
453-
for tool in tools:
454-
print(" - ", end='')
455-
print(term_color(f"{tool.name}", 'blue'), end='')
456-
print(f": {tool.url if tool.url else 'AgentStack default tool'}")
457-
458-
print("\n\n✨ Add a tool with: agentstack tools add <tool_name>")
459-
print(" https://docs.agentstack.sh/tools/core")
460-
461-
462440
def export_template(output_filename: str):
463441
"""
464442
Export the current project as a template.

agentstack/cli/tools.py

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
from typing import Optional
2+
import itertools
3+
import inquirer
4+
from agentstack.utils import term_color
5+
from agentstack import generation
6+
from agentstack.tools import get_all_tools
7+
from agentstack.agents import get_all_agents
8+
9+
10+
def list_tools():
11+
"""
12+
List all available tools by category.
13+
"""
14+
tools = get_all_tools()
15+
curr_category = None
16+
17+
print("\n\nAvailable AgentStack Tools:")
18+
for category, tools in itertools.groupby(tools, lambda x: x.category):
19+
if curr_category != category:
20+
print(f"\n{category}:")
21+
curr_category = category
22+
for tool in tools:
23+
print(" - ", end='')
24+
print(term_color(f"{tool.name}", 'blue'), end='')
25+
print(f": {tool.url if tool.url else 'AgentStack default tool'}")
26+
27+
print("\n\n✨ Add a tool with: agentstack tools add <tool_name>")
28+
print(" https://docs.agentstack.sh/tools/core")
29+
30+
31+
def add_tool(tool_name: Optional[str], agents=Optional[list[str]]):
32+
"""
33+
Add a tool to the user's project.
34+
If no tool name is provided:
35+
- prompt the user to select a tool
36+
- prompt the user to select one or more agents
37+
If a tool name is provided:
38+
- add the tool to the user's project
39+
- add the tool to the specified agents or all agents if none are specified
40+
"""
41+
if not tool_name:
42+
# ask the user for the tool name
43+
tools_list = [
44+
inquirer.List(
45+
"tool_name",
46+
message="Select a tool to add to your project",
47+
choices=[tool.name for tool in get_all_tools()],
48+
)
49+
]
50+
try:
51+
tool_name = inquirer.prompt(tools_list)['tool_name']
52+
except TypeError:
53+
return # user cancelled the prompt
54+
55+
# ask the user for the agents to add the tool to
56+
agents_list = [
57+
inquirer.Checkbox(
58+
"agents",
59+
message="Select which agents to make the tool available to",
60+
choices=[agent.name for agent in get_all_agents()],
61+
)
62+
]
63+
try:
64+
agents = inquirer.prompt(agents_list)['agents']
65+
except TypeError:
66+
return # user cancelled the prompt
67+
68+
assert tool_name # appease type checker
69+
generation.add_tool(tool_name, agents=agents)

agentstack/frameworks/crewai.py

+20-10
Original file line numberDiff line numberDiff line change
@@ -152,17 +152,27 @@ def add_agent_tools(self, agent_name: str, tool: ToolConfig):
152152
if method is None:
153153
raise ValidationError(f"`@agent` method `{agent_name}` does not exist in {ENTRYPOINT}")
154154

155-
new_tool_nodes: set[ast.expr] = set()
156-
for tool_name in tool.tools:
157-
# This prefixes the tool name with the 'tools' module
158-
node: ast.expr = asttools.create_attribute('tools', tool_name)
159-
if tool.tools_bundled: # Splat the variable if it's bundled
160-
node = ast.Starred(value=node, ctx=ast.Load())
161-
new_tool_nodes.add(node)
162-
163155
existing_node: ast.List = self.get_agent_tools(agent_name)
164-
elts: set[ast.expr] = set(existing_node.elts) | new_tool_nodes
165-
new_node = ast.List(elts=list(elts), ctx=ast.Load())
156+
existing_elts: list[ast.expr] = existing_node.elts
157+
158+
new_tool_nodes: list[ast.expr] = []
159+
for tool_name in tool.tools:
160+
# TODO there is definitely a better way to do this. We can't use
161+
# a `set` becasue the ast nodes are unique objects.
162+
_found = False
163+
for elt in existing_elts:
164+
if str(asttools.get_node_value(elt)) == tool_name:
165+
_found = True
166+
break # skip if the tool is already in the list
167+
168+
if not _found:
169+
# This prefixes the tool name with the 'tools' module
170+
node: ast.expr = asttools.create_attribute('tools', tool_name)
171+
if tool.tools_bundled: # Splat the variable if it's bundled
172+
node = ast.Starred(value=node, ctx=ast.Load())
173+
existing_elts.append(node)
174+
175+
new_node = ast.List(elts=existing_elts, ctx=ast.Load())
166176
start, end = self.get_node_range(existing_node)
167177
self.edit_node_range(start, end, new_node)
168178

agentstack/generation/tool_generation.py

+27-27
Original file line numberDiff line numberDiff line change
@@ -80,46 +80,46 @@ def remove_import_for_tool(self, tool: ToolConfig, framework: str):
8080

8181
def add_tool(tool_name: str, agents: Optional[list[str]] = []):
8282
agentstack_config = ConfigFile()
83+
tool = ToolConfig.from_tool_name(tool_name)
8384

8485
if tool_name in agentstack_config.tools:
85-
print(term_color(f'Tool {tool_name} is already installed', 'red'))
86-
sys.exit(1)
86+
print(term_color(f'Tool {tool_name} is already installed', 'blue'))
87+
else: # handle install
88+
tool_file_path = tool.get_impl_file_path(agentstack_config.framework)
8789

88-
tool = ToolConfig.from_tool_name(tool_name)
89-
tool_file_path = tool.get_impl_file_path(agentstack_config.framework)
90+
if tool.packages:
91+
packaging.install(' '.join(tool.packages))
9092

91-
if tool.packages:
92-
packaging.install(' '.join(tool.packages))
93+
# Move tool from package to project
94+
shutil.copy(tool_file_path, conf.PATH / f'src/tools/{tool.module_name}.py')
9395

94-
# Move tool from package to project
95-
shutil.copy(tool_file_path, conf.PATH / f'src/tools/{tool.module_name}.py')
96+
try: # Edit the user's project tool init file to include the tool
97+
with ToolsInitFile(conf.PATH / TOOLS_INIT_FILENAME) as tools_init:
98+
tools_init.add_import_for_tool(tool, agentstack_config.framework)
99+
except ValidationError as e:
100+
print(term_color(f"Error adding tool:\n{e}", 'red'))
96101

97-
try: # Edit the user's project tool init file to include the tool
98-
with ToolsInitFile(conf.PATH / TOOLS_INIT_FILENAME) as tools_init:
99-
tools_init.add_import_for_tool(tool, agentstack_config.framework)
100-
except ValidationError as e:
101-
print(term_color(f"Error adding tool:\n{e}", 'red'))
102+
if tool.env: # add environment variables which don't exist
103+
with EnvFile() as env:
104+
for var, value in tool.env.items():
105+
env.append_if_new(var, value)
106+
with EnvFile(".env.example") as env:
107+
for var, value in tool.env.items():
108+
env.append_if_new(var, value)
109+
110+
if tool.post_install:
111+
os.system(tool.post_install)
112+
113+
with agentstack_config as config:
114+
config.tools.append(tool.name)
102115

103116
# Edit the framework entrypoint file to include the tool in the agent definition
104117
if not agents: # If no agents are specified, add the tool to all agents
105118
agents = frameworks.get_agent_names()
106119
for agent_name in agents:
120+
print(f'Adding tool {tool.name} to agent {agent_name}')
107121
frameworks.add_tool(tool, agent_name)
108122

109-
if tool.env: # add environment variables which don't exist
110-
with EnvFile() as env:
111-
for var, value in tool.env.items():
112-
env.append_if_new(var, value)
113-
with EnvFile(".env.example") as env:
114-
for var, value in tool.env.items():
115-
env.append_if_new(var, value)
116-
117-
if tool.post_install:
118-
os.system(tool.post_install)
119-
120-
with agentstack_config as config:
121-
config.tools.append(tool.name)
122-
123123
print(term_color(f'🔨 Tool {tool.name} added to agentstack project successfully', 'green'))
124124
if tool.cta:
125125
print(term_color(f'🪩 {tool.cta}', 'blue'))

agentstack/main.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from agentstack import conf
66
from agentstack.cli import (
77
init_project_builder,
8+
add_tool,
89
list_tools,
910
configure_default_model,
1011
run_project,
@@ -120,7 +121,7 @@ def main():
120121
tools_add_parser = tools_subparsers.add_parser(
121122
"add", aliases=["a"], help="Add a new tool", parents=[global_parser]
122123
)
123-
tools_add_parser.add_argument("name", help="Name of the tool to add")
124+
tools_add_parser.add_argument("name", help="Name of the tool to add", nargs="?")
124125
tools_add_parser.add_argument(
125126
"--agents", "-a", help="Name of agents to add this tool to, comma separated"
126127
)
@@ -179,7 +180,7 @@ def main():
179180
elif args.tools_command in ["add", "a"]:
180181
agents = [args.agent] if args.agent else None
181182
agents = args.agents.split(",") if args.agents else agents
182-
generation.add_tool(args.name, agents=agents)
183+
add_tool(args.name, agents)
183184
elif args.tools_command in ["remove", "r"]:
184185
generation.remove_tool(args.name)
185186
else:

agentstack/templates/crewai/{{cookiecutter.project_metadata.project_slug}}/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ This will automatically create a new agent in the `agents.yaml` config as well a
1515

1616
Similarly, tasks can be created with `agentstack g t <tool_name>`
1717

18-
Add tools with `agentstack tools add <tool_name>` and view tools available with `agentstack tools list`
18+
Add tools with `agentstack tools add` and view tools available with `agentstack tools list`
1919

2020
## How to use your Crew
2121
In this directory, run `poetry install`

docs/tools/tools.mdx

+6-1
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,10 @@ description: 'Giving your agents tools should be easy'
77

88
Once you find the right tool for your use-case, install it with simply
99
```bash
10-
agentstack tools add <tool_name>
10+
agentstack tools add
11+
```
12+
13+
You can also specify a tool, and one or more agents to install it to:
14+
```bash
15+
agentstack tools add <tool_name> --agents=<agent_name1>,<agent_name2>
1116
```

0 commit comments

Comments
 (0)