From 942bc5ca1d9beb6f347477995bb2780720f85cb0 Mon Sep 17 00:00:00 2001 From: Dylan Boudro <121908331+starmorph@users.noreply.github.com> Date: Wed, 2 Jul 2025 10:20:45 -0700 Subject: [PATCH] Feat: Diagnose error action component (#325) * init diagnose-error component * rm old diagnose error genui v1 component * diagnose-error schema to shared/tools * CR * Update apps/web/src/components/thread/messages/ai.tsx --------- Co-authored-by: Brace Sproul --- .../graphs/programmer/nodes/diagnose-error.ts | 13 +- .../src/components/gen-ui/diagnose-error.tsx | 122 ------------------ apps/web/src/components/gen-ui/index.tsx | 2 - .../web/src/components/thread/messages/ai.tsx | 29 ++++- .../components/v2/diagnose-error-action.tsx | 89 +++++++++++++ apps/web/src/components/v2/thread-view.tsx | 1 - packages/shared/src/open-swe/tools.ts | 12 ++ 7 files changed, 132 insertions(+), 136 deletions(-) delete mode 100644 apps/web/src/components/gen-ui/diagnose-error.tsx create mode 100644 apps/web/src/components/v2/diagnose-error-action.tsx diff --git a/apps/open-swe/src/graphs/programmer/nodes/diagnose-error.ts b/apps/open-swe/src/graphs/programmer/nodes/diagnose-error.ts index ab59e0c0..bf3cfeb8 100644 --- a/apps/open-swe/src/graphs/programmer/nodes/diagnose-error.ts +++ b/apps/open-swe/src/graphs/programmer/nodes/diagnose-error.ts @@ -9,6 +9,7 @@ import { GraphUpdate, PlanItem, } from "@open-swe/shared/open-swe/types"; +import { createDiagnoseErrorToolFields } from "@open-swe/shared/open-swe/tools"; import { formatPlanPromptWithSummaries } from "../../../utils/plan-prompt.js"; import { getMessageString } from "../../../utils/message/content.js"; import { getMessageContentString } from "@open-swe/shared/messages"; @@ -56,15 +57,7 @@ const userPrompt = `Here is the full conversation history from the steps taken t Please carefully go over all of this information, and provide a helpful explanation of exactly what the issue is, and how you can fix it. When you are ready to provide your diagnosis, call the \`diagnose_error\` tool.`; -const diagnoseErrorToolSchema = z.object({ - diagnosis: z.string().describe("The diagnosis of the error."), -}); - -const diagnoseErrorTool = { - name: "diagnose_error", - description: "Diagnoses an error given a diagnosis.", - schema: diagnoseErrorToolSchema, -}; +const diagnoseErrorTool = createDiagnoseErrorToolFields(); const formatSystemPrompt = ( lastFailedActionContent: string, @@ -138,7 +131,7 @@ export async function diagnoseError( } logger.info("Diagnosed error successfully.", { - diagnosis: (toolCall.args as z.infer) + diagnosis: (toolCall.args as z.infer) .diagnosis, }); diff --git a/apps/web/src/components/gen-ui/diagnose-error.tsx b/apps/web/src/components/gen-ui/diagnose-error.tsx deleted file mode 100644 index 7013f3cf..00000000 --- a/apps/web/src/components/gen-ui/diagnose-error.tsx +++ /dev/null @@ -1,122 +0,0 @@ -"use client"; - -import "../app/globals.css"; -import { - AlertTriangle, - Loader2, - CheckCircle, - MessageSquare, - FileText, -} from "lucide-react"; -import { useState } from "react"; - -type DiagnoseErrorProps = { - status: "loading" | "generating" | "done"; - diagnosis?: string; - recommendation?: string; - reasoningText?: string; - summaryText?: string; -}; - -export function DiagnoseError({ - status, - diagnosis, - recommendation, - reasoningText, - summaryText, -}: DiagnoseErrorProps) { - const [showReasoning, setShowReasoning] = useState(false); - const [showSummary, setShowSummary] = useState(false); - - const getStatusIcon = () => { - switch (status) { - case "loading": - return ( -
- ); - case "generating": - return ; - case "done": - return ; - } - }; - - const getStatusText = () => { - switch (status) { - case "loading": - return "Preparing error analysis..."; - case "generating": - return "Diagnosing errors..."; - case "done": - return "Error diagnosis complete"; - } - }; - - return ( -
- {reasoningText && ( -
- - {showReasoning && ( -

- {reasoningText} -

- )} -
- )} - -
- - - {getStatusText()} - - {getStatusIcon()} -
- - {status === "done" && diagnosis && ( -
-
-

- Diagnosis -

-

{diagnosis}

-
- - {recommendation && ( -
-

- Recommendation -

-

- {recommendation} -

-
- )} -
- )} - - {summaryText && status === "done" && ( -
- - {showSummary && ( -

- {summaryText} -

- )} -
- )} -
- ); -} diff --git a/apps/web/src/components/gen-ui/index.tsx b/apps/web/src/components/gen-ui/index.tsx index ebf0d372..31eff43c 100644 --- a/apps/web/src/components/gen-ui/index.tsx +++ b/apps/web/src/components/gen-ui/index.tsx @@ -1,5 +1,4 @@ import { ActionStep } from "./action-step"; -import { DiagnoseError } from "./diagnose-error"; import { InitializeStep } from "./initialize-step"; import { PushChanges } from "./push-changes"; import { ReplanningStep } from "./replanning-step"; @@ -7,7 +6,6 @@ import { TaskSummary } from "./task-summary"; export default { "action-step": ActionStep, - "diagnose-error": DiagnoseError, "initialize-step": InitializeStep, "push-changes": PushChanges, "replanning-step": ReplanningStep, diff --git a/apps/web/src/components/thread/messages/ai.tsx b/apps/web/src/components/thread/messages/ai.tsx index 70805d88..427baee3 100644 --- a/apps/web/src/components/thread/messages/ai.tsx +++ b/apps/web/src/components/thread/messages/ai.tsx @@ -21,6 +21,7 @@ import { Interrupt } from "./interrupt"; import { ActionStep, ActionItemProps } from "@/components/gen-ui/action-step"; import { TaskSummary } from "@/components/gen-ui/task-summary"; import { PullRequestOpened } from "@/components/gen-ui/pull-request-opened"; +import { DiagnoseErrorAction } from "@/components/v2/diagnose-error-action"; import { ToolCall } from "@langchain/core/messages/tool"; import { createApplyPatchToolFields, @@ -30,6 +31,7 @@ import { createOpenPrToolFields, createInstallDependenciesToolFields, createTakePlannerNotesFields, + createDiagnoseErrorToolFields, } from "@open-swe/shared/open-swe/tools"; import { z } from "zod"; import { isAIMessageSDK, isToolMessageSDK } from "@/lib/langchain-messages"; @@ -54,6 +56,9 @@ type InstallDependenciesToolArgs = z.infer< const plannerNotesTool = createTakePlannerNotesFields(); type PlannerNotesToolArgs = z.infer; +const diagnoseErrorTool = createDiagnoseErrorToolFields(); +type DiagnoseErrorToolArgs = z.infer; + function CustomComponent({ message, thread, @@ -256,6 +261,10 @@ export function AssistantMessage({ ? aiToolCalls.find((tc) => tc.name === openPrTool.name) : undefined; + const diagnoseErrorToolCall = message + ? aiToolCalls.find((tc) => tc.name === diagnoseErrorTool.name) + : undefined; + // We can be sure that if the task status tool call is present, it will be the // only tool call/result we need to render for this message. if (taskStatusToolCall) { @@ -278,7 +287,25 @@ export function AssistantMessage({ ); } - // Same for PR tool. If this is present, it's the only tool call we need to render. + if (diagnoseErrorToolCall) { + const correspondingToolResult = toolResults.find( + (tr) => tr && tr.tool_call_id === diagnoseErrorToolCall.id, + ); + + const args = diagnoseErrorToolCall.args as DiagnoseErrorToolArgs; + const reasoningText = getContentString(content); + + return ( +
+ +
+ ); + } + if (openPrToolCall) { let branch: string | undefined; let targetBranch: string | undefined = "main"; diff --git a/apps/web/src/components/v2/diagnose-error-action.tsx b/apps/web/src/components/v2/diagnose-error-action.tsx new file mode 100644 index 00000000..0319d4fe --- /dev/null +++ b/apps/web/src/components/v2/diagnose-error-action.tsx @@ -0,0 +1,89 @@ +"use client"; + +import { useState } from "react"; +import { + AlertTriangle, + Loader2, + CheckCircle, + MessageSquare, +} from "lucide-react"; + +type DiagnoseErrorActionProps = { + status: "loading" | "generating" | "done"; + diagnosis?: string; + reasoningText?: string; +}; + +export function DiagnoseErrorAction({ + status, + diagnosis, + reasoningText, +}: DiagnoseErrorActionProps) { + const [showReasoning, setShowReasoning] = useState(false); + + const getStatusIcon = () => { + switch (status) { + case "loading": + return
; + case "generating": + return ( + + ); + case "done": + return ; + } + }; + + const getStatusText = () => { + switch (status) { + case "loading": + return "Preparing error analysis..."; + case "generating": + return "Diagnosing errors..."; + case "done": + return "Error diagnosis complete"; + } + }; + + return ( +
+ {reasoningText && ( +
+ + {showReasoning && ( +

+ {reasoningText} +

+ )} +
+ )} + +
+ + + {getStatusText()} + + {getStatusIcon()} +
+ + {status === "done" && diagnosis && ( +
+
+

+ Diagnosis +

+

+ {diagnosis} +

+
+
+ )} +
+ ); +} diff --git a/apps/web/src/components/v2/thread-view.tsx b/apps/web/src/components/v2/thread-view.tsx index daff9e5d..ad896e4a 100644 --- a/apps/web/src/components/v2/thread-view.tsx +++ b/apps/web/src/components/v2/thread-view.tsx @@ -127,7 +127,6 @@ export function ThreadView({ setChatInput={setChatInput} handleSendMessage={handleSendMessage} /> - {/* Right Side - Actions & Plan */}
diff --git a/packages/shared/src/open-swe/tools.ts b/packages/shared/src/open-swe/tools.ts index 315c53fd..508cb14d 100644 --- a/packages/shared/src/open-swe/tools.ts +++ b/packages/shared/src/open-swe/tools.ts @@ -269,3 +269,15 @@ export function createTakePlannerNotesFields() { "Do not duplicate any information present in the user provided 'custom rules', as we want to avoid duplicating context.", }; } + +export function createDiagnoseErrorToolFields() { + const diagnoseErrorToolSchema = z.object({ + diagnosis: z.string().describe("The diagnosis of the error."), + }); + + return { + name: "diagnose_error", + description: "Diagnoses an error given a diagnosis.", + schema: diagnoseErrorToolSchema, + }; +}