1
- import os
2
1
import re
2
+ import shutil
3
+ from datetime import datetime
3
4
from importlib .metadata import version as get_version
4
- from textwrap import dedent
5
+ from pathlib import Path
5
6
from typing import Optional
6
7
7
8
import typer
9
+ from jinja2 import Environment , FileSystemLoader , select_autoescape
8
10
from rich .console import Console
9
11
10
12
console = Console ()
11
13
12
14
# Retrieve the installed version of arcade-ai
13
15
try :
14
- VERSION = get_version ("arcade-ai" )
16
+ ARCADE_VERSION = get_version ("arcade-ai" )
15
17
except Exception as e :
16
18
console .print (f"[red]Failed to get arcade-ai version: { e } [/red]" )
17
- VERSION = "0.0.0" # Default version if unable to fetch
19
+ ARCADE_VERSION = "0.0.0" # Default version if unable to fetch
18
20
19
- DEFAULT_VERSIONS = {
20
- "python" : "^3.10" ,
21
- "arcade-ai" : f"~{ VERSION } " , # allow patch version updates
22
- "pytest" : "^8.3.0" ,
23
- }
21
+ TEMPLATE_IGNORE_PATTERN = re .compile (
22
+ r"(__pycache__|\.DS_Store|Thumbs\.db|\.git|\.svn|\.hg|\.vscode|\.idea|build|dist|.*\.egg-info|.*\.pyc|.*\.pyo)$"
23
+ )
24
24
25
25
26
26
def ask_question (question : str , default : Optional [str ] = None ) -> str :
@@ -33,67 +33,66 @@ def ask_question(question: str, default: Optional[str] = None) -> str:
33
33
return str (answer )
34
34
35
35
36
- def create_directory (path : str ) -> bool :
37
- """
38
- Create a directory if it doesn't exist.
39
- Returns True if the directory was created, False if failed to create.
40
- """
41
- try :
42
- os .makedirs (path , exist_ok = False )
43
- except FileExistsError :
44
- console .print (f"[red]Directory '{ path } ' already exists.[/red]" )
45
- return False
46
- except Exception as e :
47
- console .print (f"[red]Failed to create directory { path } : { e } [/red]" )
48
- return False
49
- return True
36
+ def render_template (env : Environment , template_string : str , context : dict ) -> str :
37
+ """Render a template string with the given variables."""
38
+ template = env .from_string (template_string )
39
+ return template .render (context )
50
40
51
41
52
- def create_file (path : str , content : str ) -> None :
53
- """
54
- Create a file with the given content.
55
- """
56
- try :
57
- with open (path , "w" ) as f :
58
- f .write (content )
59
- except Exception as e :
60
- console .print (f"[red]Failed to create file { path } : { e } [/red]" )
42
+ def write_template (path : Path , content : str ) -> None :
43
+ """Write content to a file."""
44
+ path .write_text (content )
61
45
62
46
63
- def create_pyproject_toml ( directory : str , toolkit_name : str , author : str , description : str ) -> None :
64
- """
65
- Create a pyproject.toml file for the new toolkit.
66
- """
47
+ def create_package ( env : Environment , template_path : Path , output_path : Path , context : dict ) -> None :
48
+ """Recursively create a new toolkit directory structure from jinja2 templates."""
49
+ if TEMPLATE_IGNORE_PATTERN . match ( template_path . name ):
50
+ return
67
51
68
- content = f"""
69
- [tool.poetry]
70
- name = "{ toolkit_name } "
71
- version = "0.1.0"
72
- description = "{ description } "
73
- authors = ["{ author } "]
52
+ try :
53
+ if template_path .is_dir ():
54
+ folder_name = render_template (env , template_path .name , context )
55
+ new_dir_path = output_path / folder_name
56
+ new_dir_path .mkdir (parents = True , exist_ok = True )
74
57
75
- [tool.poetry.dependencies]
76
- python = "{ DEFAULT_VERSIONS ["python" ]} "
77
- arcade-ai = "{ DEFAULT_VERSIONS ["arcade-ai" ]} "
58
+ for item in template_path .iterdir ():
59
+ create_package (env , item , new_dir_path , context )
60
+
61
+ else :
62
+ # Render the file name
63
+ file_name = render_template (env , template_path .name , context )
64
+ with open (template_path ) as f :
65
+ content = f .read ()
66
+ # Render the file content
67
+ content = render_template (env , content , context )
68
+
69
+ write_template (output_path / file_name , content )
70
+ except Exception as e :
71
+ console .print (f"[red]Failed to create package: { e } [/red]" )
72
+ raise
78
73
79
- [tool.poetry.dev-dependencies]
80
- pytest = "{ DEFAULT_VERSIONS ["pytest" ]} "
81
74
82
- [build-system]
83
- requires = ["poetry-core>=1.0.0"]
84
- build-backend = "poetry.core.masonry.api"
85
- """
86
- create_file ( os . path . join ( directory , "pyproject.toml" ), content . strip () )
75
+ def remove_toolkit ( toolkit_directory : Path , toolkit_name : str ) -> None :
76
+ """Teardown logic for when creating a new toolkit fails."""
77
+ toolkit_path = toolkit_directory / toolkit_name
78
+ if toolkit_path . exists ():
79
+ shutil . rmtree ( toolkit_path )
87
80
88
81
89
- def create_new_toolkit (directory : str ) -> None :
90
- """Generate a new Toolkit package based on user input."""
82
+ def create_new_toolkit (output_directory : str ) -> None :
83
+ """Create a new toolkit from a template with user input."""
84
+ toolkit_directory = Path (output_directory )
91
85
while True :
92
86
name = ask_question ("Name of the new toolkit?" )
93
- toolkit_name = name if name .startswith ("arcade_" ) else f"arcade_{ name } "
87
+ package_name = name if name .startswith ("arcade_" ) else f"arcade_{ name } "
94
88
95
89
# Check for illegal characters in the toolkit name
96
- if re .match (r"^[\w_]+$" , toolkit_name ):
90
+ if re .match (r"^[\w_]+$" , package_name ):
91
+ toolkit_name = package_name .replace ("arcade_" , "" , 1 )
92
+
93
+ if (toolkit_directory / toolkit_name ).exists ():
94
+ console .print (f"[red]Toolkit { toolkit_name } already exists.[/red]" )
95
+ continue
97
96
break
98
97
else :
99
98
console .print (
@@ -102,147 +101,28 @@ def create_new_toolkit(directory: str) -> None:
102
101
"Please try again.[/red]"
103
102
)
104
103
105
- description = ask_question ("Description of the toolkit?" )
106
- author_name = ask_question ("Author's name?" )
107
- author_email = ask_question ("Author's email?" )
108
- author = f"{ author_name } <{ author_email } >"
109
-
110
- yes_options = ["yes" , "y" , "ye" , "yea" , "yeah" , "true" ]
111
- generate_test_dir = (
112
- ask_question ("Generate test directory? (yes/no)" , "yes" ).lower () in yes_options
104
+ toolkit_description = ask_question ("Description of the toolkit?" )
105
+ toolkit_author_name = ask_question ("Github owner username?" )
106
+ toolkit_author_email = ask_question ("Author's email?" )
107
+
108
+ context = {
109
+ "package_name" : package_name ,
110
+ "toolkit_name" : toolkit_name ,
111
+ "toolkit_description" : toolkit_description ,
112
+ "toolkit_author_name" : toolkit_author_name ,
113
+ "toolkit_author_email" : toolkit_author_email ,
114
+ "arcade_version" : f"{ ARCADE_VERSION .rsplit ('.' , 1 )[0 ]} .*" ,
115
+ "creation_year" : datetime .now ().year ,
116
+ }
117
+ template_directory = Path (__file__ ).parent .parent / "templates" / "{{ toolkit_name }}"
118
+
119
+ env = Environment (
120
+ loader = FileSystemLoader (str (template_directory )),
121
+ autoescape = select_autoescape (["html" , "xml" ]),
113
122
)
114
- generate_eval_dir = (
115
- ask_question ("Generate eval directory? (yes/no)" , "yes" ).lower () in yes_options
116
- )
117
-
118
- top_level_dir = os .path .join (directory , name )
119
- toolkit_dir = os .path .join (directory , name , toolkit_name )
120
-
121
- # Create the top level toolkit directory
122
- if not create_directory (top_level_dir ):
123
- return
124
-
125
- # Create the toolkit directory
126
- create_directory (toolkit_dir )
127
-
128
- # Create the __init__.py file in the toolkit directory
129
- create_file (os .path .join (toolkit_dir , "__init__.py" ), "" )
130
123
131
- # Create the tools directory
132
- create_directory (os .path .join (toolkit_dir , "tools" ))
133
-
134
- # Create the __init__.py file in the tools directory
135
- create_file (os .path .join (toolkit_dir , "tools" , "__init__.py" ), "" )
136
-
137
- # Create the hello.py file in the tools directory
138
- docstring = '"""Say a greeting!"""'
139
- create_file (
140
- os .path .join (toolkit_dir , "tools" , "hello.py" ),
141
- dedent (
142
- f"""
143
- from typing import Annotated
144
- from arcade.sdk import tool
145
-
146
- @tool
147
- def hello(name: Annotated[str, "The name of the person to greet"]) -> str:
148
- { docstring }
149
-
150
- return "Hello, " + name + "!"
151
- """
152
- ).strip (),
153
- )
154
-
155
- # Create the pyproject.toml file
156
- create_pyproject_toml (top_level_dir , toolkit_name , author , description )
157
-
158
- # If the user wants to generate a test directory
159
- if generate_test_dir :
160
- create_directory (os .path .join (top_level_dir , "tests" ))
161
-
162
- # Create the __init__.py file in the tests directory
163
- create_file (os .path .join (top_level_dir , "tests" , "__init__.py" ), "" )
164
-
165
- # Create the test_hello.py file in the tests directory
166
- stripped_toolkit_name = toolkit_name .replace ("arcade_" , "" )
167
- create_file (
168
- os .path .join (top_level_dir , "tests" , f"test_{ stripped_toolkit_name } .py" ),
169
- dedent (
170
- f"""
171
- import pytest
172
- from arcade.sdk.errors import ToolExecutionError
173
- from { toolkit_name } .tools.hello import hello
174
-
175
- def test_hello():
176
- assert hello("developer") == "Hello, developer!"
177
-
178
- def test_hello_raises_error():
179
- with pytest.raises(ToolExecutionError):
180
- hello(1)
181
- """
182
- ).strip (),
183
- )
184
-
185
- # If the user wants to generate an eval directory
186
- if generate_eval_dir :
187
- create_directory (os .path .join (top_level_dir , "evals" ))
188
-
189
- # Create the eval_hello.py file
190
- stripped_toolkit_name = toolkit_name .replace ("arcade_" , "" )
191
- create_file (
192
- os .path .join (top_level_dir , "evals" , "eval_hello.py" ),
193
- dedent (
194
- f"""
195
- import { toolkit_name }
196
- from { toolkit_name } .tools.hello import hello
197
-
198
- from arcade.sdk import ToolCatalog
199
- from arcade.sdk.eval import (
200
- EvalRubric,
201
- EvalSuite,
202
- SimilarityCritic,
203
- tool_eval,
204
- )
205
-
206
- # Evaluation rubric
207
- rubric = EvalRubric(
208
- fail_threshold=0.85,
209
- warn_threshold=0.95,
210
- )
211
-
212
-
213
- catalog = ToolCatalog()
214
- catalog.add_module({ toolkit_name } )
215
-
216
-
217
- @tool_eval()
218
- def { stripped_toolkit_name } _eval_suite():
219
- suite = EvalSuite(
220
- name="{ stripped_toolkit_name } Tools Evaluation",
221
- system_message="You are an AI assistant with access to { stripped_toolkit_name } tools. Use them to help the user with their tasks.",
222
- catalog=catalog,
223
- rubric=rubric,
224
- )
225
-
226
- suite.add_case(
227
- name="Saying hello",
228
- user_message="Say hello to the developer!!!!",
229
- expected_tool_calls=[
230
- (
231
- hello,
232
- {{
233
- "name": "developer"
234
- }}
235
- )
236
- ],
237
- rubric=rubric,
238
- critics=[
239
- SimilarityCritic(critic_field="name", weight=0.5),
240
- ],
241
- )
242
-
243
- return suite
244
- """
245
- ).strip (),
246
- )
247
-
248
- console .print (f"[green]Toolkit { toolkit_name } has been created in { top_level_dir } [/green]" )
124
+ try :
125
+ create_package (env , template_directory , toolkit_directory , context )
126
+ except Exception :
127
+ remove_toolkit (toolkit_directory , toolkit_name )
128
+ raise
0 commit comments