feat: Open pull request in last step (#32)

* feat: Open pull request in last step

* cr

* cr
This commit is contained in:
Brace Sproul 2025-05-28 12:29:57 -07:00 • committed by GitHub
parent 76dadeeb72
commit b9d15e9f27
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 357 additions and 2 deletions

View file

@ -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",

View file

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

View file

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

View file

@ -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<GraphUpdate> {
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<typeof openPrToolSchema>;
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,
},
}),
],
};
}

View file

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

View file

@ -2,7 +2,7 @@
"extends": ["//"],
"tasks": {
"build": {
"outputs": [".dist/**"]
"outputs": ["dist/**"]
},
"dev": {
"dependsOn": ["^dev"]

146
yarn.lock
View file

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