diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..04aae67 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,24 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..59094e2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,17 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..158948b --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,74 @@ +name: Create Release + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + push: + branches: + - main + tags: + - "*.*.*" + +permissions: + contents: write + pull-requests: write + +env: + PLUGIN_JSON: "0.0.1" + TAG_EXISTS: false + PLUGIN_NAME: "my_plugin" + +jobs: +# This will be deleted by setup.py + check: + runs-on: ubuntu-latest + outputs: + plugin_name: ${{ steps.init.outputs.plugin_name }} + steps: + - name: Get plugin name + id: init + run: | + echo "plugin_name=${{ env.PLUGIN_NAME }}" >> $GITHUB_OUTPUT + +# This is the end of the removed section + release: + # This will be deleted by setup.py + needs: check + if: startsWith(needs.check.outputs.plugin_name, 'MY_PLUGIN') == false + # This is the end of the removed section + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Get plugin version + run: | + echo 'PLUGIN_JSON<> $GITHUB_ENV + cat ./plugin.json >> $GITHUB_ENV + echo 'EOF' >> $GITHUB_ENV + - name: Publish tag + if: env.TAG_EXISTS == false + uses: rickstaa/action-create-tag@v1 + with: + tag: "${{fromJson(env.PLUGIN_JSON).version}}" + tag_exists_error: false + message: "Latest release" + - name: Zip release + uses: TheDoctor0/zip-release@0.7.1 + with: + type: 'zip' + filename: '${{env.PLUGIN_NAME}}.zip' + exclusions: '*.git* setup.py' + directory: '.' + path: '.' + - name: Upload release + uses: ncipollo/release-action@v1.12.0 + with: + tag: "${{fromJson(env.PLUGIN_JSON).version}}" + artifacts: '${{env.PLUGIN_NAME}}.zip' + allowUpdates: true + replacesArtifacts: true + body: | + ${{ github.event.head_commit.message }} + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fe4724a --- /dev/null +++ b/.gitignore @@ -0,0 +1,155 @@ +# 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 + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__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 maintainted 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/ + +# Cheshire Cat Plugin settings +settings.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..d602b2c --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# My plugin + +[![awesome plugin](https://custom-icon-badges.demolab.com/static/v1?label=&message=awesome+plugin&color=383938&style=for-the-badge&logo=cheshire_cat_ai)](https://) +[![Awesome plugin](https://custom-icon-badges.demolab.com/static/v1?label=&message=Awesome+plugin&color=000000&style=for-the-badge&logo=cheshire_cat_ai)](https://) +[![awesome plugin](https://custom-icon-badges.demolab.com/static/v1?label=&message=awesome+plugin&color=F4F4F5&style=for-the-badge&logo=cheshire_cat_black)](https://) + +Write here all the useful information about your plugin. + +This repository is the template to automate the release of official Cheshire Cat AI plugins. + +## Usage + +1. Create a new repository clicking on the `Use this template` button. +2. Clone your new repo directly in the Cat's `plugins` folder. +3. Run the `setup.py` script: +```bash +python setup.py +``` +The script will prompt you to write the name of your plugin and make an initial setup setting the name in the files. + +4. Start developing! + +> **Important** +> A new release of your plugin is triggered every time you set a new `version` in the `plugin.json` file. +> Please, remember to set it correctly every time you want to release an update. + diff --git a/my_plugin.py b/my_plugin.py new file mode 100644 index 0000000..2f5f034 --- /dev/null +++ b/my_plugin.py @@ -0,0 +1,31 @@ +from cat.mad_hatter.decorators import tool, hook, plugin +from pydantic import BaseModel +from datetime import datetime, date + +class MySettings(BaseModel): + required_int: int + optional_int: int = 69 + required_str: str + optional_str: str = "meow" + required_date: date + optional_date: date = 1679616000 + +@plugin +def settings_model(): + return MySettings + +@tool +def get_the_day(tool_input, cat): + """Get the day of the week. Input is always None.""" + + dt = datetime.now() + + return dt.strftime('%A') + +@hook +def before_cat_sends_message(message, cat): + + prompt = f'Rephrase the following sentence in a grumpy way: {message["content"]}' + message["content"] = cat.llm(prompt) + + return message diff --git a/plugin.json b/plugin.json new file mode 100644 index 0000000..d213dea --- /dev/null +++ b/plugin.json @@ -0,0 +1,10 @@ +{ + "name": "My plugin", + "version": "0.0.1", + "description": "Description of my_plugin.", + "author_name": "Me", + "author_url": "https://mywebsite.me", + "plugin_url": "https://github.com/my_name/my_plugin", + "tags": "cat, template, example", + "thumb": "https://raw.githubusercontent.com/my_repo_path/my_plugin.png" +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..ee231e1 --- /dev/null +++ b/setup.py @@ -0,0 +1,79 @@ +import os +import pathlib +import fnmatch +import sys + +""" +Run this script once after first creating the repository from the template. + +This script is interactive and will prompt you for various inputs. +""" + + +def filter_lines(file_path, excluded_ranges): + with open(file_path, 'r') as file: + lines = file.readlines() + + new_lines = [] + current_line = 1 + for line in lines: + exclude = any(start <= current_line <= end for start, end in excluded_ranges) + if not exclude: + new_lines.append(line) + current_line += 1 + + with open(file_path, 'w') as file: + file.writelines(new_lines) + + +if __name__ == "__main__": + + repo_name = input("What is the name of your plugin (full name with spaces)? ") + + snake_name = "_".join(repo_name.lower().split(" ")) + + os.rename("my_plugin.py", f"{snake_name}.py") + + # Pipeline file's line ranges to remove + # This must reflect eventual main.yml changes + excluded_line_ranges = [(24, 35), (37, 40)] + + yaml_file_path = os.path.join(os.path.dirname(__file__), '.github', 'workflows', 'main.yml') + + filter_lines(yaml_file_path, excluded_line_ranges) + + for file in pathlib.Path(".").glob("**/*.*"): + filename = str(file) + + if fnmatch.fnmatch(filename, ".git/*"): # Exclude .git directory + continue + + if fnmatch.fnmatch(filename, "__pycache__/*"): # Exclude __pycache__ directory + continue + + if fnmatch.fnmatch(filename, "venv/*"): # Exclude venv directory + continue + + if fnmatch.fnmatch(filename, ".idea*"): + continue + + if fnmatch.fnmatch(filename, sys.argv[0]): # Exclude the script file itself + continue + + if os.path.basename(file) == "setup.py": + continue + + if file.is_dir(): + continue + else: + print(file) + with open(file, "r", encoding="utf-8", errors="ignore") as f: + content = f.read() + + replaced = content.replace("my_plugin", snake_name).replace("My plugin", repo_name.title()) + + with open(file, "w", encoding="utf-8", errors="ignore") as f: + f.write(replaced) + + print(f"All the occurrences were replaced successfully with `{repo_name}`!") + os.remove(sys.argv[0])