mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-06-10 09:25:57 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0fdfbae435 | |||
| 150d03f2e7 | |||
| 9593214065 |
@@ -16,7 +16,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
extractContentFromMessage,
|
extractContentFromMessage,
|
||||||
extractPresentFilesFromMessage,
|
extractPresentFilesFromMessage,
|
||||||
extractTextFromMessage,
|
|
||||||
getAssistantTurnCopyData,
|
getAssistantTurnCopyData,
|
||||||
getAssistantTurnUsageMessages,
|
getAssistantTurnUsageMessages,
|
||||||
getMessageGroups,
|
getMessageGroups,
|
||||||
@@ -27,9 +26,7 @@ import {
|
|||||||
isAssistantMessageGroupStreaming,
|
isAssistantMessageGroupStreaming,
|
||||||
} from "@/core/messages/utils";
|
} from "@/core/messages/utils";
|
||||||
import { useRehypeSplitWordsIntoSpans } from "@/core/rehype";
|
import { useRehypeSplitWordsIntoSpans } from "@/core/rehype";
|
||||||
import type { Subtask } from "@/core/tasks";
|
import { buildSubtaskMapFromMessages } from "@/core/tasks/derive";
|
||||||
import { useUpdateSubtask } from "@/core/tasks/context";
|
|
||||||
import { parseSubtaskResult } from "@/core/tasks/subtask-result";
|
|
||||||
import type { AgentThreadState } from "@/core/threads";
|
import type { AgentThreadState } from "@/core/threads";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
@@ -177,8 +174,8 @@ export function MessageList({
|
|||||||
}) {
|
}) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const rehypePlugins = useRehypeSplitWordsIntoSpans(thread.isLoading);
|
const rehypePlugins = useRehypeSplitWordsIntoSpans(thread.isLoading);
|
||||||
const updateSubtask = useUpdateSubtask();
|
|
||||||
const messages = thread.messages;
|
const messages = thread.messages;
|
||||||
|
const tasks = useMemo(() => buildSubtaskMapFromMessages(messages), [messages]);
|
||||||
const groupedMessages = getMessageGroups(messages);
|
const groupedMessages = getMessageGroups(messages);
|
||||||
const turnUsageMessagesByGroupIndex =
|
const turnUsageMessagesByGroupIndex =
|
||||||
getAssistantTurnUsageMessages(groupedMessages);
|
getAssistantTurnUsageMessages(groupedMessages);
|
||||||
@@ -354,42 +351,29 @@ export function MessageList({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (group.type === "assistant:subagent") {
|
} else if (group.type === "assistant:subagent") {
|
||||||
const tasks = new Set<Subtask>();
|
|
||||||
for (const message of group.messages) {
|
|
||||||
if (message.type === "ai") {
|
|
||||||
for (const toolCall of message.tool_calls ?? []) {
|
|
||||||
if (toolCall.name === "task") {
|
|
||||||
const task: Subtask = {
|
|
||||||
id: toolCall.id!,
|
|
||||||
subagent_type: toolCall.args.subagent_type,
|
|
||||||
description: toolCall.args.description,
|
|
||||||
prompt: toolCall.args.prompt,
|
|
||||||
status: "in_progress",
|
|
||||||
};
|
|
||||||
updateSubtask(task);
|
|
||||||
tasks.add(task);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (message.type === "tool") {
|
|
||||||
const taskId = message.tool_call_id;
|
|
||||||
if (taskId) {
|
|
||||||
const parsed = parseSubtaskResult(
|
|
||||||
extractTextFromMessage(message),
|
|
||||||
);
|
|
||||||
updateSubtask({ id: taskId, ...parsed });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const results: React.ReactNode[] = [];
|
const results: React.ReactNode[] = [];
|
||||||
const subagentDebugMessageIds: string[] = [];
|
const subagentDebugMessageIds: string[] = [];
|
||||||
if (tasks.size > 0) {
|
const groupTaskIds = Array.from(
|
||||||
|
new Set(
|
||||||
|
group.messages.flatMap((message) =>
|
||||||
|
message.type === "ai"
|
||||||
|
? (message.tool_calls ?? [])
|
||||||
|
.map((toolCall) =>
|
||||||
|
toolCall.name === "task" ? toolCall.id : null,
|
||||||
|
)
|
||||||
|
.filter((taskId): taskId is string => Boolean(taskId))
|
||||||
|
: [],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (groupTaskIds.length > 0) {
|
||||||
results.push(
|
results.push(
|
||||||
<div
|
<div
|
||||||
key="subtask-count"
|
key="subtask-count"
|
||||||
className="text-muted-foreground pt-2 text-sm font-normal"
|
className="text-muted-foreground pt-2 text-sm font-normal"
|
||||||
>
|
>
|
||||||
{t.subtasks.executing(tasks.size)}
|
{t.subtasks.executing(groupTaskIds.length)}
|
||||||
</div>,
|
</div>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -417,10 +401,14 @@ export function MessageList({
|
|||||||
?.filter((toolCall) => toolCall.name === "task")
|
?.filter((toolCall) => toolCall.name === "task")
|
||||||
.map((toolCall) => toolCall.id);
|
.map((toolCall) => toolCall.id);
|
||||||
for (const taskId of taskIds ?? []) {
|
for (const taskId of taskIds ?? []) {
|
||||||
|
const task = taskId ? tasks[taskId] : undefined;
|
||||||
|
if (!taskId || !task) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
results.push(
|
results.push(
|
||||||
<SubtaskCard
|
<SubtaskCard
|
||||||
key={"task-group-" + taskId}
|
key={"task-group-" + taskId}
|
||||||
taskId={taskId!}
|
task={task}
|
||||||
isLoading={thread.isLoading}
|
isLoading={thread.isLoading}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ import { useI18n } from "@/core/i18n/hooks";
|
|||||||
import { hasToolCalls } from "@/core/messages/utils";
|
import { hasToolCalls } from "@/core/messages/utils";
|
||||||
import { useRehypeSplitWordsIntoSpans } from "@/core/rehype";
|
import { useRehypeSplitWordsIntoSpans } from "@/core/rehype";
|
||||||
import { streamdownPluginsWithWordAnimation } from "@/core/streamdown";
|
import { streamdownPluginsWithWordAnimation } from "@/core/streamdown";
|
||||||
import { useSubtask } from "@/core/tasks/context";
|
import type { Subtask } from "@/core/tasks";
|
||||||
|
import { useLatestSubtaskMessage } from "@/core/tasks/context";
|
||||||
import { explainLastToolCall } from "@/core/tools/utils";
|
import { explainLastToolCall } from "@/core/tools/utils";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
@@ -31,26 +32,30 @@ import { MarkdownContent } from "./markdown-content";
|
|||||||
|
|
||||||
export function SubtaskCard({
|
export function SubtaskCard({
|
||||||
className,
|
className,
|
||||||
taskId,
|
task,
|
||||||
isLoading,
|
isLoading,
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
taskId: string;
|
task: Subtask;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const [collapsed, setCollapsed] = useState(true);
|
const [collapsed, setCollapsed] = useState(true);
|
||||||
const rehypePlugins = useRehypeSplitWordsIntoSpans(isLoading);
|
const rehypePlugins = useRehypeSplitWordsIntoSpans(isLoading);
|
||||||
const task = useSubtask(taskId)!;
|
const latestMessage = useLatestSubtaskMessage(task.id);
|
||||||
|
const mergedTask = useMemo(
|
||||||
|
() => (latestMessage ? { ...task, latestMessage } : task),
|
||||||
|
[latestMessage, task],
|
||||||
|
);
|
||||||
const icon = useMemo(() => {
|
const icon = useMemo(() => {
|
||||||
if (task.status === "completed") {
|
if (mergedTask.status === "completed") {
|
||||||
return <CheckCircleIcon className="size-3" />;
|
return <CheckCircleIcon className="size-3" />;
|
||||||
} else if (task.status === "failed") {
|
} else if (mergedTask.status === "failed") {
|
||||||
return <XCircleIcon className="size-3 text-red-500" />;
|
return <XCircleIcon className="size-3 text-red-500" />;
|
||||||
} else if (task.status === "in_progress") {
|
} else if (mergedTask.status === "in_progress") {
|
||||||
return <Loader2Icon className="size-3 animate-spin" />;
|
return <Loader2Icon className="size-3 animate-spin" />;
|
||||||
}
|
}
|
||||||
}, [task.status]);
|
}, [mergedTask.status]);
|
||||||
return (
|
return (
|
||||||
<ChainOfThought
|
<ChainOfThought
|
||||||
className={cn("relative w-full gap-2 rounded-lg border py-0", className)}
|
className={cn("relative w-full gap-2 rounded-lg border py-0", className)}
|
||||||
@@ -59,10 +64,10 @@ export function SubtaskCard({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"ambilight z-[-1]",
|
"ambilight z-[-1]",
|
||||||
task.status === "in_progress" ? "enabled" : "",
|
mergedTask.status === "in_progress" ? "enabled" : "",
|
||||||
)}
|
)}
|
||||||
></div>
|
></div>
|
||||||
{task.status === "in_progress" && (
|
{mergedTask.status === "in_progress" && (
|
||||||
<>
|
<>
|
||||||
<ShineBorder
|
<ShineBorder
|
||||||
borderWidth={1.5}
|
borderWidth={1.5}
|
||||||
@@ -81,12 +86,12 @@ export function SubtaskCard({
|
|||||||
<ChainOfThoughtStep
|
<ChainOfThoughtStep
|
||||||
className="font-normal"
|
className="font-normal"
|
||||||
label={
|
label={
|
||||||
task.status === "in_progress" ? (
|
mergedTask.status === "in_progress" ? (
|
||||||
<Shimmer duration={3} spread={3}>
|
<Shimmer duration={3} spread={3}>
|
||||||
{task.description}
|
{mergedTask.description}
|
||||||
</Shimmer>
|
</Shimmer>
|
||||||
) : (
|
) : (
|
||||||
task.description
|
mergedTask.description
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
icon={<ClipboardListIcon />}
|
icon={<ClipboardListIcon />}
|
||||||
@@ -96,19 +101,21 @@ export function SubtaskCard({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground flex items-center gap-1 text-xs font-normal",
|
"text-muted-foreground flex items-center gap-1 text-xs font-normal",
|
||||||
task.status === "failed" ? "text-red-500 opacity-67" : "",
|
mergedTask.status === "failed"
|
||||||
|
? "text-red-500 opacity-67"
|
||||||
|
: "",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
<FlipDisplay
|
<FlipDisplay
|
||||||
className="max-w-[420px] truncate pb-1"
|
className="max-w-[420px] truncate pb-1"
|
||||||
uniqueKey={task.latestMessage?.id ?? ""}
|
uniqueKey={mergedTask.latestMessage?.id ?? ""}
|
||||||
>
|
>
|
||||||
{task.status === "in_progress" &&
|
{mergedTask.status === "in_progress" &&
|
||||||
task.latestMessage &&
|
mergedTask.latestMessage &&
|
||||||
hasToolCalls(task.latestMessage)
|
hasToolCalls(mergedTask.latestMessage)
|
||||||
? explainLastToolCall(task.latestMessage, t)
|
? explainLastToolCall(mergedTask.latestMessage, t)
|
||||||
: t.subtasks[task.status]}
|
: t.subtasks[mergedTask.status]}
|
||||||
</FlipDisplay>
|
</FlipDisplay>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -123,29 +130,29 @@ export function SubtaskCard({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<ChainOfThoughtContent className="px-4 pb-4">
|
<ChainOfThoughtContent className="px-4 pb-4">
|
||||||
{task.prompt && (
|
{mergedTask.prompt && (
|
||||||
<ChainOfThoughtStep
|
<ChainOfThoughtStep
|
||||||
label={
|
label={
|
||||||
<Streamdown
|
<Streamdown
|
||||||
{...streamdownPluginsWithWordAnimation}
|
{...streamdownPluginsWithWordAnimation}
|
||||||
components={{ a: CitationLink }}
|
components={{ a: CitationLink }}
|
||||||
>
|
>
|
||||||
{task.prompt}
|
{mergedTask.prompt}
|
||||||
</Streamdown>
|
</Streamdown>
|
||||||
}
|
}
|
||||||
></ChainOfThoughtStep>
|
></ChainOfThoughtStep>
|
||||||
)}
|
)}
|
||||||
{task.status === "in_progress" &&
|
{mergedTask.status === "in_progress" &&
|
||||||
task.latestMessage &&
|
mergedTask.latestMessage &&
|
||||||
hasToolCalls(task.latestMessage) && (
|
hasToolCalls(mergedTask.latestMessage) && (
|
||||||
<ChainOfThoughtStep
|
<ChainOfThoughtStep
|
||||||
label={t.subtasks.in_progress}
|
label={t.subtasks.in_progress}
|
||||||
icon={<Loader2Icon className="size-4 animate-spin" />}
|
icon={<Loader2Icon className="size-4 animate-spin" />}
|
||||||
>
|
>
|
||||||
{explainLastToolCall(task.latestMessage, t)}
|
{explainLastToolCall(mergedTask.latestMessage, t)}
|
||||||
</ChainOfThoughtStep>
|
</ChainOfThoughtStep>
|
||||||
)}
|
)}
|
||||||
{task.status === "completed" && (
|
{mergedTask.status === "completed" && (
|
||||||
<>
|
<>
|
||||||
<ChainOfThoughtStep
|
<ChainOfThoughtStep
|
||||||
label={t.subtasks.completed}
|
label={t.subtasks.completed}
|
||||||
@@ -153,9 +160,9 @@ export function SubtaskCard({
|
|||||||
></ChainOfThoughtStep>
|
></ChainOfThoughtStep>
|
||||||
<ChainOfThoughtStep
|
<ChainOfThoughtStep
|
||||||
label={
|
label={
|
||||||
task.result ? (
|
mergedTask.result ? (
|
||||||
<MarkdownContent
|
<MarkdownContent
|
||||||
content={task.result}
|
content={mergedTask.result}
|
||||||
isLoading={false}
|
isLoading={false}
|
||||||
rehypePlugins={rehypePlugins}
|
rehypePlugins={rehypePlugins}
|
||||||
/>
|
/>
|
||||||
@@ -164,9 +171,9 @@ export function SubtaskCard({
|
|||||||
></ChainOfThoughtStep>
|
></ChainOfThoughtStep>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{task.status === "failed" && (
|
{mergedTask.status === "failed" && (
|
||||||
<ChainOfThoughtStep
|
<ChainOfThoughtStep
|
||||||
label={<div className="text-red-500">{task.error}</div>}
|
label={<div className="text-red-500">{mergedTask.error}</div>}
|
||||||
icon={<XCircleIcon className="size-4 text-red-500" />}
|
icon={<XCircleIcon className="size-4 text-red-500" />}
|
||||||
></ChainOfThoughtStep>
|
></ChainOfThoughtStep>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,23 +1,26 @@
|
|||||||
|
import type { AIMessage } from "@langchain/langgraph-sdk";
|
||||||
import { createContext, useCallback, useContext, useState } from "react";
|
import { createContext, useCallback, useContext, useState } from "react";
|
||||||
|
|
||||||
import type { Subtask } from "./types";
|
|
||||||
|
|
||||||
export interface SubtaskContextValue {
|
export interface SubtaskContextValue {
|
||||||
tasks: Record<string, Subtask>;
|
latestMessages: Record<string, AIMessage>;
|
||||||
setTasks: (tasks: Record<string, Subtask>) => void;
|
setLatestMessages: React.Dispatch<
|
||||||
|
React.SetStateAction<Record<string, AIMessage>>
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SubtaskContext = createContext<SubtaskContextValue>({
|
export const SubtaskContext = createContext<SubtaskContextValue>({
|
||||||
tasks: {},
|
latestMessages: {},
|
||||||
setTasks: () => {
|
setLatestMessages: () => {
|
||||||
/* noop */
|
/* noop */
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export function SubtasksProvider({ children }: { children: React.ReactNode }) {
|
export function SubtasksProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [tasks, setTasks] = useState<Record<string, Subtask>>({});
|
const [latestMessages, setLatestMessages] = useState<Record<string, AIMessage>>(
|
||||||
|
{},
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<SubtaskContext.Provider value={{ tasks, setTasks }}>
|
<SubtaskContext.Provider value={{ latestMessages, setLatestMessages }}>
|
||||||
{children}
|
{children}
|
||||||
</SubtaskContext.Provider>
|
</SubtaskContext.Provider>
|
||||||
);
|
);
|
||||||
@@ -33,21 +36,21 @@ export function useSubtaskContext() {
|
|||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSubtask(id: string) {
|
export function useLatestSubtaskMessage(id: string) {
|
||||||
const { tasks } = useSubtaskContext();
|
const { latestMessages } = useSubtaskContext();
|
||||||
return tasks[id];
|
return latestMessages[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useUpdateSubtask() {
|
export function useUpdateLatestMessage() {
|
||||||
const { tasks, setTasks } = useSubtaskContext();
|
const { setLatestMessages } = useSubtaskContext();
|
||||||
const updateSubtask = useCallback(
|
const updateLatestMessage = useCallback(
|
||||||
(task: Partial<Subtask> & { id: string }) => {
|
(taskId: string, message: AIMessage) => {
|
||||||
tasks[task.id] = { ...tasks[task.id], ...task } as Subtask;
|
setLatestMessages((current) => ({
|
||||||
if (task.latestMessage) {
|
...current,
|
||||||
setTasks({ ...tasks });
|
[taskId]: message,
|
||||||
}
|
}));
|
||||||
},
|
},
|
||||||
[tasks, setTasks],
|
[setLatestMessages],
|
||||||
);
|
);
|
||||||
return updateSubtask;
|
return updateLatestMessage;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import type { Message } from "@langchain/langgraph-sdk";
|
||||||
|
|
||||||
|
import { extractTextFromMessage } from "@/core/messages/utils";
|
||||||
|
|
||||||
|
import { parseSubtaskResult } from "./subtask-result";
|
||||||
|
import type { Subtask } from "./types";
|
||||||
|
|
||||||
|
export function buildSubtaskMapFromMessages(
|
||||||
|
messages: Message[],
|
||||||
|
): Record<string, Subtask> {
|
||||||
|
const tasks: Record<string, Subtask> = {};
|
||||||
|
|
||||||
|
for (const message of messages) {
|
||||||
|
if (message.type === "ai") {
|
||||||
|
for (const toolCall of message.tool_calls ?? []) {
|
||||||
|
if (toolCall.name !== "task" || !toolCall.id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks[toolCall.id] = {
|
||||||
|
id: toolCall.id,
|
||||||
|
status: "in_progress",
|
||||||
|
subagent_type: String(toolCall.args?.subagent_type ?? ""),
|
||||||
|
description: String(toolCall.args?.description ?? ""),
|
||||||
|
prompt: String(toolCall.args?.prompt ?? ""),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type !== "tool" || !message.tool_call_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const task = tasks[message.tool_call_id];
|
||||||
|
if (!task) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks[message.tool_call_id] = {
|
||||||
|
...task,
|
||||||
|
...parseSubtaskResult(extractTextFromMessage(message)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ import { useI18n } from "../i18n/hooks";
|
|||||||
import { isHiddenFromUIMessage } from "../messages/utils";
|
import { isHiddenFromUIMessage } from "../messages/utils";
|
||||||
import type { FileInMessage } from "../messages/utils";
|
import type { FileInMessage } from "../messages/utils";
|
||||||
import type { LocalSettings } from "../settings";
|
import type { LocalSettings } from "../settings";
|
||||||
import { useUpdateSubtask } from "../tasks/context";
|
import { useUpdateLatestMessage } from "../tasks/context";
|
||||||
import type { UploadedFileInfo } from "../uploads";
|
import type { UploadedFileInfo } from "../uploads";
|
||||||
import { promptInputFilePartToFile, uploadFiles } from "../uploads";
|
import { promptInputFilePartToFile, uploadFiles } from "../uploads";
|
||||||
|
|
||||||
@@ -393,7 +393,7 @@ export function useThreadStream({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const updateSubtask = useUpdateSubtask();
|
const updateLatestMessage = useUpdateLatestMessage();
|
||||||
|
|
||||||
const thread = useStream<AgentThreadState>({
|
const thread = useStream<AgentThreadState>({
|
||||||
client: getAPIClient(isMock),
|
client: getAPIClient(isMock),
|
||||||
@@ -503,7 +503,7 @@ export function useThreadStream({
|
|||||||
task_id: string;
|
task_id: string;
|
||||||
message: AIMessage;
|
message: AIMessage;
|
||||||
};
|
};
|
||||||
updateSubtask({ id: e.task_id, latestMessage: e.message });
|
updateLatestMessage(e.task_id, e.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user