mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-06 13:32:15 +00:00
feat: Optionally create gh issue from requests (#781)
* feat: Optionally create gh issue from requests * cr * cr * cr * cr * docs
This commit is contained in:
parent
c42638f9a3
commit
f316685de3
24 changed files with 261 additions and 355 deletions
|
|
@ -16,6 +16,15 @@ description: Frequently Asked Questions
|
||||||
Yes. When using Anthropic models, all input tokens are cached on Anthropic's servers.
|
Yes. When using Anthropic models, all input tokens are cached on Anthropic's servers.
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="Can I disable Open SWE from creating an issue when I submit a request?">
|
||||||
|
Yes. There's two ways to disable Open SWE from creating an issue when you submit a request:
|
||||||
|
|
||||||
|
1. Toggle the 'eye' icon in the main chat area when submitting a request.
|
||||||
|
2. In the configuration tab in settings, toggle the 'Should Create Issue' switch.
|
||||||
|
|
||||||
|
By default, it's set to `true`. By modifying this setting in the configuration tab, all runs will default to that setting. You may override this setting on a per-run basis by toggling the 'eye' icon in the main chat area.
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
<Accordion title="My run failed midway through. What now?">
|
<Accordion title="My run failed midway through. What now?">
|
||||||
We're sorry you're experiencing this! Open SWE will automatically commit any changes it makes to a draft pull request. Please check the draft pull request and make any necessary changes.
|
We're sorry you're experiencing this! Open SWE will automatically commit any changes it makes to a draft pull request. Please check the draft pull request and make any necessary changes.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ import { isLocalMode } from "@open-swe/shared/open-swe/local-mode";
|
||||||
import { PlannerGraphState } from "@open-swe/shared/open-swe/planner/types";
|
import { PlannerGraphState } from "@open-swe/shared/open-swe/planner/types";
|
||||||
import { GraphState } from "@open-swe/shared/open-swe/types";
|
import { GraphState } from "@open-swe/shared/open-swe/types";
|
||||||
import { Client } from "@langchain/langgraph-sdk";
|
import { Client } from "@langchain/langgraph-sdk";
|
||||||
|
import { shouldCreateIssue } from "../../../../utils/should-create-issue.js";
|
||||||
const logger = createLogger(LogLevel.INFO, "ClassifyMessage");
|
const logger = createLogger(LogLevel.INFO, "ClassifyMessage");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -186,6 +187,39 @@ export async function classifyMessage(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!shouldCreateIssue(config)) {
|
||||||
|
const commandUpdate: ManagerGraphUpdate = {
|
||||||
|
messages: [response],
|
||||||
|
};
|
||||||
|
if (
|
||||||
|
toolCallArgs.route === "start_planner" ||
|
||||||
|
toolCallArgs.route === "start_planner_for_followup"
|
||||||
|
) {
|
||||||
|
return new Command({
|
||||||
|
update: commandUpdate,
|
||||||
|
goto: "start-planner",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toolCallArgs.route === "create_new_issue") {
|
||||||
|
return new Command({
|
||||||
|
update: commandUpdate,
|
||||||
|
goto: "create-new-session",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toolCallArgs.route === "no_op") {
|
||||||
|
return new Command({
|
||||||
|
update: commandUpdate,
|
||||||
|
goto: END,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`Unsupported route received: ${toolCallArgs.route}\nUnable to route message there when not creating GitHub issues for request.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const { githubAccessToken } = getGitHubTokensFromConfig(config);
|
const { githubAccessToken } = getGitHubTokensFromConfig(config);
|
||||||
let githubIssueId = state.githubIssueId;
|
let githubIssueId = state.githubIssueId;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import { StreamMode } from "@langchain/langgraph-sdk";
|
||||||
import { isLocalMode } from "@open-swe/shared/open-swe/local-mode";
|
import { isLocalMode } from "@open-swe/shared/open-swe/local-mode";
|
||||||
import { regenerateInstallationToken } from "../../../utils/github/regenerate-token.js";
|
import { regenerateInstallationToken } from "../../../utils/github/regenerate-token.js";
|
||||||
import { createLogger, LogLevel } from "../../../utils/logger.js";
|
import { createLogger, LogLevel } from "../../../utils/logger.js";
|
||||||
|
import { shouldCreateIssue } from "../../../utils/should-create-issue.js";
|
||||||
|
|
||||||
const logger = createLogger(LogLevel.INFO, "CreateNewSession");
|
const logger = createLogger(LogLevel.INFO, "CreateNewSession");
|
||||||
|
|
||||||
|
|
@ -48,16 +49,21 @@ export async function createNewSession(
|
||||||
state.messages,
|
state.messages,
|
||||||
config.configurable,
|
config.configurable,
|
||||||
);
|
);
|
||||||
const { githubAccessToken } = getGitHubTokensFromConfig(config);
|
|
||||||
const newIssue = await createIssue({
|
let newIssueNumber: number | undefined;
|
||||||
owner: state.targetRepository.owner,
|
if (shouldCreateIssue(config)) {
|
||||||
repo: state.targetRepository.repo,
|
const { githubAccessToken } = getGitHubTokensFromConfig(config);
|
||||||
title: titleAndContent.title,
|
const newIssue = await createIssue({
|
||||||
body: formatContentForIssueBody(titleAndContent.body),
|
owner: state.targetRepository.owner,
|
||||||
githubAccessToken,
|
repo: state.targetRepository.repo,
|
||||||
});
|
title: titleAndContent.title,
|
||||||
if (!newIssue) {
|
body: formatContentForIssueBody(titleAndContent.body),
|
||||||
throw new Error("Failed to create new issue");
|
githubAccessToken,
|
||||||
|
});
|
||||||
|
if (!newIssue) {
|
||||||
|
throw new Error("Failed to create new issue");
|
||||||
|
}
|
||||||
|
newIssueNumber = newIssue.number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputMessages: BaseMessage[] = [
|
const inputMessages: BaseMessage[] = [
|
||||||
|
|
@ -71,7 +77,7 @@ ${ISSUE_CONTENT_OPEN_TAG}
|
||||||
${titleAndContent.body}
|
${titleAndContent.body}
|
||||||
${ISSUE_CONTENT_CLOSE_TAG}`,
|
${ISSUE_CONTENT_CLOSE_TAG}`,
|
||||||
additional_kwargs: {
|
additional_kwargs: {
|
||||||
githubIssueId: newIssue.number,
|
githubIssueId: newIssueNumber,
|
||||||
isOriginalIssue: true,
|
isOriginalIssue: true,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
@ -102,7 +108,7 @@ ${ISSUE_CONTENT_CLOSE_TAG}`,
|
||||||
|
|
||||||
const newManagerThreadId = uuidv4();
|
const newManagerThreadId = uuidv4();
|
||||||
const commandUpdate: ManagerGraphUpdate = {
|
const commandUpdate: ManagerGraphUpdate = {
|
||||||
githubIssueId: newIssue.number,
|
githubIssueId: newIssueNumber,
|
||||||
targetRepository: state.targetRepository,
|
targetRepository: state.targetRepository,
|
||||||
messages: inputMessages,
|
messages: inputMessages,
|
||||||
branchName: state.branchName ?? getBranchName(config),
|
branchName: state.branchName ?? getBranchName(config),
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import { getCustomConfigurableFields } from "../../../utils/config.js";
|
||||||
import { getRecentUserRequest } from "../../../utils/user-request.js";
|
import { getRecentUserRequest } from "../../../utils/user-request.js";
|
||||||
import { StreamMode } from "@langchain/langgraph-sdk";
|
import { StreamMode } from "@langchain/langgraph-sdk";
|
||||||
import { regenerateInstallationToken } from "../../../utils/github/regenerate-token.js";
|
import { regenerateInstallationToken } from "../../../utils/github/regenerate-token.js";
|
||||||
|
import { shouldCreateIssue } from "../../../utils/should-create-issue.js";
|
||||||
|
|
||||||
const logger = createLogger(LogLevel.INFO, "StartPlanner");
|
const logger = createLogger(LogLevel.INFO, "StartPlanner");
|
||||||
|
|
||||||
|
|
@ -68,6 +69,9 @@ export async function startPlanner(
|
||||||
branchName: state.branchName ?? getBranchName(config),
|
branchName: state.branchName ?? getBranchName(config),
|
||||||
autoAcceptPlan: state.autoAcceptPlan,
|
autoAcceptPlan: state.autoAcceptPlan,
|
||||||
...(followupMessage || localMode ? { messages: [followupMessage] } : {}),
|
...(followupMessage || localMode ? { messages: [followupMessage] } : {}),
|
||||||
|
...(!shouldCreateIssue(config) && followupMessage
|
||||||
|
? { internalMessages: [followupMessage] }
|
||||||
|
: {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const run = await langGraphClient.runs.create(
|
const run = await langGraphClient.runs.create(
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import { filterHiddenMessages } from "../../../utils/message/filter-hidden.js";
|
||||||
import { createLogger, LogLevel } from "../../../utils/logger.js";
|
import { createLogger, LogLevel } from "../../../utils/logger.js";
|
||||||
import { trackCachePerformance } from "../../../utils/caching.js";
|
import { trackCachePerformance } from "../../../utils/caching.js";
|
||||||
import { getModelManager } from "../../../utils/llms/model-manager.js";
|
import { getModelManager } from "../../../utils/llms/model-manager.js";
|
||||||
|
import { shouldCreateIssue } from "../../../utils/should-create-issue.js";
|
||||||
|
|
||||||
const logger = createLogger(LogLevel.INFO, "DetermineNeedsContext");
|
const logger = createLogger(LogLevel.INFO, "DetermineNeedsContext");
|
||||||
|
|
||||||
|
|
@ -118,7 +119,7 @@ export async function determineNeedsContext(
|
||||||
config: GraphConfig,
|
config: GraphConfig,
|
||||||
): Promise<Command> {
|
): Promise<Command> {
|
||||||
const [missingMessages, model] = await Promise.all([
|
const [missingMessages, model] = await Promise.all([
|
||||||
getMissingMessages(state, config),
|
shouldCreateIssue(config) ? getMissingMessages(state, config) : [],
|
||||||
loadModel(config, LLMTask.ROUTER),
|
loadModel(config, LLMTask.ROUTER),
|
||||||
]);
|
]);
|
||||||
const modelManager = getModelManager();
|
const modelManager = getModelManager();
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ import {
|
||||||
trackCachePerformance,
|
trackCachePerformance,
|
||||||
} from "../../../../utils/caching.js";
|
} from "../../../../utils/caching.js";
|
||||||
import { createViewTool } from "../../../../tools/builtin-tools/view.js";
|
import { createViewTool } from "../../../../tools/builtin-tools/view.js";
|
||||||
|
import { shouldCreateIssue } from "../../../../utils/should-create-issue.js";
|
||||||
|
|
||||||
const logger = createLogger(LogLevel.INFO, "GeneratePlanningMessageNode");
|
const logger = createLogger(LogLevel.INFO, "GeneratePlanningMessageNode");
|
||||||
|
|
||||||
|
|
@ -124,10 +125,14 @@ export async function generateAction(
|
||||||
: {}),
|
: {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const [missingMessages, { taskPlan: latestTaskPlan }] = await Promise.all([
|
const [missingMessages, { taskPlan: latestTaskPlan }] = shouldCreateIssue(
|
||||||
getMissingMessages(state, config),
|
config,
|
||||||
getPlansFromIssue(state, config),
|
)
|
||||||
]);
|
? await Promise.all([
|
||||||
|
getMissingMessages(state, config),
|
||||||
|
getPlansFromIssue(state, config),
|
||||||
|
])
|
||||||
|
: [[], { taskPlan: null }];
|
||||||
|
|
||||||
const inputMessages = filterMessagesWithoutContent([
|
const inputMessages = filterMessagesWithoutContent([
|
||||||
...state.messages,
|
...state.messages,
|
||||||
|
|
|
||||||
|
|
@ -21,24 +21,27 @@ import {
|
||||||
import { filterHiddenMessages } from "../../../utils/message/filter-hidden.js";
|
import { filterHiddenMessages } from "../../../utils/message/filter-hidden.js";
|
||||||
import { DO_NOT_RENDER_ID_PREFIX } from "@open-swe/shared/constants";
|
import { DO_NOT_RENDER_ID_PREFIX } from "@open-swe/shared/constants";
|
||||||
import { isLocalMode } from "@open-swe/shared/open-swe/local-mode";
|
import { isLocalMode } from "@open-swe/shared/open-swe/local-mode";
|
||||||
|
import { shouldCreateIssue } from "../../../utils/should-create-issue.js";
|
||||||
|
|
||||||
export async function prepareGraphState(
|
export async function prepareGraphState(
|
||||||
state: PlannerGraphState,
|
state: PlannerGraphState,
|
||||||
config: GraphConfig,
|
config: GraphConfig,
|
||||||
): Promise<Command> {
|
): Promise<Command> {
|
||||||
if (isLocalMode(config)) {
|
if (isLocalMode(config) || !shouldCreateIssue(config)) {
|
||||||
// In local mode, just proceed to initialize-sandbox with existing messages
|
|
||||||
return new Command({
|
return new Command({
|
||||||
update: {},
|
update: {},
|
||||||
goto: "initialize-sandbox",
|
goto: "initialize-sandbox",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!state.githubIssueId) {
|
if (!state.githubIssueId) {
|
||||||
throw new Error("No github issue id provided");
|
throw new Error("No github issue id provided");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!state.targetRepository) {
|
if (!state.targetRepository) {
|
||||||
throw new Error("No target repository provided");
|
throw new Error("No target repository provided");
|
||||||
}
|
}
|
||||||
|
|
||||||
const { githubInstallationToken } = getGitHubTokensFromConfig(config);
|
const { githubInstallationToken } = getGitHubTokensFromConfig(config);
|
||||||
const baseGetIssueInputs = {
|
const baseGetIssueInputs = {
|
||||||
owner: state.targetRepository.owner,
|
owner: state.targetRepository.owner,
|
||||||
|
|
@ -57,8 +60,6 @@ export async function prepareGraphState(
|
||||||
throw new Error(`Issue not found. Issue ID: ${state.githubIssueId}`);
|
throw new Error(`Issue not found. Issue ID: ${state.githubIssueId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the main issue & all comments are included in the state;
|
|
||||||
|
|
||||||
// If the messages state is empty, we can just include all comments as human messages.
|
// If the messages state is empty, we can just include all comments as human messages.
|
||||||
if (!state.messages?.length) {
|
if (!state.messages?.length) {
|
||||||
const commandUpdate: PlannerGraphUpdate = {
|
const commandUpdate: PlannerGraphUpdate = {
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ import {
|
||||||
cleanTaskItems,
|
cleanTaskItems,
|
||||||
} from "../../../utils/github/plan.js";
|
} from "../../../utils/github/plan.js";
|
||||||
import { regenerateInstallationToken } from "../../../utils/github/regenerate-token.js";
|
import { regenerateInstallationToken } from "../../../utils/github/regenerate-token.js";
|
||||||
|
import { shouldCreateIssue } from "../../../utils/should-create-issue.js";
|
||||||
|
|
||||||
const logger = createLogger(LogLevel.INFO, "ProposedPlan");
|
const logger = createLogger(LogLevel.INFO, "ProposedPlan");
|
||||||
|
|
||||||
|
|
@ -149,7 +150,7 @@ async function startProgrammerRun(input: {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Skip GitHub operations in local mode
|
// Skip GitHub operations in local mode
|
||||||
if (!isLocalMode(config)) {
|
if (!isLocalMode(config) && shouldCreateIssue(config)) {
|
||||||
await addTaskPlanToIssue(
|
await addTaskPlanToIssue(
|
||||||
{
|
{
|
||||||
githubIssueId: state.githubIssueId,
|
githubIssueId: state.githubIssueId,
|
||||||
|
|
|
||||||
|
|
@ -1,288 +0,0 @@
|
||||||
// TODO: NOT HOOKED UP TO THE GRAPH YET
|
|
||||||
// TODO: WILL NEED TO REFACTOR TO ALLOW FOR CHATTING WITH PLANNING SUBGRAPH
|
|
||||||
|
|
||||||
import { GraphConfig, PlanItem } from "@open-swe/shared/open-swe/types";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { tool } from "@langchain/core/tools";
|
|
||||||
import { traceable } from "langsmith/traceable";
|
|
||||||
import {
|
|
||||||
PlannerGraphState,
|
|
||||||
PlannerGraphUpdate,
|
|
||||||
} from "@open-swe/shared/open-swe/planner/types";
|
|
||||||
import {
|
|
||||||
getInitialUserRequest,
|
|
||||||
getRecentUserRequest,
|
|
||||||
} from "../../../utils/user-request.js";
|
|
||||||
import {
|
|
||||||
loadModel,
|
|
||||||
supportsParallelToolCallsParam,
|
|
||||||
} from "../../../utils/llms/index.js";
|
|
||||||
import { LLMTask } from "@open-swe/shared/open-swe/llm-task";
|
|
||||||
import { FallbackRunnable } from "../../../utils/runtime-fallback.js";
|
|
||||||
|
|
||||||
const systemPromptIdentifyChanges = `You are operating as an agentic coding assistant built by LangChain. You've previously been given a task to generate a plan of action for, to address the user's initial request.
|
|
||||||
|
|
||||||
Here is the user's initial request:
|
|
||||||
{USER_INITIAL_REQUEST}
|
|
||||||
|
|
||||||
After generating that plan, the user has submitted some feedback/change requests. You should now identify exactly which tasks in the plan should be modified based on their request.
|
|
||||||
|
|
||||||
Here is their request:
|
|
||||||
{USER_REQUEST}
|
|
||||||
|
|
||||||
The plan you generated originally, which they submitted the above request for is as follows:
|
|
||||||
{PLAN}
|
|
||||||
|
|
||||||
Please read over the generated plan, and the user's request, and identify exactly which tasks in the plan should be modified/removed. Call the 'identify_plan_changes' tool and use the indices of the tasks listed above when calling the tool.`;
|
|
||||||
|
|
||||||
const systemPrompt = `You are operating as an agentic coding assistant built by LangChain. You've previously been given a task to generate a plan of action for, to address the user's initial request.
|
|
||||||
|
|
||||||
In this step, the user has requested you rewrite/modify parts of a high-level plan. You have already identified the specific tasks in the plan that should be modified/removed based on the user's request.
|
|
||||||
|
|
||||||
Here is the user's initial request which you used to generate the initial plan:
|
|
||||||
{USER_INITIAL_REQUEST}
|
|
||||||
|
|
||||||
Here is the full plan you generated:
|
|
||||||
{PLAN}
|
|
||||||
|
|
||||||
Here is the request the user has just made which you should use to rewrite/modify the plan:
|
|
||||||
{USER_REQUEST}
|
|
||||||
|
|
||||||
And here are the specific tasks in the plan which were identified as tasks the user wants to modify/remove:
|
|
||||||
{TASKS_TO_MODIFY}
|
|
||||||
|
|
||||||
Given this context, please address the user's request to modify/remove/add the tasks in the plan.
|
|
||||||
|
|
||||||
You MUST adhere to the following criteria when generating the plan:
|
|
||||||
- Make as few changes as possible to the tasks, while still following the users request.
|
|
||||||
- You should NOT make ANY changes to the tasks in the plan that are NOT listed as tasks to modify/remove.
|
|
||||||
- Do NOT modify tasks in the plan not listed as tasks to modify/remove.
|
|
||||||
- When responding, ensure you include the unmodified tasks in the plan, as well as the modified/new tasks.
|
|
||||||
- To remove a specific task, simply do NOT include it in the response.
|
|
||||||
- To add a new task, simply include it in the response.
|
|
||||||
`;
|
|
||||||
|
|
||||||
const formatSysPromptIdentifyTasks = (
|
|
||||||
userInitialRequest: string,
|
|
||||||
userRequest: string,
|
|
||||||
previousPlan: string[],
|
|
||||||
) => {
|
|
||||||
return systemPromptIdentifyChanges
|
|
||||||
.replace("{USER_INITIAL_REQUEST}", userInitialRequest)
|
|
||||||
.replace("{USER_REQUEST}", userRequest)
|
|
||||||
.replace(
|
|
||||||
"{PLAN}",
|
|
||||||
previousPlan.map((plan, index) => `${index}: ${plan}`).join("\n"),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatSysPromptRewritePlan = (
|
|
||||||
userInitialRequest: string,
|
|
||||||
userRequest: string,
|
|
||||||
previousPlan: string[],
|
|
||||||
tasksToModify: PlanItem[],
|
|
||||||
) => {
|
|
||||||
return systemPrompt
|
|
||||||
.replace("{USER_INITIAL_REQUEST}", userInitialRequest)
|
|
||||||
.replace("{USER_REQUEST}", userRequest)
|
|
||||||
.replace(
|
|
||||||
"{PLAN}",
|
|
||||||
previousPlan.map((plan, index) => `${index}: ${plan}`).join("\n"),
|
|
||||||
)
|
|
||||||
.replace(
|
|
||||||
"{TASKS_TO_MODIFY}",
|
|
||||||
tasksToModify.map((p) => `${p.index}: ${p.plan}`).join("\n"),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
async function identifyTasksToModifyFunc(
|
|
||||||
state: PlannerGraphState,
|
|
||||||
model: FallbackRunnable,
|
|
||||||
supportsParallelToolCallsParam: boolean,
|
|
||||||
): Promise<PlanItem[]> {
|
|
||||||
if (!state.planChangeRequest) {
|
|
||||||
throw new Error("No plan change request found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const identifyPlanChangesSchema = z.object({
|
|
||||||
task_change_indices: z
|
|
||||||
.array(z.number())
|
|
||||||
.describe(
|
|
||||||
"The indices of the tasks in the plan that should be modified/removed.",
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
const identifyPlanChangesTool = tool(
|
|
||||||
(input): PlanItem[] => {
|
|
||||||
const { task_change_indices } = input;
|
|
||||||
const tasksToModify = state.proposedPlan.flatMap((plan, planIndex) => {
|
|
||||||
const planItem = task_change_indices.some(
|
|
||||||
(changeIndex) => changeIndex === planIndex,
|
|
||||||
);
|
|
||||||
if (!planItem) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
index: planIndex,
|
|
||||||
plan: plan,
|
|
||||||
completed: false,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return tasksToModify;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "identify_plan_changes",
|
|
||||||
schema: identifyPlanChangesSchema,
|
|
||||||
description:
|
|
||||||
"Identify which tasks in the plan should be modified/removed based on the user's request.",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const modelWithIdentifyChangesTool = model.bindTools(
|
|
||||||
[identifyPlanChangesTool],
|
|
||||||
{
|
|
||||||
// The model should always call the tool when identifying plan changes.
|
|
||||||
tool_choice: identifyPlanChangesTool.name,
|
|
||||||
...(supportsParallelToolCallsParam
|
|
||||||
? {
|
|
||||||
parallel_tool_calls: false,
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const userInitialRequest = getInitialUserRequest(state.messages);
|
|
||||||
const userFollowupRequest = getRecentUserRequest(state.messages);
|
|
||||||
const userRequest =
|
|
||||||
userFollowupRequest ?? userInitialRequest ?? "No user message found";
|
|
||||||
|
|
||||||
const response = await modelWithIdentifyChangesTool.invoke([
|
|
||||||
{
|
|
||||||
role: "user",
|
|
||||||
content: formatSysPromptIdentifyTasks(
|
|
||||||
userRequest,
|
|
||||||
state.planChangeRequest,
|
|
||||||
state.proposedPlan,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const toolCall = response.tool_calls?.[0];
|
|
||||||
if (!toolCall) {
|
|
||||||
throw new Error(
|
|
||||||
"Tool call not returned when attempting to identify plan changes.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const tasksToModify = await identifyPlanChangesTool.invoke(
|
|
||||||
toolCall.args as z.infer<typeof identifyPlanChangesSchema>,
|
|
||||||
);
|
|
||||||
return tasksToModify;
|
|
||||||
}
|
|
||||||
|
|
||||||
const identifyTasksToModify = traceable(identifyTasksToModifyFunc, {
|
|
||||||
name: "identify_tasks_to_modify",
|
|
||||||
});
|
|
||||||
|
|
||||||
async function updatePlanTasksFunc(
|
|
||||||
state: PlannerGraphState,
|
|
||||||
tasksToModify: PlanItem[],
|
|
||||||
model: FallbackRunnable,
|
|
||||||
supportsParallelToolCallsParam: boolean,
|
|
||||||
): Promise<string[]> {
|
|
||||||
if (!state.planChangeRequest) {
|
|
||||||
throw new Error("No plan change request found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatePlanTasksSchema = z.object({
|
|
||||||
updated_plan_tasks: z
|
|
||||||
.array(
|
|
||||||
z
|
|
||||||
.string()
|
|
||||||
.describe(
|
|
||||||
"The updated or unmodified plan for the task. Do NOT include the task index.",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.describe(
|
|
||||||
"The updated plan tasks. Must be in the order of which they should be executed in.",
|
|
||||||
),
|
|
||||||
});
|
|
||||||
const updatePlanTasksTool = {
|
|
||||||
name: "update_plan_tasks",
|
|
||||||
description: "Call this tool to respond with the updated plan.",
|
|
||||||
schema: updatePlanTasksSchema,
|
|
||||||
};
|
|
||||||
|
|
||||||
const modelWithUpdatePlanTasksTool = model.bindTools([updatePlanTasksTool], {
|
|
||||||
// The model should always call the tool when identifying plan changes.
|
|
||||||
tool_choice: updatePlanTasksTool.name,
|
|
||||||
...(supportsParallelToolCallsParam
|
|
||||||
? {
|
|
||||||
parallel_tool_calls: false,
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const userInitialRequest = getInitialUserRequest(state.messages);
|
|
||||||
const userFollowupRequest = getRecentUserRequest(state.messages);
|
|
||||||
const userRequest =
|
|
||||||
userFollowupRequest ?? userInitialRequest ?? "No user message found";
|
|
||||||
|
|
||||||
const response = await modelWithUpdatePlanTasksTool.invoke([
|
|
||||||
{
|
|
||||||
role: "user",
|
|
||||||
content: formatSysPromptRewritePlan(
|
|
||||||
userRequest,
|
|
||||||
state.planChangeRequest,
|
|
||||||
state.proposedPlan,
|
|
||||||
tasksToModify,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const toolCall = response.tool_calls?.[0];
|
|
||||||
if (!toolCall) {
|
|
||||||
throw new Error(
|
|
||||||
"Tool call not returned when attempting to update plan tasks.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
toolCall.args as z.infer<typeof updatePlanTasksSchema>
|
|
||||||
).updated_plan_tasks.map((p) => p);
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatePlanTasks = traceable(updatePlanTasksFunc, {
|
|
||||||
name: "update_plan_tasks",
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function rewritePlan(
|
|
||||||
state: PlannerGraphState,
|
|
||||||
config: GraphConfig,
|
|
||||||
): Promise<PlannerGraphUpdate> {
|
|
||||||
if (!state.planChangeRequest) {
|
|
||||||
throw new Error("No plan change request found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const model = await loadModel(config, LLMTask.PLANNER);
|
|
||||||
const modelSupportsParallelToolCallsParam = supportsParallelToolCallsParam(
|
|
||||||
config,
|
|
||||||
LLMTask.PLANNER,
|
|
||||||
);
|
|
||||||
const tasksToModify = await identifyTasksToModify(
|
|
||||||
state,
|
|
||||||
model,
|
|
||||||
modelSupportsParallelToolCallsParam,
|
|
||||||
);
|
|
||||||
const updatedPlanTasks = await updatePlanTasks(
|
|
||||||
state,
|
|
||||||
tasksToModify,
|
|
||||||
model,
|
|
||||||
modelSupportsParallelToolCallsParam,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
proposedPlan: updatedPlanTasks,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -59,6 +59,7 @@ import {
|
||||||
HumanMessage,
|
HumanMessage,
|
||||||
} from "@langchain/core/messages";
|
} from "@langchain/core/messages";
|
||||||
import { BindToolsInput } from "@langchain/core/language_models/chat_models";
|
import { BindToolsInput } from "@langchain/core/language_models/chat_models";
|
||||||
|
import { shouldCreateIssue } from "../../../../utils/should-create-issue.js";
|
||||||
|
|
||||||
const logger = createLogger(LogLevel.INFO, "GenerateMessageNode");
|
const logger = createLogger(LogLevel.INFO, "GenerateMessageNode");
|
||||||
|
|
||||||
|
|
@ -280,10 +281,14 @@ export async function generateAction(
|
||||||
const markTaskCompletedTool = createMarkTaskCompletedToolFields();
|
const markTaskCompletedTool = createMarkTaskCompletedToolFields();
|
||||||
const isAnthropicModel = modelName.includes("claude-");
|
const isAnthropicModel = modelName.includes("claude-");
|
||||||
|
|
||||||
const [missingMessages, { taskPlan: latestTaskPlan }] = await Promise.all([
|
const [missingMessages, { taskPlan: latestTaskPlan }] = shouldCreateIssue(
|
||||||
getMissingMessages(state, config),
|
config,
|
||||||
getPlansFromIssue(state, config),
|
)
|
||||||
]);
|
? await Promise.all([
|
||||||
|
getMissingMessages(state, config),
|
||||||
|
getPlansFromIssue(state, config),
|
||||||
|
])
|
||||||
|
: [[], { taskPlan: null }];
|
||||||
|
|
||||||
const { providerTools, providerMessages } = await createToolsAndPrompt(
|
const { providerTools, providerMessages } = await createToolsAndPrompt(
|
||||||
state,
|
state,
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import {
|
||||||
MAX_INTERNAL_TOKENS,
|
MAX_INTERNAL_TOKENS,
|
||||||
} from "../../../utils/tokens.js";
|
} from "../../../utils/tokens.js";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { shouldCreateIssue } from "../../../utils/should-create-issue.js";
|
||||||
|
|
||||||
const logger = createLogger(LogLevel.INFO, "HandleCompletedTask");
|
const logger = createLogger(LogLevel.INFO, "HandleCompletedTask");
|
||||||
|
|
||||||
|
|
@ -88,7 +89,7 @@ export async function handleCompletedTask(
|
||||||
summary,
|
summary,
|
||||||
);
|
);
|
||||||
// Update the github issue to reflect this task as completed.
|
// Update the github issue to reflect this task as completed.
|
||||||
if (!isLocalMode(config)) {
|
if (!isLocalMode(config) && shouldCreateIssue(config)) {
|
||||||
await addTaskPlanToIssue(
|
await addTaskPlanToIssue(
|
||||||
{
|
{
|
||||||
githubIssueId: state.githubIssueId,
|
githubIssueId: state.githubIssueId,
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ import {
|
||||||
} from "../../../utils/github/types.js";
|
} from "../../../utils/github/types.js";
|
||||||
import { getRepoAbsolutePath } from "@open-swe/shared/git";
|
import { getRepoAbsolutePath } from "@open-swe/shared/git";
|
||||||
import { GITHUB_USER_LOGIN_HEADER } from "@open-swe/shared/constants";
|
import { GITHUB_USER_LOGIN_HEADER } from "@open-swe/shared/constants";
|
||||||
|
import { shouldCreateIssue } from "../../../utils/should-create-issue.js";
|
||||||
|
|
||||||
const logger = createLogger(LogLevel.INFO, "Open PR");
|
const logger = createLogger(LogLevel.INFO, "Open PR");
|
||||||
|
|
||||||
|
|
@ -191,6 +192,9 @@ export async function openPullRequest(
|
||||||
| GitHubPullRequestList[number]
|
| GitHubPullRequestList[number]
|
||||||
| GitHubPullRequestUpdate
|
| GitHubPullRequestUpdate
|
||||||
| null = null;
|
| null = null;
|
||||||
|
|
||||||
|
const prBody = `${shouldCreateIssue(config) ? `Fixes #${state.githubIssueId}` : ""}${userLogin ? `\n\nOwner: @${userLogin}` : ""}\n\n${body}`;
|
||||||
|
|
||||||
if (!prForTask) {
|
if (!prForTask) {
|
||||||
// No PR created yet. Shouldn't be possible, but we have a condition here anyway
|
// No PR created yet. Shouldn't be possible, but we have a condition here anyway
|
||||||
pullRequest = await createPullRequest({
|
pullRequest = await createPullRequest({
|
||||||
|
|
@ -198,7 +202,7 @@ export async function openPullRequest(
|
||||||
repo,
|
repo,
|
||||||
headBranch: branchName,
|
headBranch: branchName,
|
||||||
title,
|
title,
|
||||||
body: `Fixes #${state.githubIssueId}${userLogin ? `\n\nOwner: @${userLogin}` : ""}\n\n${body}`,
|
body: prBody,
|
||||||
githubInstallationToken,
|
githubInstallationToken,
|
||||||
baseBranch: state.targetRepository.branch,
|
baseBranch: state.targetRepository.branch,
|
||||||
});
|
});
|
||||||
|
|
@ -208,7 +212,7 @@ export async function openPullRequest(
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
title,
|
title,
|
||||||
body: `Fixes #${state.githubIssueId}${userLogin ? `\n\nOwner: @${userLogin}` : ""}\n\n${body}`,
|
body: prBody,
|
||||||
pullNumber: prForTask,
|
pullNumber: prForTask,
|
||||||
githubInstallationToken,
|
githubInstallationToken,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ import {
|
||||||
REQUEST_HELP_NODE_ID,
|
REQUEST_HELP_NODE_ID,
|
||||||
} from "@open-swe/shared/open-swe/custom-node-events";
|
} from "@open-swe/shared/open-swe/custom-node-events";
|
||||||
import { postGitHubIssueComment } from "../../../utils/github/plan.js";
|
import { postGitHubIssueComment } from "../../../utils/github/plan.js";
|
||||||
|
import { shouldCreateIssue } from "../../../utils/should-create-issue.js";
|
||||||
|
import { isLocalMode } from "@open-swe/shared/open-swe/local-mode";
|
||||||
|
|
||||||
const constructDescription = (helpRequest: string): string => {
|
const constructDescription = (helpRequest: string): string => {
|
||||||
return `The agent has requested help. Here is the help request:
|
return `The agent has requested help. Here is the help request:
|
||||||
|
|
@ -60,12 +62,13 @@ export async function requestHelp(
|
||||||
throw new Error("Thread ID not found in config");
|
throw new Error("Thread ID not found in config");
|
||||||
}
|
}
|
||||||
|
|
||||||
const userLogin = config.configurable?.[GITHUB_USER_LOGIN_HEADER];
|
if (!isLocalMode(config) && shouldCreateIssue(config)) {
|
||||||
const userTag = userLogin ? `@${userLogin} ` : "";
|
const userLogin = config.configurable?.[GITHUB_USER_LOGIN_HEADER];
|
||||||
|
const userTag = userLogin ? `@${userLogin} ` : "";
|
||||||
|
const runUrl = getOpenSweAppUrl(threadId);
|
||||||
|
|
||||||
const runUrl = getOpenSweAppUrl(threadId);
|
const commentBody = runUrl
|
||||||
const commentBody = runUrl
|
? `### 🤖 Open SWE Needs Help
|
||||||
? `### 🤖 Open SWE Needs Help
|
|
||||||
|
|
||||||
${userTag}I've encountered a situation where I need human assistance to continue.
|
${userTag}I've encountered a situation where I need human assistance to continue.
|
||||||
|
|
||||||
|
|
@ -75,7 +78,7 @@ ${toolCall.args.help_request}
|
||||||
You can view and respond to this request in the [Open SWE interface](${runUrl}).
|
You can view and respond to this request in the [Open SWE interface](${runUrl}).
|
||||||
|
|
||||||
Please provide guidance so I can continue working on this issue.`
|
Please provide guidance so I can continue working on this issue.`
|
||||||
: `### 🤖 Open SWE Needs Help
|
: `### 🤖 Open SWE Needs Help
|
||||||
|
|
||||||
${userTag}I've encountered a situation where I need human assistance to continue.
|
${userTag}I've encountered a situation where I need human assistance to continue.
|
||||||
|
|
||||||
|
|
@ -84,12 +87,13 @@ ${toolCall.args.help_request}
|
||||||
|
|
||||||
Please check the Open SWE interface to respond to this request.`;
|
Please check the Open SWE interface to respond to this request.`;
|
||||||
|
|
||||||
await postGitHubIssueComment({
|
await postGitHubIssueComment({
|
||||||
githubIssueId: state.githubIssueId,
|
githubIssueId: state.githubIssueId,
|
||||||
targetRepository: state.targetRepository,
|
targetRepository: state.targetRepository,
|
||||||
commentBody,
|
commentBody,
|
||||||
config,
|
config,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const interruptInput: HumanInterrupt = {
|
const interruptInput: HumanInterrupt = {
|
||||||
action_request: {
|
action_request: {
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,8 @@ import { formatCustomRulesPrompt } from "../../../utils/custom-rules.js";
|
||||||
import { trackCachePerformance } from "../../../utils/caching.js";
|
import { trackCachePerformance } from "../../../utils/caching.js";
|
||||||
import { getModelManager } from "../../../utils/llms/model-manager.js";
|
import { getModelManager } from "../../../utils/llms/model-manager.js";
|
||||||
import { addTaskPlanToIssue } from "../../../utils/github/issue-task.js";
|
import { addTaskPlanToIssue } from "../../../utils/github/issue-task.js";
|
||||||
|
import { shouldCreateIssue } from "../../../utils/should-create-issue.js";
|
||||||
|
import { isLocalMode } from "@open-swe/shared/open-swe/local-mode";
|
||||||
|
|
||||||
const logger = createLogger(LogLevel.INFO, "UpdatePlanNode");
|
const logger = createLogger(LogLevel.INFO, "UpdatePlanNode");
|
||||||
|
|
||||||
|
|
@ -217,15 +219,17 @@ export async function updatePlan(
|
||||||
newPlanItems,
|
newPlanItems,
|
||||||
"agent",
|
"agent",
|
||||||
);
|
);
|
||||||
// Update the github issue to reflect the changes in the plan
|
if (!isLocalMode(config) && shouldCreateIssue(config)) {
|
||||||
await addTaskPlanToIssue(
|
// Update the github issue to reflect the changes in the plan
|
||||||
{
|
await addTaskPlanToIssue(
|
||||||
githubIssueId: state.githubIssueId,
|
{
|
||||||
targetRepository: state.targetRepository,
|
githubIssueId: state.githubIssueId,
|
||||||
},
|
targetRepository: state.targetRepository,
|
||||||
config,
|
},
|
||||||
newTaskPlan,
|
config,
|
||||||
);
|
newTaskPlan,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const toolMessage = new ToolMessage({
|
const toolMessage = new ToolMessage({
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ import {
|
||||||
import { trackCachePerformance } from "../../../utils/caching.js";
|
import { trackCachePerformance } from "../../../utils/caching.js";
|
||||||
import { getModelManager } from "../../../utils/llms/model-manager.js";
|
import { getModelManager } from "../../../utils/llms/model-manager.js";
|
||||||
import { createScratchpadTool } from "../../../tools/scratchpad.js";
|
import { createScratchpadTool } from "../../../tools/scratchpad.js";
|
||||||
|
import { shouldCreateIssue } from "../../../utils/should-create-issue.js";
|
||||||
|
|
||||||
const logger = createLogger(LogLevel.INFO, "FinalReview");
|
const logger = createLogger(LogLevel.INFO, "FinalReview");
|
||||||
|
|
||||||
|
|
@ -192,7 +193,7 @@ export async function finalReview(
|
||||||
"agent",
|
"agent",
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isLocalMode(config)) {
|
if (!isLocalMode(config) && shouldCreateIssue(config)) {
|
||||||
await addTaskPlanToIssue(
|
await addTaskPlanToIssue(
|
||||||
{
|
{
|
||||||
githubIssueId: state.githubIssueId,
|
githubIssueId: state.githubIssueId,
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import { DEFAULT_EXCLUDED_PATTERNS } from "./constants.js";
|
||||||
import { escapeRegExp } from "../string-utils.js";
|
import { escapeRegExp } from "../string-utils.js";
|
||||||
import { isLocalMode } from "@open-swe/shared/open-swe/local-mode";
|
import { isLocalMode } from "@open-swe/shared/open-swe/local-mode";
|
||||||
import { createShellExecutor } from "../shell-executor/index.js";
|
import { createShellExecutor } from "../shell-executor/index.js";
|
||||||
|
import { shouldCreateIssue } from "../should-create-issue.js";
|
||||||
|
|
||||||
const logger = createLogger(LogLevel.INFO, "GitHub-Git");
|
const logger = createLogger(LogLevel.INFO, "GitHub-Git");
|
||||||
|
|
||||||
|
|
@ -336,33 +337,39 @@ export async function checkoutBranchAndCommit(
|
||||||
let updatedTaskPlan: TaskPlan | undefined;
|
let updatedTaskPlan: TaskPlan | undefined;
|
||||||
const activeTask = getActiveTask(options.taskPlan);
|
const activeTask = getActiveTask(options.taskPlan);
|
||||||
const prForTask = getPullRequestNumberFromActiveTask(options.taskPlan);
|
const prForTask = getPullRequestNumberFromActiveTask(options.taskPlan);
|
||||||
|
|
||||||
if (!prForTask) {
|
if (!prForTask) {
|
||||||
logger.info("First commit detected, creating a draft pull request.");
|
logger.info("First commit detected, creating a draft pull request.");
|
||||||
|
const hasIssue = shouldCreateIssue(config);
|
||||||
|
|
||||||
const pullRequest = await createPullRequest({
|
const pullRequest = await createPullRequest({
|
||||||
owner: targetRepository.owner,
|
owner: targetRepository.owner,
|
||||||
repo: targetRepository.repo,
|
repo: targetRepository.repo,
|
||||||
headBranch: branchName,
|
headBranch: branchName,
|
||||||
title: `[WIP]: ${activeTask?.title ?? "Open SWE task"}`,
|
title: `[WIP]: ${activeTask?.title ?? "Open SWE task"}`,
|
||||||
body: `**WORK IN PROGRESS OPEN SWE PR**\n\nFixes: #${options.githubIssueId}`,
|
body: `**WORK IN PROGRESS OPEN SWE PR**${hasIssue ? `\n\nFixes: #${options.githubIssueId}` : ""}`,
|
||||||
githubInstallationToken: options.githubInstallationToken,
|
githubInstallationToken: options.githubInstallationToken,
|
||||||
draft: true,
|
draft: true,
|
||||||
baseBranch: targetRepository.branch,
|
baseBranch: targetRepository.branch,
|
||||||
nullOnError: true,
|
nullOnError: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (pullRequest) {
|
if (pullRequest) {
|
||||||
updatedTaskPlan = addPullRequestNumberToActiveTask(
|
updatedTaskPlan = addPullRequestNumberToActiveTask(
|
||||||
options.taskPlan,
|
options.taskPlan,
|
||||||
pullRequest.number,
|
pullRequest.number,
|
||||||
);
|
);
|
||||||
await addTaskPlanToIssue(
|
if (hasIssue) {
|
||||||
{
|
await addTaskPlanToIssue(
|
||||||
githubIssueId: options.githubIssueId,
|
{
|
||||||
targetRepository,
|
githubIssueId: options.githubIssueId,
|
||||||
},
|
targetRepository,
|
||||||
config,
|
},
|
||||||
updatedTaskPlan,
|
config,
|
||||||
);
|
updatedTaskPlan,
|
||||||
logger.info(`Draft pull request created: #${pullRequest.number}`);
|
);
|
||||||
|
logger.info(`Draft pull request created: #${pullRequest.number}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
5
apps/open-swe/src/utils/should-create-issue.ts
Normal file
5
apps/open-swe/src/utils/should-create-issue.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { GraphConfig } from "@open-swe/shared/open-swe/types";
|
||||||
|
|
||||||
|
export function shouldCreateIssue(config: GraphConfig): boolean {
|
||||||
|
return !!config.configurable?.shouldCreateIssue;
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ import { getMessageContentString } from "@open-swe/shared/messages";
|
||||||
import { extractContentWithoutDetailsFromIssueBody } from "./github/issue-messages.js";
|
import { extractContentWithoutDetailsFromIssueBody } from "./github/issue-messages.js";
|
||||||
import { isLocalMode } from "@open-swe/shared/open-swe/local-mode";
|
import { isLocalMode } from "@open-swe/shared/open-swe/local-mode";
|
||||||
import { GraphConfig } from "@open-swe/shared/open-swe/types";
|
import { GraphConfig } from "@open-swe/shared/open-swe/types";
|
||||||
|
import { shouldCreateIssue } from "./should-create-issue.js";
|
||||||
|
|
||||||
// TODO: Might want a better way of doing this.
|
// TODO: Might want a better way of doing this.
|
||||||
// maybe add a new kwarg `isRequest` and have this return the last human message with that field?
|
// maybe add a new kwarg `isRequest` and have this return the last human message with that field?
|
||||||
|
|
@ -55,7 +56,10 @@ export function getRecentUserRequest(
|
||||||
): string | HumanMessage {
|
): string | HumanMessage {
|
||||||
let recentUserMessage: HumanMessage | undefined;
|
let recentUserMessage: HumanMessage | undefined;
|
||||||
|
|
||||||
if (options?.config && isLocalMode(options.config)) {
|
if (
|
||||||
|
options?.config &&
|
||||||
|
(isLocalMode(options.config) || !shouldCreateIssue(options.config))
|
||||||
|
) {
|
||||||
// In local mode, get the last human message regardless of flags
|
// In local mode, get the last human message regardless of flags
|
||||||
recentUserMessage = messages.findLast(isHumanMessage);
|
recentUserMessage = messages.findLast(isHumanMessage);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
22
apps/web/src/components/icons/github-issue.tsx
Normal file
22
apps/web/src/components/icons/github-issue.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
interface GitHubIssueSVGProps {
|
||||||
|
width?: string;
|
||||||
|
height?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GitHubIssueSVG = ({
|
||||||
|
width = "100%",
|
||||||
|
height = "100%",
|
||||||
|
className,
|
||||||
|
}: GitHubIssueSVGProps) => (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
fill="currentColor"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"></path>
|
||||||
|
<path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Z"></path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Archive, ListChecks } from "lucide-react";
|
import { Archive, Eye, EyeOff, List, ListChecks } from "lucide-react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { TerminalInput } from "./terminal-input";
|
import { TerminalInput } from "./terminal-input";
|
||||||
import { useFileUpload } from "@/hooks/useFileUpload";
|
import { useFileUpload } from "@/hooks/useFileUpload";
|
||||||
|
|
@ -32,6 +32,7 @@ import { threadsToMetadata } from "@/lib/thread-utils";
|
||||||
import { Settings, BookOpen } from "lucide-react";
|
import { Settings, BookOpen } from "lucide-react";
|
||||||
import NextLink from "next/link";
|
import NextLink from "next/link";
|
||||||
import { OpenSWELogoSVG } from "../icons/openswe";
|
import { OpenSWELogoSVG } from "../icons/openswe";
|
||||||
|
import { DEFAULT_CONFIG_KEY, useConfigStore } from "@/hooks/useConfigStore";
|
||||||
|
|
||||||
function OpenSettingsButton() {
|
function OpenSettingsButton() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -86,6 +87,8 @@ export function DefaultView({ threads, threadsLoading }: DefaultViewProps) {
|
||||||
const apiUrl: string | undefined = process.env.NEXT_PUBLIC_API_URL ?? "";
|
const apiUrl: string | undefined = process.env.NEXT_PUBLIC_API_URL ?? "";
|
||||||
const [draftToLoad, setDraftToLoad] = useState("");
|
const [draftToLoad, setDraftToLoad] = useState("");
|
||||||
const assistantId: string | undefined = MANAGER_GRAPH_ID;
|
const assistantId: string | undefined = MANAGER_GRAPH_ID;
|
||||||
|
const { getConfig } = useConfigStore();
|
||||||
|
const config = getConfig(DEFAULT_CONFIG_KEY);
|
||||||
const {
|
const {
|
||||||
contentBlocks,
|
contentBlocks,
|
||||||
setContentBlocks,
|
setContentBlocks,
|
||||||
|
|
@ -95,6 +98,9 @@ export function DefaultView({ threads, threadsLoading }: DefaultViewProps) {
|
||||||
handlePaste,
|
handlePaste,
|
||||||
} = useFileUpload();
|
} = useFileUpload();
|
||||||
const [autoAccept, setAutoAccept] = useState(false);
|
const [autoAccept, setAutoAccept] = useState(false);
|
||||||
|
const [shouldCreateIssue, setShouldCreateIssue] = useState(
|
||||||
|
config?.shouldCreateIssue != null ? !!config.shouldCreateIssue : true,
|
||||||
|
);
|
||||||
|
|
||||||
const threadsMetadata = useMemo(() => threadsToMetadata(threads), [threads]);
|
const threadsMetadata = useMemo(() => threadsToMetadata(threads), [threads]);
|
||||||
const displayThreads = threadsMetadata.slice(0, 4);
|
const displayThreads = threadsMetadata.slice(0, 4);
|
||||||
|
|
@ -172,11 +178,13 @@ export function DefaultView({ threads, threadsLoading }: DefaultViewProps) {
|
||||||
draftToLoad={draftToLoad}
|
draftToLoad={draftToLoad}
|
||||||
autoAcceptPlan={autoAccept}
|
autoAcceptPlan={autoAccept}
|
||||||
setAutoAcceptPlan={setAutoAccept}
|
setAutoAcceptPlan={setAutoAccept}
|
||||||
|
shouldCreateIssue={shouldCreateIssue}
|
||||||
|
setShouldCreateIssue={setShouldCreateIssue}
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<TooltipIconButton
|
<TooltipIconButton
|
||||||
variant={autoAccept ? "default" : "ghost"}
|
variant={autoAccept ? "default" : "ghost"}
|
||||||
tooltip="Automatically accept the plan"
|
tooltip="Whether or not to automatically accept the plan"
|
||||||
className={cn(
|
className={cn(
|
||||||
"transition-colors duration-200",
|
"transition-colors duration-200",
|
||||||
autoAccept
|
autoAccept
|
||||||
|
|
@ -186,7 +194,29 @@ export function DefaultView({ threads, threadsLoading }: DefaultViewProps) {
|
||||||
onClick={() => setAutoAccept((prev) => !prev)}
|
onClick={() => setAutoAccept((prev) => !prev)}
|
||||||
side="bottom"
|
side="bottom"
|
||||||
>
|
>
|
||||||
<ListChecks className="size-4" />
|
{autoAccept ? (
|
||||||
|
<ListChecks className="size-4" />
|
||||||
|
) : (
|
||||||
|
<List className="size-4" />
|
||||||
|
)}
|
||||||
|
</TooltipIconButton>
|
||||||
|
<TooltipIconButton
|
||||||
|
variant={shouldCreateIssue ? "ghost" : "default"}
|
||||||
|
tooltip="Whether or not to create a GitHub issue for the request"
|
||||||
|
className={cn(
|
||||||
|
"transition-colors duration-200",
|
||||||
|
shouldCreateIssue
|
||||||
|
? "text-muted-foreground hover:text-foreground"
|
||||||
|
: "bg-primary hover:bg-primary/90",
|
||||||
|
)}
|
||||||
|
onClick={() => setShouldCreateIssue((prev) => !prev)}
|
||||||
|
side="bottom"
|
||||||
|
>
|
||||||
|
{shouldCreateIssue ? (
|
||||||
|
<Eye className="size-4" />
|
||||||
|
) : (
|
||||||
|
<EyeOff className="size-4" />
|
||||||
|
)}
|
||||||
</TooltipIconButton>
|
</TooltipIconButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,7 @@ interface ManagerChatProps {
|
||||||
name: string | null;
|
name: string | null;
|
||||||
email: string | null;
|
email: string | null;
|
||||||
};
|
};
|
||||||
|
disableSubmit?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractResponseFromMessage(message: Message): string {
|
function extractResponseFromMessage(message: Message): string {
|
||||||
|
|
@ -162,6 +163,7 @@ export function ManagerChat({
|
||||||
cancelRun,
|
cancelRun,
|
||||||
errorState,
|
errorState,
|
||||||
githubUser,
|
githubUser,
|
||||||
|
disableSubmit,
|
||||||
}: ManagerChatProps) {
|
}: ManagerChatProps) {
|
||||||
return (
|
return (
|
||||||
<div className="border-border bg-muted/30 flex h-full w-1/3 flex-col overflow-hidden border-r">
|
<div className="border-border bg-muted/30 flex h-full w-1/3 flex-col overflow-hidden border-r">
|
||||||
|
|
@ -252,7 +254,13 @@ export function ManagerChat({
|
||||||
placeholder="Type your message..."
|
placeholder="Type your message..."
|
||||||
className="border-border bg-background text-foreground placeholder:text-muted-foreground min-h-[60px] flex-1 resize-none text-sm"
|
className="border-border bg-background text-foreground placeholder:text-muted-foreground min-h-[60px] flex-1 resize-none text-sm"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter" && (e.metaKey || e.ctrlKey) && !isLoading) {
|
if (
|
||||||
|
e.key === "Enter" &&
|
||||||
|
(e.metaKey || e.ctrlKey) &&
|
||||||
|
!isLoading &&
|
||||||
|
!disableSubmit &&
|
||||||
|
!!chatInput.trim()
|
||||||
|
) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleSendMessage();
|
handleSendMessage();
|
||||||
}
|
}
|
||||||
|
|
@ -270,7 +278,7 @@ export function ManagerChat({
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSendMessage}
|
onClick={handleSendMessage}
|
||||||
disabled={!chatInput.trim()}
|
disabled={!chatInput.trim() || disableSubmit}
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="brand"
|
variant="brand"
|
||||||
className="size-8 rounded-full border border-white/20 transition-all duration-200 hover:border-white/30 disabled:border-transparent"
|
className="size-8 rounded-full border border-white/20 transition-all duration-200 hover:border-white/30 disabled:border-transparent"
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,8 @@ interface TerminalInputProps {
|
||||||
setQuickActionPrompt?: Dispatch<SetStateAction<string>>;
|
setQuickActionPrompt?: Dispatch<SetStateAction<string>>;
|
||||||
autoAcceptPlan: boolean;
|
autoAcceptPlan: boolean;
|
||||||
setAutoAcceptPlan: Dispatch<SetStateAction<boolean>>;
|
setAutoAcceptPlan: Dispatch<SetStateAction<boolean>>;
|
||||||
|
shouldCreateIssue: boolean;
|
||||||
|
setShouldCreateIssue: Dispatch<SetStateAction<boolean>>;
|
||||||
draftToLoad?: string;
|
draftToLoad?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,6 +73,8 @@ export function TerminalInput({
|
||||||
setQuickActionPrompt,
|
setQuickActionPrompt,
|
||||||
autoAcceptPlan,
|
autoAcceptPlan,
|
||||||
setAutoAcceptPlan,
|
setAutoAcceptPlan,
|
||||||
|
shouldCreateIssue,
|
||||||
|
setShouldCreateIssue,
|
||||||
draftToLoad,
|
draftToLoad,
|
||||||
}: TerminalInputProps) {
|
}: TerminalInputProps) {
|
||||||
const { push } = useRouter();
|
const { push } = useRouter();
|
||||||
|
|
@ -164,6 +168,7 @@ export function TerminalInput({
|
||||||
recursion_limit: 400,
|
recursion_limit: 400,
|
||||||
configurable: {
|
configurable: {
|
||||||
...defaultConfig,
|
...defaultConfig,
|
||||||
|
shouldCreateIssue,
|
||||||
[GITHUB_USER_LOGIN_HEADER]: user.login,
|
[GITHUB_USER_LOGIN_HEADER]: user.login,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -199,6 +204,11 @@ export function TerminalInput({
|
||||||
setMessage("");
|
setMessage("");
|
||||||
setContentBlocks([]);
|
setContentBlocks([]);
|
||||||
setAutoAcceptPlan(false);
|
setAutoAcceptPlan(false);
|
||||||
|
setShouldCreateIssue(
|
||||||
|
defaultConfig?.shouldCreateIssue != null
|
||||||
|
? !!defaultConfig.shouldCreateIssue
|
||||||
|
: true,
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (
|
if (
|
||||||
typeof e === "object" &&
|
typeof e === "object" &&
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,14 @@ export function ThreadView({
|
||||||
});
|
});
|
||||||
|
|
||||||
const [errorState, setErrorState] = useState<ErrorState | null>(null);
|
const [errorState, setErrorState] = useState<ErrorState | null>(null);
|
||||||
|
const [hasGitHubIssue, setHasGitHubIssue] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
stream.client.threads.get(displayThread.id).then((thread) => {
|
||||||
|
const configurable = (thread as Record<string, any>).config?.configurable;
|
||||||
|
setHasGitHubIssue(!!configurable?.shouldCreateIssue);
|
||||||
|
});
|
||||||
|
}, [displayThread.id]);
|
||||||
|
|
||||||
// Load optimistic message from sessionStorage
|
// Load optimistic message from sessionStorage
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -348,6 +356,10 @@ export function ThreadView({
|
||||||
]
|
]
|
||||||
: filteredMessages;
|
: filteredMessages;
|
||||||
|
|
||||||
|
const shouldDisableManagerInput = !hasGitHubIssue
|
||||||
|
? stream.isLoading || plannerStream.isLoading || programmerStream.isLoading
|
||||||
|
: false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-background flex h-screen flex-1 flex-col">
|
<div className="bg-background flex h-screen flex-1 flex-col">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
|
|
@ -397,6 +409,7 @@ export function ThreadView({
|
||||||
cancelRun={cancelRun}
|
cancelRun={cancelRun}
|
||||||
errorState={errorState}
|
errorState={errorState}
|
||||||
githubUser={user || undefined}
|
githubUser={user || undefined}
|
||||||
|
disableSubmit={shouldDisableManagerInput}
|
||||||
/>
|
/>
|
||||||
{/* Right Side - Actions & Plan */}
|
{/* Right Side - Actions & Plan */}
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -449,6 +449,14 @@ export const GraphConfigurationMetadata: {
|
||||||
"JSON configuration for custom MCP servers. LangGraph docs server is set by default. See the `mcpServers` field of the LangChain MCP Adapters `ClientConfig` type for information on this schema. [Documentation here](https://v03.api.js.langchain.com/types/_langchain_mcp_adapters.ClientConfig.html).",
|
"JSON configuration for custom MCP servers. LangGraph docs server is set by default. See the `mcpServers` field of the LangChain MCP Adapters `ClientConfig` type for information on this schema. [Documentation here](https://v03.api.js.langchain.com/types/_langchain_mcp_adapters.ClientConfig.html).",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
shouldCreateIssue: {
|
||||||
|
x_open_swe_ui_config: {
|
||||||
|
type: "boolean",
|
||||||
|
default: true,
|
||||||
|
description:
|
||||||
|
"Whether or not to create GitHub issues for all requests. Can be overridden on a per-request basis via the 'eye' icon in the chat input area.",
|
||||||
|
},
|
||||||
|
},
|
||||||
apiKeys: {
|
apiKeys: {
|
||||||
x_open_swe_ui_config: {
|
x_open_swe_ui_config: {
|
||||||
type: "hidden",
|
type: "hidden",
|
||||||
|
|
@ -614,6 +622,13 @@ export const GraphConfiguration = z.object({
|
||||||
maxTokens: withLangGraph(z.number().optional(), {
|
maxTokens: withLangGraph(z.number().optional(), {
|
||||||
metadata: GraphConfigurationMetadata.maxTokens,
|
metadata: GraphConfigurationMetadata.maxTokens,
|
||||||
}),
|
}),
|
||||||
|
/**
|
||||||
|
* Whether or not to create an issue for this request.
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
shouldCreateIssue: withLangGraph(z.boolean().optional(), {
|
||||||
|
metadata: GraphConfigurationMetadata.shouldCreateIssue,
|
||||||
|
}),
|
||||||
/**
|
/**
|
||||||
* User defined API keys to use
|
* User defined API keys to use
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue