Durante las últimas semanas, hemos estado trabajando diligentemente en un repositorio en rápido crecimiento en Composio . Pronto nos dimos cuenta de que tareas como actualizar archivos Léame, corregir cadenas de documentos y corregir errores menores (aunque repetitivos y mundanos) consumían gran parte de nuestro ancho de banda.
Entonces pensé, ¿por qué no construir un agente autónomo impulsado por IA para manejar estos trabajos pesados?
Queríamos un agente de IA que pudiera hacerlo.
Finalmente, construimos un metamarco simple y extensible para crear agentes de ingeniería de software.
Estos agentes pueden desempeñarse de manera similar a sus homólogos humanos en muchas de estas tareas. Delegar tareas mundanas a estos agentes tiene sentido para liberar a los desarrolladores y poder concentrarse en tareas más creativas.
En este artículo, le mostraré cómo crear un agente SWE en Typecript para automatizar sus flujos de trabajo de GitHub.
Pero antes de eso, comprendamos qué son incluso los agentes AI y SWE.
Los agentes de IA son sistemas impulsados por modelos de IA que pueden realizar tareas de forma autónoma, interactuar con su entorno y tomar decisiones basadas en su programación y los datos que procesan.
Un agente de IA consta de tres componentes cruciales:
Entonces, ¿cuándo se llama agente SWE a un agente de IA?
Los agentes SWE son agentes de IA que imitan las cualidades y características de un ingeniero de software humano, como
Estas son algunas de las características del agente SWE que vamos a construir:
Los agentes de SWE pueden acceder a sus repositorios públicos y privados, trabajar en problemas proporcionados y enviar cambios a los repositorios.
Puede ejecutar códigos utilizando la máquina host, Docker o cualquier otro entorno de nube (E2B, FlyIo). Sin embargo, sería mejor si prefiriera utilizar los dos últimos para la ejecución del código de espacio aislado.
El sandboxing ayuda a prevenir consecuencias no deseadas de la ejecución de código arbitrario.
Estos son los requisitos previos para crear correctamente el agente:
Comience instalando dependencias usando su administrador de paquetes favorito. El método recomendado es pnpm
, pero también puedes usar npm
o yarn
.
pnpm install -g composio-core
Necesitará GITHUB_ACCESS_TOKEN, OPENAI_API_KEY, COMPOSIO_API_KEY, GITHUB_USERNAME y GITHUB_USER_EMAIL para completar el proyecto.
Entonces, cree un archivo .env
y agregue las variables anteriores.
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"
El proyecto está organizado de la siguiente manera:
src
├── agentes
│ └── dulces
├── aplicaciones.ts
├── indicaciones.ts
└── utils.ts
Aquí hay una breve descripción de los archivos.
Para comenzar rápidamente, clona este repositorio e instala el resto de dependencias.
git clone https://github.com/ComposioHQ/swe-js-template.git swe-js cd swe-js && pnpm i
Ahora que ha terminado con toda la configuración. Codifiquemos nuestro agente de IA.
Comenzamos definiendo las indicaciones y objetivos del agente SWE. Explicar cada paso en detalle es crucial, ya que estas definiciones influyen significativamente en el desempeño y la ejecución del agente.
Por lo tanto, cree un archivo prompts.ts
si aún no lo ha hecho.
A continuación, defina el rol y el objetivo del agente.
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";
Aquí, definimos el rol como SWE y el objetivo es solucionar cualquier problema de codificación y crear un parche para solucionarlo usando filetool_git_patch
. Esta es una acción de Compsoio para la integración de GitHub para crear archivos de parche.
Ahora, define la historia de fondo y una descripción del agente 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`;
En el bloque de código anterior, hemos definido cuidadosa y claramente los pasos que el agente debe seguir para realizar la tarea. Esto es importante para garantizar que el agente sepa qué hacer cuando se enfrenta a obstáculos de programación comunes.
En esta sección, definiremos dos funciones principales, from GitHub
y getBranchNameFromIssue
, que extraerán información sobre un problema.
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 }; }
Entonces, esto es lo que sucede en el bloque de código anterior.
readUserInput
: esta función lee la entrada del usuario desde la línea de comando. Solo necesitamos el ID de usuario de GitHub, el nombre del repositorio y el número o descripción del problema.createGithubIssueValidator
: esta función devuelve un validador para problemas de GitHub. Puede manejar entradas como una ruta de archivo, un ID de problema numérico o una descripción de cadena simple. Si la entrada es un ID de problema numérico, obtiene los detalles del problema de GitHub usando la acción github_issues_get
de Composio.fromGitHub
: esta función combina estos elementos para recopilar y validar la información necesaria sobre un repositorio de GitHub y un problema. Ahora, defina getBranchNameFromIssue
para crear un nombre de sucursal a partir de la descripción del problema.
export function getBranchNameFromIssue(issue: string): string { return "swe/" + issue.toLowerCase().replace(/\s+/g, '-') + "-" + nanoid(); }
Esta es la sección más importante, donde definirá el agente Swe utilizando los asistentes de OpenAI y los conjuntos de herramientas de Composio.
Entonces, primero, importe las bibliotecas y defina el LLM y las herramientas.
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, }) }); */
En el bloque de código anterior,
OpenAIToolSet
con workspaceConfig
configurado en Docker. Esto es para utilizar Docker para proteger el entorno de codificación del agente Swe. También puedes utilizar intérpretes de código en la nube como E2B y FlyIo. Ahora definiremos el Agente Swe.
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 }; }
Esto es lo que sucede en el bloque de código anterior.
filetool
, file edit tool
y shelltool
. Como sugiere el nombre, se utilizarán para acceder a archivos, editar archivos y utilizar el shell para ejecutar comandos. Esta es la sección final, donde definimos el punto de entrada de la aplicación. Por lo tanto, cargue las variables de entorno e importe los módulos necesarios.
import dotenv from "dotenv"; dotenv.config(); import { fromGithub, getBranchNameFromIssue } from './utils'; import { initSWEAgent } from './agents/swe'; import { GOAL } from './prompts';
El bloque de código
Ahora, defina la función 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();
Este es nuestro archivo app.ts
completo, que se utilizará para ejecutar el flujo de trabajo agente.
Entonces, esto es lo que está sucediendo en el código anterior.
initSWEAgent
para obtener el hilo del asistente, la instancia de OpenAI, las herramientas y el conjunto de herramientas de Composio.fromGithub
.filetool_git_patch
para generar un parche.main()
para ejecutar los pasos anteriores.
Ahora, ejecute la aplicación usando pnpm start
.
Esto le pedirá que ingrese la ID de usuario de GitHub, el nombre del repositorio y la ID del problema o la descripción del problema que desea abordar.
Una vez completado, extraerá un contenedor Composio Docker del registro y comenzará a trabajar en el problema.
Finalmente, cuando se complete el flujo de trabajo, el parche se enviará al repositorio remoto. Ahora, cuando abras tu repositorio de GitHub, verás una nueva rama con la solución propuesta para el problema. Puedes compararlo con la rama principal y crear una solicitud de extracción.
Puedes encontrar el código completo.
Lo mejor de este agente SWE es que puede ampliar sus capacidades utilizando las herramientas e integraciones de Composio.
Puede agregar Slack o Discord a su agente para que le notifique cuando se complete la ejecución. También puede conectar Jira o Linear para crear y actualizar tareas automáticamente en función de las actividades del agente.
Puede unirse a nuestra comunidad para interactuar con los mantenedores y contribuir como desarrollador de código abierto. No dudes en visitar nuestro repositorio de GitHub para contribuir y crear ediciones relacionadas con Composio. Desarrollo.
Protagoniza el Composio. repositorio de desarrollo ⭐
¡Gracias por leer!