|
1 | 1 | import importlib.resources |
| 2 | +from pathlib import Path |
2 | 3 | import json |
3 | 4 | import sys |
4 | | -from typing import Optional |
| 5 | +from typing import Optional, List |
| 6 | +from pydantic import BaseModel, ValidationError |
5 | 7 |
|
6 | 8 | from .gen_utils import insert_code_after_tag, string_in_file |
7 | 9 | from ..utils import open_json_file, get_framework, term_color |
|
10 | 12 | import fileinput |
11 | 13 |
|
12 | 14 | TOOL_INIT_FILENAME = "src/tools/__init__.py" |
| 15 | +TOOLS_DATA_PATH: Path = importlib.resources.files('agentstack.tools') / 'tools.json' |
13 | 16 | AGENTSTACK_JSON_FILENAME = "agentstack.json" |
| 17 | +FRAMEWORK_FILENAMES: dict[str, str] = { |
| 18 | + 'crewai': 'src/crew.py', |
| 19 | +} |
| 20 | + |
| 21 | +def get_framework_filename(framework: str, path: str = ''): |
| 22 | + try: |
| 23 | + return FRAMEWORK_FILENAMES[framework] |
| 24 | + except KeyError: |
| 25 | + print(term_color(f'Unknown framework: {framework}', 'red')) |
| 26 | + sys.exit(1) |
| 27 | + |
| 28 | +def assert_tool_exists(name: str): |
| 29 | + tools_data = open_json_file(TOOLS_DATA_PATH) |
| 30 | + for category, tools in tools_data.items(): |
| 31 | + for tool_dict in tools: |
| 32 | + if tool_dict['name'] == name: |
| 33 | + return |
| 34 | + print(term_color(f'No known agentstack tool: {name}', 'red')) |
| 35 | + sys.exit(1) |
14 | 36 |
|
| 37 | +class ToolConfig(BaseModel): |
| 38 | + name: str |
| 39 | + tools: list[str] |
| 40 | + tools_bundled: bool = False |
| 41 | + cta: Optional[str] = None |
| 42 | + env: Optional[str] = None |
| 43 | + packages: Optional[List[str]] = None |
| 44 | + post_install: Optional[str] = None |
| 45 | + post_remove: Optional[str] = None |
| 46 | + |
| 47 | + @classmethod |
| 48 | + def from_tool_name(cls, name: str) -> 'ToolConfig': |
| 49 | + assert_tool_exists(name) |
| 50 | + return cls.from_json(importlib.resources.files('agentstack.tools') / f'{name}.json') |
| 51 | + |
| 52 | + @classmethod |
| 53 | + def from_json(cls, path: Path) -> 'ToolConfig': |
| 54 | + data = open_json_file(path) |
| 55 | + try: |
| 56 | + return cls(**data) |
| 57 | + except ValidationError as e: |
| 58 | + print(term_color(f"Error validating tool config JSON: \n{path}", 'red')) |
| 59 | + for error in e.errors(): |
| 60 | + print(f"{' '.join(error['loc'])}: {error['msg']}") |
| 61 | + sys.exit(1) |
| 62 | + |
| 63 | + def get_import_statement(self) -> str: |
| 64 | + return f"from .{self.name}_tool import {', '.join(self.tools)}" |
15 | 65 |
|
16 | 66 | def add_tool(tool_name: str, path: Optional[str] = None): |
17 | 67 | if path: |
18 | 68 | path = path.endswith('/') and path or path + '/' |
19 | 69 | else: |
20 | 70 | path = './' |
21 | | - with importlib.resources.path(f'agentstack.tools', 'tools.json') as tools_data_path: |
22 | | - tools = open_json_file(tools_data_path) |
23 | | - framework = get_framework(path) |
24 | | - 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) |
30 | | - |
31 | | - with importlib.resources.path(f'agentstack.tools', f"{tool_name}.json") as tool_data_path: |
32 | | - tool_data = open_json_file(tool_data_path) |
33 | | - |
34 | | - with importlib.resources.path(f'agentstack.templates.{framework}.tools', f"{tool_name}_tool.py") as tool_file_path: |
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 |
38 | | - add_tool_to_tools_init(tool_data, path) # Export tool from tools dir |
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 | | - if tool_data.get('post_install'): |
47 | | - os.system(tool_data['post_install']) |
48 | | - if not agentstack_json.get('tools'): |
49 | | - agentstack_json['tools'] = [] |
50 | | - agentstack_json['tools'].append(tool_name) |
51 | | - |
52 | | - with open(f'{path}{AGENTSTACK_JSON_FILENAME}', 'w') as f: |
53 | | - json.dump(agentstack_json, f, indent=4) |
54 | | - |
55 | | - print(term_color(f'🔨 Tool {tool_name} added to agentstack project successfully', 'green')) |
56 | | - if tool_data.get('cta'): |
57 | | - print(term_color(f'🪩 {tool_data["cta"]}', 'blue')) |
58 | | - |
| 71 | + |
| 72 | + framework = get_framework(path) |
| 73 | + agentstack_json = open_json_file(f'{path}{AGENTSTACK_JSON_FILENAME}') |
| 74 | + |
| 75 | + if tool_name in agentstack_json.get('tools', []): |
| 76 | + print(term_color(f'Tool {tool_name} is already installed', 'red')) |
| 77 | + sys.exit(1) |
| 78 | + |
| 79 | + tool_data = ToolConfig.from_tool_name(tool_name) |
| 80 | + tool_file_path = importlib.resources.files(f'agentstack.templates.{framework}.tools') / f'{tool_name}_tool.py' |
| 81 | + if tool_data.packages: |
| 82 | + os.system(f"poetry add {' '.join(tool_data.packages)}") # Install packages |
| 83 | + shutil.copy(tool_file_path, f'{path}src/tools/{tool_name}_tool.py') # Move tool from package to project |
| 84 | + add_tool_to_tools_init(tool_data, path) # Export tool from tools dir |
| 85 | + add_tool_to_agent_definition(framework, tool_data, path) # Add tool to agent definition |
| 86 | + if tool_data.env: # if the env vars aren't in the .env files, add them |
| 87 | + first_var_name = tool_data.env.split('=')[0] |
| 88 | + if not string_in_file(f'{path}.env', first_var_name): |
| 89 | + insert_code_after_tag(f'{path}.env', '# Tools', [tool_data.env], next_line=True) # Add env var |
| 90 | + if not string_in_file(f'{path}.env.example', first_var_name): |
| 91 | + insert_code_after_tag(f'{path}.env.example', '# Tools', [tool_data.env], next_line=True) # Add env var |
| 92 | + |
| 93 | + if tool_data.post_install: |
| 94 | + os.system(tool_data.post_install) |
| 95 | + |
| 96 | + if not agentstack_json.get('tools'): |
| 97 | + agentstack_json['tools'] = [] |
| 98 | + agentstack_json['tools'].append(tool_name) |
| 99 | + |
| 100 | + with open(f'{path}{AGENTSTACK_JSON_FILENAME}', 'w') as f: |
| 101 | + json.dump(agentstack_json, f, indent=4) |
| 102 | + |
| 103 | + print(term_color(f'🔨 Tool {tool_name} added to agentstack project successfully', 'green')) |
| 104 | + if tool_data.cta: |
| 105 | + print(term_color(f'🪩 {tool_data.cta}', 'blue')) |
59 | 106 |
|
60 | 107 | def remove_tool(tool_name: str, path: Optional[str] = None): |
61 | 108 | if path: |
62 | 109 | path = path.endswith('/') and path or path + '/' |
63 | 110 | else: |
64 | 111 | path = './' |
65 | | - with importlib.resources.path(f'agentstack.tools', 'tools.json') as tools_data_path: |
66 | | - tools = open_json_file(tools_data_path) |
67 | | - framework = get_framework() |
68 | | - assert_tool_exists(tool_name, tools) |
69 | | - agentstack_json = open_json_file(f'{path}{AGENTSTACK_JSON_FILENAME}') |
70 | | - |
71 | | - if not tool_name in agentstack_json.get('tools', []): |
72 | | - print(term_color(f'Tool {tool_name} is not installed', 'red')) |
73 | | - sys.exit(1) |
74 | | - |
75 | | - with importlib.resources.path(f'agentstack.tools', f"{tool_name}.json") as tool_data_path: |
76 | | - tool_data = open_json_file(tool_data_path) |
77 | | - if tool_data.get('packages'): |
78 | | - os.system(f"poetry remove {' '.join(tool_data['packages'])}") # Uninstall packages |
79 | | - os.remove(f'{path}src/tools/{tool_name}_tool.py') |
80 | | - remove_tool_from_tools_init(tool_data, path) |
81 | | - remove_tool_from_agent_definition(framework, tool_data, path) |
82 | | - if tool_data.get('post_remove'): |
83 | | - os.system(tool_data['post_remove']) |
84 | | - # We don't remove the .env variables to preserve user data. |
85 | | - |
86 | | - agentstack_json['tools'].remove(tool_name) |
87 | | - with open(f'{path}{AGENTSTACK_JSON_FILENAME}', 'w') as f: |
88 | | - json.dump(agentstack_json, f, indent=4) |
89 | | - |
90 | | - print(term_color(f'🔨 Tool {tool_name}', 'green'), term_color('removed', 'red'), term_color('from agentstack project successfully', 'green')) |
91 | | - |
92 | | - |
93 | | -def _format_tool_import_statement(tool_data: dict): |
94 | | - return f"from .{tool_data['name']}_tool import {', '.join([tool_name for tool_name in tool_data['tools']])}" |
95 | | - |
96 | | - |
97 | | -def add_tool_to_tools_init(tool_data: dict, path: str = ''): |
| 112 | + |
| 113 | + framework = get_framework() |
| 114 | + agentstack_json = open_json_file(f'{path}{AGENTSTACK_JSON_FILENAME}') |
| 115 | + |
| 116 | + if not tool_name in agentstack_json.get('tools', []): |
| 117 | + print(term_color(f'Tool {tool_name} is not installed', 'red')) |
| 118 | + sys.exit(1) |
| 119 | + |
| 120 | + tool_data = ToolConfig.from_tool_name(tool_name) |
| 121 | + if tool_data.packages: |
| 122 | + os.system(f"poetry remove {' '.join(tool_data.packages)}") # Uninstall packages |
| 123 | + try: |
| 124 | + os.remove(f'{path}src/tools/{tool_name}_tool.py') |
| 125 | + except FileNotFoundError: |
| 126 | + print(f'"src/tools/{tool_name}_tool.py" not found') |
| 127 | + remove_tool_from_tools_init(tool_data, path) |
| 128 | + remove_tool_from_agent_definition(framework, tool_data, path) |
| 129 | + if tool_data.post_remove: |
| 130 | + os.system(tool_data.post_remove) |
| 131 | + # We don't remove the .env variables to preserve user data. |
| 132 | + |
| 133 | + agentstack_json['tools'].remove(tool_name) |
| 134 | + with open(f'{path}{AGENTSTACK_JSON_FILENAME}', 'w') as f: |
| 135 | + json.dump(agentstack_json, f, indent=4) |
| 136 | + |
| 137 | + print(term_color(f'🔨 Tool {tool_name}', 'green'), term_color('removed', 'red'), term_color('from agentstack project successfully', 'green')) |
| 138 | + |
| 139 | +def add_tool_to_tools_init(tool_data: ToolConfig, path: str = ''): |
98 | 140 | file_path = f'{path}{TOOL_INIT_FILENAME}' |
99 | 141 | tag = '# tool import' |
100 | | - code_to_insert = [_format_tool_import_statement(tool_data), ] |
| 142 | + code_to_insert = [tool_data.get_import_statement(), ] |
101 | 143 | insert_code_after_tag(file_path, tag, code_to_insert, next_line=True) |
102 | 144 |
|
103 | | - |
104 | | -def remove_tool_from_tools_init(tool_data: dict, path: str = ''): |
| 145 | +def remove_tool_from_tools_init(tool_data: ToolConfig, path: str = ''): |
105 | 146 | """Search for the import statement in the init and remove it.""" |
106 | 147 | file_path = f'{path}{TOOL_INIT_FILENAME}' |
107 | | - import_statement = _format_tool_import_statement(tool_data) |
| 148 | + import_statement = tool_data.get_import_statement() |
108 | 149 | with fileinput.input(files=file_path, inplace=True) as f: |
109 | 150 | for line in f: |
110 | 151 | if line.strip() != import_statement: |
111 | 152 | print(line, end='') |
112 | 153 |
|
113 | | - |
114 | | -def _framework_filename(framework: str, path: str = ''): |
115 | | - if framework == 'crewai': |
116 | | - return f'{path}src/crew.py' |
117 | | - |
118 | | - print(term_color(f'Unknown framework: {framework}', 'red')) |
119 | | - sys.exit(1) |
120 | | - |
121 | | - |
122 | | -def add_tool_to_agent_definition(framework: str, tool_data: dict, path: str = ''): |
123 | | - filename = _framework_filename(framework, path) |
124 | | - with fileinput.input(files=filename, inplace=True) as f: |
| 154 | +def add_tool_to_agent_definition(framework: str, tool_data: ToolConfig, path: str = ''): |
| 155 | + with fileinput.input(files=get_framework_filename(framework, path), inplace=True) as f: |
125 | 156 | for line in f: |
126 | | - 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='') |
127 | | - |
| 157 | + print(line.replace('tools=[', f'tools=[{"*" if tool_data.tools_bundled else ""}tools.{", tools.".join(tool_data.tools)}, '), end='') |
128 | 158 |
|
129 | | -def remove_tool_from_agent_definition(framework: str, tool_data: dict, path: str = ''): |
130 | | - filename = _framework_filename(framework, path) |
131 | | - with fileinput.input(files=filename, inplace=True) as f: |
| 159 | +def remove_tool_from_agent_definition(framework: str, tool_data: ToolConfig, path: str = ''): |
| 160 | + with fileinput.input(files=get_framework_filename(framework, path), inplace=True) as f: |
132 | 161 | for line in f: |
133 | | - print(line.replace(f'{", ".join([f"tools.{tool_name}" for tool_name in tool_data["tools"]])}, ', ''), end='') |
134 | | - |
135 | | - |
136 | | -def assert_tool_exists(tool_name: str, tools: dict): |
137 | | - for cat in tools.keys(): |
138 | | - for tool_dict in tools[cat]: |
139 | | - if tool_dict['name'] == tool_name: |
140 | | - return |
141 | | - |
142 | | - print(term_color(f'No known agentstack tool: {tool_name}', 'red')) |
143 | | - sys.exit(1) |
| 162 | + print(line.replace(f'{", ".join([f"tools.{name}" for name in tool_data.tools])}, ', ''), end='') |
144 | 163 |
|
0 commit comments