Add arcade new Improvements (#156)

# PR Description
This PR is a part of the community contributed toolkits story.

* `arcade new` now uses jinja templates
* `arcade new` now creates a "cookiecutter" toolkit equipped with
everything a community contributed toolkit needs to be easily tested,
published to PyPi, etc. as its own Github repo
* I created the following toolkit with `arcade new`:
- [PyPi](https://pypi.org/project/arcade-local-file-management/0.1.5/)
-
[Github](https://github.com/EricGustin/local_file_management/tree/0.1.5)
This commit is contained in:
Eric Gustin 2024-12-02 17:44:09 -08:00 committed by GitHub
parent bebfcab1e9
commit 8dbbe23d73
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 776 additions and 201 deletions

View file

@ -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/.*"

View file

@ -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

View file

@ -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

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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/

View file

@ -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

View file

@ -0,0 +1,2 @@
# Ignore Python files for Prettier
*.py

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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')

View file

@ -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
```

View file

@ -0,0 +1,9 @@
coverage:
range: 70..100
round: down
precision: 1
status:
project:
default:
target: 90%
threshold: 0.5%

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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 + "!"

View file

@ -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"