diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8b7c9ce7..273304f6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,9 @@ repos: - id: check-merge-conflict - id: check-toml - id: check-yaml + exclude: ".*/templates/.*" - id: end-of-file-fixer + exclude: ".*/templates/.*" - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit @@ -14,4 +16,6 @@ repos: hooks: - id: ruff args: [--fix] + exclude: ".*/templates/.*" - id: ruff-format + exclude: ".*/templates/.*" diff --git a/arcade/arcade/cli/new.py b/arcade/arcade/cli/new.py index 3c901964..0950a449 100644 --- a/arcade/arcade/cli/new.py +++ b/arcade/arcade/cli/new.py @@ -1,26 +1,26 @@ -import os import re +import shutil +from datetime import datetime from importlib.metadata import version as get_version -from textwrap import dedent +from pathlib import Path from typing import Optional import typer +from jinja2 import Environment, FileSystemLoader, select_autoescape from rich.console import Console console = Console() # Retrieve the installed version of arcade-ai try: - VERSION = get_version("arcade-ai") + ARCADE_VERSION = get_version("arcade-ai") except Exception as e: console.print(f"[red]Failed to get arcade-ai version: {e}[/red]") - VERSION = "0.0.0" # Default version if unable to fetch + ARCADE_VERSION = "0.0.0" # Default version if unable to fetch -DEFAULT_VERSIONS = { - "python": "^3.10", - "arcade-ai": f"~{VERSION}", # allow patch version updates - "pytest": "^8.3.0", -} +TEMPLATE_IGNORE_PATTERN = re.compile( + r"(__pycache__|\.DS_Store|Thumbs\.db|\.git|\.svn|\.hg|\.vscode|\.idea|build|dist|.*\.egg-info|.*\.pyc|.*\.pyo)$" +) def ask_question(question: str, default: Optional[str] = None) -> str: @@ -33,67 +33,66 @@ def ask_question(question: str, default: Optional[str] = None) -> str: return str(answer) -def create_directory(path: str) -> bool: - """ - Create a directory if it doesn't exist. - Returns True if the directory was created, False if failed to create. - """ +def render_template(env: Environment, template_string: str, context: dict) -> str: + """Render a template string with the given variables.""" + template = env.from_string(template_string) + return template.render(context) + + +def write_template(path: Path, content: str) -> None: + """Write content to a file.""" + path.write_text(content) + + +def create_package(env: Environment, template_path: Path, output_path: Path, context: dict) -> None: + """Recursively create a new toolkit directory structure from jinja2 templates.""" + if TEMPLATE_IGNORE_PATTERN.match(template_path.name): + return + try: - os.makedirs(path, exist_ok=False) - except FileExistsError: - console.print(f"[red]Directory '{path}' already exists.[/red]") - return False + if template_path.is_dir(): + folder_name = render_template(env, template_path.name, context) + new_dir_path = output_path / folder_name + new_dir_path.mkdir(parents=True, exist_ok=True) + + for item in template_path.iterdir(): + create_package(env, item, new_dir_path, context) + + else: + # Render the file name + file_name = render_template(env, template_path.name, context) + with open(template_path) as f: + content = f.read() + # Render the file content + content = render_template(env, content, context) + + write_template(output_path / file_name, content) except Exception as e: - console.print(f"[red]Failed to create directory {path}: {e}[/red]") - return False - return True + console.print(f"[red]Failed to create package: {e}[/red]") + raise -def create_file(path: str, content: str) -> None: - """ - Create a file with the given content. - """ - try: - with open(path, "w") as f: - f.write(content) - except Exception as e: - console.print(f"[red]Failed to create file {path}: {e}[/red]") +def remove_toolkit(toolkit_directory: Path, toolkit_name: str) -> None: + """Teardown logic for when creating a new toolkit fails.""" + toolkit_path = toolkit_directory / toolkit_name + if toolkit_path.exists(): + shutil.rmtree(toolkit_path) -def create_pyproject_toml(directory: str, toolkit_name: str, author: str, description: str) -> None: - """ - Create a pyproject.toml file for the new toolkit. - """ - - content = f""" -[tool.poetry] -name = "{toolkit_name}" -version = "0.1.0" -description = "{description}" -authors = ["{author}"] - -[tool.poetry.dependencies] -python = "{DEFAULT_VERSIONS["python"]}" -arcade-ai = "{DEFAULT_VERSIONS["arcade-ai"]}" - -[tool.poetry.dev-dependencies] -pytest = "{DEFAULT_VERSIONS["pytest"]}" - -[build-system] -requires = ["poetry-core>=1.0.0"] -build-backend = "poetry.core.masonry.api" -""" - create_file(os.path.join(directory, "pyproject.toml"), content.strip()) - - -def create_new_toolkit(directory: str) -> None: - """Generate a new Toolkit package based on user input.""" +def create_new_toolkit(output_directory: str) -> None: + """Create a new toolkit from a template with user input.""" + toolkit_directory = Path(output_directory) while True: name = ask_question("Name of the new toolkit?") - toolkit_name = name if name.startswith("arcade_") else f"arcade_{name}" + package_name = name if name.startswith("arcade_") else f"arcade_{name}" # Check for illegal characters in the toolkit name - if re.match(r"^[\w_]+$", toolkit_name): + if re.match(r"^[\w_]+$", package_name): + toolkit_name = package_name.replace("arcade_", "", 1) + + if (toolkit_directory / toolkit_name).exists(): + console.print(f"[red]Toolkit {toolkit_name} already exists.[/red]") + continue break else: console.print( @@ -102,147 +101,28 @@ def create_new_toolkit(directory: str) -> None: "Please try again.[/red]" ) - description = ask_question("Description of the toolkit?") - author_name = ask_question("Author's name?") - author_email = ask_question("Author's email?") - author = f"{author_name} <{author_email}>" + toolkit_description = ask_question("Description of the toolkit?") + toolkit_author_name = ask_question("Github owner username?") + toolkit_author_email = ask_question("Author's email?") - yes_options = ["yes", "y", "ye", "yea", "yeah", "true"] - generate_test_dir = ( - ask_question("Generate test directory? (yes/no)", "yes").lower() in yes_options - ) - generate_eval_dir = ( - ask_question("Generate eval directory? (yes/no)", "yes").lower() in yes_options + context = { + "package_name": package_name, + "toolkit_name": toolkit_name, + "toolkit_description": toolkit_description, + "toolkit_author_name": toolkit_author_name, + "toolkit_author_email": toolkit_author_email, + "arcade_version": f"{ARCADE_VERSION.rsplit('.', 1)[0]}.*", + "creation_year": datetime.now().year, + } + template_directory = Path(__file__).parent.parent / "templates" / "{{ toolkit_name }}" + + env = Environment( + loader=FileSystemLoader(str(template_directory)), + autoescape=select_autoescape(["html", "xml"]), ) - top_level_dir = os.path.join(directory, name) - toolkit_dir = os.path.join(directory, name, toolkit_name) - - # Create the top level toolkit directory - if not create_directory(top_level_dir): - return - - # Create the toolkit directory - create_directory(toolkit_dir) - - # Create the __init__.py file in the toolkit directory - create_file(os.path.join(toolkit_dir, "__init__.py"), "") - - # Create the tools directory - create_directory(os.path.join(toolkit_dir, "tools")) - - # Create the __init__.py file in the tools directory - create_file(os.path.join(toolkit_dir, "tools", "__init__.py"), "") - - # Create the hello.py file in the tools directory - docstring = '"""Say a greeting!"""' - create_file( - os.path.join(toolkit_dir, "tools", "hello.py"), - dedent( - f""" - from typing import Annotated - from arcade.sdk import tool - - @tool - def hello(name: Annotated[str, "The name of the person to greet"]) -> str: - {docstring} - - return "Hello, " + name + "!" - """ - ).strip(), - ) - - # Create the pyproject.toml file - create_pyproject_toml(top_level_dir, toolkit_name, author, description) - - # If the user wants to generate a test directory - if generate_test_dir: - create_directory(os.path.join(top_level_dir, "tests")) - - # Create the __init__.py file in the tests directory - create_file(os.path.join(top_level_dir, "tests", "__init__.py"), "") - - # Create the test_hello.py file in the tests directory - stripped_toolkit_name = toolkit_name.replace("arcade_", "") - create_file( - os.path.join(top_level_dir, "tests", f"test_{stripped_toolkit_name}.py"), - dedent( - f""" - import pytest - from arcade.sdk.errors import ToolExecutionError - from {toolkit_name}.tools.hello import hello - - def test_hello(): - assert hello("developer") == "Hello, developer!" - - def test_hello_raises_error(): - with pytest.raises(ToolExecutionError): - hello(1) - """ - ).strip(), - ) - - # If the user wants to generate an eval directory - if generate_eval_dir: - create_directory(os.path.join(top_level_dir, "evals")) - - # Create the eval_hello.py file - stripped_toolkit_name = toolkit_name.replace("arcade_", "") - create_file( - os.path.join(top_level_dir, "evals", "eval_hello.py"), - dedent( - f""" - import {toolkit_name} - from {toolkit_name}.tools.hello import hello - - from arcade.sdk import ToolCatalog - from arcade.sdk.eval import ( - EvalRubric, - EvalSuite, - SimilarityCritic, - tool_eval, - ) - - # Evaluation rubric - rubric = EvalRubric( - fail_threshold=0.85, - warn_threshold=0.95, - ) - - - catalog = ToolCatalog() - catalog.add_module({toolkit_name}) - - - @tool_eval() - def {stripped_toolkit_name}_eval_suite(): - suite = EvalSuite( - name="{stripped_toolkit_name} Tools Evaluation", - system_message="You are an AI assistant with access to {stripped_toolkit_name} tools. Use them to help the user with their tasks.", - catalog=catalog, - rubric=rubric, - ) - - suite.add_case( - name="Saying hello", - user_message="Say hello to the developer!!!!", - expected_tool_calls=[ - ( - hello, - {{ - "name": "developer" - }} - ) - ], - rubric=rubric, - critics=[ - SimilarityCritic(critic_field="name", weight=0.5), - ], - ) - - return suite - """ - ).strip(), - ) - - console.print(f"[green]Toolkit {toolkit_name} has been created in {top_level_dir} [/green]") + try: + create_package(env, template_directory, toolkit_directory, context) + except Exception: + remove_toolkit(toolkit_directory, toolkit_name) + raise diff --git a/arcade/arcade/templates/{{ toolkit_name }}/.editorconfig b/arcade/arcade/templates/{{ toolkit_name }}/.editorconfig new file mode 100644 index 00000000..4f001bfb --- /dev/null +++ b/arcade/arcade/templates/{{ toolkit_name }}/.editorconfig @@ -0,0 +1,14 @@ +# Stop the editor from looking for .editorconfig files in the parent directories +root = true + +[*] +charset = utf-8 +insert_final_newline = true +end_of_line = lf +indent_style = space +indent_size = 4 +max_line_length = 100 # This is also set in .ruff.toml for ruff + +[*.{json,jsonc,yml,yaml}] +indent_style = space +indent_size = 2 # This is also set in .prettierrc.toml diff --git a/arcade/arcade/templates/{{ toolkit_name }}/.github/actions/setup-poetry-env/action.yml b/arcade/arcade/templates/{{ toolkit_name }}/.github/actions/setup-poetry-env/action.yml new file mode 100644 index 00000000..d9f5c9cc --- /dev/null +++ b/arcade/arcade/templates/{{ toolkit_name }}/.github/actions/setup-poetry-env/action.yml @@ -0,0 +1,38 @@ +name: "setup-poetry-env"{% raw %} +description: "Composite action to setup the Python and poetry environment." + +inputs: + python-version: + required: false + description: "The python version to use" + default: "3.11" + +runs: + using: "composite" + steps: + - name: Set up python + uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python-version }} + + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + virtualenvs-in-project: true + + - name: Generate poetry.lock + run: poetry lock --no-update + shell: bash + + - name: Load cached venv + id: cached-poetry-dependencies + uses: actions/cache@v4 + with: + path: .venv + key: venv-${{ runner.os }}-${{ inputs.python-version }}-${{ hashFiles('poetry.lock') }} + + - name: Install dependencies + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' + run: poetry install --no-interaction --all-extras + shell: bash +{% endraw %} diff --git a/arcade/arcade/templates/{{ toolkit_name }}/.github/workflows/main.yml b/arcade/arcade/templates/{{ toolkit_name }}/.github/workflows/main.yml new file mode 100644 index 00000000..bf9c2de4 --- /dev/null +++ b/arcade/arcade/templates/{{ toolkit_name }}/.github/workflows/main.yml @@ -0,0 +1,59 @@ +name: Main{% raw %} + +on: + push: + branches: + - main + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + +jobs: + quality: + runs-on: ubuntu-latest + steps: + - name: Check out + uses: actions/checkout@v4 + + - uses: actions/cache@v4 + with: + path: ~/.cache/pre-commit + key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + + - name: Set up the environment + uses: ./.github/actions/setup-poetry-env + + - name: Run checks + run: make check + + tox: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12"] + fail-fast: false + steps: + - name: Check out + uses: actions/checkout@v4 + + - name: Set up python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Poetry + uses: snok/install-poetry@v1 + + - name: Load cached venv + uses: actions/cache@v4 + with: + path: .tox + key: venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('poetry.lock') }} + + - name: Install tox + run: | + python -m pip install --upgrade pip + python -m pip install tox tox-gh-actions + + - name: Test with tox + run: tox +{% endraw %} diff --git a/arcade/arcade/templates/{{ toolkit_name }}/.github/workflows/publish-to-pypi.yml b/arcade/arcade/templates/{{ toolkit_name }}/.github/workflows/publish-to-pypi.yml new file mode 100644 index 00000000..6868b034 --- /dev/null +++ b/arcade/arcade/templates/{{ toolkit_name }}/.github/workflows/publish-to-pypi.yml @@ -0,0 +1,38 @@ +name: Publish to PyPI + +on: + release: + types: [published] + +jobs: + pypi-publish: + name: Publish to PyPi + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/project/{{ package_name }} + permissions: + id-token: write + steps: + - name: Check out + uses: actions/checkout@v4 + + - name: Set up python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build twine + + - name: Build package + run: python -m build + + - name: Publish to PyPI + env: + TWINE_USERNAME: "__token__"{% raw %} + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + run: twine upload dist/* +{% endraw %} diff --git a/arcade/arcade/templates/{{ toolkit_name }}/.gitignore b/arcade/arcade/templates/{{ toolkit_name }}/.gitignore new file mode 100644 index 00000000..26644de0 --- /dev/null +++ b/arcade/arcade/templates/{{ toolkit_name }}/.gitignore @@ -0,0 +1,166 @@ +.DS_Store + +*.lock + +# From https://raw.githubusercontent.com/github/gitignore/main/Python.gitignore + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/arcade/arcade/templates/{{ toolkit_name }}/.pre-commit-config.yaml b/arcade/arcade/templates/{{ toolkit_name }}/.pre-commit-config.yaml new file mode 100644 index 00000000..3953e996 --- /dev/null +++ b/arcade/arcade/templates/{{ toolkit_name }}/.pre-commit-config.yaml @@ -0,0 +1,18 @@ +files: ^./ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: "v4.4.0" + hooks: + - id: check-case-conflict + - id: check-merge-conflict + - id: check-toml + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.6.7 + hooks: + - id: ruff + args: [--fix] + - id: ruff-format diff --git a/arcade/arcade/templates/{{ toolkit_name }}/.prettierignore b/arcade/arcade/templates/{{ toolkit_name }}/.prettierignore new file mode 100644 index 00000000..ae715301 --- /dev/null +++ b/arcade/arcade/templates/{{ toolkit_name }}/.prettierignore @@ -0,0 +1,2 @@ +# Ignore Python files for Prettier +*.py diff --git a/arcade/arcade/templates/{{ toolkit_name }}/.prettierrc.toml b/arcade/arcade/templates/{{ toolkit_name }}/.prettierrc.toml new file mode 100644 index 00000000..3f1ac43d --- /dev/null +++ b/arcade/arcade/templates/{{ toolkit_name }}/.prettierrc.toml @@ -0,0 +1,15 @@ +# See https://prettier.io/docs/en/configuration + +# Note: This prettier config is only for the non-python files in this repo. +# Python files are formatted with ruff and ignored in .prettierignore + +trailingComma = "es5" +tabWidth = 4 +semi = false +singleQuote = false + +[[overrides]] +files = [ "*.json", "*.jsonc", "*.yml", "*.yaml" ] + + [overrides.options] + tabWidth = 2 diff --git a/arcade/arcade/templates/{{ toolkit_name }}/.ruff.toml b/arcade/arcade/templates/{{ toolkit_name }}/.ruff.toml new file mode 100644 index 00000000..36a7a4ed --- /dev/null +++ b/arcade/arcade/templates/{{ toolkit_name }}/.ruff.toml @@ -0,0 +1,44 @@ +target-version = "py39" +line-length = 100 +fix = true + +[lint] +select = [ + # flake8-2020 + "YTT", + # flake8-bandit + "S", + # flake8-bugbear + "B", + # flake8-builtins + "A", + # flake8-comprehensions + "C4", + # flake8-debugger + "T10", + # flake8-simplify + "SIM", + # isort + "I", + # mccabe + "C90", + # pycodestyle + "E", "W", + # pyflakes + "F", + # pygrep-hooks + "PGH", + # pyupgrade + "UP", + # ruff + "RUF", + # tryceratops + "TRY", +] + +[lint.per-file-ignores] +"**/tests/*" = ["S101"] + +[format] +preview = true +skip-magic-trailing-comma = false diff --git a/arcade/arcade/templates/{{ toolkit_name }}/LICENSE b/arcade/arcade/templates/{{ toolkit_name }}/LICENSE new file mode 100644 index 00000000..6a779d1b --- /dev/null +++ b/arcade/arcade/templates/{{ toolkit_name }}/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) {{ creation_year }}, {{ toolkit_author_name }} + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/arcade/arcade/templates/{{ toolkit_name }}/Makefile b/arcade/arcade/templates/{{ toolkit_name }}/Makefile new file mode 100644 index 00000000..c8f552fe --- /dev/null +++ b/arcade/arcade/templates/{{ toolkit_name }}/Makefile @@ -0,0 +1,62 @@ +.PHONY: help + +help: + @echo "🛠️ {{ toolkit_name }} Commands:\n" + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +.PHONY: install +install: ## Install the poetry environment and install the pre-commit hooks + @echo "📦 Checking if Poetry is installed" + @if ! command -v poetry &> /dev/null; then \ + echo "📦 Installing Poetry with pip"; \ + pip install poetry; \ + else \ + echo "📦 Poetry is already installed"; \ + fi + @echo "📦 Checking for poetry.lock file" + @if [ ! -f poetry.lock ]; then \ + echo "📦 Creating poetry.lock file"; \ + poetry lock; \ + fi + @echo "🚀 Installing package in development mode with all extras" + poetry install --all-extras + +.PHONY: build +build: clean-build ## Build wheel file using poetry + @echo "🚀 Creating wheel file" + poetry build + +.PHONY: clean-build +clean-build: ## clean build artifacts + rm -rf dist + +.PHONY: clean-dist +clean-dist: ## Clean all built distributions + @echo "🗑️ Cleaning dist directory" + @rm -rf dist + +.PHONY: test +test: ## Test the code with pytest + @echo "🚀 Testing code: Running pytest" + @poetry run pytest -W ignore -v --cov --cov-config=pyproject.toml --cov-report=xml + +.PHONY: coverage +coverage: ## Generate coverage report + @echo "coverage report" + coverage report + @echo "Generating coverage report" + coverage html + +.PHONY: bump-version +bump-version: ## Bump the version in the pyproject.toml file + @echo "🚀 Bumping version in pyproject.toml" + poetry version patch + +.PHONY: check +check: ## Run code quality tools. + @echo "🚀 Checking Poetry lock file consistency with 'pyproject.toml': Running poetry check --lock" + @poetry check --lock + @echo "🚀 Linting code: Running pre-commit" + @poetry run pre-commit run -a + @echo "🚀 Static type checking: Running mypy" + @poetry run mypy $(git ls-files '*.py') diff --git a/arcade/arcade/templates/{{ toolkit_name }}/README.md b/arcade/arcade/templates/{{ toolkit_name }}/README.md new file mode 100644 index 00000000..65a32368 --- /dev/null +++ b/arcade/arcade/templates/{{ toolkit_name }}/README.md @@ -0,0 +1,73 @@ +# {{ toolkit_name }} + +{{ toolkit_description }} + +## Creating your new toolkit repo + +1. Run `arcade new` and answer all the questions +2. Navigate to your new toolkit directory: + ```bash + cd {{ toolkit_name }} + ``` +3. Initialize git repository: + ```bash + git init + ``` +4. Create a new repository in Github with the same name as your new toolkit + > Note: Don't create a README, LICENSE or .gitignore when creating the repository because `arcade new` has already created these files for you. +5. Add remote and push your code: + ```bash + git remote add origin https://github.com/{{ toolkit_author_name }}/{{ toolkit_name }}.git + git branch -M main + git add . + git commit -m "Initial commit" + git push -u origin main + ``` + +## Publishing to PyPi + +### Generating a PyPi API Key + +1. Log into your PyPi account +2. Navigate to your Account settings and add an API token +3. Copy the token +4. In your Github repository: + - Go to Settings > Secrets and variables > Actions + - Click "New repository secret" + - Name your secret `PYPI_TOKEN` + - Paste your API Token into the Secret field + +### Creating a Release + +1. Navigate to your Github repository and click on Releases +2. Create a new tag that corresponds to the version in your toolkit's `pyproject.toml` file + > Note: This will be 0.0.1 for your first release + +## How to install the toolkit: +1. Run `make install` from the root of the repository + +## How to run tests: +1. Run `make test` from the root of the repository + +## How to run evals: +1. [Install the Arcade Engine Locally](https://docs.arcade-ai.com/home/install/local) +2. Install extra dependencies needed for evals: + ```bash + pip install 'arcade-ai[fastapi,evals]' + ``` +3. Log into Arcade AI: + ```bash + arcade login + ``` +4. Start the Arcade Engine and Actor: + ```bash + arcade dev + ``` +5. In a separate terminal, navigate to the `evals` directory: + ```bash + cd evals + ``` +5. Run the evals: + ```bash + arcade evals --host localhost --details + ``` diff --git a/arcade/arcade/templates/{{ toolkit_name }}/codecov.yaml b/arcade/arcade/templates/{{ toolkit_name }}/codecov.yaml new file mode 100644 index 00000000..058cfb76 --- /dev/null +++ b/arcade/arcade/templates/{{ toolkit_name }}/codecov.yaml @@ -0,0 +1,9 @@ +coverage: + range: 70..100 + round: down + precision: 1 + status: + project: + default: + target: 90% + threshold: 0.5% diff --git a/arcade/arcade/templates/{{ toolkit_name }}/evals/eval_{{ toolkit_name }}.py b/arcade/arcade/templates/{{ toolkit_name }}/evals/eval_{{ toolkit_name }}.py new file mode 100644 index 00000000..a0b2ae92 --- /dev/null +++ b/arcade/arcade/templates/{{ toolkit_name }}/evals/eval_{{ toolkit_name }}.py @@ -0,0 +1,50 @@ +from arcade.sdk import ToolCatalog +from arcade.sdk.eval import ( + EvalRubric, + EvalSuite, + SimilarityCritic, + tool_eval, +) + +import {{ package_name }} +from {{ package_name }}.tools.hello import say_hello + +# Evaluation rubric +rubric = EvalRubric( + fail_threshold=0.85, + warn_threshold=0.95, +) + + +catalog = ToolCatalog() +catalog.add_module({{ package_name }}) + + +@tool_eval() +def {{ toolkit_name }}_eval_suite() -> EvalSuite: + suite = EvalSuite( + name="{{ toolkit_name }} Tools Evaluation", + system_message=( + "You are an AI assistant with access to {{ toolkit_name }} tools. " + "Use them to help the user with their tasks." + ), + catalog=catalog, + rubric=rubric, + ) + + suite.add_case( + name="Saying hello", + user_message="He's actually right here, say hi to him!", + expected_tool_calls=[(say_hello, {"name": "John Doe"})], + rubric=rubric, + critics=[ + SimilarityCritic(critic_field="name", weight=0.5), + ], + additional_messages=[ + {"role": "user", "content": "My friend's name is John Doe."}, + {"role": "assistant", "content": "It is great that you have a friend named John Doe!"}, + ], + ) + + return suite + diff --git a/arcade/arcade/templates/{{ toolkit_name }}/pyproject.toml b/arcade/arcade/templates/{{ toolkit_name }}/pyproject.toml new file mode 100644 index 00000000..adbfe192 --- /dev/null +++ b/arcade/arcade/templates/{{ toolkit_name }}/pyproject.toml @@ -0,0 +1,39 @@ +[tool.poetry] +name = "{{ package_name }}" +version = "0.0.1" +description = "{{ toolkit_description }}" +authors = ["{{ toolkit_author_name }} <{{ toolkit_author_email }}>"] + +[tool.poetry.dependencies] +python = "^3.10" +arcade-ai = "{{ arcade_version }}" + +[tool.poetry.dev-dependencies] +pytest = "^8.3.0" +pytest-cov = "^4.0.0" +mypy = "^1.5.1" +pre-commit = "^3.4.0" +tox = "^4.11.1" +ruff = "^0.7.4" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.mypy] +files = ["{{ package_name }}/**/*.py"] +python_version = "3.10" +disallow_untyped_defs = "True" +disallow_any_unimported = "True" +no_implicit_optional = "True" +check_untyped_defs = "True" +warn_return_any = "True" +warn_unused_ignores = "True" +show_error_codes = "True" +ignore_missing_imports = "True" + +[tool.pytest.ini_options] +testpaths = ["tests"] + +[tool.coverage.report] +skip_empty = true diff --git a/arcade/arcade/templates/{{ toolkit_name }}/tests/__init__.py b/arcade/arcade/templates/{{ toolkit_name }}/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/arcade/arcade/templates/{{ toolkit_name }}/tests/test_{{ toolkit_name }}.py b/arcade/arcade/templates/{{ toolkit_name }}/tests/test_{{ toolkit_name }}.py new file mode 100644 index 00000000..9c244304 --- /dev/null +++ b/arcade/arcade/templates/{{ toolkit_name }}/tests/test_{{ toolkit_name }}.py @@ -0,0 +1,13 @@ +import pytest +from arcade.sdk.errors import ToolExecutionError + +from {{ package_name }}.tools.hello import say_hello + + +def test_hello() -> None: + assert say_hello("developer") == "Hello, developer!" + +def test_hello_raises_error() -> None: + with pytest.raises(ToolExecutionError): + say_hello(1) + diff --git a/arcade/arcade/templates/{{ toolkit_name }}/tox.ini b/arcade/arcade/templates/{{ toolkit_name }}/tox.ini new file mode 100644 index 00000000..fc0917c3 --- /dev/null +++ b/arcade/arcade/templates/{{ toolkit_name }}/tox.ini @@ -0,0 +1,17 @@ +[tox] +skipsdist = true +envlist = py310, py311, py312 + +[gh-actions] +python = + 3.10: py310 + 3.11: py311 + 3.12: py312 + +[testenv] +passenv = PYTHON_VERSION +allowlist_externals = poetry +commands = + poetry install -v --all-extras + pytest --doctest-modules tests --cov --cov-config=pyproject.toml --cov-report=xml +# mypy diff --git a/arcade/arcade/templates/{{ toolkit_name }}/{{ package_name }}/__init__.py b/arcade/arcade/templates/{{ toolkit_name }}/{{ package_name }}/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/arcade/arcade/templates/{{ toolkit_name }}/{{ package_name }}/tools/__init__.py b/arcade/arcade/templates/{{ toolkit_name }}/{{ package_name }}/tools/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/arcade/arcade/templates/{{ toolkit_name }}/{{ package_name }}/tools/hello.py b/arcade/arcade/templates/{{ toolkit_name }}/{{ package_name }}/tools/hello.py new file mode 100644 index 00000000..a69f77d5 --- /dev/null +++ b/arcade/arcade/templates/{{ toolkit_name }}/{{ package_name }}/tools/hello.py @@ -0,0 +1,11 @@ +from typing import Annotated + +from arcade.sdk import tool + + +@tool +def say_hello(name: Annotated[str, "The name of the person to greet"]) -> str: + """Say a greeting!""" + + return "Hello, " + name + "!" + diff --git a/arcade/pyproject.toml b/arcade/pyproject.toml index 1e9ee333..9ac578a3 100644 --- a/arcade/pyproject.toml +++ b/arcade/pyproject.toml @@ -18,6 +18,7 @@ python = ">=3.10,<4.0" pydantic = "^2.7.0" typer = "^0.9.0" rich = "^13.7.1" +Jinja2 = ">=2.7,<4.0.0" toml = "^0.10.2" pyyaml = "^6.0" tomlkit = "^0.12.4" @@ -61,6 +62,7 @@ arcade = "arcade.cli.main:cli" [tool.mypy] files = ["arcade"] +exclude = "arcade/templates" python_version = "3.10" disallow_untyped_defs = "True" disallow_any_unimported = "True"