DEV Community

Cover image for How to Extend your AI Agent for Custom Developer Workflows
Rizèl Scarlett
Rizèl Scarlett

Posted on • Edited on

How to Extend your AI Agent for Custom Developer Workflows

The beauty of open source lies in its ability to breed innovation, which is why I was excited to discover Goose, an open source AI developer agent I could contribute to. Until now, many AI developer tools I've used have been closed source, limiting users to whatever features the product offers.

While watching other engineers interact with Goose, I was pleasantly surprised to learn that anyone can extend its capabilities. This means I can take Goose's base functionality and customize it for my own use cases, no matter how unconventional they might be. I've seen engineers use it for practical purposes, like summarizing repositories or integrating with the GitHub CLI, as well as experimental ones, like having Goose browse the web for plushy toys or enabling voice interactions.

Excited by these possibilities, I set out to create a toolkit of my own. I had ambitious ideas. I wanted to use Goose to:

  • Assist in training AI models that interpret American Sign Language
  • Generate memes
  • Create a Sonic Pi integration for live coding music from the terminal
  • Help plan Thanksgiving dinner

Unfortunately, I failed at all of these attempts. I initially thought the problem was that I'm not a Pythonista, but I quickly realized I didn't fully understand how to create toolkits—and if I didn't, perhaps others were in the same boat. In this blog post, I'll share how to create your first toolkit using a simple but practical example: a to-do list manager.

What Are Toolkits? 🤔

The Official Definition

According to the official Goose documentation, toolkits are plugins that "provide Goose with tools (functions) it can call and optionally will load additional context into the system prompt (such as 'The Github CLI is called via gh and you should use it to run git commands'). Toolkits can do basically anything, from calling external APIs, to taking screenshots, to summarizing your current project."

My Definition

Think of toolkits like applications on your phone. At baseline, your phone can make calls, but you can download apps that extend its capabilities—from playing games to taking pictures and listening to music. Similarly, while Goose starts with basic capabilities like managing sessions and versions, toolkits extend its functionality to:

  • Take screenshots for debugging
  • Interact with the GitHub CLI
  • Manage Jira projects
  • Summarize repositories

Want to see a toolkit in action? Check out my previous blog post where I use the screen toolkit to fix UI bugs.

How to Create Your Own Toolkit 📝

Step 1: Install Goose

To install Goose, run the following commands in your terminal:

brew install pipx
pipx ensurepath
pipx install goose-ai
Enter fullscreen mode Exit fullscreen mode

Step 2: Fork the Goose Plugin Repository

The Goose plugin repo is where your toolkit will live. Follow the directions outlined in the README to get started.

Step 3: Start a Development Session

Now, you can create a Goose development session. This is different from starting a regular session with Goose - you're starting a session where you want to build with Goose:

uv run goose session start
Enter fullscreen mode Exit fullscreen mode

Note: You may need to install uv first.

Step 4: Create Your Toolkit File

Create a file called mytodo.py in this directory goose-plugins/src/goose_plugins/toolkits

Step 5: Set Up Boilerplate Code

Add the following lines of code to your mytodo.py file:

from goose.toolkit.base import tool, Toolkit

class MyTodoToolkit(Toolkit):
    """A simple to-do list toolkit for managing tasks."""

    def __init__(self, *args: tuple, **kwargs: dict) -> None:
        super().__init__(*args, **kwargs)
        # Initialize tasks as a list of dictionaries with 'description' and 'completed' fields
        self.tasks = []
Enter fullscreen mode Exit fullscreen mode

So far, in this file, we've:

  • Imported the base toolkit library
  • Created a class called MyTodoToolkit which extends the Toolkit class
  • Initialized an empty dictionary that will hold our list of tasks

Step 6: Add Your Toolkit's First Action

Let's start with writing a method to add tasks to our list. Add the following code to mytodo.py:

@tool
def add_task(self, task: str) -> str:
    """Add a new task to the to-do list.

    Args:
        task (str): The task description to add to the list.
    """
    self.tasks.append({"description": task, "completed": False})
    self.notifier.log(f"Added task: '{task}'")
    return f"Added task: '{task}'"
Enter fullscreen mode Exit fullscreen mode

Let's break down what's happening in this method:

  1. The @tool Decorator: This decorator tags a function as a tool, allowing it to be automatically identified and managed within a toolkit.

  2. Method Definition: We created a method called add_task. In toolkits, you create methods to define specific actions that the toolkit can perform. In this case, we want our task management toolkit to be able to add tasks.

  3. Docstring: The docstring is required by Goose - it's not just for documentation. Goose uses it to:

    • Validate the method's functionality
    • Check if the parameters match

According to the creator of Goose, Bradley Axen, the docstrings often help Goose produce more refined results when processing commands.

Step 7: Complete Your Toolkit

Now you can add more methods that handle removing tasks, updating tasks, listing all tasks, and listing completed tasks. Here's what your completed mytodo.py file should look like:

from goose.toolkit.base import tool, Toolkit


class MyTodoToolkit(Toolkit):
    """A simple to-do list toolkit for managing tasks."""

    def __init__(self, *args: tuple, **kwargs: dict) -> None:
        super().__init__(*args, **kwargs)
        # Initialize tasks as a list of dictionaries with 'description' and 'completed' fields
        self.tasks = []

    @tool
    def add_task(self, task: str) -> str:
        """Add a new task to the to-do list.

        Args:
            task (str): The task description to add to the list.
        """
        # Store task as dictionary with description and completion status
        self.tasks.append({"description": task, "completed": False})
        self.notifier.log(f"Added task: '{task}'")
        return f"Added task: '{task}'"

    @tool
    def list_tasks(self) -> str:
        """List all tasks in the to-do list."""
        if not self.tasks:
            self.notifier.log("No tasks in the to-do list.")
            return "No tasks in the to-do list. Give user instructions on how to add tasks."

        task_list = []
        for index, task in enumerate(self.tasks, start=1):
            status = "" if task["completed"] else " "
            task_list.append(f"{index}. [{status}] {task['description']}")
        self.notifier.log("\n".join(task_list))
        return f"Tasks listed successfully: {task_list}"

    @tool
    def remove_task(self, task_number: int) -> str:
        """Remove a task from the to-do list by its number.

        Args:
            task_number (int): The index number of the task to remove (starting from 1).
        """
        try:
            removed_task = self.tasks.pop(task_number - 1)
            self.notifier.log(f"Removed task: '{removed_task['description']}'")
            return f"Removed task: '{removed_task['description']}'"
        except IndexError:
            self.notifier.log("Invalid task number. Please try again.")
            return "User input invalid task number and needs to try again."

    @tool
    def mark_as_complete(self, task_number: int) -> str:
        """Mark a task as complete by its number.

        Args:
            task_number (int): The index number of the task to mark as complete (starting from 1).

        Raises:
            IndexError: If the task number is invalid.
        """
        try:
            self.tasks[task_number - 1]["completed"] = True
            self.notifier.log(f"Marked task {task_number} as complete: '{self.tasks[task_number - 1]['description']}'")
            return f"Marked task {task_number} as complete: '{self.tasks[task_number - 1]['description']}'"
        except IndexError:
            self.notifier.log("Invalid task number. Please try again.")
            return "User input invalid task number and needs to try again."

    @tool
    def list_completed_tasks(self) -> str:
        """List all completed tasks."""
        completed_tasks = [task for task in self.tasks if task["completed"]]
        if not completed_tasks:
            self.notifier.log("No completed tasks.")
            return "No completed tasks. Provide instructions for marking tasks as complete."

        task_list = []
        for index, task in enumerate(completed_tasks, start=1):
            task_list.append(f"{index}. [✓] {task['description']}")

        self.notifier.log("\n".join(task_list))
        return f"Tasks listed successfully: {task_list}"

    @tool
    def update_task(self, task_number: int, new_description: str) -> str:
        """Update the description of a task by its number.

        Args:
            task_number (int): The index number of the task to update (starting from 1).
            new_description (str): The new description for the task.

        Raises:
            IndexError: If the task number is invalid.
        """
        try:
            old_description = self.tasks[task_number - 1]["description"]
            self.tasks[task_number - 1]["description"] = new_description
            self.notifier.log(f"Updated task {task_number} from '{old_description}' to '{new_description}'")
            return f"Updated task {task_number} successfully."
        except IndexError:
            self.notifier.log("Invalid task number. Unable to update.")
            return "Invalid task number. Unable to update."
Enter fullscreen mode Exit fullscreen mode

Step 8: Make Your Toolkit Available to Others

We now have to make the Toolkit available to other users by adding it to our pyproject.toml:

[project.entry-points."goose.toolkit"]
developer = "goose.toolkit.developer:Developer"
github = "goose.toolkit.github:Github"
# Add a line like this - the key becomes the name used in profiles
mytodo = "goose_plugins.toolkits.mytodo:MyTodoToolkit"
Enter fullscreen mode Exit fullscreen mode

This follows the format module.submodule:ClassName:

  • goose_plugins is the base module
  • toolkits is a submodule within goose_plugins
  • mytodo is a further submodule within toolkits
  • MyTodoToolkit is the class name

Step 9: Enable Your Toolkit

Now, you can elect to use this toolkit by adding it to your ~/.config/goose/profiles.yaml:

default:
  provider: openai
  processor: gpt-4o
  accelerator: gpt-4o-mini
  moderator: truncate
  toolkits:
  - name: developer
    requires: {}
  - name: mytodo
    requires: {}
Enter fullscreen mode Exit fullscreen mode

Now you can use the toolkit by prompting it in natural language to add tasks to your to-do list, mark tasks as complete, and update tasks. Here's an example of how it works:

Reference my Pull Request 👀

You can take a look at my PR to see the full implementation

The Future of Toolkits 🚀

The way we build toolkits is evolving. Check out the product roadmap plans to see what's coming next.

interacting with todo toolkit

Join Our Community 🤝

The Goose Open Source Community continues to grow, and I invite you join the fun by:

Can't wait to see what you build!

Top comments (0)