feat: make goosehints jinja templated (#43)
This commit is contained in:
@@ -126,10 +126,28 @@ Rules designed to control or manage the output of the model. Moderators that cur
|
|||||||
|
|
||||||
`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)
|
* `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
|
* `github`: for awareness and suggestions on how to use github
|
||||||
* `repo_context`: for summarizing and understanding a repository you are working in.
|
* `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
|
### Examples
|
||||||
#### provider as `anthropic`
|
#### provider as `anthropic`
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ integration *FLAGS:
|
|||||||
uv run pytest tests -m integration {{FLAGS}}
|
uv run pytest tests -m integration {{FLAGS}}
|
||||||
|
|
||||||
format:
|
format:
|
||||||
ruff check . --fix
|
uvx ruff check . --fix
|
||||||
ruff format .
|
uvx ruff format .
|
||||||
|
|
||||||
coverage *FLAGS:
|
coverage *FLAGS:
|
||||||
uv run coverage run -m pytest tests -m "not integration" {{FLAGS}}
|
uv run coverage run -m pytest tests -m "not integration" {{FLAGS}}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from rich.table import Table
|
|||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
|
|
||||||
from goose.toolkit.base import Toolkit, tool
|
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:
|
def keep_unsafe_command_prompt(command: str) -> PromptType:
|
||||||
@@ -37,7 +37,7 @@ class Developer(Toolkit):
|
|||||||
hints_path = Path(".goosehints")
|
hints_path = Path(".goosehints")
|
||||||
system_prompt = Message.load("prompts/developer.jinja").text
|
system_prompt = Message.load("prompts/developer.jinja").text
|
||||||
if hints_path.is_file():
|
if hints_path.is_file():
|
||||||
goosehints = hints_path.read_text()
|
goosehints = render_template(hints_path)
|
||||||
system_prompt = f"{system_prompt}\n\nHints:\n{goosehints}"
|
system_prompt = f"{system_prompt}\n\nHints:\n{goosehints}"
|
||||||
return system_prompt
|
return system_prompt
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from pygments.lexers import get_lexer_for_filename
|
from pygments.lexers import get_lexer_for_filename
|
||||||
from pygments.util import ClassNotFound
|
from pygments.util import ClassNotFound
|
||||||
|
|
||||||
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
|
||||||
|
|
||||||
def get_language(filename: Path) -> str:
|
def get_language(filename: Path) -> str:
|
||||||
"""
|
"""
|
||||||
@@ -19,3 +22,23 @@ def get_language(filename: Path) -> str:
|
|||||||
return lexer.name
|
return lexer.name
|
||||||
except ClassNotFound:
|
except ClassNotFound:
|
||||||
return ""
|
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 {})
|
||||||
|
|||||||
@@ -1,12 +1,23 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
from unittest.mock import MagicMock, Mock
|
from unittest.mock import MagicMock, Mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from goose.toolkit.base import Requirements
|
from goose.toolkit.base import Requirements
|
||||||
from goose.toolkit.developer import Developer
|
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
|
@pytest.fixture
|
||||||
@@ -30,6 +41,20 @@ def developer_toolkit():
|
|||||||
return 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):
|
def test_update_plan(developer_toolkit):
|
||||||
tasks = [
|
tasks = [
|
||||||
{"description": "Task 1", "status": "planned"},
|
{"description": "Task 1", "status": "planned"},
|
||||||
|
|||||||
Reference in New Issue
Block a user