I got tired of solving issues over GitHub, so I created my own AI bot... 🤔

Sunil Kumar Dash - Aug 19 - - Dev Community

TL;DR

In this article, you will build an AI coder to fix your GitHub codebase's documentation and bugs, create new features and debug existing problems. The AI Coder receives an issue, finds the solution, creates a separate branch, writes the code, and pushes a pull request to the remote repository.

Hog rider git hub GIF

We will use Composio, GitHub, Docker, and OpenAI GPT-4o to complete the project.

Here is how you will build it.

  • Connect GitHub with Composio to manage the codebase.
  • Use OpenAI GPT-4o to understand codes and execute actions.
  • Add Docker to let the bot write and execute codes.

Composio - The First AI Integration Platform

Here’s a quick introduction about us.

Composio is an open-source tooling infrastructure for building robust and reliable AI applications. We provide over 100+ tools and integrations across industry verticals from CRM, HRM, and Sales to Productivity, Dev, and Social Media.

They handle user authentication and authorization for all these applications and make connecting all the API endpoints with various AI models and frameworks simple.

Guy Struggling gif

Please help us with a star. 🥹

It would help us to create more articles like this 💖

Star the Composio.dev repository ⭐


How does it work?

So, before diving into the coding part, let’s understand how the AI programmer works.

The bot can

  • Access any given GitHub repository.
  • Read, write, update, and delete files as and when needed using Composio’s local tools.
  • Orchestrate workflow using OpenAI SDK. However, Composio is framework agnostic, so you can use frameworks like LangChain and LlamaIndex.
  • Accepts an issue and Creates a separate branch for changes.
  • Runs the program in a sandboxed coding environment, such as Docker, or cloud-hosted environments like E2B, FlyIo, etc.
  • Finally, push the fix to the remote repository.

SWE Agent workflow


Prerequisites for Building SWE Agent

These are requisites for completing this project. Make sure you have configured these correctly before moving ahead.

Composio API key

Create a user account with Composio to get one.

login page

Now, log in with your GitHub, Gmail, or any other email ID. Once logged in, you can access your dashboard to see the catalogue of applications you can connect to empower AI bots.

integrations list

Now, navigate to the Settings tab and copy the API key.

OpenAI API key

Create a user account and generate an API key. Ensure you have added the credits to use one of their models for inferencing.

OpenAI dashboard

GitHub Access Token

Next, you must create an access token for your GitHub account to let Composio push changes to it. Click on the link and create a basic access token.

github access token


Let’s Get Started 🔥

Dependencies

Begin by installing dependencies using your favourite package manager. The recommended method is pnpm, but you can also use npm or yarn.



pnpm install -g composio-core


Enter fullscreen mode Exit fullscreen mode

Set up Environment Variables

You will need a GITHUB_ACCESS_TOKEN, OPENAI_API_KEY, COMPOSIO_API_KEY, GITHUB_USERNAME, and GITHUB_USER_EMAIL to complete the project.

So, create a .env file and add the above variables.



GITHUB_ACCESS_TOKEN="your access token"
OPENAI_API_KEY="openai_key"
COMPOSIO_API_KEY="composio-api-key"
GITHUB_USER_NAME="GitHub username"
GITHUB_USER_EMAIL="GitHub user email"


Enter fullscreen mode Exit fullscreen mode

Execute the below command to set them as environment variables.



export $(grep -v '^#' .env | xargs)


Enter fullscreen mode Exit fullscreen mode

Project Structure

The project is organized as follows:

src
├── agents
│ └── swe.ts
├── app.ts
├── prompts.ts
└── utils.ts

Here’s a brief description of the files.

  • agents/swe.ts: Consists of the code for implementing the software engineering bot.
  • app.ts: The application's main entry point.
  • prompts.ts: Defines the prompts used by the agents.
  • utils.ts: Utility functions used throughout the project.

To start quickly, clone this repository and install the rest of the dependencies.



git clone https://github.com/ComposioHQ/swe-js-template.git swe-js

cd swe-js && pnpm i


Enter fullscreen mode Exit fullscreen mode

Now that you have finished with the whole set-up. Let’s code our AI agent.


Defining Prompts and Goals

Let’s start with defining the prompts and goals for the AI programmer. Explaining each step in detail is crucial, as these definitions significantly influence the agent's performance and execution capability.

So, create a prompts.ts file if you haven’t done so.

Now, define the role and goal of the agent.



export const ROLE = "Software Engineer";

export const GOAL = "Fix the coding issues given by the user, and finally generate a patch with the newly created files using `filetool_git_patch` tool";


Enter fullscreen mode Exit fullscreen mode

Here, we defined the role as SWE, and the goal is to fix any coding issue and create a patch for the fix using filetool_git_patch. This is a Compsoio Action for the GitHub integration for creating patch files.

Now, define a detailed backstory and a description of the AI programmer.



export const BACKSTORY = `You are an autonomous programmer; your task is to
solve the issue given in the task with the tools in hand. Your mentor gave you
the following tips.
  1. Please clone the github repo using the 'FILETOOL_GIT_CLONE' tool, and if it
     already exists - you can proceed with the rest of the instructions after
     going into the directory using \`cd\` shell command.
  2. PLEASE READ THE CODE AND UNDERSTAND THE FILE STRUCTURE OF THE CODEBASE
    USING GIT REPO TREE ACTION.
  3. POST THAT READ ALL THE RELEVANT READMES AND TRY TO LOOK AT THE FILES
    RELATED TO THE ISSUE.
  4. Form a thesis around the issue and the codebase. Think step by step.
    Form pseudocode in case of large problems.
  5. THEN TRY TO REPLICATE THE BUG THAT THE ISSUES DISCUSS.
     - If the issue includes code for reproducing the bug, we recommend that you
      re-implement that in your environment, and run it to make sure you can
      reproduce the bug.
     - Then start trying to fix it.
     - When you think you've fixed the bug, re-run the bug reproduction script
      to make sure that the bug has indeed been fixed.
     - If the bug reproduction script does not print anything when it is successfully
      runs, we recommend adding a print("Script completed successfully, no errors.")
      command at the end of the file so that you can be sure that the script
      indeed, it ran fine all the way through.
  6. If you run a command that doesn't work, try running a different one.
    A command that did not work once will not work the second time unless you
    modify it!
  7. If you open a file and need to get to an area around a specific line that
    is not in the first 100 lines, say line 583, don't just use the scroll_down
    command multiple times. Instead, use the goto 583 command. It's much quicker.
  8. If the bug reproduction script requires inputting/reading a specific file,
    such as buggy-input.png, and you'd like to understand how to input that file,
    conduct a search in the existing repo code to see whether someone else has
    I've already done that. Do this by running the command find_file "buggy-input.png"
    If that doesn't work, use the Linux 'find' command.
  9. Always make sure to look at the currently open file and the current working
    directory (which appears right after the currently open file). The currently
    open file might be in a different directory than the working directory! Some commands, such as 'create', open files, so they might change the
    currently open file.
  10. When editing files, it is easy to accidentally specify a wrong line number or write code with incorrect indentation. Always check the code after
    You issue an edit to ensure it reflects what you want to accomplish.
    If it didn't, issue another command to fix it.
  11. When you FINISH WORKING on the issue, USE THE 'filetool_git_patch' ACTION with the
      new files using the "new_file_paths" parameters created to create the final patch to be submitted to fix the issue. Example,
      if you add \`js/src/app.js\`, then pass \`new_file_paths\` for the action like below,
      {
        "new_file_paths": ["js/src/app.js"]
      }
`;

export const DESCRIPTION = `We're solving the following issue within our repository. 
Here's the issue text:
  ISSUE: {issue}
  REPO: {repo}

Now, you're going to solve this issue on your own. When you're satisfied with all
your changes, you can submit them to the code base by simply
running the submit command. Note, however, that you cannot use any interactive
session commands (e.g. python, vim) in this environment, but you can write
scripts and run them. E.g. you can write a Python script and then run it
with \`python </path/to/script>.py\`.

If you face a "module not found error", you can install dependencies.
Example: in case the error is "pandas not found", install pandas like this \`pip install pandas\`

Respond to the human as helpfully and accurately as possible`;



Enter fullscreen mode Exit fullscreen mode

In the above code block, we have carefully and clearly defined the steps the agent must take to accomplish the task.

This helps ground the responses of LLMs when faced with common programming challenges.


Defining Utility Functions

In this section, we will define two main functions, from GitHub and getBranchNameFromIssue, which will extract information about a given GitHub issue.



import * as fs from 'fs';
import * as path from 'path';
import * as readline from 'readline';
import { ComposioToolSet } from "composio-core/lib/sdk/base.toolset";
import { nanoid } from "nanoid";

type InputType = any;

function readUserInput(
  prompt: string,
  metavar: string,
  validator: (value: string) => InputType
): InputType {
  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
  });

  return new Promise<InputType>((resolve, reject) => {
    rl.question(`${prompt} > `, (value) => {
      try {
        const validatedValue = validator(value);
        rl.close();
        resolve(validatedValue);
      } catch (e) {
        console.error(`Invalid value for \`${metavar}\` error parsing \`${value}\`; ${e}`);
        rl.close();
        reject(e);
      }
    });
  });
}

function createGithubIssueValidator(owner: string, name: string, toolset: ComposioToolSet) {
  return async function githubIssueValidator(value: string): Promise<string> {
    const resolvedPath = path.resolve(value);
    if (fs.existsSync(resolvedPath)) {
      return fs.readFileSync(resolvedPath, 'utf-8');
    }

    if (/^\d+$/.test(value)) {
      const responseData = await toolset.executeAction('github_issues_get', {
        owner,
        repo: name,
        issue_number: parseInt(value, 10),
      });
      return responseData.body as string;
    }

    return value;
  };
}

export async function fromGithub(toolset: ComposioToolSet): Promise<{ repo: string; issue: string }> {
  const owner = await readUserInput(
    'Enter github repository owner',
    'github repository owner',
    (value: string) => value
  );
  const name = await readUserInput(
    'Enter github repository name',
    'github repository name',
    (value: string) => value
  );
  const repo = `${owner}/${name.replace(",", "")}`;
  const issue = await readUserInput(
    'Enter the github issue ID or description or path to the file containing the description',
    'github issue',
    createGithubIssueValidator(owner, name, toolset)
  );
  return { repo, issue };
}


Enter fullscreen mode Exit fullscreen mode

So, here is what is going on in the above code block.

  • readUserInput: This helper function reads user input from the command line. We only need the GitHub user ID, repository name, and issue number or description.
  • createGithubIssueValidator: The function returns a validator for GitHub issues. It can handle input as a file path, a numeric issue ID, or a plain string description. If the input is a numeric issue ID, it fetches the issue details from GitHub using Composio’s github_issues_get action.
  • fromGitHub: This function combines the above helper functions to accept user inputs, validate GitHub issues and finally return the repository name and the provided issue.

Now, define the getBranchNameFromIssue to create a branch name from the issue description.



export function getBranchNameFromIssue(issue: string): string {
  return "swe/" + issue.toLowerCase().replace(/\s+/g, '-') + "-" + nanoid();
}


Enter fullscreen mode Exit fullscreen mode

This will help the AI programmer create a new branch from the issues before working on the code.


Defining the Swe Agent

Now, let's get to the main event, where you will define the Swe agent using the OpenAI assistants and Composio toolsets.

So, first, import the libraries and define the LLM and tools.



import { OpenAIToolSet, Workspace } from 'composio-core';
import { BACKSTORY, DESCRIPTION, GOAL } from '../prompts';
import OpenAI from 'openai';

// Initialize tool.
const llm = new OpenAI({
    apiKey: process.env.OPENAI_API_KEY,
});
const composioToolset = new OpenAIToolSet({ 
    workspaceConfig: Workspace.Docker({})
});



Enter fullscreen mode Exit fullscreen mode

In the above code block,

  • We created an instance of OpenAI with the API key.
  • We also created an instance of OpenAIToolSet with workspaceConfig set to Docker. This is to use Docker to sandbox the coding environment for the AI Coder. You can also use cloud code interpreters like E2B and FlyIo.

Now,let’s define the AI Programmer.



export async function initSWEAgent(): Promise<{composioToolset: OpenAIToolSet; assistantThread: OpenAI.Beta.Thread; llm: OpenAI; tools: Array<any>}> {
    let tools = await composioToolset.getTools({
        apps: [
            "filetool",
            "fileedittool",
            "shelltool"
        ],
    });

    tools = tools.map((a) => {
        if (a.function?.description?.length || 0 > 1024) {
            a.function.description = a.function.description?.substring(0, 1024);
        }
        return a;
    });

    tools = tools.map((tool) => {
        const updateNullToEmptyArray = (obj) => {
            for (const key in obj) {
                if (obj[key] === null) {
                    obj[key] = [];
                } else if (typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
                    updateNullToEmptyArray(obj[key]);
                }
            }
        };

        updateNullToEmptyArray(tool);
        return tool;
    });

    const assistantThread = await llm.beta.threads.create({
        messages: [
            {
                role: "assistant",
                content:`${BACKSTORY}\n\n${GOAL}\n\n${DESCRIPTION}`
            }
        ]
    });

    return { assistantThread, llm, tools, composioToolset };
}



Enter fullscreen mode Exit fullscreen mode

Here is what is going on in the above code block.

  • Get Tools: Fetches tools from the Composio toolset for filetool, file edit tool, and shelltool. As the name suggests, these will be used to access files, edit files, and use shell for executing commands.
  • Trim Tool Descriptions: Limits tool descriptions to 1024 characters. This is to limit hogging up the LLM context window.
  • Update Null Values: Replaces null values in tool configurations with empty arrays.
  • Create Assistant Thread: Initiates an OpenAI assistant thread with the prompts we defined earlier.
  • Return Statement: Pzreturns the tools, assistant thread, OpenAI instance, and Composio toolset.

Defining the Entry-point to the Application

This is the final section, where we define the application's entry point. Therefore, load the environment variables and import the required modules.



import dotenv from "dotenv";
dotenv.config();

import { fromGithub, getBranchNameFromIssue } from './utils';
import { initSWEAgent } from './agents/swe';
import { GOAL } from './prompts';


Enter fullscreen mode Exit fullscreen mode

The code block

  • Loads environment variables.
  • Imports the necessary utility functions.
  • Imports the Swe Agent and the agent Goal that we defined earlier.

Now, define the main function.



async function main() {
  /**Run the agent.**/
  const { assistantThread, llm, tools, composioToolset } = await initSWEAgent();
  const { repo, issue } = await fromGithub(composioToolset);

  const assistant = await llm.beta.assistants.create({
    name: "SWE agent",
    instructions: GOAL + `\nRepo is: ${repo} and your goal is to ${issue}`,
    model: "gpt-4o",
    tools: tools
  });

  await llm.beta.threads.messages.create(
    assistantThread.id,
    {
      role: "user",
      content: issue
    }
  );

  const stream = await llm.beta.threads.runs.createAndPoll(assistantThread.id, {
    assistant_id: assistant.id,
    instructions: `Repo is: ${repo} and your goal is to ${issue}`,
    tool_choice: "required"
  });

  await composioToolset.waitAndHandleAssistantToolCalls(llm as any, stream, assistantThread, "default");

  const response = await composioToolset.executeAction("filetool_git_patch", {
  });

  if (response.patch && response.patch?.length > 0) {
    console.log('=== Generated Patch ===\n' + response.patch, response);
    const branchName = getBranchNameFromIssue(issue);
    const output = await composioToolset.executeAction("SHELL_EXEC_COMMAND", {
      cmd: `cp -r ${response.current_working_directory} git_repo && cd git_repo && git config --global --add safe.directory '*' && git config --global user.name ${process.env.GITHUB_USER_NAME} && git config --global user.email ${process.env.GITHUB_USER_EMAIL} && git checkout -b ${branchName} && git commit -m 'feat: ${issue}' && git push origin ${branchName}`
    });

    // Wait for 2s
    await new Promise((resolve) => setTimeout(() => resolve(true), 2000));

    console.log("Have pushed the code changes to the repo. Let's create the PR now", output);

    await composioToolset.executeAction("GITHUB_PULLS_CREATE", {
      owner: repo.split("/")[0],
      repo: repo.split("/")[1],
      head: branchName,
      base: "master",
      title: `SWE: ${issue}`
    })

    console.log("Done! The PR has been created for this issue in " + repo);
  } else {
    console.log('No output available - no patch was generated :(');
  }

  await composioToolset.workspace.close();
}

main();


Enter fullscreen mode Exit fullscreen mode

So, here is what is going on in the above code.

  • Initialize SWE Agent: Calls initSWEAgent to get the assistant thread, OpenAI instance, tools, and Composio toolset.
  • Fetch Repository and Issue: Fetches repository name and issue details from fromGithub.
  • Create Assistant: Initializes the OpenAI assistant instance with tools, GPT-4o, and custom instruction.
  • Send Issue to Assistant: Sends the issue content as a message to the assistant thread.
  • Run Assistant and Poll: Runs the assistant and polls for tool call responses. For more information about polling responses, refer to the OpenAI SDK repository.
  • Execute Patch Action: Executes filetool_git_patch to generate a patch file.
  • Handle Patch Response: If a patch is generated, log it, create a branch, commit, and push changes. Wait for 2 seconds before creating a pull request. Create a pull request on GitHub.
  • Close Workspace: Closes the Composio toolset workspace.
  • Run Main Function: Calls main() to execute the above steps.

Now, run the application using pnpm start.

This will prompt you to enter the GitHub user ID, repository name, and the issue ID or description of the issue you want to address.

shell requiremnts

Once completed, it will pull a Composio Docker container from the registry and start working on the issue.

Finally, the patch will be pushed to the remote repository when the workflow is completed. Now, when you open your GitHub repository, you will see a new branch with the proposed fix for the issue. You can compare it with the main branch and create a pull request.

SWE Agent in Action

You can find the complete code here on GitHub.


Next Steps

The best thing about this AI programmer is you can extend the capabilities of the SWE agents using Composio tools and integrations.

You can add Slack or Discord to your agent to notify you when the execution is completed. You can also connect Jira or Linear to automatically create and update tasks based on the agent's activities.

Explore the Composio repository and give us a Star.

 
 

star the repo
Star the Composio.dev repository ⭐

 
 

. . . . . . . . . . . . . . . . . . . . . .