mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 13:53:15 +00:00
fix: Better prompt handling of followups (#434)
* fix: Better prompt handling of followups * cr * cr * cr * cr
This commit is contained in:
parent
0953468b5c
commit
ed85a4c884
25 changed files with 330 additions and 146 deletions
|
|
@ -25,6 +25,7 @@ import {
|
|||
import { getGitHubTokensFromConfig } from "../../../../utils/github-tokens.js";
|
||||
import { createIssueFieldsFromMessages } from "../../utils/generate-issue-fields.js";
|
||||
import {
|
||||
extractContentWithoutDetailsFromIssueBody,
|
||||
extractIssueTitleAndContentFromMessage,
|
||||
formatContentForIssueBody,
|
||||
} from "../../../../utils/github/issue-messages.js";
|
||||
|
|
@ -110,7 +111,12 @@ export async function classifyMessage(
|
|||
role: "system",
|
||||
content: prompt,
|
||||
},
|
||||
userMessage,
|
||||
{
|
||||
role: "user",
|
||||
content: extractContentWithoutDetailsFromIssueBody(
|
||||
getMessageContentString(userMessage.content),
|
||||
),
|
||||
},
|
||||
]);
|
||||
|
||||
const toolCall = response.tool_calls?.[0];
|
||||
|
|
@ -217,8 +223,14 @@ export async function classifyMessage(
|
|||
new HumanMessage({
|
||||
...message,
|
||||
additional_kwargs: {
|
||||
githubIssueId: githubIssueId,
|
||||
githubIssueId,
|
||||
githubIssueCommentId: createdIssue.id,
|
||||
...((toolCallArgs.route as string) ===
|
||||
"start_planner_for_followup"
|
||||
? {
|
||||
isFollowup: true,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
|
@ -228,6 +240,8 @@ export async function classifyMessage(
|
|||
await Promise.all(createCommentsPromise);
|
||||
|
||||
let newPlannerId: string | undefined;
|
||||
let goto = END;
|
||||
|
||||
if (plannerStatus === "interrupted") {
|
||||
if (!state.plannerSession?.threadId) {
|
||||
throw new Error("No planner session found. Unable to resume planner.");
|
||||
|
|
@ -255,6 +269,10 @@ export async function classifyMessage(
|
|||
});
|
||||
}
|
||||
|
||||
if (toolCallArgs.route === "start_planner_for_followup") {
|
||||
goto = "start-planner";
|
||||
}
|
||||
|
||||
// After creating the new comment, we can add the message to state and end.
|
||||
const commandUpdate: ManagerGraphUpdate = {
|
||||
messages: newMessages,
|
||||
|
|
@ -269,7 +287,7 @@ export async function classifyMessage(
|
|||
};
|
||||
return new Command({
|
||||
update: commandUpdate,
|
||||
goto: END,
|
||||
goto,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -294,7 +312,10 @@ export async function classifyMessage(
|
|||
});
|
||||
}
|
||||
|
||||
if (toolCallArgs.route === "start_planner") {
|
||||
if (
|
||||
toolCallArgs.route === "start_planner" ||
|
||||
toolCallArgs.route === "start_planner_for_followup"
|
||||
) {
|
||||
// Always kickoff a new start planner node. This will enqueue new runs on the planner graph.
|
||||
return new Command({
|
||||
update: commandUpdate,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ export const UPDATE_PROGRAMMER_ROUTING_OPTION = `- update_programmer: You should
|
|||
|
||||
export const START_PLANNER_ROUTING_OPTION = `- start_planner: You should call this route if the user's message is a complete request you can send to the planner, which it can use to generate a plan. This route may be called when the planner has not started yet.\n`;
|
||||
|
||||
export const START_PLANNER_FOR_FOLLOWUP_ROUTING_OPTION = `- start_planner: You should call this route if the user's message is a complete followup request you can send to the planner, which it can use to generate a plan new plan to address the user's feedback/followup request. This route may be called when the planner and programmer are no longer running (e.g. after the user's initial request has been completed).\n`;
|
||||
export const START_PLANNER_FOR_FOLLOWUP_ROUTING_OPTION = `- start_planner_for_followup: You should call this route if the user's message is a followup request you can send to the planner, which it can use to generate a plan new plan to address the user's feedback/followup request. This route may be called when the planner and programmer are no longer running (e.g. after the user's initial request has been completed).\n`;
|
||||
|
||||
export const UPDATE_PLANNER_ROUTING_OPTION = `- update_planner: You should call this route if the user sends a new message containing anything from a related request that the planner should plan for, additional context about their previous request/the codebase, or something which the planner should be aware of.\n`;
|
||||
|
||||
|
|
@ -54,8 +54,8 @@ The programmer's current status is: {PROGRAMMER_STATUS}
|
|||
# Routing Options
|
||||
Based on all of the context provided above, generate a response to send to the user, including messaging about the route you'll select from the below options in your next step.
|
||||
Your routing options are:
|
||||
- no_op: This should be called when the user's message is not a new request, additional context, or a new issue to create. This should only be called when none of the routing options are appropriate.
|
||||
{UPDATE_PROGRAMMER_ROUTING_OPTION}{START_PLANNER_ROUTING_OPTION}{UPDATE_PLANNER_ROUTING_OPTION}{RESUME_AND_UPDATE_PLANNER_ROUTING_OPTION}{CREATE_NEW_ISSUE_ROUTING_OPTION}{START_PLANNER_FOR_FOLLOWUP_ROUTING_OPTION}
|
||||
- no_op: This should be called when the user's message is not a new request, additional context, or a new issue to create. This should only be called when none of the routing options are appropriate.
|
||||
|
||||
# Response
|
||||
Your response should be clear, concise and straight to the point. Do NOT include any additional context, such as an idea for how to implement their request.
|
||||
|
|
|
|||
|
|
@ -116,15 +116,14 @@ export function createClassificationPromptAndToolSchema(inputs: {
|
|||
inputs.programmerStatus !== "not_started" ||
|
||||
inputs.plannerStatus !== "not_started";
|
||||
|
||||
const routingOptions: [string, ...string[]] = [
|
||||
"no_op",
|
||||
const routingOptions = [
|
||||
...(programmerRunning ? ["update_programmer"] : []),
|
||||
...((plannerNotStarted ?? plannerAndProgrammerIdle)
|
||||
? ["start_planner"]
|
||||
: []),
|
||||
...(plannerNotStarted ? ["start_planner"] : []),
|
||||
...(plannerAndProgrammerIdle ? ["start_planner_for_followup"] : []),
|
||||
...(plannerRunning ? ["update_planner"] : []),
|
||||
...(plannerInterrupted ? ["resume_and_update_planner"] : []),
|
||||
...(showCreateIssueOption ? ["create_new_issue"] : []),
|
||||
"no_op",
|
||||
];
|
||||
|
||||
const prompt = CLASSIFICATION_SYSTEM_PROMPT.replaceAll(
|
||||
|
|
@ -169,7 +168,9 @@ export function createClassificationPromptAndToolSchema(inputs: {
|
|||
formattedConversationHistoryPrompt ?? "",
|
||||
);
|
||||
|
||||
const schema = createClassificationSchema(routingOptions);
|
||||
const schema = createClassificationSchema(
|
||||
routingOptions as [string, ...string[]],
|
||||
);
|
||||
|
||||
return {
|
||||
prompt,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { getBranchName } from "../../../utils/github/git.js";
|
|||
import { PlannerGraphUpdate } from "@open-swe/shared/open-swe/planner/types";
|
||||
import { getDefaultHeaders } from "../../../utils/default-headers.js";
|
||||
import { getCustomConfigurableFields } from "../../../utils/config.js";
|
||||
import { getRecentUserRequest } from "../../../utils/user-request.js";
|
||||
|
||||
const logger = createLogger(LogLevel.INFO, "StartPlanner");
|
||||
|
||||
|
|
@ -27,6 +28,9 @@ export async function startPlanner(
|
|||
});
|
||||
|
||||
const plannerThreadId = state.plannerSession?.threadId ?? uuidv4();
|
||||
const followupMessage = getRecentUserRequest(state.messages, {
|
||||
returnFullMessage: true,
|
||||
});
|
||||
try {
|
||||
const runInput: PlannerGraphUpdate = {
|
||||
// github issue ID & target repo so the planning agent can fetch the user's request, and clone the repo.
|
||||
|
|
@ -36,6 +40,7 @@ export async function startPlanner(
|
|||
taskPlan: state.taskPlan,
|
||||
branchName: state.branchName ?? getBranchName(config),
|
||||
autoAcceptPlan: state.autoAcceptPlan,
|
||||
...(followupMessage && { messages: [followupMessage] }),
|
||||
};
|
||||
const run = await langGraphClient.runs.create(
|
||||
plannerThreadId,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
PlannerGraphState,
|
||||
PlannerGraphUpdate,
|
||||
} from "@open-swe/shared/open-swe/planner/types";
|
||||
import { getUserRequest } from "../../../../utils/user-request.js";
|
||||
import { formatUserRequestPrompt } from "../../../../utils/user-request.js";
|
||||
import {
|
||||
formatFollowupMessagePrompt,
|
||||
isFollowupRequest,
|
||||
|
|
@ -27,7 +27,6 @@ import { filterMessagesWithoutContent } from "../../../../utils/message/content.
|
|||
function formatSystemPrompt(state: PlannerGraphState): string {
|
||||
// It's a followup if there's more than one human message.
|
||||
const isFollowup = isFollowupRequest(state.taskPlan, state.proposedPlan);
|
||||
const userRequest = getUserRequest(state.messages);
|
||||
const plannerNotes = getPlannerNotes(state.messages)
|
||||
.map((n) => `- ${n}`)
|
||||
.join("\n");
|
||||
|
|
@ -39,7 +38,7 @@ function formatSystemPrompt(state: PlannerGraphState): string {
|
|||
"\n\n"
|
||||
: "",
|
||||
)
|
||||
.replace("{USER_REQUEST}", userRequest)
|
||||
.replace("{USER_REQUEST_PROMPT}", formatUserRequestPrompt(state.messages))
|
||||
.replaceAll("{CUSTOM_RULES}", formatCustomRulesPrompt(state.customRules))
|
||||
.replaceAll(
|
||||
"{PLANNER_NOTES}",
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ Generate an execution plan to address the user's request. Your plan will guide t
|
|||
It should contain enough information to not require many additional context gathering steps to execute.
|
||||
|
||||
<user_request>
|
||||
{USER_REQUEST}
|
||||
{USER_REQUEST_PROMPT}
|
||||
</user_request>
|
||||
</task>
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
Task,
|
||||
} from "../../../utils/load-model.js";
|
||||
import { getMessageString } from "../../../utils/message/content.js";
|
||||
import { getUserRequest } from "../../../utils/user-request.js";
|
||||
import { formatUserRequestPrompt } from "../../../utils/user-request.js";
|
||||
import { formatCustomRulesPrompt } from "../../../utils/custom-rules.js";
|
||||
import { getPlannerNotes } from "../utils/get-notes.js";
|
||||
import { ToolMessage } from "@langchain/core/messages";
|
||||
|
|
@ -47,9 +47,7 @@ You MUST adhere to the following criteria when generating your notes:
|
|||
- Carefully inspect the proposed plan. Your notes should be focused on context which will be most useful to you when you execute the plan. You may reference specific proposed plan items in your notes.
|
||||
{EXTRA_RULES}
|
||||
|
||||
Here is the user's request
|
||||
## User request:
|
||||
{USER_REQUEST}
|
||||
{USER_REQUEST_PROMPT}
|
||||
|
||||
Here is the conversation history:
|
||||
## Conversation history:
|
||||
|
|
@ -63,14 +61,12 @@ With all of this in mind, please carefully inspect the conversation history, and
|
|||
`;
|
||||
|
||||
const formatPrompt = (state: PlannerGraphState): string => {
|
||||
const userRequest =
|
||||
getUserRequest(state.messages) || "No user request provided.";
|
||||
const plannerNotes = getPlannerNotes(state.messages)
|
||||
.map((n) => ` - ${n}`)
|
||||
.join("\n");
|
||||
|
||||
return systemPrompt
|
||||
.replace("{USER_REQUEST}", userRequest)
|
||||
.replace("{USER_REQUEST_PROMPT}", formatUserRequestPrompt(state.messages))
|
||||
.replace(
|
||||
"{CONVERSATION_HISTORY}",
|
||||
state.messages.map(getMessageString).join("\n"),
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import {
|
|||
PlannerGraphState,
|
||||
PlannerGraphUpdate,
|
||||
} from "@open-swe/shared/open-swe/planner/types";
|
||||
import { Command, END } from "@langchain/langgraph";
|
||||
import { Command } from "@langchain/langgraph";
|
||||
import { getGitHubTokensFromConfig } from "../../../utils/github-tokens.js";
|
||||
import { getIssue, getIssueComments } from "../../../utils/github/api.js";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
|
@ -19,6 +19,7 @@ import {
|
|||
getUntrackedComments,
|
||||
} from "../../../utils/github/issue-messages.js";
|
||||
import { filterHiddenMessages } from "../../../utils/message/filter-hidden.js";
|
||||
import { DO_NOT_RENDER_ID_PREFIX } from "@open-swe/shared/constants";
|
||||
|
||||
export async function prepareGraphState(
|
||||
state: PlannerGraphState,
|
||||
|
|
@ -86,28 +87,27 @@ export async function prepareGraphState(
|
|||
state.githubIssueId,
|
||||
comments ?? [],
|
||||
);
|
||||
if (!untrackedComments?.length) {
|
||||
// If there are already messages in the state, and no comments, we can assume the issue is already handled.
|
||||
return new Command({
|
||||
goto: END,
|
||||
});
|
||||
}
|
||||
|
||||
// Remove all messages not marked as summaryMessage, hidden, and not human messages.
|
||||
const removedNonSummaryMessages = filterHiddenMessages(state.messages)
|
||||
.filter((m) => !m.additional_kwargs?.summaryMessage && !isHumanMessage(m))
|
||||
.map((m: BaseMessage) => new RemoveMessage({ id: m.id ?? "" }));
|
||||
const summaryMessage = new AIMessage({
|
||||
id: uuidv4(),
|
||||
content: state.contextGatheringNotes,
|
||||
additional_kwargs: {
|
||||
summaryMessage: true,
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: We should prob have a UI component for "Previous Task Notes" so we can surface this in the UI.
|
||||
const summaryMessage = state.contextGatheringNotes
|
||||
? new AIMessage({
|
||||
id: `${DO_NOT_RENDER_ID_PREFIX}${uuidv4()}`,
|
||||
content: `Here are the notes taken while planning for the previous task:\n${state.contextGatheringNotes}`,
|
||||
additional_kwargs: {
|
||||
summaryMessage: true,
|
||||
},
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const commandUpdate: PlannerGraphUpdate = {
|
||||
messages: [
|
||||
...removedNonSummaryMessages,
|
||||
summaryMessage,
|
||||
...(summaryMessage ? [summaryMessage] : []),
|
||||
...untrackedComments,
|
||||
],
|
||||
// Reset plan context summary as it's now included in the messages array.
|
||||
|
|
|
|||
|
|
@ -14,7 +14,10 @@ import {
|
|||
} from "@langchain/langgraph/prebuilt";
|
||||
import { getSandboxWithErrorHandling } from "../../../utils/sandbox.js";
|
||||
import { createNewTask } from "@open-swe/shared/open-swe/tasks";
|
||||
import { getUserRequest } from "../../../utils/user-request.js";
|
||||
import {
|
||||
getInitialUserRequest,
|
||||
getRecentUserRequest,
|
||||
} from "../../../utils/user-request.js";
|
||||
import {
|
||||
PLAN_INTERRUPT_ACTION_TITLE,
|
||||
PLAN_INTERRUPT_DELIMITER,
|
||||
|
|
@ -41,8 +44,9 @@ function createAcceptedPlanMessage(input: {
|
|||
planTitle: string;
|
||||
planItems: PlanItem[];
|
||||
interruptType: HumanResponse["type"];
|
||||
runId: string;
|
||||
}) {
|
||||
const { planTitle, planItems, interruptType } = input;
|
||||
const { planTitle, planItems, interruptType, runId } = input;
|
||||
const acceptedPlanEvent: CustomNodeEvent = {
|
||||
nodeId: ACCEPTED_PLAN_NODE_ID,
|
||||
actionId: uuidv4(),
|
||||
|
|
@ -53,6 +57,7 @@ function createAcceptedPlanMessage(input: {
|
|||
planTitle,
|
||||
planItems,
|
||||
interruptType,
|
||||
runId,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -143,7 +148,9 @@ export async function interruptProposedPlan(
|
|||
}
|
||||
|
||||
let planItems: PlanItem[];
|
||||
const userRequest = getUserRequest(state.messages);
|
||||
const userRequest = getInitialUserRequest(state.messages);
|
||||
const userFollowupRequest = getRecentUserRequest(state.messages);
|
||||
const userTaskRequest = userFollowupRequest || userRequest;
|
||||
const runInput: GraphUpdate = {
|
||||
contextGatheringNotes: state.contextGatheringNotes,
|
||||
branchName: state.branchName,
|
||||
|
|
@ -160,7 +167,7 @@ export async function interruptProposedPlan(
|
|||
completed: false,
|
||||
}));
|
||||
runInput.taskPlan = createNewTask(
|
||||
userRequest,
|
||||
userTaskRequest,
|
||||
state.proposedPlanTitle,
|
||||
planItems,
|
||||
{ existingTaskPlan: state.taskPlan },
|
||||
|
|
@ -177,6 +184,7 @@ export async function interruptProposedPlan(
|
|||
planTitle: state.proposedPlanTitle,
|
||||
planItems,
|
||||
interruptType: "accept",
|
||||
runId: config.configurable?.run_id ?? "",
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
|
@ -238,7 +246,7 @@ export async function interruptProposedPlan(
|
|||
}));
|
||||
|
||||
runInput.taskPlan = createNewTask(
|
||||
userRequest,
|
||||
userTaskRequest,
|
||||
state.proposedPlanTitle,
|
||||
planItems,
|
||||
{ existingTaskPlan: state.taskPlan },
|
||||
|
|
@ -255,7 +263,7 @@ export async function interruptProposedPlan(
|
|||
}));
|
||||
|
||||
runInput.taskPlan = createNewTask(
|
||||
userRequest,
|
||||
userTaskRequest,
|
||||
state.proposedPlanTitle,
|
||||
planItems,
|
||||
{ existingTaskPlan: state.taskPlan },
|
||||
|
|
@ -275,6 +283,7 @@ export async function interruptProposedPlan(
|
|||
planTitle: state.proposedPlanTitle,
|
||||
planItems,
|
||||
interruptType: humanResponse.type,
|
||||
runId: config.configurable?.run_id ?? "",
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,7 +10,10 @@ import {
|
|||
PlannerGraphState,
|
||||
PlannerGraphUpdate,
|
||||
} from "@open-swe/shared/open-swe/planner/types";
|
||||
import { getUserRequest } from "../../../utils/user-request.js";
|
||||
import {
|
||||
getInitialUserRequest,
|
||||
getRecentUserRequest,
|
||||
} from "../../../utils/user-request.js";
|
||||
import {
|
||||
loadModel,
|
||||
supportsParallelToolCallsParam,
|
||||
|
|
@ -149,12 +152,16 @@ async function identifyTasksToModifyFunc(
|
|||
},
|
||||
);
|
||||
|
||||
const userRequest = getUserRequest(state.messages);
|
||||
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 || "No user message found",
|
||||
userRequest,
|
||||
state.planChangeRequest,
|
||||
state.proposedPlan,
|
||||
),
|
||||
|
|
@ -217,12 +224,16 @@ async function updatePlanTasksFunc(
|
|||
: {}),
|
||||
});
|
||||
|
||||
const userRequest = getUserRequest(state.messages);
|
||||
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 || "No user message found",
|
||||
userRequest,
|
||||
state.planChangeRequest,
|
||||
state.proposedPlan,
|
||||
tasksToModify,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { loadModel, Task } from "../../../utils/load-model.js";
|
|||
import { getMessageContentString } from "@open-swe/shared/messages";
|
||||
import { getMessageString } from "../../../utils/message/content.js";
|
||||
import { createLogger, LogLevel } from "../../../utils/logger.js";
|
||||
import { getUserRequest } from "../../../utils/user-request.js";
|
||||
import { formatUserRequestPrompt } from "../../../utils/user-request.js";
|
||||
import {
|
||||
completeTask,
|
||||
getActivePlanItems,
|
||||
|
|
@ -40,11 +40,10 @@ export async function generateConclusion(
|
|||
): Promise<GraphUpdate> {
|
||||
const model = await loadModel(config, Task.SUMMARIZER);
|
||||
|
||||
const userRequest = getUserRequest(state.internalMessages);
|
||||
const userMessage = `The user's initial request is as follows:
|
||||
${userRequest || "No user message found"}
|
||||
const userRequestPrompt = formatUserRequestPrompt(state.messages);
|
||||
const userMessage = `${userRequestPrompt}
|
||||
|
||||
The conversation history is as follows:
|
||||
The full conversation history is as follows:
|
||||
${state.internalMessages.map(getMessageString).join("\n")}
|
||||
|
||||
Given all of this, please respond with the concise conclusion. Do not include any additional text besides the conclusion.`;
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ import {
|
|||
Task,
|
||||
} from "../../../utils/load-model.js";
|
||||
import { formatPlanPromptWithSummaries } from "../../../utils/plan-prompt.js";
|
||||
import { getUserRequest } from "../../../utils/user-request.js";
|
||||
import { AIMessage, ToolMessage } from "@langchain/core/messages";
|
||||
import { formatUserRequestPrompt } from "../../../utils/user-request.js";
|
||||
import { AIMessage, BaseMessage, ToolMessage } from "@langchain/core/messages";
|
||||
import {
|
||||
deleteSandbox,
|
||||
getSandboxWithErrorHandling,
|
||||
|
|
@ -38,16 +38,18 @@ You have just completed all of your tasks, and are now ready to open a pull requ
|
|||
Here are all of the tasks you completed:
|
||||
{COMPLETED_TASKS}
|
||||
|
||||
And here is the user's original request:
|
||||
{USER_REQUEST}
|
||||
{USER_REQUEST_PROMPT}
|
||||
|
||||
With all of this in mind, please use the \`open_pr\` tool to open a pull request.`;
|
||||
|
||||
const formatPrompt = (taskPlan: PlanItem[], userRequest: string): string => {
|
||||
const formatPrompt = (
|
||||
taskPlan: PlanItem[],
|
||||
messages: BaseMessage[],
|
||||
): string => {
|
||||
const completedTasks = taskPlan.filter((task) => task.completed);
|
||||
return openPrSysPrompt
|
||||
.replace("{COMPLETED_TASKS}", formatPlanPromptWithSummaries(completedTasks))
|
||||
.replace("{USER_REQUEST}", userRequest);
|
||||
.replace("{USER_REQUEST_PROMPT}", formatUserRequestPrompt(messages));
|
||||
};
|
||||
|
||||
export async function openPullRequest(
|
||||
|
|
@ -108,11 +110,13 @@ export async function openPullRequest(
|
|||
: {}),
|
||||
});
|
||||
|
||||
const userRequest = getUserRequest(state.internalMessages);
|
||||
const response = await modelWithTool.invoke([
|
||||
{
|
||||
role: "user",
|
||||
content: formatPrompt(getActivePlanItems(state.taskPlan), userRequest),
|
||||
content: formatPrompt(
|
||||
getActivePlanItems(state.taskPlan),
|
||||
state.internalMessages,
|
||||
),
|
||||
},
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -14,8 +14,7 @@ import {
|
|||
import { formatPlanPrompt } from "../../../utils/plan-prompt.js";
|
||||
import { Command } from "@langchain/langgraph";
|
||||
import { getMessageString } from "../../../utils/message/content.js";
|
||||
import { removeFirstHumanMessage } from "../../../utils/message/modify-array.js";
|
||||
import { getUserRequest } from "../../../utils/user-request.js";
|
||||
import { formatUserRequestPrompt } from "../../../utils/user-request.js";
|
||||
import {
|
||||
completePlanItem,
|
||||
getActivePlanItems,
|
||||
|
|
@ -88,12 +87,11 @@ export async function progressPlanStep(
|
|||
},
|
||||
);
|
||||
|
||||
const userRequest = getUserRequest(state.internalMessages, {
|
||||
returnFullMessage: true,
|
||||
});
|
||||
const conversationHistoryStr = `Here is the full conversation history after the user's request:
|
||||
const conversationHistoryStr = `Here is the full conversation history including the user's request(s):
|
||||
|
||||
${removeFirstHumanMessage(state.internalMessages).map(getMessageString).join("\n")}
|
||||
${state.internalMessages.map(getMessageString).join("\n")}
|
||||
|
||||
${formatUserRequestPrompt(state.internalMessages)}
|
||||
|
||||
Take all of this information, and determine whether or not you have completed this task in the plan.
|
||||
Once you've determined the status of the current task, call either the \`mark_task_completed\` or \`mark_task_not_completed\` tool.`;
|
||||
|
|
@ -105,7 +103,6 @@ Once you've determined the status of the current task, call either the \`mark_ta
|
|||
role: "system",
|
||||
content: formatPrompt(activePlanItems),
|
||||
},
|
||||
userRequest,
|
||||
{
|
||||
role: "user",
|
||||
content: conversationHistoryStr,
|
||||
|
|
|
|||
|
|
@ -18,9 +18,24 @@ import { getMessageContentString } from "@open-swe/shared/messages";
|
|||
import { getMessageString } from "../../../utils/message/content.js";
|
||||
import { getActivePlanItems } from "@open-swe/shared/open-swe/tasks";
|
||||
import { createConversationHistorySummaryToolFields } from "@open-swe/shared/open-swe/tools";
|
||||
import { getUserRequest } from "../../../utils/user-request.js";
|
||||
import { formatUserRequestPrompt } from "../../../utils/user-request.js";
|
||||
import { getMessagesSinceLastSummary } from "../../../utils/tokens.js";
|
||||
|
||||
const SINGLE_USER_REQUEST_PROMPT = `Here is the user's request:
|
||||
<user_request>
|
||||
{USER_REQUEST}
|
||||
</user_request>`;
|
||||
|
||||
const USER_SENDING_FOLLOWUP_PROMPT = `Here is the user's initial request:
|
||||
<user_initial_request>
|
||||
{USER_REQUEST}
|
||||
</user_initial_request>
|
||||
|
||||
And here is the user's followup request you're now processing:
|
||||
<user_followup_request>
|
||||
{USER_FOLLOWUP_REQUEST}
|
||||
</user_followup_request>`;
|
||||
|
||||
const taskSummarySysPrompt = `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.
|
||||
|
||||
<role>
|
||||
|
|
@ -37,10 +52,7 @@ This context will then overwrite the conversation history presented below. Becau
|
|||
To aid with this, you'll be provided with the user's request, as well as all of the tasks in the plan you generated to fulfil the user's request. Additionally, if a task has already been completed you'll be provided with the summary of the steps taken to complete it.
|
||||
</objective_information>
|
||||
|
||||
Here is the user's request:
|
||||
<user_request>
|
||||
{USER_REQUEST}
|
||||
</user_request>
|
||||
{USER_REQUEST_PROMPT}
|
||||
|
||||
Here is the full list of tasks in the plan you're in the middle of, as well as the summary of the completed tasks:
|
||||
<tasks_and_summaries>
|
||||
|
|
@ -70,7 +82,7 @@ Respond ONLY with the extracted context. Do not include any additional informati
|
|||
const logger = createLogger(LogLevel.INFO, "SummarizeConversationHistory");
|
||||
|
||||
const formatPrompt = (inputs: {
|
||||
userRequest: string;
|
||||
messages: BaseMessage[];
|
||||
plan: PlanItem[];
|
||||
conversationHistoryToSummarize: BaseMessage[];
|
||||
}): string => {
|
||||
|
|
@ -82,7 +94,14 @@ const formatPrompt = (inputs: {
|
|||
includeSummaries: true,
|
||||
}),
|
||||
)
|
||||
.replace("{USER_REQUEST}", inputs.userRequest)
|
||||
.replace(
|
||||
"{USER_REQUEST_PROMPT}",
|
||||
formatUserRequestPrompt(
|
||||
inputs.messages,
|
||||
SINGLE_USER_REQUEST_PROMPT,
|
||||
USER_SENDING_FOLLOWUP_PROMPT,
|
||||
),
|
||||
)
|
||||
.replace(
|
||||
"{CONVERSATION_HISTORY}",
|
||||
inputs.conversationHistoryToSummarize.map(getMessageString).join("\n"),
|
||||
|
|
@ -129,7 +148,6 @@ export async function summarizeHistory(
|
|||
): Promise<GraphUpdate> {
|
||||
const model = await loadModel(config, Task.SUMMARIZER);
|
||||
|
||||
const userRequest = getUserRequest(state.messages);
|
||||
const plan = getActivePlanItems(state.taskPlan);
|
||||
const conversationHistoryToSummarize = await getMessagesSinceLastSummary(
|
||||
state.internalMessages,
|
||||
|
|
@ -147,7 +165,7 @@ export async function summarizeHistory(
|
|||
{
|
||||
role: "user",
|
||||
content: formatPrompt({
|
||||
userRequest,
|
||||
messages: state.messages,
|
||||
plan,
|
||||
conversationHistoryToSummarize,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import {
|
|||
ReviewerGraphState,
|
||||
ReviewerGraphUpdate,
|
||||
} from "@open-swe/shared/open-swe/reviewer/types";
|
||||
import { getUserRequest } from "../../../utils/user-request.js";
|
||||
import { formatUserRequestPrompt } from "../../../utils/user-request.js";
|
||||
import { formatPlanPromptWithSummaries } from "../../../utils/plan-prompt.js";
|
||||
import {
|
||||
getActivePlanItems,
|
||||
|
|
@ -41,8 +41,7 @@ If you determine that the task has not been fully completed, you may call the \`
|
|||
Here is the full list of actions you took during your review:
|
||||
{REVIEW_ACTIONS}
|
||||
|
||||
Here is the user's original request:
|
||||
{USER_REQUEST}
|
||||
{USER_REQUEST_PROMPT}
|
||||
|
||||
And here are the tasks which were outlined in the plan, and completed by the Programmer Assistant:
|
||||
{PLANNED_TASKS}
|
||||
|
|
@ -60,14 +59,17 @@ const formatSystemPrompt = (state: ReviewerGraphState) => {
|
|||
const markCompletedToolName = createCodeReviewMarkTaskCompletedFields().name;
|
||||
const markNotCompleteToolName =
|
||||
createCodeReviewMarkTaskNotCompleteFields().name;
|
||||
const userRequest = getUserRequest(state.messages);
|
||||
const activePlan = getActivePlanItems(state.taskPlan);
|
||||
const tasksString = formatPlanPromptWithSummaries(activePlan);
|
||||
const messagesString = state.reviewerMessages
|
||||
.map(getMessageString)
|
||||
.join("\n");
|
||||
|
||||
return SYSTEM_PROMPT.replaceAll("{REVIEW_ACTIONS}", messagesString)
|
||||
.replaceAll("{USER_REQUEST}", userRequest)
|
||||
.replaceAll(
|
||||
"{USER_REQUEST_PROMPT}",
|
||||
formatUserRequestPrompt(state.messages),
|
||||
)
|
||||
.replaceAll("{PLANNED_TASKS}", tasksString)
|
||||
.replaceAll("{COMPLETE_TOOL_NAME}", markCompletedToolName)
|
||||
.replaceAll("{NOT_COMPLETE_TOOL_NAME}", markNotCompleteToolName);
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import {
|
|||
createInstallDependenciesTool,
|
||||
} from "../../../../tools/index.js";
|
||||
import { formatCustomRulesPrompt } from "../../../../utils/custom-rules.js";
|
||||
import { getUserRequest } from "../../../../utils/user-request.js";
|
||||
import { formatUserRequestPrompt } from "../../../../utils/user-request.js";
|
||||
import { getActivePlanItems } from "@open-swe/shared/open-swe/tasks";
|
||||
import { formatPlanPromptWithSummaries } from "../../../../utils/plan-prompt.js";
|
||||
import {
|
||||
|
|
@ -31,7 +31,6 @@ import { getMessageString } from "../../../../utils/message/content.js";
|
|||
const logger = createLogger(LogLevel.INFO, "GenerateReviewActionsNode");
|
||||
|
||||
function formatSystemPrompt(state: ReviewerGraphState): string {
|
||||
const userRequest = getUserRequest(state.messages);
|
||||
const activePlan = getActivePlanItems(state.taskPlan);
|
||||
const tasksString = formatPlanPromptWithSummaries(activePlan);
|
||||
const codeReview = getCodeReviewFields(state.internalMessages);
|
||||
|
|
@ -52,7 +51,10 @@ function formatSystemPrompt(state: ReviewerGraphState): string {
|
|||
"{DEPENDENCIES_INSTALLED}",
|
||||
state.dependenciesInstalled ? "Yes" : "No",
|
||||
)
|
||||
.replaceAll("{USER_REQUEST}", userRequest)
|
||||
.replaceAll(
|
||||
"{USER_REQUEST_PROMPT}",
|
||||
formatUserRequestPrompt(state.messages),
|
||||
)
|
||||
.replaceAll(
|
||||
"{PREVIOUS_REVIEW_PROMPT}",
|
||||
codeReview
|
||||
|
|
|
|||
|
|
@ -117,6 +117,5 @@ Generated via: \`git diff {BASE_BRANCH_NAME} --name-only\`
|
|||
{PREVIOUS_REVIEW_PROMPT}
|
||||
|
||||
<task_context>
|
||||
The user's request is as follows (it's also included in the conversation history below).
|
||||
{USER_REQUEST}
|
||||
{USER_REQUEST_PROMPT}
|
||||
</task_context>`;
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ export async function initializeSandbox(
|
|||
...base.data,
|
||||
status,
|
||||
...(error ? { error } : {}),
|
||||
runId: config.configurable?.run_id ?? "",
|
||||
},
|
||||
};
|
||||
events.push(event);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { GitHubIssue, GitHubIssueComment } from "./types.js";
|
|||
import { getIssue, getIssueComments } from "./api.js";
|
||||
import { GraphConfig, TargetRepository } from "@open-swe/shared/open-swe/types";
|
||||
import { getGitHubTokensFromConfig } from "../github-tokens.js";
|
||||
import { DETAILS_OPEN_TAG } from "./issue-task.js";
|
||||
|
||||
export function getUntrackedComments(
|
||||
existingMessages: BaseMessage[],
|
||||
|
|
@ -146,6 +147,19 @@ function extractContentFromIssueBody(body: string): string {
|
|||
);
|
||||
}
|
||||
|
||||
export function extractContentWithoutDetailsFromIssueBody(
|
||||
body: string,
|
||||
): string {
|
||||
if (!body.includes(DETAILS_OPEN_TAG)) {
|
||||
return extractContentFromIssueBody(body);
|
||||
}
|
||||
|
||||
const bodyWithoutDetails = extractContentFromIssueBody(
|
||||
body.split(DETAILS_OPEN_TAG)[0],
|
||||
);
|
||||
return bodyWithoutDetails;
|
||||
}
|
||||
|
||||
export function getMessageContentFromIssue(
|
||||
issue: GitHubIssue | GitHubIssueComment,
|
||||
): string {
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ export const TASK_CLOSE_TAG = "</open-swe-do-not-edit-task-plan>";
|
|||
export const PROPOSED_PLAN_OPEN_TAG = "<open-swe-do-not-edit-proposed-plan>";
|
||||
export const PROPOSED_PLAN_CLOSE_TAG = "</open-swe-do-not-edit-proposed-plan>";
|
||||
|
||||
const DETAILS_OPEN_TAG = "<details>";
|
||||
const DETAILS_CLOSE_TAG = "</details>";
|
||||
export const DETAILS_OPEN_TAG = "<details>";
|
||||
export const DETAILS_CLOSE_TAG = "</details>";
|
||||
const AGENT_CONTEXT_DETAILS_SUMMARY = "<summary>Agent Context</summary>";
|
||||
|
||||
function typeNarrowTaskPlan(taskPlan: unknown): taskPlan is TaskPlan {
|
||||
|
|
|
|||
|
|
@ -4,28 +4,95 @@ import {
|
|||
HumanMessage,
|
||||
} from "@langchain/core/messages";
|
||||
import { getMessageContentString } from "@open-swe/shared/messages";
|
||||
import { extractContentWithoutDetailsFromIssueBody } from "./github/issue-messages.js";
|
||||
|
||||
// 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?
|
||||
export function getUserRequest(
|
||||
export function getInitialUserRequest(
|
||||
messages: BaseMessage[],
|
||||
options?: { returnFullMessage?: never | false },
|
||||
): string;
|
||||
export function getUserRequest(
|
||||
export function getInitialUserRequest(
|
||||
messages: BaseMessage[],
|
||||
options?: { returnFullMessage?: true },
|
||||
): HumanMessage;
|
||||
export function getUserRequest(
|
||||
export function getInitialUserRequest(
|
||||
messages: BaseMessage[],
|
||||
options?: { returnFullMessage?: boolean },
|
||||
): string | HumanMessage {
|
||||
const initialMessage = messages.findLast(
|
||||
(m) => isHumanMessage(m) && m.additional_kwargs?.isOriginalIssue,
|
||||
);
|
||||
|
||||
if (!initialMessage) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const parsedContent = extractContentWithoutDetailsFromIssueBody(
|
||||
getMessageContentString(initialMessage.content),
|
||||
);
|
||||
return options?.returnFullMessage
|
||||
? new HumanMessage({
|
||||
...initialMessage,
|
||||
content: parsedContent,
|
||||
})
|
||||
: parsedContent;
|
||||
}
|
||||
|
||||
export function getRecentUserRequest(
|
||||
messages: BaseMessage[],
|
||||
options?: { returnFullMessage?: never | false },
|
||||
): string;
|
||||
export function getRecentUserRequest(
|
||||
messages: BaseMessage[],
|
||||
options?: { returnFullMessage?: true },
|
||||
): HumanMessage;
|
||||
export function getRecentUserRequest(
|
||||
messages: BaseMessage[],
|
||||
options?: { returnFullMessage?: boolean },
|
||||
): string | HumanMessage {
|
||||
const recentUserMessage = messages.findLast(
|
||||
(m) => isHumanMessage(m) && m.additional_kwargs?.isOriginalIssue,
|
||||
(m) => isHumanMessage(m) && m.additional_kwargs?.isFollowup,
|
||||
);
|
||||
|
||||
if (!recentUserMessage) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const parsedContent = extractContentWithoutDetailsFromIssueBody(
|
||||
getMessageContentString(recentUserMessage.content),
|
||||
);
|
||||
return options?.returnFullMessage
|
||||
? recentUserMessage
|
||||
: getMessageContentString(recentUserMessage.content);
|
||||
? new HumanMessage({
|
||||
...recentUserMessage,
|
||||
content: parsedContent,
|
||||
})
|
||||
: parsedContent;
|
||||
}
|
||||
|
||||
const DEFAULT_SINGLE_USER_REQUEST_PROMPT = `Here is the user's request:
|
||||
{USER_REQUEST}`;
|
||||
|
||||
const DEFAULT_USER_SENDING_FOLLOWUP_PROMPT = `Here is the user's initial request:
|
||||
{USER_REQUEST}
|
||||
|
||||
And here is the user's followup request you're now processing:
|
||||
{USER_FOLLOWUP_REQUEST}`;
|
||||
|
||||
export function formatUserRequestPrompt(
|
||||
messages: BaseMessage[],
|
||||
singleRequestPrompt: string = DEFAULT_SINGLE_USER_REQUEST_PROMPT,
|
||||
followupRequestPrompt: string = DEFAULT_USER_SENDING_FOLLOWUP_PROMPT,
|
||||
): string {
|
||||
const noRequestMessage = "No user request provided.";
|
||||
const userRequest = getInitialUserRequest(messages) || noRequestMessage;
|
||||
const userFollowupRequest = getRecentUserRequest(messages);
|
||||
|
||||
if (userFollowupRequest) {
|
||||
return singleRequestPrompt
|
||||
.replace("{USER_REQUEST}", userRequest)
|
||||
.replace("{USER_FOLLOWUP_REQUEST}", userFollowupRequest);
|
||||
}
|
||||
|
||||
return followupRequestPrompt.replace("{USER_REQUEST}", userRequest);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,37 +7,22 @@ import {
|
|||
GitBranch,
|
||||
ChevronDown,
|
||||
} from "lucide-react";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Step } from "@open-swe/shared/open-swe/custom-node-events";
|
||||
import { Button } from "../ui/button";
|
||||
|
||||
type InitializeStepProps = {
|
||||
status: "loading" | "generating" | "done";
|
||||
success?: boolean;
|
||||
steps?: Step[];
|
||||
collapse?: boolean;
|
||||
};
|
||||
|
||||
export function InitializeStep({
|
||||
status,
|
||||
success,
|
||||
steps,
|
||||
collapse: collapseProp,
|
||||
}: InitializeStepProps) {
|
||||
const [collapsed, setCollapsed] = useState(collapseProp ?? false);
|
||||
const wasDone = useRef(false);
|
||||
|
||||
// Auto-collapse when status is 'done' and success is true
|
||||
useEffect(() => {
|
||||
if (status === "done" && success && !collapsed && !wasDone.current) {
|
||||
setCollapsed(true);
|
||||
wasDone.current = true;
|
||||
}
|
||||
if (status !== "done") {
|
||||
wasDone.current = false;
|
||||
}
|
||||
}, [status, success, collapsed]);
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
|
||||
const stepStatusIcon = {
|
||||
waiting: (
|
||||
|
|
@ -97,11 +82,10 @@ export function InitializeStep({
|
|||
{getStatusText()}
|
||||
</span>
|
||||
{getStatusIcon()}
|
||||
<Button
|
||||
<button
|
||||
aria-label={collapsed ? "Expand" : "Collapse"}
|
||||
onClick={() => setCollapsed((c) => !c)}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-muted-foreground hover:text-foreground ml-2 cursor-pointer"
|
||||
>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
|
|
@ -109,7 +93,7 @@ export function InitializeStep({
|
|||
collapsed ? "rotate-0" : "rotate-180",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
{/* Only render the rest if not collapsed */}
|
||||
{!collapsed && steps && steps.length > 0 && (
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
import { isAIMessageSDK, isHumanMessageSDK } from "@/lib/langchain-messages";
|
||||
import { UseStream, useStream } from "@langchain/langgraph-sdk/react";
|
||||
import { AssistantMessage } from "../thread/messages/ai";
|
||||
import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { ManagerGraphState } from "@open-swe/shared/open-swe/manager/types";
|
||||
import { useCancelStream } from "@/hooks/useCancelStream";
|
||||
import {
|
||||
|
|
@ -130,12 +137,20 @@ export function ActionsRenderer<State extends PlannerGraphState | GraphState>({
|
|||
streamName: graphId === "planner" ? "Planner" : "Programmer",
|
||||
});
|
||||
|
||||
const initializeEvents = customNodeEvents.filter(
|
||||
(e) => e.nodeId === INITIALIZE_NODE_ID,
|
||||
const initializeEvents = useMemo(
|
||||
() =>
|
||||
customNodeEvents.filter(
|
||||
(e) => e.nodeId === INITIALIZE_NODE_ID && e.data.runId === runId,
|
||||
),
|
||||
[customNodeEvents, runId],
|
||||
);
|
||||
|
||||
const acceptedPlanEvents = customNodeEvents.filter(
|
||||
(e) => e.nodeId === ACCEPTED_PLAN_NODE_ID,
|
||||
const acceptedPlanEvents = useMemo(
|
||||
() =>
|
||||
customNodeEvents.filter(
|
||||
(e) => e.nodeId === ACCEPTED_PLAN_NODE_ID && e.data.runId === runId,
|
||||
),
|
||||
[customNodeEvents, runId],
|
||||
);
|
||||
|
||||
const steps = mapCustomEventsToSteps(initializeEvents);
|
||||
|
|
@ -243,10 +258,6 @@ export function ActionsRenderer<State extends PlannerGraphState | GraphState>({
|
|||
}
|
||||
}, [stream.values, graphId]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(stream.messages);
|
||||
}, [stream.messages]);
|
||||
|
||||
if (streamLoading) {
|
||||
return <LoadingActionsCardContent />;
|
||||
}
|
||||
|
|
@ -258,7 +269,6 @@ export function ActionsRenderer<State extends PlannerGraphState | GraphState>({
|
|||
status={initStatus}
|
||||
steps={steps}
|
||||
success={allSuccess}
|
||||
collapse={initStatus === "done" && allSuccess}
|
||||
/>
|
||||
)}
|
||||
{filteredMessages?.map((m) => (
|
||||
|
|
@ -274,9 +284,16 @@ export function ActionsRenderer<State extends PlannerGraphState | GraphState>({
|
|||
{acceptedPlanEvents.length > 0 &&
|
||||
isAcceptedPlanEvents(acceptedPlanEvents) && (
|
||||
<AcceptedPlanStep
|
||||
planTitle={acceptedPlanEvents[0].data.planTitle}
|
||||
planItems={acceptedPlanEvents[0].data.planItems}
|
||||
interruptType={acceptedPlanEvents[0].data.interruptType}
|
||||
planTitle={
|
||||
acceptedPlanEvents[acceptedPlanEvents.length - 1].data.planTitle
|
||||
}
|
||||
planItems={
|
||||
acceptedPlanEvents[acceptedPlanEvents.length - 1].data.planItems
|
||||
}
|
||||
interruptType={
|
||||
acceptedPlanEvents[acceptedPlanEvents.length - 1].data
|
||||
.interruptType
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{/* If the last message is hidden, but there's an interrupt, we must manually render the interrupt */}
|
||||
|
|
|
|||
|
|
@ -48,11 +48,27 @@ export function ThreadView({
|
|||
const [selectedTab, setSelectedTab] = useState<"planner" | "programmer">(
|
||||
"planner",
|
||||
);
|
||||
const plannerThreadId = stream.values?.plannerSession?.threadId;
|
||||
const plannerRunId = stream.values?.plannerSession?.runId;
|
||||
const [plannerSession, setPlannerSession] =
|
||||
useState<ManagerGraphState["plannerSession"]>();
|
||||
const [programmerSession, setProgrammerSession] =
|
||||
useState<ManagerGraphState["programmerSession"]>();
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
stream?.values?.plannerSession &&
|
||||
plannerSession?.runId !== stream.values.plannerSession.runId
|
||||
) {
|
||||
// State shouldn't update before we use it below, but still create a copy to avoid race conditions
|
||||
const prevPlannerSession = plannerSession;
|
||||
setPlannerSession(stream.values.plannerSession);
|
||||
|
||||
if (prevPlannerSession && selectedTab === "programmer") {
|
||||
// If we already has a planner session, and the user is currently on the programmer tab, bring them back to the planner tab
|
||||
setSelectedTab("planner");
|
||||
}
|
||||
}
|
||||
}, [stream?.values]);
|
||||
|
||||
const { status: realTimeStatus } = useThreadStatus(displayThread.id);
|
||||
|
||||
const getStatusDotColor = (status: string) => {
|
||||
|
|
@ -185,8 +201,8 @@ export function ThreadView({
|
|||
plannerCancelRef.current && (
|
||||
<CancelStreamButton
|
||||
stream={stream}
|
||||
threadId={plannerThreadId}
|
||||
runId={plannerRunId}
|
||||
threadId={plannerSession?.threadId}
|
||||
runId={plannerSession?.runId}
|
||||
streamName="Planner"
|
||||
/>
|
||||
)}
|
||||
|
|
@ -195,8 +211,8 @@ export function ThreadView({
|
|||
programmerCancelRef.current && (
|
||||
<CancelStreamButton
|
||||
stream={stream}
|
||||
threadId={plannerThreadId}
|
||||
runId={plannerRunId}
|
||||
threadId={programmerSession?.threadId}
|
||||
runId={programmerSession?.runId}
|
||||
streamName="Programmer"
|
||||
/>
|
||||
)}
|
||||
|
|
@ -206,11 +222,11 @@ export function ThreadView({
|
|||
<TabsContent value="planner">
|
||||
<Card className="border-border bg-card px-0 py-4 dark:bg-gray-950">
|
||||
<CardContent className="space-y-2 p-3 pt-0">
|
||||
{plannerThreadId && plannerRunId && (
|
||||
{plannerSession && (
|
||||
<ActionsRenderer<PlannerGraphState>
|
||||
graphId={PLANNER_GRAPH_ID}
|
||||
threadId={plannerThreadId}
|
||||
runId={plannerRunId}
|
||||
threadId={plannerSession.threadId}
|
||||
runId={plannerSession.runId}
|
||||
setProgrammerSession={setProgrammerSession}
|
||||
programmerSession={programmerSession}
|
||||
setSelectedTab={setSelectedTab}
|
||||
|
|
@ -223,7 +239,7 @@ export function ThreadView({
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
{!(plannerThreadId && plannerRunId) && (
|
||||
{!plannerSession && (
|
||||
<div className="flex items-center justify-center gap-2 py-8">
|
||||
<Clock className="text-muted-foreground size-4" />
|
||||
<span className="text-muted-foreground text-sm">
|
||||
|
|
|
|||
|
|
@ -364,6 +364,14 @@ export const GraphConfigurationMetadata: {
|
|||
"The maximum number of tokens to generate in an individual LLM call. Increasing/decreasing this number _will_ effect how many tokens are generated by the model. It will _not_ simply cut off the generation after the specified number of tokens is reached.",
|
||||
},
|
||||
},
|
||||
mcpServers: {
|
||||
x_open_swe_ui_config: {
|
||||
type: "json",
|
||||
default: JSON.stringify(DEFAULT_MCP_SERVERS, null, 2),
|
||||
description:
|
||||
"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).",
|
||||
},
|
||||
},
|
||||
[GITHUB_TOKEN_COOKIE]: {
|
||||
x_open_swe_ui_config: {
|
||||
type: "hidden",
|
||||
|
|
@ -394,17 +402,31 @@ export const GraphConfigurationMetadata: {
|
|||
type: "hidden",
|
||||
},
|
||||
},
|
||||
mcpServers: {
|
||||
thread_id: {
|
||||
x_open_swe_ui_config: {
|
||||
type: "json",
|
||||
default: JSON.stringify(DEFAULT_MCP_SERVERS, null, 2),
|
||||
description:
|
||||
"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).",
|
||||
type: "hidden",
|
||||
},
|
||||
},
|
||||
run_id: {
|
||||
x_open_swe_ui_config: {
|
||||
type: "hidden",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const GraphConfiguration = z.object({
|
||||
/**
|
||||
* The thread ID, generated by the manager graph.
|
||||
*/
|
||||
thread_id: withLangGraph(z.string().optional(), {
|
||||
metadata: GraphConfigurationMetadata.thread_id,
|
||||
}),
|
||||
/**
|
||||
* The run ID, generated by the manager graph.
|
||||
*/
|
||||
run_id: withLangGraph(z.string().optional(), {
|
||||
metadata: GraphConfigurationMetadata.run_id,
|
||||
}),
|
||||
/**
|
||||
* The maximum number of context gathering actions to take during planning.
|
||||
* Each action consists of 2 messages (request & result), plus 1 human message.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue