From 3d43abd4a0dc4ad05af526020d72179e1e188f42 Mon Sep 17 00:00:00 2001 From: Brace Sproul Date: Thu, 19 Jun 2025 10:56:06 -0700 Subject: [PATCH] feat: Allow manager to support followups (#248) * feat: Allow manager to support followups * cr * cr --- .../planner/nodes/generate-message/index.ts | 12 ++++++-- .../src/graphs/planner/utils/followup.ts | 2 +- .../programmer/nodes/summarize-task-steps.ts | 2 -- .../src/components/v2/actions-renderer.tsx | 28 ++++++++----------- apps/web/src/components/v2/thread-view.tsx | 18 +++++++++++- 5 files changed, 40 insertions(+), 22 deletions(-) diff --git a/apps/open-swe/src/graphs/planner/nodes/generate-message/index.ts b/apps/open-swe/src/graphs/planner/nodes/generate-message/index.ts index e44863c5..9fda477f 100644 --- a/apps/open-swe/src/graphs/planner/nodes/generate-message/index.ts +++ b/apps/open-swe/src/graphs/planner/nodes/generate-message/index.ts @@ -14,6 +14,7 @@ import { import { SYSTEM_PROMPT } from "./prompt.js"; import { getRepoAbsolutePath } from "@open-swe/shared/git"; import { getMissingMessages } from "../../../../utils/github/issue-messages.js"; +import { getTaskPlanFromIssue } from "../../../../utils/github/issue-task.js"; const logger = createLogger(LogLevel.INFO, "GeneratePlanningMessageNode"); @@ -47,13 +48,19 @@ export async function generateAction( parallel_tool_calls: false, }); - const missingMessages = await getMissingMessages(state, config); + const [missingMessages, latestTaskPlan] = await Promise.all([ + getMissingMessages(state, config), + getTaskPlanFromIssue(state, config), + ]); const response = await modelWithTools .withConfig({ tags: ["nostream"] }) .invoke([ { role: "system", - content: formatSystemPrompt(state), + content: formatSystemPrompt({ + ...state, + taskPlan: latestTaskPlan ?? state.taskPlan, + }), }, ...state.messages, ...missingMessages, @@ -71,5 +78,6 @@ export async function generateAction( return { messages: [...missingMessages, response], + ...(latestTaskPlan && { taskPlan: latestTaskPlan }), }; } diff --git a/apps/open-swe/src/graphs/planner/utils/followup.ts b/apps/open-swe/src/graphs/planner/utils/followup.ts index 23b400e1..cec5937d 100644 --- a/apps/open-swe/src/graphs/planner/utils/followup.ts +++ b/apps/open-swe/src/graphs/planner/utils/followup.ts @@ -1,7 +1,7 @@ import { getActivePlanItems } from "@open-swe/shared/open-swe/tasks"; import { TaskPlan } from "@open-swe/shared/open-swe/types"; -const previousCompletedPlanPrompt = `Here is the complete list of previous requests made by the user, along with the tasks you generated to complete these requests, and the task summaries of each task you completed previously: +const previousCompletedPlanPrompt = `Here is the list of tasks from the previous session. You've already completed all of these tasks. Use the tasks, and task summaries as context when generating a new plan: {PREVIOUS_PLAN}`; const previousProposedPlanPrompt = `Here is the complete list of the proposed plan you generated before the user sent their followup request: diff --git a/apps/open-swe/src/graphs/programmer/nodes/summarize-task-steps.ts b/apps/open-swe/src/graphs/programmer/nodes/summarize-task-steps.ts index 9c9b09fc..3e54e21d 100644 --- a/apps/open-swe/src/graphs/programmer/nodes/summarize-task-steps.ts +++ b/apps/open-swe/src/graphs/programmer/nodes/summarize-task-steps.ts @@ -165,7 +165,6 @@ export async function summarizeTaskSteps( const allTasksCompleted = activePlanItems.every((p) => p.completed); if (allTasksCompleted) { const commandUpdate: GraphUpdate = { - messages: [condensedTaskMessage], internalMessages: newMessagesStateUpdate, taskPlan: updatedTaskPlan, }; @@ -176,7 +175,6 @@ export async function summarizeTaskSteps( } const commandUpdate: GraphUpdate = { - messages: [condensedTaskMessage], internalMessages: newMessagesStateUpdate, taskPlan: updatedTaskPlan, }; diff --git a/apps/web/src/components/v2/actions-renderer.tsx b/apps/web/src/components/v2/actions-renderer.tsx index 0ebd2a68..dbc402d5 100644 --- a/apps/web/src/components/v2/actions-renderer.tsx +++ b/apps/web/src/components/v2/actions-renderer.tsx @@ -1,8 +1,10 @@ import { isHumanMessageSDK } from "@/lib/langchain-messages"; import { UseStream, useStream } from "@langchain/langgraph-sdk/react"; import { AssistantMessage } from "../thread/messages/ai"; -import { useEffect, useRef } from "react"; +import { Dispatch, SetStateAction, useEffect, useRef } from "react"; import { ManagerGraphState } from "@open-swe/shared/open-swe/manager/types"; +import { PlannerGraphState } from "@open-swe/shared/open-swe/planner/types"; +import { GraphState } from "@open-swe/shared/open-swe/types"; interface ActionsRendererProps { graphId: string; @@ -12,14 +14,16 @@ interface ActionsRendererProps { session: ManagerGraphState["programmerSession"], ) => void; programmerSession?: ManagerGraphState["programmerSession"]; + setSelectedTab?: Dispatch>; } -export function ActionsRenderer>({ +export function ActionsRenderer({ graphId, threadId, runId, setProgrammerSession, programmerSession, + setSelectedTab, }: ActionsRendererProps) { const stream = useStream({ apiUrl: process.env.NEXT_PUBLIC_API_URL, @@ -43,22 +47,14 @@ export function ActionsRenderer>({ // TODO: Need a better way to handle this. Not great like this... useEffect(() => { if ( - stream.values?.programmerSession && - typeof stream.values.programmerSession === "object" && + "programmerSession" in stream.values && stream.values.programmerSession && - ( - stream.values - .programmerSession as ManagerGraphState["programmerSession"] - )?.runId && - ( - stream.values - .programmerSession as ManagerGraphState["programmerSession"] - )?.threadId && - !programmerSession + (stream.values.programmerSession.runId !== programmerSession?.runId || + stream.values.programmerSession.threadId !== + programmerSession?.threadId) ) { - const programmerSession = stream.values - .programmerSession as ManagerGraphState["programmerSession"]; - setProgrammerSession?.(programmerSession); + setProgrammerSession?.(stream.values.programmerSession); + setSelectedTab?.("programmer"); } }, [stream.values]); diff --git a/apps/web/src/components/v2/thread-view.tsx b/apps/web/src/components/v2/thread-view.tsx index ad2edbd0..1a001b35 100644 --- a/apps/web/src/components/v2/thread-view.tsx +++ b/apps/web/src/components/v2/thread-view.tsx @@ -1,5 +1,6 @@ "use client"; +import { v4 as uuidv4 } from "uuid"; import { useState } from "react"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; @@ -14,6 +15,7 @@ import { ManagerGraphState } from "@open-swe/shared/open-swe/manager/types"; import { PlannerGraphState } from "@open-swe/shared/open-swe/planner/types"; import { ActionsRenderer } from "./actions-renderer"; import { ThemeToggle } from "../theme-toggle"; +import { HumanMessage } from "@langchain/core/messages"; const PROGRAMMER_ASSISTANT_ID = process.env.NEXT_PUBLIC_PROGRAMMER_ASSISTANT_ID; const PLANNER_ASSISTANT_ID = process.env.NEXT_PUBLIC_PLANNER_ASSISTANT_ID; @@ -32,6 +34,9 @@ export function ThreadView({ onBackToHome, }: ThreadViewProps) { const [chatInput, setChatInput] = useState(""); + const [selectedTab, setSelectedTab] = useState<"planner" | "programmer">( + "planner", + ); const plannerThreadId = stream.values?.plannerSession?.threadId; const plannerRunId = stream.values?.plannerSession?.runId; const [programmerSession, setProgrammerSession] = @@ -43,7 +48,13 @@ export function ThreadView({ const handleSendMessage = () => { if (chatInput.trim()) { - alert("SENDING MANAGER FOLLOWUPS NOT HOOKED UP YET"); + const newHumanMessage = new HumanMessage({ + id: uuidv4(), + content: chatInput, + }); + stream.submit({ + messages: [newHumanMessage], + }); setChatInput(""); } }; @@ -160,6 +171,10 @@ export function ThreadView({ + setSelectedTab(value as "planner" | "programmer") + } > Planner @@ -177,6 +192,7 @@ export function ThreadView({ runId={plannerRunId} setProgrammerSession={setProgrammerSession} programmerSession={programmerSession} + setSelectedTab={setSelectedTab} /> )}