From 5b3f4455ed26040f490e5c20110e692ee0ac739f Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Mon, 26 Aug 2024 20:46:58 +1000 Subject: [PATCH 01/16] conditionally publish only when config changes (#9) * conditionally publish only when config changes * only on tags --- .github/workflows/pypi_release.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/pypi_release.yaml b/.github/workflows/pypi_release.yaml index fc407c7d..d47641db 100644 --- a/.github/workflows/pypi_release.yaml +++ b/.github/workflows/pypi_release.yaml @@ -2,8 +2,6 @@ name: PYPI Release on: push: - branches: - - main tags: - 'v*' From dbbd27a64697379490427eab0d0d9e534cfc291c Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Mon, 26 Aug 2024 20:48:14 +1000 Subject: [PATCH 02/16] add prompts (#11) * add prompts Some real world prompts in the readme * addressing feedback * small typo --- README.md | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8bd87f2c..0c9bcf7f 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,10 @@ You will see a prompt `G❯`: G❯ type your instructions here exactly as you would tell a developer. ``` -From here you can talk directly with goose - send along your instructions. If you are looking to exit, use `CTRL+D`, -although goose should help you figure that out if you forget. +> [!NOTE] +> From here you can talk directly with goose - send along your instructions. If you are looking to exit, use `CTRL+D`, +> although goose should help you figure that out if you forget. See below for some examples. + When you exit a session, it will save the history and you can resume it later on: @@ -108,7 +110,36 @@ To configure for example the screen toolkit, edit `~/.config/goose/profiles.yaml - name: screen requires: {} ``` - + +### Examples + +Here are some examples that have been used: + +``` +G❯ Looking at the in progress changes in this repo, help me finish off the feature. CONTRIBUTING.md shows how to run the tests. +``` + +``` +G❯ In this golang project, I want you to add open telemetry to help me get started with it. Look in the moneymovements module, run the `just test` command to check things work. +``` + +``` +G❯ This project uses an old version of jooq. Upgrade to the latest version, and ensure there are no incompatibilities by running all tests. Dependency versions are in gradle/libs.versions.toml and to run gradle, use the binary located in bin/gradle +``` + +``` +G❯ This is a fresh checkout of a golang project. I do not have my golang environment set up. Set it up and run tests for this project, and ensure they pass. Use the zookeeper jar included in this repository rather than installing zookeeper via brew. +``` + +``` +G❯ In this repo, I want you to look at how to add a new provider for azure. +Some hints are in this github issue: https://github.com/square/exchange/issues +/4 (you can use gh cli to access it). +``` + +``` +G❯ I want you to help me increase the test coverage in src/java... use mvn test to run the unit tests to check it works. +``` #### Advanced LLM config From 6ab1df00f23afac7690d25974b6db07e6850af91 Mon Sep 17 00:00:00 2001 From: Luke Alvoeiro Date: Mon, 26 Aug 2024 20:16:51 -0700 Subject: [PATCH 03/16] chore: gitignore generated lockfile (#15) --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 9df1010a..8733789a 100644 --- a/.gitignore +++ b/.gitignore @@ -137,4 +137,7 @@ dmypy.json docs/docs/reference ## goose session files -.goose \ No newline at end of file +.goose + +# ignore lockfile +uv.lock From 5aa12ec9ed39b6b5e25aca14401e8e73fbc29611 Mon Sep 17 00:00:00 2001 From: Lifei Zhou Date: Tue, 27 Aug 2024 14:45:52 +1000 Subject: [PATCH 04/16] Modified the readme to be more friendly to new users (#16) * modified README to be friendly to new users --- README.md | 214 +++++++++++++++++++++++++++++------------------------- 1 file changed, 116 insertions(+), 98 deletions(-) diff --git a/README.md b/README.md index 0c9bcf7f..cb0c3232 100644 --- a/README.md +++ b/README.md @@ -10,20 +10,46 @@ goose

Usage • -Installation • -Tips +Configuration • +Tips • +FAQ • +Open Source

`goose` assists in solving a wide range of programming and operational tasks. It is a live virtual developer you can interact with, guide, and learn from. -To solve problems, goose breaks down instructions into sequences of tasks and carries them out using tools. Its ability to connect its changes with real outcomes (e.g. errors) and course correct is its most powerful and exciting feature. goose is free open source software and is built to be extensible and customizable. +To solve problems, `goose` breaks down instructions into sequences of tasks and carries them out using tools. Its ability to connect its changes with real outcomes (e.g. errors) and course correct is its most powerful and exciting feature. `goose` is free open source software and is built to be extensible and customizable. + +![goose_demo](https://github.com/user-attachments/assets/0794eaba-97ab-40ef-af64-6fc7f68eb8e2) + + ## Usage +### Installation -You interact with goose in conversational sessions - something like a natural language driven code interpreter. -The default toolkit lets it take actions through shell commands and file edits. -You can interrupt Goose at any time to help redirect its efforts. +To install `goose`, we recommend `pipx` + +First make sure you've [installed pipx][pipx] - for example + +``` sh +brew install pipx +pipx ensurepath +``` + +Then you can install `goose` with + +```sh +pipx install goose-ai +``` +### LLM provider access setup +`goose` works on top of LLMs (you need to bring your own LLM). By default, `goose` uses `openai` as LLM provider. You need to set OPENAI_API_KEY as an environment variable if you would like to use `openai`. +```sh +export OPENAI_API_KEY=your_open_api_key +``` +Otherwise, please refer Configuration to customise `goose` + +### Start `goose` session From your terminal, navigate to the directory you'd like to start from and run: ```sh goose session start @@ -34,83 +60,118 @@ You will see a prompt `G❯`: ``` G❯ type your instructions here exactly as you would tell a developer. ``` +Now you are interact with `goose` in conversational sessions - something like a natural language driven code interpreter. +The default toolkit lets it take actions through shell commands and file edits. +You can interrupt `goose` at any time to help redirect its efforts. -> [!NOTE] -> From here you can talk directly with goose - send along your instructions. If you are looking to exit, use `CTRL+D`, -> although goose should help you figure that out if you forget. See below for some examples. +### Exit `goose` session +If you are looking to exit, use `CTRL+D`, although `goose` should help you figure that out if you forget. See below for some examples. -When you exit a session, it will save the history and you can resume it later on: +### Resume `goose` session +When you exit a session, it will save the history in `~/.config/goose/sessions` directory and you can resume it later on: ``` sh goose session resume ``` -## Tips +## Configuration -Here are some collected tips we have for working efficiently with Goose +`goose` can detect what LLM and toolkits it can work with from the configuration file `~/.config/goose/profiles.yaml` automatically. -- **goose can and will edit files**. Use a git strategy to avoid losing anything - such as staging your - personal edits and leaving goose edits unstaged until reviewed. Or consider using indivdual commits which can be reverted. -- **goose can and will run commands**. You can ask it to check with you first if you are concerned. It will check commands for safety as well. -- You can interrupt goose with `CTRL+C` to correct it or give it more info. -- goose works best when solving concrete problems - experiment with how far you need to break that problem - down to get goose to solve it. Be specific! E.g. it will likely fail to `"create a banking app"`, - but probably does a good job if prompted with `"create a Fastapi app with an endpoint for deposit and withdrawal - and with account balances stored in mysql keyed by id"` -- If goose doesn't have enough context to start with, it might go down the wrong direction. Tell it - to read files that you are refering to or search for objects in code. Even better, ask it to summarize - them for you, which will help it set up its own next steps. -- Refer to any objects in files with something that is easy to search for, such as `"the MyExample class" -- goose *loves* to know how to run tests to get a feedback loop going, just like you do. If you tell it how you test things locally and quickly, it can make use of that when working on your project -- You can use goose for tasks that would require scripting at times, even looking at your screen and correcting designs/helping you fix bugs, try asking it to help you in a way you would ask a person. -- goose will make mistakes, and go in the wrong direction from times, feel free to correct it, or start again. -- You can tell goose to run things for you continuously (and it will iterate, try, retry) but you can also tell it to check with you before doing things (and then later on tell it to go off on its own and do its best to solve). -- Goose can run anywhere, doesn't have to be in a repo, just ask it! +### Configuration options +Example: -## Installation +```yaml +default: + provider: openai + processor: gpt-4o + accelerator: gpt-4o-mini + moderator: truncate + toolkits: + - name: developer + requires: {} + - name: screen + requires: {} +``` -To install goose, we recommend `pipx` +You can edit this configuration file to use different LLMs and toolkits in `goose`. `goose can also be extended to support any LLM or combination of LLMs -First make sure you've [installed pipx][pipx] - for example +#### provider +Provider of LLM. LLM providers that currently are supported by `goose`: -``` sh -brew install pipx -pipx ensurepath -``` +| Provider | Required environment variable(s) to access provider | +| :----- | :------------------------------ | +| openai | `OPENAI_API_KEY` | +| anthropic | `ANTHROPIC_API_KEY` | +| databricks | `DATABRICKS_HOST` and `DATABRICKS_TOKEN` | -Then you can install goose with -``` sh -pipx install goose-ai -``` +#### processor +Model for complex, multi-step tasks such as writing code and executing commands. Example: `gpt-4o`. You should choose the model based the provider you configured. + +#### accelerator +Small model for fast, lightweight tasks. Example: `gpt-4o-mini`. You should choose the model based the provider you configured. -### Config +#### moderator +Rules designed to control or manage the output of the model. Moderators that currently are supported by `goose`: -Goose will try to detect what LLM it can work with and place a config in `~/.config/goose/profiles.yaml` automatically. +- `passive`: does not actively intervene in every response +- `truncate`: truncates the first contexts when the contexts exceed the max token size -#### Toolkits +#### toolkits -Goose can be extended with toolkits, and out of the box there are some available: +`goose` can be extended with toolkits, and out of the box there are some available: * `screen`: for letting goose take a look at your screen to help debug or work on designs (gives goose eyes) * `github`: for awareness and suggestions on how to use github * `repo_context`: for summarizing and understanding a repository you are working in. -To configure for example the screen toolkit, edit `~/.config/goose/profiles.yaml`: +### Examples +#### provider as `anthropic` ```yaml - provider: openai - processor: gpt-4o - accelerator: gpt-4o-mini +default: + provider: anthropic + processor: claude-3-5-sonnet-20240620 + accelerator: claude-3-5-sonnet-20240620 +... +``` +#### provider as `databricks` +```yaml +default: + provider: databricks + processor: databricks-meta-llama-3-1-70b-instruct + accelerator: databricks-meta-llama-3-1-70b-instruct moderator: passive toolkits: - name: developer requires: {} - - name: screen - requires: {} ``` +## Tips + +Here are some collected tips we have for working efficiently with `goose` + +- **`goose` can and will edit files**. Use a git strategy to avoid losing anything - such as staging your + personal edits and leaving `goose` edits unstaged until reviewed. Or consider using individual commits which can be reverted. +- **`goose` can and will run commands**. You can ask it to check with you first if you are concerned. It will check commands for safety as well. +- You can interrupt `goose` with `CTRL+C` to correct it or give it more info. +- `goose` works best when solving concrete problems - experiment with how far you need to break that problem + down to get `goose` to solve it. Be specific! E.g. it will likely fail to `"create a banking app"`, + but probably does a good job if prompted with `"create a Fastapi app with an endpoint for deposit and withdrawal + and with account balances stored in mysql keyed by id"` +- If `goose` doesn't have enough context to start with, it might go down the wrong direction. Tell it + to read files that you are referring to or search for objects in code. Even better, ask it to summarize + them for you, which will help it set up its own next steps. +- Refer to any objects in files with something that is easy to search for, such as `"the MyExample class" +- `goose` *loves* to know how to run tests to get a feedback loop going, just like you do. If you tell it how you test things locally and quickly, it can make use of that when working on your project +- You can use `goose` for tasks that would require scripting at times, even looking at your screen and correcting designs/helping you fix bugs, try asking it to help you in a way you would ask a person. +- `goose` will make mistakes, and go in the wrong direction from times, feel free to correct it, or start again. +- You can tell `goose` to run things for you continuously (and it will iterate, try, retry) but you can also tell it to check with you before doing things (and then later on tell it to go off on its own and do its best to solve). +- `goose` can run anywhere, doesn't have to be in a repo, just ask it! + + ### Examples Here are some examples that have been used: @@ -141,61 +202,18 @@ Some hints are in this github issue: https://github.com/square/exchange/issues G❯ I want you to help me increase the test coverage in src/java... use mvn test to run the unit tests to check it works. ``` +## FAQ -#### Advanced LLM config - -goose works on top of LLMs (you bring your own LLM). If you need to customize goose, one way is via editing: `~/.config/goose/profiles.yaml`. - -It will look by default something like: - -```yaml -default: - provider: openai - processor: gpt-4o - accelerator: gpt-4o-mini - moderator: truncate - toolkits: - - name: developer - requires: {} -``` - -*Note: This requires the environment variable `OPENAI_API_KEY` to be set to your OpenAI API key. goose uses at least 2 LLMs: one for acceleration for fast operating, and processing for writing code and executing commands.* - -You can tell it to use another provider for example for Anthropic: - -```yaml -default: - provider: anthropic - processor: claude-3-5-sonnet-20240620 - accelerator: claude-3-5-sonnet-20240620 -... -``` - -*Note: This will then use the claude-sonnet model, you will need to set the `ANTHROPIC_API_KEY` environment variable to your anthropic API key.* - -For Databricks hosted models: - -```yaml -default: - provider: databricks - processor: databricks-meta-llama-3-1-70b-instruct - accelerator: databricks-meta-llama-3-1-70b-instruct - moderator: passive - toolkits: - - name: developer - requires: {} -``` - -This requires `DATABRICKS_HOST` and `DATABRICKS_TOKEN` to be set accordingly +**Q:** Why did I get error message of "The model `gpt-4o` does not exist or you do not have access to it.` when I talked goose? -(goose can be extended to support any LLM or combination of LLMs). +**A:** You can find out the LLM provider and models in the configuration file `~/.config/goose/profiles.yaml` here to check whether your LLM provider account has access to the models. For example, after you have made a successful payment of $5 or more (usage tier 1), you'll be able to access the GPT-4, GPT-4 Turbo, GPT-4o models via the OpenAI API. [How can I access GPT-4, GPT-4 Turbo, GPT-4o, and GPT-4o mini?](https://help.openai.com/en/articles/7102672-how-can-i-access-gpt-4-gpt-4-turbo-gpt-4o-and-gpt-4o-mini). ## Open Source -Yes, goose is open source and always will be. goose is released under the ASL2.0 license meaning you can use it however you like. +Yes, `goose` is open source and always will be. `goose` is released under the ASL2.0 license meaning you can use it however you like. See LICENSE.md for more details. -To run goose from source, please see `CONTRIBUTING.md` for instructions on how to set up your environment and you can then run `uv run goose session start`. +To run `goose` from source, please see `CONTRIBUTING.md` for instructions on how to set up your environment and you can then run `uv run `goose` session start`. [pipx]: https://github.com/pypa/pipx?tab=readme-ov-file#install-pipx From 6fef25aa62fd26d77ecd8449ded032d6184191c2 Mon Sep 17 00:00:00 2001 From: Lifei Zhou Date: Thu, 29 Aug 2024 09:03:12 +1000 Subject: [PATCH 05/16] Enable cli options for plugin (#22) * added the entry point for plugin with cli group option --- pyproject.toml | 6 ++++-- src/goose/cli/main.py | 25 +++++++++++++++++-------- tests/cli/test_main.py | 42 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 62 insertions(+), 11 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 18bff707..124304d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "goose-ai" description = "a programming agent that runs on your machine" -version = "0.8.0" +version = "0.8.1" readme = "README.md" requires-python = ">=3.10" dependencies = [ @@ -30,9 +30,11 @@ default = "goose.profile:default_profile" [project.entry-points."goose.command"] file = "goose.command.file:FileCommand" -[project.entry-points."goose.cli"] +[project.entry-points."goose.cli.group"] goose = "goose.cli.main:goose_cli" +[project.entry-points."goose.cli.group_option"] + [project.scripts] goose = "goose.cli.main:cli" diff --git a/src/goose/cli/main.py b/src/goose/cli/main.py index 30bf3d9b..61c481ea 100644 --- a/src/goose/cli/main.py +++ b/src/goose/cli/main.py @@ -12,12 +12,10 @@ from goose.utils import load_plugins from goose.utils.session_file import list_sorted_session_files - @click.group() def goose_cli() -> None: pass - @goose_cli.command() def version() -> None: """Lists the version of goose and any plugins""" @@ -81,7 +79,7 @@ def session_start(profile: str, plan: Optional[str] = None) -> None: @session.command(name="resume") @click.argument("name", required=False) @click.option("--profile") -def session_resume(name: str, profile: str) -> None: +def session_resume(name: Optional[str], profile: str) -> None: """Resume an existing goose session""" if name is None: session_files = get_session_files() @@ -97,6 +95,7 @@ def session_resume(name: str, profile: str) -> None: @session.command(name="list") def session_list() -> None: + """List goose sessions""" session_files = get_session_files().items() for session_name, session_file in session_files: print(f"{datetime.fromtimestamp(session_file.stat().st_mtime).strftime('%Y-%m-%d %H:%M:%S')} {session_name}") @@ -105,6 +104,7 @@ def session_list() -> None: @session.command(name="clear") @click.option("--keep", default=3, help="Keep this many entries, default 3") def session_clear(keep: int) -> None: + """Delete old goose sessions, keeping the most recent sessions up to the specified number""" for i, (_, session_file) in enumerate(get_session_files().items()): if i >= keep: session_file.unlink() @@ -113,13 +113,22 @@ def session_clear(keep: int) -> None: def get_session_files() -> Dict[str, Path]: return list_sorted_session_files(SESSIONS_PATH) +@click.group( + invoke_without_command=True, + name="goose", + help="AI-powered tool to assist in solving programming and operational tasks",) +@click.pass_context +def cli(_: click.Context, **kwargs: Dict) -> None: + pass -# merging goose cli with additional cli plugins. -def cli() -> None: - clis = load_plugins("goose.cli") - cli_list = list(clis.values()) or [] - click.CommandCollection(sources=cli_list)() +all_cli_group_options = load_plugins("goose.cli.group_option") +for option in all_cli_group_options.values(): + cli = option()(cli) +all_cli_groups = load_plugins("goose.cli.group") +for group in all_cli_groups.values(): + for command in group.commands.values(): + cli.add_command(command) if __name__ == "__main__": cli() diff --git a/tests/cli/test_main.py b/tests/cli/test_main.py index 38d4c6c7..253aa3a3 100644 --- a/tests/cli/test_main.py +++ b/tests/cli/test_main.py @@ -1,11 +1,13 @@ from datetime import datetime +import importlib from time import time from unittest.mock import MagicMock, patch +import click import pytest from click.testing import CliRunner from exchange import Message -from goose.cli.main import goose_cli +from goose.cli.main import cli, goose_cli @pytest.fixture @@ -78,3 +80,41 @@ def test_session_clear_command(mock_session_files_path, create_session_file): session_files = list(mock_session_files_path.glob("*.jsonl")) assert len(session_files) == 1 assert session_files[0].stem == "second" + + +def test_combined_group_option(): + with patch("goose.utils.load_plugins") as mock_load_plugin: + group_option_name = "--describe-commands" + def option_callback(ctx, *_): + click.echo("Option callback") + ctx.exit() + mock_group_options = { + 'option1': lambda: click.option( + group_option_name, + is_flag=True, + callback=option_callback, + ), + } + def side_effect_func(param): + if param == "goose.cli.group_option": + return mock_group_options + elif param == "goose.cli.group": + return { } + mock_load_plugin.side_effect = side_effect_func + + # reload cli after mocking + importlib.reload(importlib.import_module('goose.cli.main')) + import goose.cli.main + cli = goose.cli.main.cli + + runner = CliRunner() + result = runner.invoke(cli, [group_option_name]) + assert result.exit_code == 0 + +def test_combined_group_commands(mock_session): + mock_session_class, mock_session_instance = mock_session + runner = CliRunner() + runner.invoke(cli, ["session", "resume", "session1", "--profile", "default"]) + mock_session_class.assert_called_once_with(name="session1", profile="default") + mock_session_instance.run.assert_called_once() + From 25cf8d2d099f2ce148b1936fbe470ba48afb1e83 Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Thu, 29 Aug 2024 11:12:12 +1000 Subject: [PATCH 06/16] link to vs code extension (#20) --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index cb0c3232..a43a1872 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,9 @@ Then you can install `goose` with ```sh pipx install goose-ai ``` +#### IDEs +There is an early version of a VS Code extension with goose support you can try here: https://github.com/square/goose-vscode - more to come soon. + ### LLM provider access setup `goose` works on top of LLMs (you need to bring your own LLM). By default, `goose` uses `openai` as LLM provider. You need to set OPENAI_API_KEY as an environment variable if you would like to use `openai`. ```sh From dd510a794a28b2b64397cf640b91d3284377af3d Mon Sep 17 00:00:00 2001 From: Elena Zherdeva <107525751+elenazherdeva@users.noreply.github.com> Date: Thu, 29 Aug 2024 17:35:36 -0400 Subject: [PATCH 07/16] fix (#24) --- pyproject.toml | 8 +++++--- src/goose/cli/main.py | 27 +++++++-------------------- 2 files changed, 12 insertions(+), 23 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 124304d1..56b1727f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,14 +1,14 @@ [project] name = "goose-ai" description = "a programming agent that runs on your machine" -version = "0.8.1" +version = "0.8.2" readme = "README.md" requires-python = ">=3.10" dependencies = [ "attrs>=23.2.0", "rich>=13.7.1", "ruamel-yaml>=0.18.6", - "ai-exchange>=0.8.0", + "ai-exchange>=0.8.1", "click>=8.1.7", "prompt-toolkit>=3.0.47", ] @@ -18,6 +18,9 @@ packages = [{ include = "goose", from = "src" }] [tool.hatch.build.targets.wheel] packages = ["src/goose"] +[project.entry-points."metadata.plugins"] +goose-ai = "" + [project.entry-points."goose.toolkit"] developer = "goose.toolkit.developer:Developer" github = "goose.toolkit.github:Github" @@ -47,4 +50,3 @@ dev-dependencies = [ "pytest>=8.3.2", "codecov>=2.1.13", ] - diff --git a/src/goose/cli/main.py b/src/goose/cli/main.py index 61c481ea..b79bbfab 100644 --- a/src/goose/cli/main.py +++ b/src/goose/cli/main.py @@ -21,28 +21,15 @@ def version() -> None: """Lists the version of goose and any plugins""" from importlib.metadata import entry_points, version - print(f"[green]Goose[/green]: [bold][cyan]{version('goose')}[/cyan][/bold]") + print(f"[green]Goose-ai[/green]: [bold][cyan]{version('goose-ai')}[/cyan][/bold]") print("[green]Plugins[/green]:") - filtered_groups = {} + entry_points = entry_points(group="metadata.plugins") modules = set() - if sys.version_info.minor >= 12: - for ep in entry_points(): - group = getattr(ep, "group", None) - if group and (group.startswith("exchange.") or group.startswith("goose.")): - filtered_groups.setdefault(group, []).append(ep) - for eps in filtered_groups.values(): - for ep in eps: - module_name = ep.module.split(".")[0] - modules.add(module_name) - else: - eps = entry_points() - for group, entries in eps.items(): - if group.startswith("exchange.") or group.startswith("goose."): - for entry in entries: - module_name = entry.value.split(".")[0] - modules.add(module_name) - - modules.remove("goose") + + for ep in entry_points: + module_name = ep.name + modules.add(module_name) + modules.remove("goose-ai") for module in sorted(list(modules)): # TODO: figure out how to get this to work for goose plugins block # as the module name is set to block.goose.cli From 9444ed16ea0acffcf1f31faacf3b551f5a882de9 Mon Sep 17 00:00:00 2001 From: Zaki Ali Date: Thu, 29 Aug 2024 15:33:13 -0700 Subject: [PATCH 08/16] fix: export metadata.plugins export should have valid module (#30) --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 56b1727f..11b7b532 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,14 +1,14 @@ [project] name = "goose-ai" description = "a programming agent that runs on your machine" -version = "0.8.2" +version = "0.8.3" readme = "README.md" requires-python = ">=3.10" dependencies = [ "attrs>=23.2.0", "rich>=13.7.1", "ruamel-yaml>=0.18.6", - "ai-exchange>=0.8.1", + "ai-exchange>=0.8.2", "click>=8.1.7", "prompt-toolkit>=3.0.47", ] @@ -19,7 +19,7 @@ packages = [{ include = "goose", from = "src" }] packages = ["src/goose"] [project.entry-points."metadata.plugins"] -goose-ai = "" +goose-ai = "goose.module_name" [project.entry-points."goose.toolkit"] developer = "goose.toolkit.developer:Developer" From 3c930e17664453fbdf7421b18f4ff8f83f4ddeee Mon Sep 17 00:00:00 2001 From: Luke Alvoeiro Date: Mon, 2 Sep 2024 20:40:02 -0700 Subject: [PATCH 09/16] feat: upgrade `ai-exchange` to version `0.8.3` and fix tests (#34) --- pyproject.toml | 4 ++-- src/goose/utils/ask.py | 6 +++--- tests/utils/test_ask.py | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 11b7b532..df120245 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,14 +1,14 @@ [project] name = "goose-ai" description = "a programming agent that runs on your machine" -version = "0.8.3" +version = "0.8.4" readme = "README.md" requires-python = ">=3.10" dependencies = [ "attrs>=23.2.0", "rich>=13.7.1", "ruamel-yaml>=0.18.6", - "ai-exchange>=0.8.2", + "ai-exchange>=0.8.3", "click>=8.1.7", "prompt-toolkit>=3.0.47", ] diff --git a/src/goose/utils/ask.py b/src/goose/utils/ask.py index e3c057a2..0e34b444 100644 --- a/src/goose/utils/ask.py +++ b/src/goose/utils/ask.py @@ -1,4 +1,4 @@ -from exchange import Exchange, Message +from exchange import Exchange, Message, CheckpointData def ask_an_ai(input: str, exchange: Exchange, prompt: str = "", no_history: bool = True) -> Message: @@ -61,9 +61,9 @@ def clear_exchange(exchange: Exchange, clear_tools: bool = False) -> Exchange: """ if clear_tools: - new_exchange = exchange.replace(messages=[], checkpoints=[], tools=()) + new_exchange = exchange.replace(messages=[], checkpoint_data=CheckpointData(), tools=()) else: - new_exchange = exchange.replace(messages=[], checkpoints=[]) + new_exchange = exchange.replace(messages=[], checkpoint_data=CheckpointData()) return new_exchange diff --git a/tests/utils/test_ask.py b/tests/utils/test_ask.py index 419f3a5b..b7bd8269 100644 --- a/tests/utils/test_ask.py +++ b/tests/utils/test_ask.py @@ -1,7 +1,7 @@ from unittest.mock import MagicMock, patch import pytest -from exchange import Exchange +from exchange import Exchange, CheckpointData from goose.utils.ask import ask_an_ai, clear_exchange, replace_prompt @@ -76,7 +76,7 @@ def test_clear_exchange_without_tools(): new_exchange = clear_exchange(exchange, clear_tools=False) # Assert - exchange.replace.assert_called_once_with(messages=[], checkpoints=[]) + exchange.replace.assert_called_once_with(messages=[], checkpoint_data=CheckpointData()) assert new_exchange == exchange.replace.return_value, "Should return the modified exchange" @@ -89,7 +89,7 @@ def test_clear_exchange_with_tools(): new_exchange = clear_exchange(exchange, clear_tools=True) # Assert - exchange.replace.assert_called_once_with(messages=[], checkpoints=[], tools=()) + exchange.replace.assert_called_once_with(messages=[], checkpoint_data=CheckpointData(), tools=()) assert new_exchange == exchange.replace.return_value, "Should return the modified exchange with tools cleared" From fadeba9ef933bcb621ee6fa11953d086bf95b2ef Mon Sep 17 00:00:00 2001 From: Luke Alvoeiro Date: Mon, 2 Sep 2024 20:44:49 -0700 Subject: [PATCH 10/16] fix: resuming sessions (#35) --- pyproject.toml | 2 +- src/goose/cli/session.py | 18 +++++++++++++++++- tests/cli/test_session.py | 39 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 55 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index df120245..f731bc3f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "goose-ai" description = "a programming agent that runs on your machine" -version = "0.8.4" +version = "0.8.5" readme = "README.md" requires-python = ">=3.10" dependencies = [ diff --git a/src/goose/cli/session.py b/src/goose/cli/session.py index 3da620d3..713bd1f8 100644 --- a/src/goose/cli/session.py +++ b/src/goose/cli/session.py @@ -2,7 +2,7 @@ from pathlib import Path from typing import Any, Dict, List, Optional -from exchange import Message, ToolResult, ToolUse +from exchange import Message, ToolResult, ToolUse, Text from prompt_toolkit.shortcuts import confirm from rich import print from rich.console import RenderableType @@ -24,6 +24,8 @@ from goose.utils import droid, load_plugins from goose.utils.session_file import read_from_file, write_to_file +RESUME_MESSAGE = "I see we were interrupted. How can I help you?" + def load_provider() -> str: # We try to infer a provider, by going in order of what will auth @@ -91,8 +93,22 @@ def __init__( if name is not None and self.session_file_path.exists(): messages = self.load_session() + if messages and messages[-1].role == "user": + if type(messages[-1].content[-1]) is Text: + # remove the last user message + messages.pop() + elif type(messages[-1].content[-1]) is ToolResult: + # if we remove this message, we would need to remove + # the previous assistant message as well. instead of doing + # that, we just add a new assistant message to prompt the user + messages.append(Message.assistant(RESUME_MESSAGE)) + if messages and type(messages[-1].content[-1]) is ToolUse: + # remove the last request for a tool to be used messages.pop() + + # add a new assistant text message to prompt the user + messages.append(Message.assistant(RESUME_MESSAGE)) self.exchange.messages.extend(messages) if len(self.exchange.messages) == 0 and plan: diff --git a/tests/cli/test_session.py b/tests/cli/test_session.py index 83dd6eba..79a7c4a2 100644 --- a/tests/cli/test_session.py +++ b/tests/cli/test_session.py @@ -1,7 +1,7 @@ from unittest.mock import MagicMock, patch import pytest -from exchange import Message +from exchange import Message, ToolUse, ToolResult from goose.cli.prompt.goose_prompt_session import GoosePromptSession from goose.cli.prompt.user_input import PromptAction, UserInput from goose.cli.session import Session @@ -32,7 +32,7 @@ def create_session(session_attributes: dict = {}): yield create_session -def test_session_does_not_extend_last_user_message_on_init( +def test_session_does_not_extend_last_user_text_message_on_init( create_session_with_mock_configs, mock_sessions_path, create_session_file ): messages = [Message.user("Hello"), Message.assistant("Hi"), Message.user("Last should be removed")] @@ -44,6 +44,41 @@ def test_session_does_not_extend_last_user_message_on_init( assert [message.text for message in session.exchange.messages] == ["Hello", "Hi"] +def test_session_adds_resume_message_if_last_message_is_tool_result( + create_session_with_mock_configs, mock_sessions_path, create_session_file +): + messages = [ + Message.user("Hello"), + Message(role="assistant", content=[ToolUse(id="1", name="first_tool", parameters={})]), + Message(role="user", content=[ToolResult(tool_use_id="1", output="output")]), + ] + create_session_file(messages, mock_sessions_path / f"{SESSION_NAME}.jsonl") + + session = create_session_with_mock_configs({"name": SESSION_NAME}) + print("Messages after session init:", session.exchange.messages) # Debugging line + assert len(session.exchange.messages) == 4 + assert session.exchange.messages[-1].role == "assistant" + assert session.exchange.messages[-1].text == "I see we were interrupted. How can I help you?" + + +def test_session_removes_tool_use_and_adds_resume_message_if_last_message_is_tool_use( + create_session_with_mock_configs, mock_sessions_path, create_session_file +): + messages = [ + Message.user("Hello"), + Message(role="assistant", content=[ToolUse(id="1", name="first_tool", parameters={})]), + ] + create_session_file(messages, mock_sessions_path / f"{SESSION_NAME}.jsonl") + + session = create_session_with_mock_configs({"name": SESSION_NAME}) + print("Messages after session init:", session.exchange.messages) # Debugging line + assert len(session.exchange.messages) == 2 + assert [message.text for message in session.exchange.messages] == [ + "Hello", + "I see we were interrupted. How can I help you?", + ] + + def test_save_session_create_session(mock_sessions_path, create_session_with_mock_configs, mock_specified_session_name): session = create_session_with_mock_configs() session.exchange.messages.append(Message.assistant("Hello")) From 1c440d6310a51613d4e28d8d5fe9c7a2a95c89a5 Mon Sep 17 00:00:00 2001 From: Luke Alvoeiro Date: Mon, 2 Sep 2024 22:01:57 -0700 Subject: [PATCH 11/16] chore: upgrade ai-exchange dependency (#36) --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f731bc3f..f1647255 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,14 +1,14 @@ [project] name = "goose-ai" description = "a programming agent that runs on your machine" -version = "0.8.5" +version = "0.8.6" readme = "README.md" requires-python = ">=3.10" dependencies = [ "attrs>=23.2.0", "rich>=13.7.1", "ruamel-yaml>=0.18.6", - "ai-exchange>=0.8.3", + "ai-exchange>=0.8.4", "click>=8.1.7", "prompt-toolkit>=3.0.47", ] From bdee4f7ebd24ad4fb5bed8334ec4382483e1da8f Mon Sep 17 00:00:00 2001 From: Zaki Ali Date: Tue, 3 Sep 2024 18:30:23 -0700 Subject: [PATCH 12/16] chore: Update publish github workflow to check package versions before publishing (#19) --- .github/workflows/pypi_release.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/pypi_release.yaml b/.github/workflows/pypi_release.yaml index d47641db..98758fb9 100644 --- a/.github/workflows/pypi_release.yaml +++ b/.github/workflows/pypi_release.yaml @@ -21,8 +21,26 @@ jobs: - name: Build with UV run: uvx --from build pyproject-build --installer uv + - name: Check version + id: check_version + run: | + PACKAGE_NAME=$(grep '^name =' pyproject.toml | sed -E 's/name = "(.*)"/\1/') + TAG_VERSION=$(echo "$GITHUB_REF" | sed -E 's/refs\/tags\/v(.+)/\1/') + CURRENT_VERSION=$(curl -s https://pypi.org/pypi/$PACKAGE_NAME/json | jq -r .info.version) + PROJECT_VERSION=$(grep '^version =' pyproject.toml | sed -E 's/version = "(.*)"/\1/') + if [ "$TAG_VERSION" != "$PROJECT_VERSION" ]; then + echo "Tag version does not match version in pyproject.toml" + exit 1 + fi + if python -c "from packaging.version import parse as parse_version; exit(0 if parse_version('$TAG_VERSION') > parse_version('$CURRENT_VERSION') else 1)"; then + echo "new_version=true" >> $GITHUB_OUTPUT + else + exit 1 + fi + - name: Publish uses: pypa/gh-action-pypi-publish@v1.4.2 + if: steps.check_version.outputs.new_version == 'true' with: user: __token__ password: ${{ secrets.PYPI_TOKEN_TEMP }} From 466ce2375c5638bbf1e498c71a8d9b47ffbb5b04 Mon Sep 17 00:00:00 2001 From: Max Novich Date: Tue, 3 Sep 2024 21:10:08 -0700 Subject: [PATCH 13/16] added some regex based checks for dangerous commands (#38) --- src/goose/toolkit/developer.py | 7 +++--- src/goose/utils/check_shell_command.py | 33 +++++++++++++++++++++++++ tests/utils/test_check_shell_command.py | 27 ++++++++++++++++++++ 3 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 src/goose/utils/check_shell_command.py create mode 100644 tests/utils/test_check_shell_command.py diff --git a/src/goose/toolkit/developer.py b/src/goose/toolkit/developer.py index 6f53ca4d..e977d13d 100644 --- a/src/goose/toolkit/developer.py +++ b/src/goose/toolkit/developer.py @@ -1,6 +1,7 @@ from pathlib import Path from subprocess import CompletedProcess, run from typing import List +from goose.utils.check_shell_command import is_dangerous_command from exchange import Message from rich import box @@ -135,7 +136,7 @@ def shell(self, command: str) -> str: command (str): The shell command to run. It can support multiline statements if you need to run more than one at a time """ - self.notifier.status("running shell command") + self.notifier.status("planning to run shell command") # Log the command being executed in a visually structured format (Markdown). # The `.log` method is used here to log the command execution in the application's UX # this method is dynamically attached to functions in the Goose framework to handle user-visible @@ -156,13 +157,13 @@ def shell(self, command: str) -> str: rating = int(rating) except ValueError: rating = 5 # if we can't interpret we default to unsafe - if int(rating) > 3: + if is_dangerous_command(command) or int(rating) > 3: if not keep_unsafe_command_prompt(command): raise RuntimeError( f"The command {command} was rejected as dangerous by the user." + " Do not proceed further, instead ask for instructions." ) - + self.notifier.status("running shell command") result: CompletedProcess = run(command, shell=True, text=True, capture_output=True, check=False) if result.returncode == 0: output = "Command succeeded" diff --git a/src/goose/utils/check_shell_command.py b/src/goose/utils/check_shell_command.py new file mode 100644 index 00000000..6081de30 --- /dev/null +++ b/src/goose/utils/check_shell_command.py @@ -0,0 +1,33 @@ +import re + + +def is_dangerous_command(command: str) -> bool: + """ + Check if the command matches any dangerous patterns. + + Args: + command (str): The shell command to check. + + Returns: + bool: True if the command is dangerous, False otherwise. + """ + dangerous_patterns = [ + r"\brm\b", # rm command + r"\bgit\s+push\b", # git push command + r"\bsudo\b", # sudo command + # Add more dangerous command patterns here + r"\bmv\b", # mv command + r"\bchmod\b", # chmod command + r"\bchown\b", # chown command + r"\bmkfs\b", # mkfs command + r"\bsystemctl\b", # systemctl command + r"\breboot\b", # reboot command + r"\bshutdown\b", # shutdown command + # Manipulating files in ~/ directly or dot files + r"^~/[^/]+$", # Files directly in home directory + r"/\.[^/]+$", # Dot files + ] + for pattern in dangerous_patterns: + if re.search(pattern, command): + return True + return False diff --git a/tests/utils/test_check_shell_command.py b/tests/utils/test_check_shell_command.py new file mode 100644 index 00000000..7d94a46d --- /dev/null +++ b/tests/utils/test_check_shell_command.py @@ -0,0 +1,27 @@ +import pytest +from goose.utils.check_shell_command import is_dangerous_command + + +@pytest.mark.parametrize( + "command", + [ + "rm -rf /", + "git push origin master", + "sudo reboot", + "mv /etc/passwd /tmp/", + "chmod 777 /etc/passwd", + "chown root:root /etc/passwd", + "mkfs -t ext4 /dev/sda1", + "systemctl stop nginx", + "reboot", + "shutdown now", + "echo hello > ~/.bashrc", + ], +) +def test_dangerous_commands(command): + assert is_dangerous_command(command) + + +@pytest.mark.parametrize("command", ["ls -la", 'echo "Hello World"', "cp ~/folder/file.txt /tmp/"]) +def test_safe_commands(command): + assert not is_dangerous_command(command) From bb8966bb02c6bf8c6010b4b4290339f21c4f03d9 Mon Sep 17 00:00:00 2001 From: Bradley Axen Date: Tue, 3 Sep 2024 21:47:47 -0700 Subject: [PATCH 14/16] Apply ruff and add to CI (#40) --- .github/workflows/ci.yaml | 21 +++++++++++++-------- src/goose/cli/main.py | 12 ++++++++---- tests/cli/test_main.py | 13 +++++++++---- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ef5afb99..0fb1701e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,14 +9,19 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - name: Install UV - run: curl -LsSf https://astral.sh/uv/install.sh | sh + - name: Install UV + run: curl -LsSf https://astral.sh/uv/install.sh | sh - - name: Source Cargo Environment - run: source $HOME/.cargo/env + - name: Source Cargo Environment + run: source $HOME/.cargo/env - - name: Run tests - run: | - uv run pytest tests -m 'not integration' \ No newline at end of file + - name: Ruff + run: | + uvx ruff check + uvx ruff format --check + + - name: Run tests + run: | + uv run pytest tests -m 'not integration' diff --git a/src/goose/cli/main.py b/src/goose/cli/main.py index b79bbfab..4ebcc281 100644 --- a/src/goose/cli/main.py +++ b/src/goose/cli/main.py @@ -1,4 +1,3 @@ -import sys from datetime import datetime from pathlib import Path from typing import Dict, Optional @@ -12,10 +11,12 @@ from goose.utils import load_plugins from goose.utils.session_file import list_sorted_session_files + @click.group() def goose_cli() -> None: pass + @goose_cli.command() def version() -> None: """Lists the version of goose and any plugins""" @@ -100,14 +101,17 @@ def session_clear(keep: int) -> None: def get_session_files() -> Dict[str, Path]: return list_sorted_session_files(SESSIONS_PATH) + @click.group( - invoke_without_command=True, - name="goose", - help="AI-powered tool to assist in solving programming and operational tasks",) + invoke_without_command=True, + name="goose", + help="AI-powered tool to assist in solving programming and operational tasks", +) @click.pass_context def cli(_: click.Context, **kwargs: Dict) -> None: pass + all_cli_group_options = load_plugins("goose.cli.group_option") for option in all_cli_group_options.values(): cli = option()(cli) diff --git a/tests/cli/test_main.py b/tests/cli/test_main.py index 253aa3a3..617b3d5c 100644 --- a/tests/cli/test_main.py +++ b/tests/cli/test_main.py @@ -85,36 +85,41 @@ def test_session_clear_command(mock_session_files_path, create_session_file): def test_combined_group_option(): with patch("goose.utils.load_plugins") as mock_load_plugin: group_option_name = "--describe-commands" + def option_callback(ctx, *_): click.echo("Option callback") ctx.exit() + mock_group_options = { - 'option1': lambda: click.option( + "option1": lambda: click.option( group_option_name, is_flag=True, callback=option_callback, ), } + def side_effect_func(param): if param == "goose.cli.group_option": return mock_group_options elif param == "goose.cli.group": - return { } + return {} + mock_load_plugin.side_effect = side_effect_func # reload cli after mocking - importlib.reload(importlib.import_module('goose.cli.main')) + importlib.reload(importlib.import_module("goose.cli.main")) import goose.cli.main + cli = goose.cli.main.cli runner = CliRunner() result = runner.invoke(cli, [group_option_name]) assert result.exit_code == 0 + def test_combined_group_commands(mock_session): mock_session_class, mock_session_instance = mock_session runner = CliRunner() runner.invoke(cli, ["session", "resume", "session1", "--profile", "default"]) mock_session_class.assert_called_once_with(name="session1", profile="default") mock_session_instance.run.assert_called_once() - From 13db5150bdedad957ad0dfd4c3996e74212eefbe Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Wed, 4 Sep 2024 16:18:01 +1000 Subject: [PATCH 15/16] adding in ability to provide per repo hints (#32) * adding in ability to provide per repo hints * tidy up test --- src/goose/toolkit/developer.py | 7 ++++++- tests/toolkit/test_developer.py | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/goose/toolkit/developer.py b/src/goose/toolkit/developer.py index e977d13d..1114df19 100644 --- a/src/goose/toolkit/developer.py +++ b/src/goose/toolkit/developer.py @@ -34,7 +34,12 @@ class Developer(Toolkit): def system(self) -> str: """Retrieve system configuration details for developer""" - return Message.load("prompts/developer.jinja").text + hints_path = Path('.goosehints') + system_prompt = Message.load("prompts/developer.jinja").text + if hints_path.is_file(): + goosehints = hints_path.read_text() + system_prompt = f"{system_prompt}\n\nHints:\n{goosehints}" + return system_prompt @tool def update_plan(self, tasks: List[dict]) -> List[dict]: diff --git a/tests/toolkit/test_developer.py b/tests/toolkit/test_developer.py index 915380df..e049ee9f 100644 --- a/tests/toolkit/test_developer.py +++ b/tests/toolkit/test_developer.py @@ -1,4 +1,6 @@ from pathlib import Path + + from tempfile import TemporaryDirectory from unittest.mock import MagicMock, Mock @@ -66,3 +68,5 @@ def test_write_file(temp_dir, developer_toolkit): content = "Hello World" developer_toolkit.write_file(test_file.as_posix(), content) assert test_file.read_text() == content + + From c91b11b3c522a65655cd8e43f176415f9068b185 Mon Sep 17 00:00:00 2001 From: Luke Alvoeiro Date: Wed, 4 Sep 2024 08:52:13 -0700 Subject: [PATCH 16/16] feat: show available toolkits (#37) --- src/goose/cli/main.py | 14 ++++++++++++++ src/goose/toolkit/developer.py | 2 +- src/goose/toolkit/lint.py | 12 ++++++++++++ src/goose/toolkit/repo_context/repo_context.py | 2 ++ tests/test_linting.py | 5 +++++ 5 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 src/goose/toolkit/lint.py create mode 100644 tests/test_linting.py diff --git a/src/goose/cli/main.py b/src/goose/cli/main.py index 4ebcc281..0e266dd7 100644 --- a/src/goose/cli/main.py +++ b/src/goose/cli/main.py @@ -48,6 +48,20 @@ def session() -> None: pass +@goose_cli.group() +def toolkit() -> None: + """Manage toolkits""" + pass + + +@toolkit.command(name="list") +def list_toolkits() -> None: + print("[green]Available toolkits:[/green]") + for toolkit_name, toolkit in load_plugins("goose.toolkit").items(): + first_line_of_doc = toolkit.__doc__.split("\n")[0] + print(f" - [bold]{toolkit_name}[/bold]: {first_line_of_doc}") + + @session.command(name="start") @click.option("--profile") @click.option("--plan", type=click.Path(exists=True)) diff --git a/src/goose/toolkit/developer.py b/src/goose/toolkit/developer.py index 1114df19..7a735749 100644 --- a/src/goose/toolkit/developer.py +++ b/src/goose/toolkit/developer.py @@ -26,7 +26,7 @@ def keep_unsafe_command_prompt(command: str) -> PromptType: class Developer(Toolkit): - """The developer toolkit provides a set of general purpose development capabilities + """Provides a set of general purpose development capabilities The tools include plan management, a general purpose shell execution tool, and file operations. We also include some default shell strategies in the prompt, such as using ripgrep diff --git a/src/goose/toolkit/lint.py b/src/goose/toolkit/lint.py new file mode 100644 index 00000000..0f08f222 --- /dev/null +++ b/src/goose/toolkit/lint.py @@ -0,0 +1,12 @@ +from goose.utils import load_plugins + + +def lint_toolkits() -> None: + for toolkit_name, toolkit in load_plugins("goose.toolkit").items(): + assert toolkit.__doc__ is not None, f"`{toolkit_name}` toolkit must have a docstring" + first_line_of_docstring = toolkit.__doc__.split("\n")[0] + assert len(first_line_of_docstring.split(" ")) > 5, f"`{toolkit_name}` toolkit docstring is too short" + assert len(first_line_of_docstring.split(" ")) < 12, f"`{toolkit_name}` toolkit docstring is too long" + assert first_line_of_docstring[ + 0 + ].isupper(), f"`{toolkit_name}` toolkit docstring must start with a capital letter" diff --git a/src/goose/toolkit/repo_context/repo_context.py b/src/goose/toolkit/repo_context/repo_context.py index 89a01a76..8be8794f 100644 --- a/src/goose/toolkit/repo_context/repo_context.py +++ b/src/goose/toolkit/repo_context/repo_context.py @@ -14,6 +14,8 @@ class RepoContext(Toolkit): + """Provides context about the current repository""" + def __init__(self, notifier: Notifier, requires: Requirements) -> None: super().__init__(notifier=notifier, requires=requires) diff --git a/tests/test_linting.py b/tests/test_linting.py new file mode 100644 index 00000000..f6e246ff --- /dev/null +++ b/tests/test_linting.py @@ -0,0 +1,5 @@ +from goose.toolkit.lint import lint_toolkits + + +def test_lint_toolkits(): + lint_toolkits()