diff --git a/apps/open-swe/package.json b/apps/open-swe/package.json index e6ce601b..8d7be831 100644 --- a/apps/open-swe/package.json +++ b/apps/open-swe/package.json @@ -29,6 +29,7 @@ "@langchain/langgraph": "^0.2.73", "@langchain/langgraph-sdk": "^0.0.78", "@langchain/openai": "^0.5.10", + "@octokit/rest": "^22.0.0", "diff": "^8.0.1", "langchain": "^0.3.26", "langsmith": "^0.3.29", diff --git a/apps/open-swe/src/index.ts b/apps/open-swe/src/index.ts index b41f3713..b1eb8154 100644 --- a/apps/open-swe/src/index.ts +++ b/apps/open-swe/src/index.ts @@ -9,6 +9,7 @@ import { progressPlanStep, summarizeTaskSteps, generateConclusion, + openPullRequest, diagnoseError, } from "./nodes/index.js"; import { isAIMessage } from "@langchain/core/messages"; @@ -54,6 +55,7 @@ const workflow = new StateGraph(GraphAnnotation, GraphConfiguration) ends: ["generate-action", "generate-conclusion"], }) .addNode("generate-conclusion", generateConclusion) + .addNode("open-pr", openPullRequest) .addNode("diagnose-error", diagnoseError) .addEdge(START, "initialize") .addEdge("initialize", "generate-plan-subgraph") @@ -61,8 +63,9 @@ const workflow = new StateGraph(GraphAnnotation, GraphConfiguration) // Always interrupt after rewriting the plan. .addEdge("rewrite-plan", "interrupt-plan") .addConditionalEdges("generate-action", takeActionOrEnd, ["take-action", END]) + .addEdge("generate-conclusion", "open-pr") .addEdge("diagnose-error", "generate-action") - .addEdge("generate-conclusion", END); + .addEdge("open-pr", END); // Zod types are messed up export const graph = workflow.compile() as any; diff --git a/apps/open-swe/src/nodes/index.ts b/apps/open-swe/src/nodes/index.ts index a386a091..9f8c9801 100644 --- a/apps/open-swe/src/nodes/index.ts +++ b/apps/open-swe/src/nodes/index.ts @@ -6,4 +6,5 @@ export * from "./interrupt-plan.js"; export * from "./progress-plan-step.js"; export * from "./summarize-task-steps.js"; export * from "./generate-conclusion.js"; +export * from "./open-pr.js"; export * from "./diagnose-error.js"; diff --git a/apps/open-swe/src/nodes/open-pr.ts b/apps/open-swe/src/nodes/open-pr.ts new file mode 100644 index 00000000..ef16903c --- /dev/null +++ b/apps/open-swe/src/nodes/open-pr.ts @@ -0,0 +1,148 @@ +import { GraphConfig, GraphState, GraphUpdate, PlanItem } from "../types.js"; +import { Sandbox } from "@e2b/code-interpreter"; +import { + checkoutBranchAndCommit, + createPullRequest, + getBranchName, + getChangedFilesStatus, + getRepoAbsolutePath, +} from "../utils/git/index.js"; +import { createLogger, LogLevel } from "../utils/logger.js"; +import { z } from "zod"; +import { loadModel, Task } from "../utils/load-model.js"; +import { formatPlanPromptWithSummaries } from "../utils/plan-prompt.js"; +import { isHumanMessage, ToolMessage } from "@langchain/core/messages"; +import { getMessageContentString } from "../utils/message/content.js"; + +const logger = createLogger(LogLevel.INFO, "Open PR"); + +const openPrSysPrompt = `You are operating as a terminal-based agentic coding assistant built by LangChain. It wraps LLM models to enable natural language interaction with a local codebase. You are expected to be precise, safe, and helpful. + +You have just completed all of your tasks, and are now ready to open a pull request. + +Here are all of the tasks you completed: +{COMPLETED_TASKS} + +And here is the user's original request: +{USER_REQUEST} + +With all of this in mind, please use the \`open_pr\` tool to open a pull request.`; + +const openPrToolSchema = z.object({ + title: z + .string() + .describe( + "The title of the pull request. Ensure this is a concise and thoughtful title. You should follow conventional commit title format (e.g. prefixing with 'fix:', 'feat:', 'chore:', etc.).", + ), + body: z + .string() + .optional() + .describe( + "The body of the pull request. This should provide a detailed description of the changes you've made, and why you've made them. Ensure you do not over-explain the changes, as we do not want to waste the user's time.", + ), +}); + +const openPrTool = { + name: "open_pr", + schema: openPrToolSchema, + description: "Use this tool to open a pull request.", +}; + +const formatPrompt = (plan: PlanItem[], userRequest: string): string => { + const completedTasks = plan.filter((task) => task.completed); + return openPrSysPrompt + .replace("{COMPLETED_TASKS}", formatPlanPromptWithSummaries(completedTasks)) + .replace("{USER_REQUEST}", userRequest); +}; + +export async function openPullRequest( + state: GraphState, + config: GraphConfig, +): Promise { + const sandboxSessionId = state.sandboxSessionId; + if (!sandboxSessionId) { + throw new Error( + "Failed to open pull request: No sandbox session ID found in state.", + ); + } + + const sandbox = await Sandbox.connect(sandboxSessionId); + + const { owner, repo } = config.configurable?.target_repository ?? {}; + + if (!owner || !repo) { + throw new Error( + "Failed to open pull request: No target repository found in config.", + ); + } + + const changedFiles = await getChangedFilesStatus( + getRepoAbsolutePath(config), + sandbox, + ); + let branchName = state.branchName; + if (changedFiles.length > 0) { + logger.info(`Has ${changedFiles.length} changed files. Committing.`, { + changedFiles, + }); + branchName = await checkoutBranchAndCommit(config, sandbox, { + branchName, + }); + } + + const model = await loadModel(config, Task.SUMMARIZER); + const modelWithTool = model.bindTools([openPrTool], { + tool_choice: openPrTool.name, + }); + + const firstUserMessage = state.messages.find(isHumanMessage); + if (!firstUserMessage) { + throw new Error( + "Failed to open pull request: No user message found in state.", + ); + } + + const response = await modelWithTool.invoke([ + { + role: "user", + content: formatPrompt( + state.plan, + getMessageContentString(firstUserMessage.content), + ), + }, + ]); + + const toolCall = response.tool_calls?.[0]; + + if (!toolCall) { + throw new Error( + "Failed to generate a tool call when opening a pull request.", + ); + } + + const { title, body } = toolCall.args as z.infer; + + const pr = await createPullRequest({ + owner, + repo, + headBranch: branchName ?? getBranchName(config), + title, + body, + }); + + return { + messages: [ + response, + new ToolMessage({ + tool_call_id: toolCall.id ?? "", + content: pr + ? `Created pull request: ${pr.html_url}` + : "Failed to create pull request.", + name: toolCall.name, + additional_kwargs: { + pull_request: pr, + }, + }), + ], + }; +} diff --git a/apps/open-swe/src/utils/git/index.ts b/apps/open-swe/src/utils/git/index.ts index b7870b15..07db05ba 100644 --- a/apps/open-swe/src/utils/git/index.ts +++ b/apps/open-swe/src/utils/git/index.ts @@ -1,3 +1,4 @@ +import { Octokit } from "@octokit/rest"; import { CommandResult, Sandbox } from "@e2b/code-interpreter"; import { createLogger, LogLevel } from "../logger.js"; import { GraphConfig } from "../../types.js"; @@ -390,3 +391,58 @@ export async function checkoutBranchAndCommit( return branchName; } + +export async function createPullRequest({ + owner, + repo, + headBranch, + title, + body = "", +}: { + owner: string; + repo: string; + headBranch: string; + title: string; + body?: string; +}) { + // Initialize Octokit with the personal access token + const token = process.env.GITHUB_PAT; + if (!token) { + throw new Error("GITHUB_PAT environment variable is not set"); + } + + const octokit = new Octokit({ + auth: token, + }); + + try { + // Step 1: Get repository information to find the default branch + const { data: repository } = await octokit.repos.get({ + owner, + repo, + }); + + const defaultBranch = repository.default_branch; + logger.info( + `Creating pull request against default branch: ${defaultBranch}`, + ); + + // Step 2: Create the pull request + const { data: pullRequest } = await octokit.pulls.create({ + owner, + repo, + title, + body, + head: headBranch, + base: defaultBranch, + }); + + logger.info(`🐙 Pull request created: ${pullRequest.html_url}`); + return pullRequest; + } catch (error) { + logger.error(`Failed to create pull request`, { + error, + }); + return null; + } +} diff --git a/apps/open-swe/turbo.json b/apps/open-swe/turbo.json index 4b7f6560..bf79edc4 100644 --- a/apps/open-swe/turbo.json +++ b/apps/open-swe/turbo.json @@ -2,7 +2,7 @@ "extends": ["//"], "tasks": { "build": { - "outputs": [".dist/**"] + "outputs": ["dist/**"] }, "dev": { "dependsOn": ["^dev"] diff --git a/yarn.lock b/yarn.lock index a9c44545..2e200b98 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2472,6 +2472,130 @@ __metadata: languageName: node linkType: hard +"@octokit/auth-token@npm:^6.0.0": + version: 6.0.0 + resolution: "@octokit/auth-token@npm:6.0.0" + checksum: 9c23be526c7f8e282aa7ccec6f3a72a1beec44eae736327e9ba78419fa28ba75e2c686e9eac75f35ce99bdb55eff9605f7ef7588a9d4f4e18ad5ed16a5d887ab + languageName: node + linkType: hard + +"@octokit/core@npm:^7.0.2": + version: 7.0.2 + resolution: "@octokit/core@npm:7.0.2" + dependencies: + "@octokit/auth-token": ^6.0.0 + "@octokit/graphql": ^9.0.1 + "@octokit/request": ^10.0.2 + "@octokit/request-error": ^7.0.0 + "@octokit/types": ^14.0.0 + before-after-hook: ^4.0.0 + universal-user-agent: ^7.0.0 + checksum: 569e65c7fc23518c5e058aad280b669f33a82fa9f4c26afa4481ed0488147ec779907b3cd0a333eda850b0127ab46dc0caeeb27fb72eca0ed60db23c30d75bf0 + languageName: node + linkType: hard + +"@octokit/endpoint@npm:^11.0.0": + version: 11.0.0 + resolution: "@octokit/endpoint@npm:11.0.0" + dependencies: + "@octokit/types": ^14.0.0 + universal-user-agent: ^7.0.2 + checksum: 1c4bd71b3041bf935535c13e9636cb9846469655050583e0ad2595f2f1f840eba2a3f5f43a0dbc82fd695ad0124ab4fc389a2ef3d0770d642fed717e31e4300f + languageName: node + linkType: hard + +"@octokit/graphql@npm:^9.0.1": + version: 9.0.1 + resolution: "@octokit/graphql@npm:9.0.1" + dependencies: + "@octokit/request": ^10.0.2 + "@octokit/types": ^14.0.0 + universal-user-agent: ^7.0.0 + checksum: 3d59773cf56333be8668f7708c473f5746ad49552c0e542ae32c0442e0f16c4b6389408c8868f211b45d0292f1ebfdc92160ee6c2cbf12c5fa0d9a938713bcf9 + languageName: node + linkType: hard + +"@octokit/openapi-types@npm:^25.1.0": + version: 25.1.0 + resolution: "@octokit/openapi-types@npm:25.1.0" + checksum: 441b17f801254629b3ddb4b878c589fee1fd23015253c8b72a3acb3eeedbe981691bb311649ab5f955005c5d7adb940f19e18eaf0c875752fe0cc12b3dc1d24b + languageName: node + linkType: hard + +"@octokit/plugin-paginate-rest@npm:^13.0.1": + version: 13.0.1 + resolution: "@octokit/plugin-paginate-rest@npm:13.0.1" + dependencies: + "@octokit/types": ^14.1.0 + peerDependencies: + "@octokit/core": ">=6" + checksum: 4b74c601346a055d1ebf007137bc9e47f0a4276b0767fb7d833e81b3ffda5901b4f0e1bac0d059e733b2caddaeb7df0cae36b98d9085d1690aa81dcd5bb9994a + languageName: node + linkType: hard + +"@octokit/plugin-request-log@npm:^6.0.0": + version: 6.0.0 + resolution: "@octokit/plugin-request-log@npm:6.0.0" + peerDependencies: + "@octokit/core": ">=6" + checksum: 8a79973b1429bfead9113c4117f418aaef5ff368795daded3415ba14623d97d5fc08d1e822dbd566ecc9f041119e1a48a11853a9c48d9eb1caa62baa79c17f83 + languageName: node + linkType: hard + +"@octokit/plugin-rest-endpoint-methods@npm:^16.0.0": + version: 16.0.0 + resolution: "@octokit/plugin-rest-endpoint-methods@npm:16.0.0" + dependencies: + "@octokit/types": ^14.1.0 + peerDependencies: + "@octokit/core": ">=6" + checksum: 8ebb30b41628b839cca0b1051459f92001db37f4c3b8a87551915cdf5707d9e87f300565795ca2c12b5767b0d294b15bb0cd1ab3da99620c82690b37eddbd635 + languageName: node + linkType: hard + +"@octokit/request-error@npm:^7.0.0": + version: 7.0.0 + resolution: "@octokit/request-error@npm:7.0.0" + dependencies: + "@octokit/types": ^14.0.0 + checksum: c4370d2c31f599c1f366c480d5a02bc93442e5a0e151ec5caf0d5a5b0f0f91b50ecedc945aa6ea61b4c9ed1e89153dc7727daf4317680d33e916f829da7d141b + languageName: node + linkType: hard + +"@octokit/request@npm:^10.0.2": + version: 10.0.2 + resolution: "@octokit/request@npm:10.0.2" + dependencies: + "@octokit/endpoint": ^11.0.0 + "@octokit/request-error": ^7.0.0 + "@octokit/types": ^14.0.0 + fast-content-type-parse: ^3.0.0 + universal-user-agent: ^7.0.2 + checksum: 66e56fbf8d3df538c68881eab393e4ebca418204c6f452d39f95d050ec6151169efd1dac3cea9def3f1ebfc8ddc7fad7721f39d2f793f60d52d63cd386b11a90 + languageName: node + linkType: hard + +"@octokit/rest@npm:^22.0.0": + version: 22.0.0 + resolution: "@octokit/rest@npm:22.0.0" + dependencies: + "@octokit/core": ^7.0.2 + "@octokit/plugin-paginate-rest": ^13.0.1 + "@octokit/plugin-request-log": ^6.0.0 + "@octokit/plugin-rest-endpoint-methods": ^16.0.0 + checksum: 6a7eff019c0889b23c0820831936e5dc8fa7643bdf0e98ba073b36a10f5602b9f283ca2c74ec8172b8529d0647dfa4a7857dcd81ca028b303937f26750a6c7f6 + languageName: node + linkType: hard + +"@octokit/types@npm:^14.0.0, @octokit/types@npm:^14.1.0": + version: 14.1.0 + resolution: "@octokit/types@npm:14.1.0" + dependencies: + "@octokit/openapi-types": ^25.1.0 + checksum: 0513520e26dc5395c3b3b407568151d32be1f51bedb151f5b294cadc72dc3fe2d0dbbccad96f01dc80d26247b4aed3358de0ce31ad3c013eb22b96e6234feeb5 + languageName: node + linkType: hard + "@open-agent-platform/docs@workspace:apps/docs": version: 0.0.0-use.local resolution: "@open-agent-platform/docs@workspace:apps/docs" @@ -2495,6 +2619,7 @@ __metadata: "@langchain/langgraph-cli": latest "@langchain/langgraph-sdk": ^0.0.78 "@langchain/openai": ^0.5.10 + "@octokit/rest": ^22.0.0 "@tsconfig/recommended": ^1.0.8 "@types/jest": ^29.5.0 "@types/node": ^22.13.5 @@ -5328,6 +5453,13 @@ __metadata: languageName: node linkType: hard +"before-after-hook@npm:^4.0.0": + version: 4.0.0 + resolution: "before-after-hook@npm:4.0.0" + checksum: a8cbd4d3c48f42f44307ef5966be152b836d2e5908834f2f885ddf104c2e2ba66dbb5e6ef89a37e77371b1d22d5c75b74df1472286c684a037c1a6db43f5617b + languageName: node + linkType: hard + "better-opn@npm:^3.0.2": version: 3.0.2 resolution: "better-opn@npm:3.0.2" @@ -7769,6 +7901,13 @@ __metadata: languageName: node linkType: hard +"fast-content-type-parse@npm:^3.0.0": + version: 3.0.0 + resolution: "fast-content-type-parse@npm:3.0.0" + checksum: 490199423215b8a9c6e24a5a01a0d072af8ebfe24c13deac0a393dcac36b732295dd8cec5a2c4241249ed0fffc6983ba138f3001b13286afefb66360b6715a46 + languageName: node + linkType: hard + "fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" @@ -15715,6 +15854,13 @@ __metadata: languageName: node linkType: hard +"universal-user-agent@npm:^7.0.0, universal-user-agent@npm:^7.0.2": + version: 7.0.3 + resolution: "universal-user-agent@npm:7.0.3" + checksum: c497e85f8b11eb8fa4dce584d7a39cc98710164959f494cafc3c269b51abb20fff269951838efd7424d15f6b3d001507f3cb8b52bb5676fdb642019dfd17e63e + languageName: node + linkType: hard + "universalify@npm:^2.0.0": version: 2.0.1 resolution: "universalify@npm:2.0.1"