Skip to content

Commit 9694ab5

Browse files
authored
Merge pull request #49 from tcdent/issue-35
Implement tool removal command #35
2 parents 2ebaa93 + 5d972fa commit 9694ab5

17 files changed

+114
-34
lines changed

agentstack/generation/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
from .agent_generation import generate_agent
22
from .task_generation import generate_task
3-
from .tool_generation import add_tool
3+
from .tool_generation import add_tool, remove_tool

agentstack/generation/gen_utils.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,9 @@ def insert_after_tasks(file_path, code_to_insert):
5959
return True
6060
return False
6161

62+
63+
def string_in_file(file_path: str, str_to_match: str) -> bool:
64+
with open(file_path, 'r') as file:
65+
file_content = file.read()
66+
return str_to_match in file_content
67+

agentstack/generation/tool_generation.py

Lines changed: 88 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,71 +3,139 @@
33
import sys
44
from typing import Optional
55

6-
from .gen_utils import insert_code_after_tag
6+
from .gen_utils import insert_code_after_tag, string_in_file
77
from ..utils import open_json_file, get_framework, term_color
88
import os
99
import shutil
1010
import fileinput
1111

12+
TOOL_INIT_FILENAME = "src/tools/__init__.py"
13+
AGENTSTACK_JSON_FILENAME = "agentstack.json"
14+
1215

1316
def add_tool(tool_name: str, path: Optional[str] = None):
17+
if path:
18+
path = path.endswith('/') and path or path + '/'
19+
else:
20+
path = './'
1421
with importlib.resources.path(f'agentstack.tools', 'tools.json') as tools_data_path:
1522
tools = open_json_file(tools_data_path)
1623
framework = get_framework(path)
1724
assert_tool_exists(tool_name, tools)
25+
agentstack_json = open_json_file(f'{path}{AGENTSTACK_JSON_FILENAME}')
26+
27+
if tool_name in agentstack_json.get('tools', []):
28+
print(term_color(f'Tool {tool_name} is already installed', 'red'))
29+
sys.exit(1)
1830

1931
with importlib.resources.path(f'agentstack.tools', f"{tool_name}.json") as tool_data_path:
2032
tool_data = open_json_file(tool_data_path)
2133

2234
with importlib.resources.path(f'agentstack.templates.{framework}.tools', f"{tool_name}_tool.py") as tool_file_path:
23-
os.system(tool_data['package']) # Install package
24-
shutil.copy(tool_file_path, f'{path + "/" if path else ""}src/tools/{tool_name}_tool.py') # Move tool from package to project
35+
if tool_data.get('packages'):
36+
os.system(f"poetry add {' '.join(tool_data['packages'])}") # Install packages
37+
shutil.copy(tool_file_path, f'{path}src/tools/{tool_name}_tool.py') # Move tool from package to project
2538
add_tool_to_tools_init(tool_data, path) # Export tool from tools dir
26-
add_tool_to_agent_definition(framework, tool_data, path)
27-
insert_code_after_tag(f'{path + "/" if path else ""}.env', '# Tools', [tool_data['env']], next_line=True) # Add env var
28-
insert_code_after_tag(f'{path + "/" if path else ""}.env.example', '# Tools', [tool_data['env']], next_line=True) # Add env var
29-
30-
agentstack_json = open_json_file(f'{path + "/" if path else ""}agentstack.json')
39+
add_tool_to_agent_definition(framework, tool_data, path) # Add tool to agent definition
40+
if tool_data.get('env'): # if the env vars aren't in the .env files, add them
41+
first_var_name = tool_data['env'].split('=')[0]
42+
if not string_in_file(f'{path}.env', first_var_name):
43+
insert_code_after_tag(f'{path}.env', '# Tools', [tool_data['env']], next_line=True) # Add env var
44+
if not string_in_file(f'{path}.env.example', first_var_name):
45+
insert_code_after_tag(f'{path}.env.example', '# Tools', [tool_data['env']], next_line=True) # Add env var
46+
3147
if not agentstack_json.get('tools'):
3248
agentstack_json['tools'] = []
3349
agentstack_json['tools'].append(tool_name)
3450

35-
with open(f'{path + "/" if path else ""}agentstack.json', 'w') as f:
51+
with open(f'{path}{AGENTSTACK_JSON_FILENAME}', 'w') as f:
3652
json.dump(agentstack_json, f, indent=4)
3753

3854
print(term_color(f'🔨 Tool {tool_name} added to agentstack project successfully', 'green'))
3955
if tool_data.get('cta'):
4056
print(term_color(f'🪩 {tool_data["cta"]}', 'blue'))
4157

4258

43-
def add_tool_to_tools_init(tool_data: dict, path: Optional[str] = None):
44-
file_path = f'{path + "/" if path else ""}src/tools/__init__.py'
59+
def remove_tool(tool_name: str, path: Optional[str] = None):
60+
if path:
61+
path = path.endswith('/') and path or path + '/'
62+
else:
63+
path = './'
64+
with importlib.resources.path(f'agentstack.tools', 'tools.json') as tools_data_path:
65+
tools = open_json_file(tools_data_path)
66+
framework = get_framework()
67+
assert_tool_exists(tool_name, tools)
68+
agentstack_json = open_json_file(f'{path}{AGENTSTACK_JSON_FILENAME}')
69+
70+
if not tool_name in agentstack_json.get('tools', []):
71+
print(term_color(f'Tool {tool_name} is not installed', 'red'))
72+
sys.exit(1)
73+
74+
with importlib.resources.path(f'agentstack.tools', f"{tool_name}.json") as tool_data_path:
75+
tool_data = open_json_file(tool_data_path)
76+
if tool_data.get('packages'):
77+
os.system(f"poetry remove {' '.join(tool_data['packages'])}") # Uninstall packages
78+
os.remove(f'{path}src/tools/{tool_name}_tool.py')
79+
remove_tool_from_tools_init(tool_data, path)
80+
remove_tool_from_agent_definition(framework, tool_data, path)
81+
# We don't remove the .env variables to preserve user data.
82+
83+
agentstack_json['tools'].remove(tool_name)
84+
with open(f'{path}{AGENTSTACK_JSON_FILENAME}', 'w') as f:
85+
json.dump(agentstack_json, f, indent=4)
86+
87+
print(term_color(f'🔨 Tool {tool_name}', 'green'), term_color('removed', 'red'), term_color('from agentstack project successfully', 'green'))
88+
89+
90+
def _format_tool_import_statement(tool_data: dict):
91+
return f"from .{tool_data['name']}_tool import {', '.join([tool_name for tool_name in tool_data['tools']])}"
92+
93+
94+
def add_tool_to_tools_init(tool_data: dict, path: str = ''):
95+
file_path = f'{path}{TOOL_INIT_FILENAME}'
4596
tag = '# tool import'
46-
code_to_insert = [
47-
f"from .{tool_data['name']}_tool import {', '.join([tool_name for tool_name in tool_data['tools']])}"
48-
]
97+
code_to_insert = [_format_tool_import_statement(tool_data), ]
4998
insert_code_after_tag(file_path, tag, code_to_insert, next_line=True)
5099

51100

52-
def add_tool_to_agent_definition(framework: str, tool_data: dict, path: Optional[str] = None):
53-
filename = ''
101+
def remove_tool_from_tools_init(tool_data: dict, path: str = ''):
102+
"""Search for the import statement in the init and remove it."""
103+
file_path = f'{path}{TOOL_INIT_FILENAME}'
104+
import_statement = _format_tool_import_statement(tool_data)
105+
with fileinput.input(files=file_path, inplace=True) as f:
106+
for line in f:
107+
if line.strip() != import_statement:
108+
print(line, end='')
109+
110+
111+
def _framework_filename(framework: str, path: str = ''):
54112
if framework == 'crewai':
55-
filename = 'src/crew.py'
113+
return f'{path}src/crew.py'
56114

57-
if path:
58-
filename = f'{path}/{filename}'
115+
print(term_color(f'Unknown framework: {framework}', 'red'))
116+
sys.exit(1)
59117

118+
119+
def add_tool_to_agent_definition(framework: str, tool_data: dict, path: str = ''):
120+
filename = _framework_filename(framework, path)
60121
with fileinput.input(files=filename, inplace=True) as f:
61122
for line in f:
62123
print(line.replace('tools=[', f'tools=[{"*" if tool_data.get("tools_bundled") else ""}tools.{", tools.".join([tool_name for tool_name in tool_data["tools"]])}, '), end='')
63124

64125

126+
def remove_tool_from_agent_definition(framework: str, tool_data: dict, path: str = ''):
127+
filename = _framework_filename(framework, path)
128+
with fileinput.input(files=filename, inplace=True) as f:
129+
for line in f:
130+
print(line.replace(f'{", ".join([f"tools.{tool_name}" for tool_name in tool_data["tools"]])}, ', ''), end='')
131+
132+
65133
def assert_tool_exists(tool_name: str, tools: dict):
66134
for cat in tools.keys():
67135
for tool_dict in tools[cat]:
68136
if tool_dict['name'] == tool_name:
69137
return
70138

71-
print(f"\033[31mNo known AgentStack tool: '{tool_name}'\033[0m")
139+
print(term_color(f'No known agentstack tool: {tool_name}', 'red'))
72140
sys.exit(1)
73141

agentstack/main.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ def main():
6262
tools_add_parser = tools_subparsers.add_parser('add', aliases=['a'], help='Add a new tool')
6363
tools_add_parser.add_argument('name', help='Name of the tool to add')
6464

65+
# 'remove' command under 'tools'
66+
tools_remove_parser = tools_subparsers.add_parser('remove', aliases=['r'], help='Remove a tool')
67+
tools_remove_parser.add_argument('name', help='Name of the tool to remove')
68+
6569
# Parse arguments
6670
args = parser.parse_args()
6771

@@ -89,6 +93,8 @@ def main():
8993
list_tools()
9094
elif args.tools_command in ['add', 'a']:
9195
generation.add_tool(args.name)
96+
elif args.tools_command in ['remove', 'r']:
97+
generation.remove_tool(args.name)
9298
else:
9399
tools_parser.print_help()
94100
else:
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "agent-connect",
3-
"package": "poetry add agent-connect",
3+
"packages": ["agent-connect"],
44
"env": "HOST_DOMAIN=...\nHOST_PORT=\"80\"\nHOST_WS_PATH=\"/ws\"\nDID_DOCUMENT_PATH=...\nSSL_CERT_PATH=...\nSSL_KEY_PATH=...",
55
"tools": ["send_message", "receive_message"]
66
}

agentstack/tools/browserbase.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "browserbase",
3-
"package": "poetry add browserbase playwright",
3+
"packages": ["browserbase", "playwright"],
44
"env": "BROWSERBASE_API_KEY=...\nBROWSERBASE_PROJECT_ID=...",
55
"tools": ["browserbase"],
66
"cta": "Create an API key at https://www.browserbase.com/"
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "code_interpreter",
3-
"package": "poetry add crewai-tools",
3+
"packages": [],
44
"env": "",
55
"tools": ["code_interpreter"]
66
}

agentstack/tools/composio.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "composio",
3-
"package": "poetry add composio-crewai",
3+
"packages": ["composio-crewai"],
44
"env": "COMPOSIO_API_KEY=...",
55
"tools": ["composio_tools"],
66
"tools_bundled": true,
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "dir_search_tool",
3-
"package": "poetry add crewai-tools",
3+
"packages": [],
44
"env": "",
55
"tools": ["dir_search_tool"]
66
}

agentstack/tools/exa.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "exa",
3-
"package": "poetry add exa_py",
3+
"packages": ["exa_py"],
44
"env": "EXA_API_KEY=...",
55
"tools": ["search_and_contents"],
66
"cta": "Get your Exa API key at https://dashboard.exa.ai/api-keys"

0 commit comments

Comments
 (0)