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 <braceasproul@gmail.com>
This commit is contained in:
Dylan Boudro 2025-07-02 10:20:45 -07:00 • committed by GitHub
parent 4e52be54f8
commit 942bc5ca1d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 132 additions and 136 deletions

View file

@ -9,6 +9,7 @@ import {
GraphUpdate, GraphUpdate,
PlanItem, PlanItem,
} from "@open-swe/shared/open-swe/types"; } from "@open-swe/shared/open-swe/types";
import { createDiagnoseErrorToolFields } from "@open-swe/shared/open-swe/tools";
import { formatPlanPromptWithSummaries } from "../../../utils/plan-prompt.js"; import { formatPlanPromptWithSummaries } from "../../../utils/plan-prompt.js";
import { getMessageString } from "../../../utils/message/content.js"; import { getMessageString } from "../../../utils/message/content.js";
import { getMessageContentString } from "@open-swe/shared/messages"; 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.`; 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({ const diagnoseErrorTool = createDiagnoseErrorToolFields();
diagnosis: z.string().describe("The diagnosis of the error."),
});
const diagnoseErrorTool = {
name: "diagnose_error",
description: "Diagnoses an error given a diagnosis.",
schema: diagnoseErrorToolSchema,
};
const formatSystemPrompt = ( const formatSystemPrompt = (
lastFailedActionContent: string, lastFailedActionContent: string,
@ -138,7 +131,7 @@ export async function diagnoseError(
} }
logger.info("Diagnosed error successfully.", { logger.info("Diagnosed error successfully.", {
diagnosis: (toolCall.args as z.infer<typeof diagnoseErrorToolSchema>) diagnosis: (toolCall.args as z.infer<typeof diagnoseErrorTool.schema>)
.diagnosis, .diagnosis,
}); });

View file

@ -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 (
<div className="h-3.5 w-3.5 rounded-full border border-gray-300" />
);
case "generating":
return <Loader2 className="h-3.5 w-3.5 animate-spin text-gray-500" />;
case "done":
return <CheckCircle className="h-3.5 w-3.5 text-green-500" />;
}
};
const getStatusText = () => {
switch (status) {
case "loading":
return "Preparing error analysis...";
case "generating":
return "Diagnosing errors...";
case "done":
return "Error diagnosis complete";
}
};
return (
<div className="overflow-hidden rounded-md border border-gray-200">
{reasoningText && (
<div className="border-b border-blue-100 bg-blue-50 p-2">
<button
onClick={() => setShowReasoning(!showReasoning)}
className="flex items-center gap-1 text-xs font-normal text-blue-700 hover:text-blue-800"
>
<MessageSquare className="h-3 w-3" />
{showReasoning ? "Hide reasoning" : "Show reasoning"}
</button>
{showReasoning && (
<p className="mt-1 text-xs font-normal text-blue-800">
{reasoningText}
</p>
)}
</div>
)}
<div className="flex items-center border-b border-gray-200 bg-gray-50 p-2">
<AlertTriangle className="mr-2 h-3.5 w-3.5 text-amber-500" />
<span className="flex-1 text-xs font-normal text-gray-800">
{getStatusText()}
</span>
{getStatusIcon()}
</div>
{status === "done" && diagnosis && (
<div className="p-2">
<div className="mb-2">
<h3 className="mb-1 text-xs font-normal text-gray-500">
Diagnosis
</h3>
<p className="text-xs font-normal text-gray-800">{diagnosis}</p>
</div>
{recommendation && (
<div>
<h3 className="mb-1 text-xs font-normal text-gray-500">
Recommendation
</h3>
<p className="text-xs font-normal text-gray-800">
{recommendation}
</p>
</div>
)}
</div>
)}
{summaryText && status === "done" && (
<div className="border-t border-green-100 bg-green-50 p-2">
<button
onClick={() => setShowSummary(!showSummary)}
className="flex items-center gap-1 text-xs font-normal text-green-700 hover:text-green-800"
>
<FileText className="h-3 w-3" />
{showSummary ? "Hide summary" : "Show summary"}
</button>
{showSummary && (
<p className="mt-1 text-xs font-normal text-green-800">
{summaryText}
</p>
)}
</div>
)}
</div>
);
}

View file

@ -1,5 +1,4 @@
import { ActionStep } from "./action-step"; import { ActionStep } from "./action-step";
import { DiagnoseError } from "./diagnose-error";
import { InitializeStep } from "./initialize-step"; import { InitializeStep } from "./initialize-step";
import { PushChanges } from "./push-changes"; import { PushChanges } from "./push-changes";
import { ReplanningStep } from "./replanning-step"; import { ReplanningStep } from "./replanning-step";
@ -7,7 +6,6 @@ import { TaskSummary } from "./task-summary";
export default { export default {
"action-step": ActionStep, "action-step": ActionStep,
"diagnose-error": DiagnoseError,
"initialize-step": InitializeStep, "initialize-step": InitializeStep,
"push-changes": PushChanges, "push-changes": PushChanges,
"replanning-step": ReplanningStep, "replanning-step": ReplanningStep,

View file

@ -21,6 +21,7 @@ import { Interrupt } from "./interrupt";
import { ActionStep, ActionItemProps } from "@/components/gen-ui/action-step"; import { ActionStep, ActionItemProps } from "@/components/gen-ui/action-step";
import { TaskSummary } from "@/components/gen-ui/task-summary"; import { TaskSummary } from "@/components/gen-ui/task-summary";
import { PullRequestOpened } from "@/components/gen-ui/pull-request-opened"; 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 { ToolCall } from "@langchain/core/messages/tool";
import { import {
createApplyPatchToolFields, createApplyPatchToolFields,
@ -30,6 +31,7 @@ import {
createOpenPrToolFields, createOpenPrToolFields,
createInstallDependenciesToolFields, createInstallDependenciesToolFields,
createTakePlannerNotesFields, createTakePlannerNotesFields,
createDiagnoseErrorToolFields,
} from "@open-swe/shared/open-swe/tools"; } from "@open-swe/shared/open-swe/tools";
import { z } from "zod"; import { z } from "zod";
import { isAIMessageSDK, isToolMessageSDK } from "@/lib/langchain-messages"; import { isAIMessageSDK, isToolMessageSDK } from "@/lib/langchain-messages";
@ -54,6 +56,9 @@ type InstallDependenciesToolArgs = z.infer<
const plannerNotesTool = createTakePlannerNotesFields(); const plannerNotesTool = createTakePlannerNotesFields();
type PlannerNotesToolArgs = z.infer<typeof plannerNotesTool.schema>; type PlannerNotesToolArgs = z.infer<typeof plannerNotesTool.schema>;
const diagnoseErrorTool = createDiagnoseErrorToolFields();
type DiagnoseErrorToolArgs = z.infer<typeof diagnoseErrorTool.schema>;
function CustomComponent({ function CustomComponent({
message, message,
thread, thread,
@ -256,6 +261,10 @@ export function AssistantMessage({
? aiToolCalls.find((tc) => tc.name === openPrTool.name) ? aiToolCalls.find((tc) => tc.name === openPrTool.name)
: undefined; : 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 // 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. // only tool call/result we need to render for this message.
if (taskStatusToolCall) { 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 (
<div className="flex flex-col gap-4">
<DiagnoseErrorAction
status={correspondingToolResult ? "done" : "generating"}
diagnosis={args.diagnosis}
reasoningText={reasoningText}
/>
</div>
);
}
if (openPrToolCall) { if (openPrToolCall) {
let branch: string | undefined; let branch: string | undefined;
let targetBranch: string | undefined = "main"; let targetBranch: string | undefined = "main";

View file

@ -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 <div className="border-border size-3.5 rounded-full border" />;
case "generating":
return (
<Loader2 className="text-muted-foreground size-3.5 animate-spin" />
);
case "done":
return <CheckCircle className="size-3.5 text-green-500" />;
}
};
const getStatusText = () => {
switch (status) {
case "loading":
return "Preparing error analysis...";
case "generating":
return "Diagnosing errors...";
case "done":
return "Error diagnosis complete";
}
};
return (
<div className="border-border overflow-hidden rounded-md border">
{reasoningText && (
<div className="border-b border-blue-100 bg-blue-50 p-2 dark:border-blue-800 dark:bg-blue-950">
<button
onClick={() => setShowReasoning(!showReasoning)}
className="flex items-center gap-1 text-xs font-normal text-blue-700 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
>
<MessageSquare className="h-3 w-3" />
{showReasoning ? "Hide reasoning" : "Show reasoning"}
</button>
{showReasoning && (
<p className="mt-1 text-xs font-normal text-blue-800 dark:text-blue-300">
{reasoningText}
</p>
)}
</div>
)}
<div className="border-border flex items-center border-b bg-gray-50 p-2 dark:bg-gray-800">
<AlertTriangle className="mr-2 size-3.5 text-amber-500" />
<span className="text-foreground/80 flex-1 text-xs font-normal">
{getStatusText()}
</span>
{getStatusIcon()}
</div>
{status === "done" && diagnosis && (
<div className="p-2">
<div className="mb-2">
<h3 className="text-muted-foreground mb-1 text-xs font-normal">
Diagnosis
</h3>
<p className="text-foreground/80 text-xs font-normal">
{diagnosis}
</p>
</div>
</div>
)}
</div>
);
}

View file

@ -127,7 +127,6 @@ export function ThreadView({
setChatInput={setChatInput} setChatInput={setChatInput}
handleSendMessage={handleSendMessage} handleSendMessage={handleSendMessage}
/> />
{/* Right Side - Actions & Plan */} {/* Right Side - Actions & Plan */}
<div className="flex h-full flex-1 flex-col"> <div className="flex h-full flex-1 flex-col">
<div className="relative flex-1"> <div className="relative flex-1">

View file

@ -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.", "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,
};
}