mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-22 05:34:45 +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 { Button } from "@/components/ui/button";
|
||||||
import { ShineBorder } from "@/components/ui/shine-border";
|
import { ShineBorder } from "@/components/ui/shine-border";
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
|
import { hasToolCalls } from "@/core/messages/utils";
|
||||||
import {
|
import {
|
||||||
streamdownPlugins,
|
streamdownPlugins,
|
||||||
streamdownPluginsWithWordAnimation,
|
streamdownPluginsWithWordAnimation,
|
||||||
} from "@/core/streamdown";
|
} from "@/core/streamdown";
|
||||||
import { useSubtask } from "@/core/tasks/context";
|
import { useSubtask } from "@/core/tasks/context";
|
||||||
|
import { explainLastToolCall } from "@/core/tools/utils";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
import { FlipDisplay } from "../flip-display";
|
||||||
|
|
||||||
export function SubtaskCard({
|
export function SubtaskCard({
|
||||||
className,
|
className,
|
||||||
taskId,
|
taskId,
|
||||||
@@ -84,7 +88,16 @@ export function SubtaskCard({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{icon}
|
{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>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ChevronUp
|
<ChevronUp
|
||||||
@@ -107,6 +120,16 @@ export function SubtaskCard({
|
|||||||
}
|
}
|
||||||
></ChainOfThoughtStep>
|
></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" && (
|
{task.status === "completed" && (
|
||||||
<>
|
<>
|
||||||
<ChainOfThoughtStep
|
<ChainOfThoughtStep
|
||||||
|
|||||||
@@ -195,6 +195,7 @@ export const enUS: Translations = {
|
|||||||
presentFiles: "Present files",
|
presentFiles: "Present files",
|
||||||
needYourHelp: "Need your help",
|
needYourHelp: "Need your help",
|
||||||
useTool: (toolName: string) => `Use "${toolName}" tool`,
|
useTool: (toolName: string) => `Use "${toolName}" tool`,
|
||||||
|
searchFor: (query: string) => `Search for "${query}"`,
|
||||||
searchForRelatedInfo: "Search for related information",
|
searchForRelatedInfo: "Search for related information",
|
||||||
searchForRelatedImages: "Search for related images",
|
searchForRelatedImages: "Search for related images",
|
||||||
searchForRelatedImagesFor: (query: string) =>
|
searchForRelatedImagesFor: (query: string) =>
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ export interface Translations {
|
|||||||
useTool: (toolName: string) => string;
|
useTool: (toolName: string) => string;
|
||||||
searchForRelatedInfo: string;
|
searchForRelatedInfo: string;
|
||||||
searchForRelatedImages: string;
|
searchForRelatedImages: string;
|
||||||
|
searchFor: (query: string) => string;
|
||||||
searchForRelatedImagesFor: (query: string) => string;
|
searchForRelatedImagesFor: (query: string) => string;
|
||||||
searchOnWebFor: (query: string) => string;
|
searchOnWebFor: (query: string) => string;
|
||||||
viewWebPage: string;
|
viewWebPage: string;
|
||||||
|
|||||||
@@ -191,6 +191,7 @@ export const zhCN: Translations = {
|
|||||||
presentFiles: "展示文件",
|
presentFiles: "展示文件",
|
||||||
needYourHelp: "需要你的协助",
|
needYourHelp: "需要你的协助",
|
||||||
useTool: (toolName: string) => `使用 “${toolName}” 工具`,
|
useTool: (toolName: string) => `使用 “${toolName}” 工具`,
|
||||||
|
searchFor: (query: string) => `搜索 “${query}”`,
|
||||||
searchForRelatedInfo: "搜索相关信息",
|
searchForRelatedInfo: "搜索相关信息",
|
||||||
searchForRelatedImages: "搜索相关图片",
|
searchForRelatedImages: "搜索相关图片",
|
||||||
searchForRelatedImagesFor: (query: string) => `搜索相关图片 “${query}”`,
|
searchForRelatedImagesFor: (query: string) => `搜索相关图片 “${query}”`,
|
||||||
|
|||||||
@@ -3,17 +3,21 @@ import { createContext, useCallback, useContext, useState } from "react";
|
|||||||
import type { Subtask } from "./types";
|
import type { Subtask } from "./types";
|
||||||
|
|
||||||
export interface SubtaskContextValue {
|
export interface SubtaskContextValue {
|
||||||
tasks: Map<string, Subtask>;
|
tasks: Record<string, Subtask>;
|
||||||
|
setTasks: (tasks: Record<string, Subtask>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SubtaskContext = createContext<SubtaskContextValue>({
|
export const SubtaskContext = createContext<SubtaskContextValue>({
|
||||||
tasks: new Map(),
|
tasks: {},
|
||||||
|
setTasks: () => {
|
||||||
|
/* noop */
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export function SubtasksProvider({ children }: { children: React.ReactNode }) {
|
export function SubtasksProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [tasks] = useState<Map<string, Subtask>>(new Map());
|
const [tasks, setTasks] = useState<Record<string, Subtask>>({});
|
||||||
return (
|
return (
|
||||||
<SubtaskContext.Provider value={{ tasks }}>
|
<SubtaskContext.Provider value={{ tasks, setTasks }}>
|
||||||
{children}
|
{children}
|
||||||
</SubtaskContext.Provider>
|
</SubtaskContext.Provider>
|
||||||
);
|
);
|
||||||
@@ -31,16 +35,19 @@ export function useSubtaskContext() {
|
|||||||
|
|
||||||
export function useSubtask(id: string) {
|
export function useSubtask(id: string) {
|
||||||
const { tasks } = useSubtaskContext();
|
const { tasks } = useSubtaskContext();
|
||||||
return tasks.get(id);
|
return tasks[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useUpdateSubtask() {
|
export function useUpdateSubtask() {
|
||||||
const { tasks } = useSubtaskContext();
|
const { tasks, setTasks } = useSubtaskContext();
|
||||||
const updateSubtask = useCallback(
|
const updateSubtask = useCallback(
|
||||||
(task: Partial<Subtask> & { id: string }) => {
|
(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;
|
return updateSubtask;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
|
import type { AIMessage } from "@langchain/langgraph-sdk";
|
||||||
|
|
||||||
export interface Subtask {
|
export interface Subtask {
|
||||||
id: string;
|
id: string;
|
||||||
status: "in_progress" | "completed" | "failed";
|
status: "in_progress" | "completed" | "failed";
|
||||||
subagent_type: string;
|
subagent_type: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
latestMessage?: AIMessage;
|
||||||
prompt: string;
|
prompt: string;
|
||||||
result?: string;
|
result?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { HumanMessage } from "@langchain/core/messages";
|
import type { HumanMessage } from "@langchain/core/messages";
|
||||||
|
import type { AIMessage } from "@langchain/langgraph-sdk";
|
||||||
import type { ThreadsClient } from "@langchain/langgraph-sdk/client";
|
import type { ThreadsClient } from "@langchain/langgraph-sdk/client";
|
||||||
import { useStream, type UseStream } from "@langchain/langgraph-sdk/react";
|
import { useStream, type UseStream } from "@langchain/langgraph-sdk/react";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
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 type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
||||||
|
|
||||||
import { getAPIClient } from "../api";
|
import { getAPIClient } from "../api";
|
||||||
|
import { useUpdateSubtask } from "../tasks/context";
|
||||||
import { uploadFiles } from "../uploads";
|
import { uploadFiles } from "../uploads";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@@ -25,14 +27,28 @@ export function useThreadStream({
|
|||||||
onFinish?: (state: AgentThreadState) => void;
|
onFinish?: (state: AgentThreadState) => void;
|
||||||
}) {
|
}) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const updateSubtask = useUpdateSubtask();
|
||||||
const thread = useStream<AgentThreadState>({
|
const thread = useStream<AgentThreadState>({
|
||||||
client: getAPIClient(),
|
client: getAPIClient(),
|
||||||
assistantId: "lead_agent",
|
assistantId: "lead_agent",
|
||||||
threadId: isNewThread ? undefined : threadId,
|
threadId: isNewThread ? undefined : threadId,
|
||||||
reconnectOnMount: true,
|
reconnectOnMount: true,
|
||||||
fetchStateHistory: true,
|
fetchStateHistory: true,
|
||||||
onCustomEvent(event) {
|
onCustomEvent(event: unknown) {
|
||||||
console.info(event);
|
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) {
|
||||||
onFinish?.(state.values);
|
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