mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 09:13:14 +00:00
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:
parent
4e52be54f8
commit
942bc5ca1d
7 changed files with 132 additions and 136 deletions
|
|
@ -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<typeof diagnoseErrorToolSchema>)
|
||||
diagnosis: (toolCall.args as z.infer<typeof diagnoseErrorTool.schema>)
|
||||
.diagnosis,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<typeof plannerNotesTool.schema>;
|
||||
|
||||
const diagnoseErrorTool = createDiagnoseErrorToolFields();
|
||||
type DiagnoseErrorToolArgs = z.infer<typeof diagnoseErrorTool.schema>;
|
||||
|
||||
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 (
|
||||
<div className="flex flex-col gap-4">
|
||||
<DiagnoseErrorAction
|
||||
status={correspondingToolResult ? "done" : "generating"}
|
||||
diagnosis={args.diagnosis}
|
||||
reasoningText={reasoningText}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (openPrToolCall) {
|
||||
let branch: string | undefined;
|
||||
let targetBranch: string | undefined = "main";
|
||||
|
|
|
|||
89
apps/web/src/components/v2/diagnose-error-action.tsx
Normal file
89
apps/web/src/components/v2/diagnose-error-action.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -127,7 +127,6 @@ export function ThreadView({
|
|||
setChatInput={setChatInput}
|
||||
handleSendMessage={handleSendMessage}
|
||||
/>
|
||||
|
||||
{/* Right Side - Actions & Plan */}
|
||||
<div className="flex h-full flex-1 flex-col">
|
||||
<div className="relative flex-1">
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue