mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 09:13:14 +00:00
feat: Add task status/summary gen ui component
This commit is contained in:
parent
96298ad22b
commit
ea8427a508
4 changed files with 99 additions and 60 deletions
|
|
@ -23,6 +23,7 @@ import {
|
|||
} from "../../../utils/current-task.js";
|
||||
import { ToolMessage } from "@langchain/core/messages";
|
||||
import { addTaskPlanToIssue } from "../../../utils/github/issue-task.js";
|
||||
import { createSetTaskStatusToolFields } from "@open-swe/shared/open-swe/tools";
|
||||
|
||||
const logger = createLogger(LogLevel.INFO, "ProgressPlanStep");
|
||||
|
||||
|
|
@ -40,26 +41,6 @@ Take all of this information, and determine whether or not you have completed th
|
|||
Once you've determined the status of the current task, call the \`set_task_status\` tool.
|
||||
`;
|
||||
|
||||
const setTaskStatusToolSchema = z.object({
|
||||
reasoning: z
|
||||
.string()
|
||||
.describe(
|
||||
"A concise reasoning summary for the status of the current task, explaining why you think it is completed or not completed.",
|
||||
),
|
||||
task_status: z
|
||||
.enum(["completed", "not_completed"])
|
||||
.describe(
|
||||
"The status of the current task, based on the reasoning provided.",
|
||||
),
|
||||
});
|
||||
|
||||
const setTaskStatusTool = {
|
||||
name: "set_task_status",
|
||||
description:
|
||||
"The status of the current task, along with a concise reasoning summary to support the status.",
|
||||
schema: setTaskStatusToolSchema,
|
||||
};
|
||||
|
||||
const formatPrompt = (taskPlan: PlanItem[]): string => {
|
||||
return systemPrompt.replace(
|
||||
"{PLAN_PROMPT}",
|
||||
|
|
@ -71,6 +52,7 @@ export async function progressPlanStep(
|
|||
state: GraphState,
|
||||
config: GraphConfig,
|
||||
): Promise<Command> {
|
||||
const setTaskStatusTool = createSetTaskStatusToolFields();
|
||||
const model = await loadModel(config, Task.PROGRESS_PLAN_CHECKER);
|
||||
const modelWithTools = model.bindTools([setTaskStatusTool], {
|
||||
tool_choice: setTaskStatusTool.name,
|
||||
|
|
@ -109,7 +91,7 @@ Once you've determined the status of the current task, call the \`set_task_statu
|
|||
}
|
||||
|
||||
const isCompleted =
|
||||
(toolCall.args as z.infer<typeof setTaskStatusToolSchema>).task_status ===
|
||||
(toolCall.args as z.infer<typeof setTaskStatusTool.schema>).task_status ===
|
||||
"completed";
|
||||
const currentTask = getCurrentPlanItem(activePlanItems);
|
||||
const toolMessage = new ToolMessage({
|
||||
|
|
|
|||
|
|
@ -1,43 +1,44 @@
|
|||
"use client";
|
||||
|
||||
import "../app/globals.css";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
CheckCircle,
|
||||
Loader2,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
MessageSquare,
|
||||
FileText,
|
||||
MinusCircle,
|
||||
} from "lucide-react";
|
||||
|
||||
type TaskSummaryProps = {
|
||||
status: "loading" | "generating" | "done";
|
||||
completed?: boolean;
|
||||
summary?: string;
|
||||
reasoningText?: string;
|
||||
summaryText?: string;
|
||||
};
|
||||
|
||||
export function TaskSummary({
|
||||
status,
|
||||
completed,
|
||||
summary,
|
||||
reasoningText,
|
||||
summaryText,
|
||||
}: TaskSummaryProps) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
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" />
|
||||
);
|
||||
return <div className="border-border size-3.5 rounded-full border" />;
|
||||
case "generating":
|
||||
return <Loader2 className="h-3.5 w-3.5 animate-spin text-gray-500" />;
|
||||
return (
|
||||
<Loader2 className="text-muted-foreground size-3.5 animate-spin" />
|
||||
);
|
||||
case "done":
|
||||
return <CheckCircle className="h-3.5 w-3.5 text-green-500" />;
|
||||
if (completed === false) {
|
||||
return <MinusCircle className="size-3.5 text-amber-500" />;
|
||||
}
|
||||
return <CheckCircle className="size-3.5 text-green-500" />;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -48,31 +49,17 @@ export function TaskSummary({
|
|||
case "generating":
|
||||
return "Generating summary...";
|
||||
case "done":
|
||||
if (completed === false) {
|
||||
return "Task not completed";
|
||||
}
|
||||
return "Task completed";
|
||||
}
|
||||
};
|
||||
|
||||
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="border-border overflow-hidden rounded-md border">
|
||||
<div
|
||||
className={`flex items-center bg-gray-50 p-2 ${status === "done" && summary ? "cursor-pointer" : ""}`}
|
||||
className={`flex items-center border-b bg-gray-50 p-2 dark:bg-gray-800 ${status === "done" && summary ? "cursor-pointer" : ""}`}
|
||||
onClick={
|
||||
status === "done" && summary
|
||||
? () => setExpanded(!expanded)
|
||||
|
|
@ -80,40 +67,56 @@ export function TaskSummary({
|
|||
}
|
||||
>
|
||||
{getStatusIcon()}
|
||||
<span className="ml-2 flex-1 text-xs font-normal text-gray-800">
|
||||
<span className="text-foreground/80 ml-2 flex-1 text-xs font-normal">
|
||||
{getStatusText()}
|
||||
</span>
|
||||
{status === "done" && summary && (
|
||||
<button className="text-gray-500 hover:text-gray-700">
|
||||
<button className="text-muted-foreground hover:text-foreground">
|
||||
{expanded ? (
|
||||
<ChevronUp className="h-3.5 w-3.5" />
|
||||
<ChevronUp className="size-3.5" />
|
||||
) : (
|
||||
<ChevronDown className="h-3.5 w-3.5" />
|
||||
<ChevronDown className="size-3.5" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{expanded && summary && status === "done" && (
|
||||
<div className="border-t border-gray-200 p-2">
|
||||
<h3 className="mb-1 text-xs font-normal text-gray-500">
|
||||
<div className="border-border border-t p-2">
|
||||
<h3 className="text-muted-foreground mb-1 text-xs font-normal">
|
||||
Task Summary
|
||||
</h3>
|
||||
<p className="text-xs font-normal text-gray-800">{summary}</p>
|
||||
<p className="text-foreground/80 text-xs font-normal">{summary}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{summaryText && status === "done" && (
|
||||
<div className="border-t border-green-100 bg-green-50 p-2">
|
||||
<div
|
||||
className={`border-t p-2 ${
|
||||
completed === false
|
||||
? "border-amber-300 bg-amber-100/50 dark:border-amber-800 dark:bg-amber-900/50"
|
||||
: "border-green-300 bg-green-100/50 dark:border-green-800 dark:bg-green-900/50"
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
onClick={() => setShowSummary(!showSummary)}
|
||||
className="flex items-center gap-1 text-xs font-normal text-green-700 hover:text-green-800"
|
||||
className={`flex cursor-pointer items-center gap-1 text-xs font-normal ${
|
||||
completed === false
|
||||
? "text-amber-600 hover:text-amber-700 dark:text-amber-400 dark:hover:text-amber-300"
|
||||
: "text-green-600 hover:text-green-700 dark:text-green-400 dark:hover:text-green-300"
|
||||
}`}
|
||||
>
|
||||
<FileText className="h-3 w-3" />
|
||||
{showSummary ? "Hide summary" : "Show summary"}
|
||||
</button>
|
||||
{showSummary && (
|
||||
<p className="mt-1 text-xs font-normal text-green-800">
|
||||
<p
|
||||
className={`mt-1 text-xs font-normal ${
|
||||
completed === false
|
||||
? "text-amber-700 dark:text-amber-300"
|
||||
: "text-green-700 dark:text-green-300"
|
||||
}`}
|
||||
>
|
||||
{summaryText}
|
||||
</p>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -19,10 +19,12 @@ import { Fragment } from "react/jsx-runtime";
|
|||
import { useQueryState, parseAsBoolean } from "nuqs";
|
||||
import { Interrupt } from "./interrupt";
|
||||
import { ActionStep, ActionItemProps } from "@/components/gen-ui/action-step";
|
||||
import { TaskSummary } from "@/components/gen-ui/task-summary";
|
||||
import { ToolCall } from "@langchain/core/messages/tool";
|
||||
import {
|
||||
createApplyPatchToolFields,
|
||||
createShellToolFields,
|
||||
createSetTaskStatusToolFields,
|
||||
} from "@open-swe/shared/open-swe/tools";
|
||||
import { z } from "zod";
|
||||
import { isAIMessageSDK, isToolMessageSDK } from "@/lib/langchain-messages";
|
||||
|
|
@ -34,6 +36,8 @@ const shellTool = createShellToolFields(dummyRepo);
|
|||
type ShellToolArgs = z.infer<typeof shellTool.schema>;
|
||||
const applyPatchTool = createApplyPatchToolFields(dummyRepo);
|
||||
type ApplyPatchToolArgs = z.infer<typeof applyPatchTool.schema>;
|
||||
const setTaskStatusTool = createSetTaskStatusToolFields();
|
||||
type SetTaskStatusToolArgs = z.infer<typeof setTaskStatusTool.schema>;
|
||||
|
||||
function CustomComponent({
|
||||
message,
|
||||
|
|
@ -193,6 +197,32 @@ export function AssistantMessage({
|
|||
)
|
||||
: [];
|
||||
|
||||
const taskStatusToolCall = message
|
||||
? aiToolCalls.find((tc) => tc.name === setTaskStatusTool.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) {
|
||||
const args = taskStatusToolCall.args as SetTaskStatusToolArgs;
|
||||
const correspondingToolResult = toolResults.find(
|
||||
(tr) => tr && tr.tool_call_id === taskStatusToolCall.id,
|
||||
);
|
||||
|
||||
const status = correspondingToolResult ? "done" : "generating";
|
||||
const completed = args.task_status === "completed";
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<TaskSummary
|
||||
status={status}
|
||||
completed={completed}
|
||||
summaryText={args.reasoning}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (shellOrPatchToolCalls.length > 0) {
|
||||
const actionItems = shellOrPatchToolCalls.map((toolCall) => {
|
||||
const correspondingToolResult = toolResults.find(
|
||||
|
|
|
|||
|
|
@ -100,3 +100,27 @@ export function createUpdatePlanToolFields() {
|
|||
"\nThe reasoning you pass to this tool will be used in the step that actually updates the plan, so ensure it is useful and concise.",
|
||||
};
|
||||
}
|
||||
|
||||
export function createSetTaskStatusToolFields() {
|
||||
const setTaskStatusToolSchema = z.object({
|
||||
reasoning: z
|
||||
.string()
|
||||
.describe(
|
||||
"A concise reasoning summary for the status of the current task, explaining why you think it is completed or not completed.",
|
||||
),
|
||||
task_status: z
|
||||
.enum(["completed", "not_completed"])
|
||||
.describe(
|
||||
"The status of the current task, based on the reasoning provided.",
|
||||
),
|
||||
});
|
||||
|
||||
const setTaskStatusTool = {
|
||||
name: "set_task_status",
|
||||
description:
|
||||
"The status of the current task, along with a concise reasoning summary to support the status.",
|
||||
schema: setTaskStatusToolSchema,
|
||||
};
|
||||
|
||||
return setTaskStatusTool;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue