paint-brush
我如何构建 AI 程序员来自动修复 GitHub 问题经过@sunilkumardash9
486 讀數
486 讀數

我如何构建 AI 程序员来自动修复 GitHub 问题

经过 Sunil Kumar Dash16m2024/08/01
Read on Terminal Reader

太長; 讀書

了解如何使用 Typescript 创建 AI 驱动的 SWE 代理来自动化 GitHub 工作流程。本指南介绍了自主代理的设置、组件和实现,该代理可以处理更新文档、修复错误和推送补丁等任务,让开发人员可以专注于更具创造性的工作。
featured image - 我如何构建 AI 程序员来自动修复 GitHub 问题
Sunil Kumar Dash HackerNoon profile picture


在过去的几周里,我们一直在努力开发Composio上一个快速增长的存储库。我们很快意识到,更新自述文件、修复文档字符串和修复小错误等任务虽然重复又单调,但却消耗了我们大量的带宽。


所以,我想,为什么不建立一个人工智能驱动的自主代理来处理这些繁重的工作呢?

我们想要一个能够做到这一点的人工智能代理。


  • 访问 GitHub 存储库。
  • 针对任何给定的问题并写出解决方案。
  • 如果需要,请运行代码来检查它是否良好。
  • 最后,将补丁文件推送到远程存储库。


最后,我们构建了一个用于构建软件工程代理的简单且可扩展的元框架。

这些代理在许多此类任务中的表现与人类相似。将单调乏味的任务交给这些代理是明智之举,这样开发人员就可以腾出时间专注于更具创造性的任务。


杰瑞


在本文中,我将向您展示如何在 Typescript 中构建 SWE 代理来自动化您的 GitHub 工作流程。

但在此之前,让我们先了解一下 AI 和 SWE 代理是什么。


什么是 AI 代理?

人工智能代理是由人工智能模型驱动的系统,可以自主执行任务、与环境交互并根据其编程和处理的数据做出决策。

AI 代理的组件

人工智能代理由三个关键组件组成:

  • LLM :大型语言模型,负责推理,决策,工具调用等。
  • 内存:管理短期和长期信息以跟踪工作流程。
  • 工具:实现与外部环境的交互,例如用于从存储库中提取信息并进行必要更改的 GitHub 工具。

什么是 SWE 代理?

那么,什么时候将 AI 代理称为 SWE 代理?

SWE 代理是模仿人类软件工程师的品质和特征的 AI 代理,例如

  • 长期规划和推理。
  • 使用标准开发工具。
  • 通过测试和反馈提高代码质量。
  • 自主调试和解决问题。

斯威基特

SWE 代理概述

以下是我们将要构建的 SWE 代理的一些特征:

  • 它与框架无关,因此您可以使用任何框架,例如 LangChain、OpenAI SDK 等……
  • 您可以添加工具来扩展其多功能性,例如用于互联网访问的 Tavily。


SWE 代理可以访问您的公共和私有存储库,处理提供的问题,并将更改推送到存储库。

它可以使用宿主机、Docker 或任何其他云环境(E2B、FlyIo)执行代码。不过,如果你更喜欢使用后两者来进行沙盒代码执行,那就更好了。


沙盒有助于防止任意代码执行造成的任何意外后果。


SWE 代理的先决条件

以下是成功构建代理的先决条件:

  1. OpenAI API 密钥:我们将使用 OpenAI SDK 访问 GPT 模型并协调工具调用。
  2. GitHub 访问令牌:您必须使用个人访问令牌使 SWE 代理能够访问和修改您的代码存储库。
  3. Composio API 密钥:您还需要 Composio 的 API 密钥。要获取密钥,请创建用户帐户使用 Composio 并导航到仪表板上的“设置”选项卡。

登录页面


让我们开始吧🔥

依赖项

首先使用您最喜欢的包管理器安装依赖项。推荐的方法是pnpm ,但您也可以使用npmyarn

 pnpm install -g composio-core

设置环境变量

您将需要 GITHUB_ACCESS_TOKEN、OPENAI_API_KEY、COMPOSIO_API_KEY、GITHUB_USERNAME 和 GITHUB_USER_EMAIL 来完成项目。


因此,创建一个.env文件并添加上述变量。

 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"

项目结构

该项目组织如下:

源码
═── 代理商
│ └── swe.ts
│ ...
│ │ 提示.ts
└── utils.ts

以下是这些文件的简要说明。

  • agent/swe.ts :包含 SWE 代理的实现。
  • app.ts :应用程序的主要入口点。
  • prompts.ts :定义代理使用的提示。
  • utils.ts :整个项目使用的实用功能。

要快速开始,请克隆此存储库并安装其余依赖项。

 git clone https://github.com/ComposioHQ/swe-js-template.git swe-js cd swe-js && pnpm i


现在您已完成整个设置。让我们编写我们的 AI 代理。


编码员佩佩



定义提示和目标🎯

我们首先定义 SWE 代理的提示和目标。详细解释每个步骤至关重要,因为这些定义会显著影响代理的性能和执行。

因此,如果您还没有这样做,请创建一个prompts.ts文件。

接下来,定义代理的角色和目标。

 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";


在这里,我们将角色定义为 SWE,目标是修复任何编码问题并使用filetool_git_patch创建修复补丁。这是用于创建补丁文件的 GitHub 集成的 Compsoio 操作。

现在,定义 Swe 代理的背景故事和描述。

 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 (eg python, vim) in this environment, but you can write scripts and run them. Eg 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`;

在上面的代码块中,我们仔细而清晰地定义了代理完成任务需要采取的步骤。这对于确保代理在遇到常见的编程障碍时知道该怎么做非常重要。


定义实用函数

在本节中,我们将定义两个主要函数, from GitHubgetBranchNameFromIssue ,它们将提取有关问题的信息。

 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 }; }

因此,以下是上述代码块中发生的情况。

  • readUserInput :此函数从命令行读取用户输入。我们只需要 GitHub 用户 ID、存储库名称以及问题编号或描述。
  • createGithubIssueValidator :此函数返回 GitHub 问题的验证器。它可以将输入处理为文件路径、数字问题 ID 或纯字符串描述。如果输入是数字问题 ID,它会使用 Composio 的github_issues_get操作从 GitHub 获取问题详细信息。
  • fromGitHub :此函数结合这些元素来收集并验证有关 GitHub 存储库和问题的必要信息。

现在,定义getBranchNameFromIssue以根据问题描述创建分支名称。

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

定义 Swe 代理

这是最重要的部分,您将使用 OpenAI 助手和 Composio 工具集定义 Swe 代理。

因此,首先,导入库并定义 LLM 和工具。

 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({}) }); // To use E2B Code interpreter /* const composioToolset = new OpenAIToolSet({ workspaceConfig: Workspace.E2B({ apiKey: process.env.E2B_API_KEY, }) }); */

在上面的代码块中,

  • 我们使用 API 密钥创建了一个 OpenAI 实例。
  • 我们还创建了一个OpenAIToolSet实例,并将workspaceConfig设置为 Docker。这是使用 Docker 为 Swe 代理沙盒化编码环境。您还可以使用 E2B 和 FlyIo 等云代码解释器。

现在,我们将定义 Swe Agent。

 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 }; }

以下是上述代码块中发生的情况。

  • 获取工具:从 Composio 工具集中获取filetoolfile edit toolshelltool 。顾名思义,这些工具将用于访问文件、编辑文件以及使用 shell 执行命令。
  • 修剪工具描述:将工具描述限制为最多 1024 个字符。
  • 更新空值:用空数组替换工具配置中的空值。
  • 创建助手线程:使用预定义提示启动 OpenAI 助手线程。
  • 返回语句:提供初始化的工具、助手线程、OpenAI实例和Composio工具集。

定义应用程序的入口点

这是最后一部分,我们在这里定义应用程序的入口点。因此,加载环境变量并导入所需的模块。

 import dotenv from "dotenv"; dotenv.config(); import { fromGithub, getBranchNameFromIssue } from './utils'; import { initSWEAgent } from './agents/swe'; import { GOAL } from './prompts';

代码块

  • 加载环境变量。
  • 导入必要的实用功能。
  • 导入我们之前定义的 Swe 代理和代理目标。

现在,定义main函数。

 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();


这是我们的完整app.ts文件,将用于执行代理工作流。


因此,这就是上述代码中发生的情况。

  • 初始化 SWE Agent :调用initSWEAgent获取助手线程、OpenAI 实例、工具和 Composio 工具集。
  • 获取存储库和问题:从fromGithub获取存储库和问题详细信息。
  • 创建助手:使用指令、工具和语言模型初始化 OpenAI 助手。
  • 发送问题给助手:将问题内容作为消息发送到助手线程。
  • 运行助手和轮询:运行助手并轮询工具调用响应。有关轮询响应的更多信息,请参阅OpenAI SDK 存储库。
  • 执行补丁操作:执行filetool_git_patch来生成补丁。
  • 处理补丁响应:如果生成了补丁,则记录它,创建分支,提交并推送更改。等待 2 秒钟后再创建拉取请求。在 GitHub 上创建拉取请求。
  • 关闭工作区:关闭 Composio 工具集工作区。
  • 运行主函数:调用main()执行上述步骤。


现在,使用pnpm start运行该应用程序。


这将提示您输入 GitHub 用户 ID、存储库名称以及您要解决的问题 ID 或描述。

一旦完成后,它将从注册表中提取 Composio Docker 容器并开始处理问题。


最后,当工作流程完成后,补丁将被推送到远程存储库。现在,当你打开你的 GitHub 存储库时,你将看到一个新分支,其中包含针对该问题的建议修复。你可以将其与主分支进行比较并创建拉取请求。
谢谢你,迈克·斯科特

你可以找到完整的代码在 GitHub 上


下一步

这个 SWE 代理的最大优点是您可以使用 Composio 工具和集成来扩展其功能。

您可以为代理添加 Slack 或 Discord,以便在执行完成时通知您。您还可以连接 Jira 或 Linear,以根据代理的活动自动创建和更新任务。


让我们联系起来!🔌

您可以加入我们的社区,与维护人员互动,并作为开源开发者做出贡献。请随时访问我们的 GitHub 存储库,以做出贡献并创建与 Composio 相关的问题。开发。

为 Composio.dev 存储库加注星标 ⭐

星标 repo


感谢您的阅读!