mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-02 22:02:13 +08:00
feat: add realtime subagent status report
This commit is contained in:
@@ -17,13 +17,17 @@ import { Shimmer } from "@/components/ai-elements/shimmer";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ShineBorder } from "@/components/ui/shine-border";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { hasToolCalls } from "@/core/messages/utils";
|
||||
import {
|
||||
streamdownPlugins,
|
||||
streamdownPluginsWithWordAnimation,
|
||||
} from "@/core/streamdown";
|
||||
import { useSubtask } from "@/core/tasks/context";
|
||||
import { explainLastToolCall } from "@/core/tools/utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { FlipDisplay } from "../flip-display";
|
||||
|
||||
export function SubtaskCard({
|
||||
className,
|
||||
taskId,
|
||||
@@ -84,7 +88,16 @@ export function SubtaskCard({
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
{t.subtasks[task.status]}
|
||||
<FlipDisplay
|
||||
className="pb-1"
|
||||
uniqueKey={task.latestMessage?.id ?? ""}
|
||||
>
|
||||
{task.status === "in_progress" &&
|
||||
task.latestMessage &&
|
||||
hasToolCalls(task.latestMessage)
|
||||
? explainLastToolCall(task.latestMessage, t)
|
||||
: t.subtasks[task.status]}
|
||||
</FlipDisplay>
|
||||
</div>
|
||||
)}
|
||||
<ChevronUp
|
||||
@@ -107,6 +120,16 @@ export function SubtaskCard({
|
||||
}
|
||||
></ChainOfThoughtStep>
|
||||
)}
|
||||
{task.status === "in_progress" &&
|
||||
task.latestMessage &&
|
||||
hasToolCalls(task.latestMessage) && (
|
||||
<ChainOfThoughtStep
|
||||
label={t.subtasks.in_progress}
|
||||
icon={<Loader2Icon className="size-4 animate-spin" />}
|
||||
>
|
||||
{explainLastToolCall(task.latestMessage, t)}
|
||||
</ChainOfThoughtStep>
|
||||
)}
|
||||
{task.status === "completed" && (
|
||||
<>
|
||||
<ChainOfThoughtStep
|
||||
|
||||
@@ -195,6 +195,7 @@ export const enUS: Translations = {
|
||||
presentFiles: "Present files",
|
||||
needYourHelp: "Need your help",
|
||||
useTool: (toolName: string) => `Use "${toolName}" tool`,
|
||||
searchFor: (query: string) => `Search for "${query}"`,
|
||||
searchForRelatedInfo: "Search for related information",
|
||||
searchForRelatedImages: "Search for related images",
|
||||
searchForRelatedImagesFor: (query: string) =>
|
||||
|
||||
@@ -144,6 +144,7 @@ export interface Translations {
|
||||
useTool: (toolName: string) => string;
|
||||
searchForRelatedInfo: string;
|
||||
searchForRelatedImages: string;
|
||||
searchFor: (query: string) => string;
|
||||
searchForRelatedImagesFor: (query: string) => string;
|
||||
searchOnWebFor: (query: string) => string;
|
||||
viewWebPage: string;
|
||||
|
||||
@@ -191,6 +191,7 @@ export const zhCN: Translations = {
|
||||
presentFiles: "展示文件",
|
||||
needYourHelp: "需要你的协助",
|
||||
useTool: (toolName: string) => `使用 “${toolName}” 工具`,
|
||||
searchFor: (query: string) => `搜索 “${query}”`,
|
||||
searchForRelatedInfo: "搜索相关信息",
|
||||
searchForRelatedImages: "搜索相关图片",
|
||||
searchForRelatedImagesFor: (query: string) => `搜索相关图片 “${query}”`,
|
||||
|
||||
@@ -3,17 +3,21 @@ import { createContext, useCallback, useContext, useState } from "react";
|
||||
import type { Subtask } from "./types";
|
||||
|
||||
export interface SubtaskContextValue {
|
||||
tasks: Map<string, Subtask>;
|
||||
tasks: Record<string, Subtask>;
|
||||
setTasks: (tasks: Record<string, Subtask>) => void;
|
||||
}
|
||||
|
||||
export const SubtaskContext = createContext<SubtaskContextValue>({
|
||||
tasks: new Map(),
|
||||
tasks: {},
|
||||
setTasks: () => {
|
||||
/* noop */
|
||||
},
|
||||
});
|
||||
|
||||
export function SubtasksProvider({ children }: { children: React.ReactNode }) {
|
||||
const [tasks] = useState<Map<string, Subtask>>(new Map());
|
||||
const [tasks, setTasks] = useState<Record<string, Subtask>>({});
|
||||
return (
|
||||
<SubtaskContext.Provider value={{ tasks }}>
|
||||
<SubtaskContext.Provider value={{ tasks, setTasks }}>
|
||||
{children}
|
||||
</SubtaskContext.Provider>
|
||||
);
|
||||
@@ -31,16 +35,19 @@ export function useSubtaskContext() {
|
||||
|
||||
export function useSubtask(id: string) {
|
||||
const { tasks } = useSubtaskContext();
|
||||
return tasks.get(id);
|
||||
return tasks[id];
|
||||
}
|
||||
|
||||
export function useUpdateSubtask() {
|
||||
const { tasks } = useSubtaskContext();
|
||||
const { tasks, setTasks } = useSubtaskContext();
|
||||
const updateSubtask = useCallback(
|
||||
(task: Partial<Subtask> & { id: string }) => {
|
||||
tasks.set(task.id, { ...tasks.get(task.id), ...task } as Subtask);
|
||||
tasks[task.id] = { ...tasks[task.id], ...task } as Subtask;
|
||||
if (task.latestMessage) {
|
||||
setTasks({ ...tasks });
|
||||
}
|
||||
},
|
||||
[tasks],
|
||||
[tasks, setTasks],
|
||||
);
|
||||
return updateSubtask;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import type { AIMessage } from "@langchain/langgraph-sdk";
|
||||
|
||||
export interface Subtask {
|
||||
id: string;
|
||||
status: "in_progress" | "completed" | "failed";
|
||||
subagent_type: string;
|
||||
description: string;
|
||||
latestMessage?: AIMessage;
|
||||
prompt: string;
|
||||
result?: string;
|
||||
error?: string;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { HumanMessage } from "@langchain/core/messages";
|
||||
import type { AIMessage } from "@langchain/langgraph-sdk";
|
||||
import type { ThreadsClient } from "@langchain/langgraph-sdk/client";
|
||||
import { useStream, type UseStream } from "@langchain/langgraph-sdk/react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
@@ -7,6 +8,7 @@ import { useCallback } from "react";
|
||||
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
||||
|
||||
import { getAPIClient } from "../api";
|
||||
import { useUpdateSubtask } from "../tasks/context";
|
||||
import { uploadFiles } from "../uploads";
|
||||
|
||||
import type {
|
||||
@@ -25,14 +27,28 @@ export function useThreadStream({
|
||||
onFinish?: (state: AgentThreadState) => void;
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
const updateSubtask = useUpdateSubtask();
|
||||
const thread = useStream<AgentThreadState>({
|
||||
client: getAPIClient(),
|
||||
assistantId: "lead_agent",
|
||||
threadId: isNewThread ? undefined : threadId,
|
||||
reconnectOnMount: true,
|
||||
fetchStateHistory: true,
|
||||
onCustomEvent(event) {
|
||||
onCustomEvent(event: unknown) {
|
||||
console.info(event);
|
||||
if (
|
||||
typeof event === "object" &&
|
||||
event !== null &&
|
||||
"type" in event &&
|
||||
event.type === "task_running"
|
||||
) {
|
||||
const e = event as {
|
||||
type: "task_running";
|
||||
task_id: string;
|
||||
message: AIMessage;
|
||||
};
|
||||
updateSubtask({ id: e.task_id, latestMessage: e.message });
|
||||
}
|
||||
},
|
||||
onFinish(state) {
|
||||
onFinish?.(state.values);
|
||||
|
||||
29
frontend/src/core/tools/utils.ts
Normal file
29
frontend/src/core/tools/utils.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { ToolCall } from "@langchain/core/messages";
|
||||
import type { AIMessage } from "@langchain/langgraph-sdk";
|
||||
|
||||
import type { Translations } from "../i18n";
|
||||
import { hasToolCalls } from "../messages/utils";
|
||||
|
||||
export function explainLastToolCall(message: AIMessage, t: Translations) {
|
||||
if (hasToolCalls(message)) {
|
||||
const lastToolCall = message.tool_calls![message.tool_calls!.length - 1]!;
|
||||
return explainToolCall(lastToolCall, t);
|
||||
}
|
||||
return t.common.thinking;
|
||||
}
|
||||
|
||||
export function explainToolCall(toolCall: ToolCall, t: Translations) {
|
||||
if (toolCall.name === "web_search" || toolCall.name === "image_search") {
|
||||
return t.toolCalls.searchFor(toolCall.args.query);
|
||||
} else if (toolCall.name === "web_fetch") {
|
||||
return t.toolCalls.viewWebPage;
|
||||
} else if (toolCall.name === "present_files") {
|
||||
return t.toolCalls.presentFiles;
|
||||
} else if (toolCall.name === "write_todos") {
|
||||
return t.toolCalls.writeTodos;
|
||||
} else if (toolCall.args.description) {
|
||||
return toolCall.args.description;
|
||||
} else {
|
||||
return t.toolCalls.useTool(toolCall.name);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user