mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-02 02:43:15 +00:00
feat: Send followup comment to issues when plan is proposed/accepted (#444)
* feat: Send followup comment to issues when plan is proposed/accepted * cr * cr
This commit is contained in:
parent
81dbcbaa4d
commit
38767397e9
2 changed files with 154 additions and 0 deletions
|
|
@ -37,9 +37,97 @@ import {
|
|||
} from "@open-swe/shared/open-swe/custom-node-events";
|
||||
import { getDefaultHeaders } from "../../../utils/default-headers.js";
|
||||
import { getCustomConfigurableFields } from "../../../utils/config.js";
|
||||
import { getGitHubTokensFromConfig } from "../../../utils/github-tokens.js";
|
||||
import {
|
||||
createIssueComment,
|
||||
getIssueComments,
|
||||
updateIssueComment,
|
||||
} from "../../../utils/github/api.js";
|
||||
|
||||
const logger = createLogger(LogLevel.INFO, "ProposedPlan");
|
||||
|
||||
const PLAN_MESSAGE_OPEN_TAG = "<open-swe-plan-message>";
|
||||
const PLAN_MESSAGE_CLOSE_TAG = "</open-swe-plan-message>";
|
||||
|
||||
function formatBodyWithPlanMessage(body: string, message: string): string {
|
||||
if (
|
||||
body.includes(PLAN_MESSAGE_OPEN_TAG) &&
|
||||
body.includes(PLAN_MESSAGE_CLOSE_TAG)
|
||||
) {
|
||||
const bodyBeforeTag = body.split(PLAN_MESSAGE_OPEN_TAG)[0];
|
||||
const bodyAfterTag = body.split(PLAN_MESSAGE_CLOSE_TAG)[1];
|
||||
const newInnerContents = `\n${PLAN_MESSAGE_OPEN_TAG}\n\n${message}\n\n${PLAN_MESSAGE_CLOSE_TAG}\n`;
|
||||
return `${bodyBeforeTag}${newInnerContents}${bodyAfterTag}`;
|
||||
}
|
||||
|
||||
return `${body}\n${PLAN_MESSAGE_OPEN_TAG}\n\n${message}\n\n${PLAN_MESSAGE_CLOSE_TAG}`;
|
||||
}
|
||||
|
||||
function cleanTaskItems(taskItem: string): string {
|
||||
return "```\n" + taskItem.replace("```", "\\```") + "\n```";
|
||||
}
|
||||
|
||||
/**
|
||||
* Posts a comment to a GitHub issue using the installation token
|
||||
*/
|
||||
async function postGitHubIssueComment(input: {
|
||||
githubIssueId: number;
|
||||
targetRepository: { owner: string; repo: string };
|
||||
commentBody: string;
|
||||
config: GraphConfig;
|
||||
}): Promise<void> {
|
||||
const { githubIssueId, targetRepository, commentBody, config } = input;
|
||||
const githubAppName = process.env.GITHUB_APP_NAME;
|
||||
if (!githubAppName) {
|
||||
throw new Error("GITHUB_APP_NAME not set");
|
||||
}
|
||||
|
||||
try {
|
||||
const { githubInstallationToken } = getGitHubTokensFromConfig(config);
|
||||
const existingComments = await getIssueComments({
|
||||
owner: targetRepository.owner,
|
||||
repo: targetRepository.repo,
|
||||
issueNumber: githubIssueId,
|
||||
githubInstallationToken,
|
||||
});
|
||||
|
||||
const existingOpenSWEComment = existingComments?.findLast((c) =>
|
||||
c.user?.login?.startsWith(githubAppName),
|
||||
);
|
||||
|
||||
if (!existingOpenSWEComment) {
|
||||
await createIssueComment({
|
||||
owner: targetRepository.owner,
|
||||
repo: targetRepository.repo,
|
||||
issueNumber: githubIssueId,
|
||||
body: commentBody,
|
||||
githubToken: githubInstallationToken,
|
||||
});
|
||||
|
||||
logger.info(`Posted comment to GitHub issue #${githubIssueId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the comment
|
||||
const newCommentBody = formatBodyWithPlanMessage(
|
||||
existingOpenSWEComment.body ?? "",
|
||||
commentBody,
|
||||
);
|
||||
await updateIssueComment({
|
||||
owner: targetRepository.owner,
|
||||
repo: targetRepository.repo,
|
||||
commentId: existingOpenSWEComment.id,
|
||||
body: newCommentBody,
|
||||
githubInstallationToken,
|
||||
});
|
||||
|
||||
logger.info(`Updated comment to GitHub issue #${githubIssueId}`);
|
||||
} catch (error) {
|
||||
logger.error("Failed to post GitHub comment:", error);
|
||||
// Don't throw - we don't want to fail the entire process if comment posting fails
|
||||
}
|
||||
}
|
||||
|
||||
function createAcceptedPlanMessage(input: {
|
||||
planTitle: string;
|
||||
planItems: PlanItem[];
|
||||
|
|
@ -161,6 +249,15 @@ export async function interruptProposedPlan(
|
|||
|
||||
if (state.autoAcceptPlan) {
|
||||
logger.info("Auto accepting plan.");
|
||||
|
||||
// Post comment to GitHub issue about auto-accepting the plan
|
||||
await postGitHubIssueComment({
|
||||
githubIssueId: state.githubIssueId,
|
||||
targetRepository: state.targetRepository,
|
||||
commentBody: `### 🤖 Plan Generated\n\nI've generated a plan for this issue and will proceed to implement it since auto-accept is enabled.\n\n**Plan: ${state.proposedPlanTitle}**\n\n${proposedPlan.map((step, index) => `- Task ${index + 1}:\n${cleanTaskItems(step)}`).join("\n")}\n\nProceeding to implementation...`,
|
||||
config,
|
||||
});
|
||||
|
||||
planItems = proposedPlan.map((p, index) => ({
|
||||
index,
|
||||
plan: p,
|
||||
|
|
@ -199,6 +296,14 @@ export async function interruptProposedPlan(
|
|||
proposedPlan,
|
||||
);
|
||||
|
||||
// Post comment to GitHub issue about plan being ready for approval
|
||||
await postGitHubIssueComment({
|
||||
githubIssueId: state.githubIssueId,
|
||||
targetRepository: state.targetRepository,
|
||||
commentBody: `### 🟠 Plan Ready for Approval 🟠\n\nI've generated a plan for this issue and it's ready for your review.\n\n**Plan: ${state.proposedPlanTitle}**\n\n${proposedPlan.map((step, index) => `- Task ${index + 1}:\n${cleanTaskItems(step)}`).join("\n")}\n\nPlease review the plan and let me know if you'd like me to proceed, make changes, or if you have any feedback.`,
|
||||
config,
|
||||
});
|
||||
|
||||
const interruptResponse = interrupt<
|
||||
HumanInterrupt,
|
||||
HumanResponse[] | HumanResponse
|
||||
|
|
@ -251,6 +356,14 @@ export async function interruptProposedPlan(
|
|||
planItems,
|
||||
{ existingTaskPlan: state.taskPlan },
|
||||
);
|
||||
|
||||
// Update the comment to notify the user that the plan was accepted
|
||||
await postGitHubIssueComment({
|
||||
githubIssueId: state.githubIssueId,
|
||||
targetRepository: state.targetRepository,
|
||||
commentBody: `### ✅ Plan Accepted ✅\n\nThe proposed plan was accepted.\n\n**Plan: ${state.proposedPlanTitle}**\n\n${planItems.map((step, index) => `- Task ${index + 1}:\n${cleanTaskItems(step.plan)}`).join("\n")}\n\nProceeding to implementation...`,
|
||||
config,
|
||||
});
|
||||
} else if (humanResponse.type === "edit") {
|
||||
const editedPlan = (humanResponse.args as ActionRequest).args.plan
|
||||
.split(PLAN_INTERRUPT_DELIMITER)
|
||||
|
|
@ -268,6 +381,13 @@ export async function interruptProposedPlan(
|
|||
planItems,
|
||||
{ existingTaskPlan: state.taskPlan },
|
||||
);
|
||||
|
||||
await postGitHubIssueComment({
|
||||
githubIssueId: state.githubIssueId,
|
||||
targetRepository: state.targetRepository,
|
||||
commentBody: `### ✅ Plan Edited & Submitted ✅\n\nThe proposed plan was edited and submitted.\n\n**Plan: ${state.proposedPlanTitle}**\n\n${planItems.map((step, index) => `- Task ${index + 1}:\n${cleanTaskItems(step.plan)}`).join("\n")}\n\nProceeding to implementation...`,
|
||||
config,
|
||||
});
|
||||
} else {
|
||||
throw new Error("Unknown interrupt type." + humanResponse.type);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -341,3 +341,37 @@ export async function createIssueComment({
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateIssueComment({
|
||||
owner,
|
||||
repo,
|
||||
commentId,
|
||||
body,
|
||||
githubInstallationToken,
|
||||
}: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
commentId: number;
|
||||
body: string;
|
||||
githubInstallationToken: string;
|
||||
}): Promise<GitHubIssueComment | null> {
|
||||
const octokit = new Octokit({
|
||||
auth: githubInstallationToken,
|
||||
});
|
||||
|
||||
try {
|
||||
const { data: comment } = await octokit.issues.updateComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: commentId,
|
||||
body,
|
||||
});
|
||||
|
||||
return comment;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to update issue comment`, {
|
||||
error,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue