From 0242a5d9dd2029d5aa8fe65bd4d76904d7de6dc1 Mon Sep 17 00:00:00 2001 From: Salman Mohammed Date: Wed, 4 Sep 2024 11:44:34 -0700 Subject: [PATCH] feat: make goosehints jinja templated (#43) --- README.md | 46 +++++++++++++++++++++++---------- justfile | 4 +-- src/goose/toolkit/developer.py | 4 +-- src/goose/toolkit/utils.py | 23 +++++++++++++++++ tests/toolkit/test_developer.py | 29 +++++++++++++++++++-- 5 files changed, 86 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index a43a1872..873e31ee 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ goose

-Usage • +UsageConfigurationTipsFAQ • @@ -25,7 +25,7 @@ To solve problems, `goose` breaks down instructions into sequences of tasks and ## Usage -### Installation +### Installation To install `goose`, we recommend `pipx` @@ -36,7 +36,7 @@ brew install pipx pipx ensurepath ``` -Then you can install `goose` with +Then you can install `goose` with ```sh pipx install goose-ai @@ -45,7 +45,7 @@ pipx install goose-ai 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`. +`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 ``` @@ -80,10 +80,10 @@ goose session resume ## Configuration -`goose` can detect what LLM and toolkits it can work with from the configuration file `~/.config/goose/profiles.yaml` automatically. +`goose` can detect what LLM and toolkits it can work with from the configuration file `~/.config/goose/profiles.yaml` automatically. ### Configuration options -Example: +Example: ```yaml default: @@ -124,12 +124,30 @@ Rules designed to control or manage the output of the model. Moderators that cur #### 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: +* `developer`: for general-purpose development capabilities, including plan management, shell execution, and file operations, with default shell strategies like using ripgrep. * `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. +#### Configuring goose per repo + +If you are using the `developer` toolkit, `goose` adds the content from `.goosehints` + file in working directory to the system prompt of the `developer` toolkit. The hints +file is meant to provide additional context about your project. The context can be +user-specific or at the project level in which case, you +can commit it to git. `.goosehints` file is Jinja templated so you could have something +like this: +``` +Here is an overview of how to contribute: +{% include 'CONTRIBUTING.md' %} + +The following justfile shows our common commands: +```just +{% include 'justfile' %} +``` + ### Examples #### provider as `anthropic` @@ -158,10 +176,10 @@ 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. +- **`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"`, + 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 @@ -169,7 +187,7 @@ Here are some collected tips we have for working efficiently with `goose` 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. +- 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! @@ -177,14 +195,14 @@ Here are some collected tips we have for working efficiently with `goose` ### Examples -Here are some examples that have been used: +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❯ 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. ``` ``` @@ -196,7 +214,7 @@ G❯ This is a fresh checkout of a golang project. I do not have my golang envir ``` ``` -G❯ In this repo, I want you to look at how to add a new provider for azure. +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). ``` @@ -213,7 +231,7 @@ G❯ I want you to help me increase the test coverage in src/java... use mvn tes ## 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`. diff --git a/justfile b/justfile index a7f021ed..994da5f9 100644 --- a/justfile +++ b/justfile @@ -10,8 +10,8 @@ integration *FLAGS: uv run pytest tests -m integration {{FLAGS}} format: - ruff check . --fix - ruff format . + uvx ruff check . --fix + uvx ruff format . coverage *FLAGS: uv run coverage run -m pytest tests -m "not integration" {{FLAGS}} diff --git a/src/goose/toolkit/developer.py b/src/goose/toolkit/developer.py index aa27b3a7..eeb084e1 100644 --- a/src/goose/toolkit/developer.py +++ b/src/goose/toolkit/developer.py @@ -12,7 +12,7 @@ from rich.text import Text from goose.toolkit.base import Toolkit, tool -from goose.toolkit.utils import get_language +from goose.toolkit.utils import get_language, render_template def keep_unsafe_command_prompt(command: str) -> PromptType: @@ -37,7 +37,7 @@ def system(self) -> str: hints_path = Path(".goosehints") system_prompt = Message.load("prompts/developer.jinja").text if hints_path.is_file(): - goosehints = hints_path.read_text() + goosehints = render_template(hints_path) system_prompt = f"{system_prompt}\n\nHints:\n{goosehints}" return system_prompt diff --git a/src/goose/toolkit/utils.py b/src/goose/toolkit/utils.py index 1517c55e..61632b77 100644 --- a/src/goose/toolkit/utils.py +++ b/src/goose/toolkit/utils.py @@ -1,8 +1,11 @@ from pathlib import Path +from typing import Optional from pygments.lexers import get_lexer_for_filename from pygments.util import ClassNotFound +from jinja2 import Environment, FileSystemLoader + def get_language(filename: Path) -> str: """ @@ -19,3 +22,23 @@ def get_language(filename: Path) -> str: return lexer.name except ClassNotFound: return "" + + +def render_template(template_path: Path, context: Optional[dict] = None) -> str: + """ + Renders a Jinja2 template given a Pathlib path, with no context needed. + + :param template_path: Path to the Jinja2 template file. + :param context: Optional dictionary containing the context for rendering the template. + :return: Rendered template as a string. + """ + # Ensure the path is absolute and exists + if not template_path.is_absolute(): + template_path = template_path.resolve() + + if not template_path.exists(): + raise FileNotFoundError(f"Template file {template_path} does not exist.") + + env = Environment(loader=FileSystemLoader(template_path.parent)) + template = env.get_template(template_path.name) + return template.render(context or {}) diff --git a/tests/toolkit/test_developer.py b/tests/toolkit/test_developer.py index 53c1c844..a3a291af 100644 --- a/tests/toolkit/test_developer.py +++ b/tests/toolkit/test_developer.py @@ -1,12 +1,23 @@ from pathlib import Path - - from tempfile import TemporaryDirectory from unittest.mock import MagicMock, Mock import pytest from goose.toolkit.base import Requirements from goose.toolkit.developer import Developer +from contextlib import contextmanager +import os + + +@contextmanager +def change_dir(new_dir): + """Context manager to temporarily change the current working directory.""" + original_dir = os.getcwd() + os.chdir(new_dir) + try: + yield + finally: + os.chdir(original_dir) @pytest.fixture @@ -30,6 +41,20 @@ def developer_toolkit(): return toolkit +def test_system_prompt_with_goosehints(temp_dir, developer_toolkit): + readme_file = temp_dir / "README.md" + readme_file.write_text("This is from the README.md file.") + + hints_file = temp_dir / ".goosehints" + jinja_template_content = "Hints:\n\n{% include 'README.md' %}\nEnd." + hints_file.write_text(jinja_template_content) + + with change_dir(temp_dir): + system_prompt = developer_toolkit.system() + expected_end = "Hints:\n\nThis is from the README.md file.\nEnd." + assert system_prompt.endswith(expected_end) + + def test_update_plan(developer_toolkit): tasks = [ {"description": "Task 1", "status": "planned"},