Skip to main content

Add an AI agent to an Existing Project

Now that you've learned the basics of creating an AI agent with Dagger in Built an AI agent, its time to apply those lessons to a more realistic use case. In this guide, you will build an AI agent that builds features for an existing project.

For this guide you will start with a Daggerized project. The agent will use the Dagger functions in this project that are already used locally and in CI for testing the project.

Requirements

This quickstart will take you approximately 10 minutes to complete. You should ideally have completed the AI agent and CI quickstart and should be familiar with programming in Go, Python, TypeScript, PHP, or Java. Before beginning, ensure that:

Inspect the example application

This quickstart uses the Daggerized project from the CI quickstart. To verify that your project is in the correct state, change to the project's working directory and list the available Dagger Functions:

cd hello-dagger
dagger functions

This should show:

Name        Description
build Build the application container
build-env Build a ready-to-use development environment
publish Publish the application container after building and testing it on-the-fly
test Return the result of running unit tests

Create a workspace for the agent

The goal of this quickstart is to create an agent to interact with the existing codebase and add features to it. This means that the agent needs the ability to list, read and write files in the project directory.

To give the agent these tools, create a Dagger submodule called workspace. This module will have Dagger Functions to list, read, and write files. The agent can use these Dagger Functions to interact with the codebase. By giving the agent a module with a limited set of tools, the agent has less room for error and will be more successful at its tasks.

Start by creating the submodule:

dagger init --sdk=go .dagger/workspace

Replace the .dagger/workspace/main.go file with the following contents:

// A module for editing code

package main

import (
"context"
"dagger/workspace/internal/dagger"
)

type Workspace struct {
Source *dagger.Directory
}

func New(
// The source directory
source *dagger.Directory,
) *Workspace {
return &Workspace{Source: source}
}

// Read a file in the Workspace
func (w *Workspace) ReadFile(
ctx context.Context,
// The path to the file in the workspace
path string,
) (string, error) {
return w.Source.File(path).Contents(ctx)
}

// Write a file to the Workspace
func (w *Workspace) WriteFile(
// The path to the file in the workspace
path string,
// The new contents of the file
contents string,
) *Workspace {
w.Source = w.Source.WithNewFile(path, contents)
return w
}

// List all of the files in the Workspace
func (w *Workspace) ListFiles(ctx context.Context) (string, error) {
return dag.Container().
From("alpine:3").
WithDirectory("/src", w.Source).
WithWorkdir("/src").
WithExec([]string{"tree", "./src"}).
Stdout(ctx)
}

// Get the source code directory from the Workspace
func (w *Workspace) GetSource() *dagger.Directory {
return w.Source
}

In this Dagger module, each Dagger Function performs a different operation:

  • The read-file Dagger Function reads a file in the workspace.
  • The write-file Dagger Function writes a file to the workspace.
  • The list-files Dagger Function lists all the files in the workspace.
  • The get-source Dagger Function gets the source code directory from the workspace.

See the functions of the new workspace module:

dagger -m .dagger/workspace functions

Now install the new submodule as a dependency to the main module:

dagger install ./.dagger/workspace

Create an agentic function

This agent will make changes to the application and use the existing test function from the Daggerized CI pipeline to validate the changes.

Add the following new function to the main dagger module in .dagger/main.go:

// A coding agent for developing new features
func (m *HelloDagger) Develop(
ctx context.Context,
// Assignment to complete
assignment string,
// +defaultPath="/"
source *dagger.Directory,
) (*dagger.Directory, error) {
// Environment with agent inputs and outputs
environment := dag.Env(dagger.EnvOpts{Privileged: true}).
WithStringInput("assignment", assignment, "the assignment to complete").
WithWorkspaceInput(
"workspace",
dag.Workspace(source),
"the workspace with tools to edit code").
WithWorkspaceOutput(
"completed",
"the workspace with the completed assignment")

// Detailed prompt stored in markdown file
promptFile := dag.CurrentModule().Source().File("develop_prompt.md")

// Put it all together to form the agent
work := dag.LLM().
WithEnv(environment).
WithPromptFile(promptFile)

// Get the output from the agent
completed := work.
Env().
Output("completed").
AsWorkspace()
completedDirectory := completed.GetSource().WithoutDirectory("node_modules")

// Make sure the tests really pass
_, err := m.Test(ctx, completedDirectory)
if err != nil {
return nil, err
}

// Return the Directory with the assignment completed
return completedDirectory, nil
}

This code creates a Dagger Function called develop that takes an assignment and codebase as input and returns a Directory with the modified codebase containing the completed assignment.

There are a few important points to note here:

  • The variable environment is the environment to define inputs and outputs for the agent. Each input and output provides a description which acts as a declarative form of prompting.
  • Other Dagger module dependencies are automatically detected and made available for use in this environment. In this example, Dagger finds the workspace sub-module installed and automatically generates bindings for it: with-workspace-input and with-workspace-output. These bindings can be used to provide (or retrieve) the workspace to the agent.
  • The environment is privileged, which means that the environment includes all the Dagger Functions in the existing Daggerized CI pipeline, plus the core Dagger API. This allows the agent to use the existing test function to run unit tests.
  • The LLM is supplied with a prompt file instead of an inline prompt. By separating the prompt from the Dagger Function code, it becomes easier to maintain and update the instructions given to the LLM.

The final step is to add the prompt file. Create a file at .dagger/develop_prompt.md with the following content:

You are a developer on the HelloDagger Vue.js project.
You will be given an assignment and the tools to complete the assignment.
Your assignment is: $assignment

## Constraints
- Before writing code, analyze the Workspace to understand the project.
- Do not make unneccessary changes.
- Run tests with the HelloDagger Test tool.
- Read and write files with the Workspace_read_file, Workspace_list_files, and Workspace_write_file tools
- You must pass the Directory from the Workspace_get_source to the HelloDagger Test tool.
- Do not select Container or Directory tools. Only use Workspace and HelloDagger tools
- Always run tests to validate your code changes.
- Do not stop until you have completed the assignment and the tests pass.
note

This prompt is longer and more structured than the prompt from the first quickstart. The tasks the agent will be asked to complete will be more complex because its making modifications to an existing codebase and will need to understand more context. To create a reliable agent that can operate with more complexity, the prompt structure and tools are extremely important.

Run the agent

Type dagger to launch Dagger Shell in interactive mode.

Check the help text for the new Dagger Function:

.help develop

This should show how to use the develop function.

Make sure your LLM provider has been properly configured:

llm | model

This should show the model you've configured to use with your provider.

Call the agent with an assignment:

develop "make the main page blue"

If all goes well, the agent will explore the project to find the correct place to make the changes, write the appropriate changes, and then validate them by calling the test function.

tip

If the agent misbehaves, try providing more guidance in the prompt file based on what went wrong.

Since the agent returns a Directory, use Dagger Shell to do more with that Directory. For example:

# Save the directory containing the completed assignment to a variable
completed=$(develop "make the main page blue")
# Get a terminal in the build container with the directory
build-env --source $completed | terminal
# Run the app with the updated source code
build --source $completed | as-service | up --ports 8080:80
# Save the changes to your filesystem
$completed | export .
# Exit
exit

Now you have built an agent that can write code for your project!

Deploy the agent to GitHub

The next step is to take this agent and deploy it so that it can run automatically. For this part of the guide, you will configure GitHub so that when you label a GitHub issue with "develop", the agent will automatically pick up the task and open a pull request with its solution.

The following steps require that you used the GitHub template and have your own hello-dagger repository.

Create the develop-issue function

The develop-issue function will use the github-issue Dagger module from the Daggerverse. This module provides functions to read GitHub issues and create pull requests.

Install the dependency:

dagger install github.com/kpenfound/dag/github-issue@b316e472d3de5bb0e54fe3991be68dc85e33ef38

Add the following new function to the main dagger module in .dagger/main.go:

// Develop with a Github issue as the assignment and open a pull request
func (m *HelloDagger) DevelopIssue(
ctx context.Context,
// Github Token with permissions to write issues and contents
githubToken *dagger.Secret,
// Github issue number
issueID int,
// Github repository url
repository string,
// +defaultPath="/"
source *dagger.Directory,
) (string, error) {
// Get the Github issue
issueClient := dag.GithubIssue(dagger.GithubIssueOpts{Token: githubToken})
issue := issueClient.Read(repository, issueID)

// Get information from the Github issue
assignment, err := issue.Body(ctx)
if err != nil {
return "", err
}

// Solve the issue with the Develop agent
feature, err := m.Develop(ctx, assignment, source)
if err != nil {
return "", err
}

// Open a pull request
title, err := issue.Title(ctx)
if err != nil {
return "", err
}
url, err := issue.URL(ctx)
if err != nil {
return "", err
}
body := assignment + "\n\nCloses " + url
pr := issueClient.CreatePullRequest(repository, title, body, feature)

return pr.URL(ctx)
}

The develop-issue function connects the dots in the automation flow. It will be called when a GitHub issue is ready to be developed. Using the github-issue dependency, this code will:

  • Read the title and body from the GitHub issue
  • Use the body as the assignment for the agent
  • Open a pull request with the Directory returned by the agent

Create a GitHub Actions workflow

  1. GitHub Actions will need the ability to call out to an LLM. This means that you must configure GitHub Actions with the same environment variables that you previously configured locally, this time as repository secrets.

To configure new repository secrets:

  • From your GitHub repository, select the Settings tab
  • Under Security, expand the section "Secrets and Variables" and select Actions
  • Select "New repository secret"
  • Create a secret for each variable required for your LLM provider. For example: GEMINI_API_KEY

Configure GitHub Actions repository secrets

If you signed up for Dagger Cloud, you should also configure a DAGGER_CLOUD_TOKEN repository secret to see your GitHub Actions traces in Dagger Cloud.

To find your Dagger Cloud token, navigate to the organization settings page using the cogwheel icon in the top navigation bar. Under the Tokens sub-menu, click the eye icon to view the token. You can also use this URL pattern: https://v3.dagger.cloud/{Your Org Name}/settings?tab=Tokens

Get token

  1. The repository also needs to be configured to give GitHub Actions the ability to create pull requests. On the left side, select Actions -> General and find the option "Allow GitHub Actions to create and approve pull requests". Make sure this is enabled.

Allow GitHub Actions to create pull requests

  1. Create the file .github/workflows/develop.yml with the following content:
name: develop
on:
issues:
types:
- labeled
jobs:
develop:
if: github.event.label.name == 'develop'
name: develop
runs-on: ubuntu-latest
permissions:
contents: write
issues: read
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: dagger/dagger-for-github@8.0.0
- name: Develop
run: |
dagger call develop-issue \
--github-token env://GH_TOKEN \
--issue-id ${{ github.event.issue.number }} \
--repository https://github.com/${{ github.repository }}
env:
DAGGER_CLOUD_TOKEN: ${{ secrets.DAGGER_CLOUD_TOKEN }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

This snippet shows an example with Google Gemini by specifying GEMINI_API_KEY. Change this to match the repository secrets you created earlier.

This GitHub Actions workflow will call the develop-issue function when the label "develop" is added to an issue. The GitHub token passed to the agent has permissions to read issues, write contents, and write pull requests.

  1. Commit and push your changes to GitHub:
git add .
git commit -m'deploy agent to github'
git push

Run the agent in GitHub Actions

  1. Create a new GitHub issue in your repository. You can title it "change the page color" with the description "make the main page green".

Create a GitHub issue

  1. Now add the label called "develop" by typing "develop" in the labels input field.

Add issue label develop

  1. When the label is added, it triggers the GitHub Actions workflow. See the workflow by clicking the "Actions" tab or by opening Dagger Cloud (if you configured it).

Watch in actions tab

  1. Once the workflow completes, there will be a new pull request ready to review!

See the agents PR

Next steps

Congratulations! You created an agentic function with a Daggerized project and successfully ran it, both locally and in automatically in GitHub.

We encourage you to join our awesome community on Discord to introduce yourself and ask questions. And starring our GitHub repository is always appreciated!