From a7859de625f9d6ae43349b2629b8b9f26ad84366 Mon Sep 17 00:00:00 2001 From: zxhlyh Date: Wed, 24 Dec 2025 14:15:55 +0800 Subject: [PATCH 1/6] feat: llm node support tools --- .../nodes/llm/components/tools/index.tsx | 29 +++++++++++++++++++ web/i18n/en-US/workflow.ts | 3 ++ 2 files changed, 32 insertions(+) create mode 100644 web/app/components/workflow/nodes/llm/components/tools/index.tsx diff --git a/web/app/components/workflow/nodes/llm/components/tools/index.tsx b/web/app/components/workflow/nodes/llm/components/tools/index.tsx new file mode 100644 index 0000000000..4b6fc3960d --- /dev/null +++ b/web/app/components/workflow/nodes/llm/components/tools/index.tsx @@ -0,0 +1,29 @@ +import { memo } from 'react' +import { useTranslation } from 'react-i18next' +import Tooltip from '@/app/components/base/tooltip' +import Field from '@/app/components/workflow/nodes/_base/components/field' + +const i18nPrefix = 'workflow.nodes.llm' + +const Tools = () => { + const { t } = useTranslation() + + return ( + + + )} + > +
+
Tools
+
+
+ ) +} + +export default memo(Tools) diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index a023ac2b91..b1f394f147 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -560,6 +560,9 @@ const translation = { saveSchema: 'Please finish editing the current field before saving the schema', }, }, + tools: { + title: 'Tools', + }, }, knowledgeRetrieval: { queryVariable: 'Query Variable', From d60348572ed79cc5cdbb03a224ab7938c6976e18 Mon Sep 17 00:00:00 2001 From: zxhlyh Date: Mon, 29 Dec 2025 14:55:26 +0800 Subject: [PATCH 2/6] feat: llm node support tools --- .../nodes/_base/components/layout/box.tsx | 3 + .../_base/components/layout/field-title.tsx | 4 +- .../nodes/_base/components/layout/group.tsx | 3 + .../nodes/_base/hooks/use-node-crud.ts | 26 +++++++++ .../nodes/llm/components/tools/index.tsx | 58 ++++++++++++++----- .../llm/components/tools/max-iterations.tsx | 40 +++++++++++++ .../llm/components/tools/use-node-tools.ts | 23 ++++++++ .../components/workflow/nodes/llm/panel.tsx | 7 +++ .../components/workflow/nodes/llm/types.ts | 15 +++++ 9 files changed, 162 insertions(+), 17 deletions(-) create mode 100644 web/app/components/workflow/nodes/llm/components/tools/max-iterations.tsx create mode 100644 web/app/components/workflow/nodes/llm/components/tools/use-node-tools.ts diff --git a/web/app/components/workflow/nodes/_base/components/layout/box.tsx b/web/app/components/workflow/nodes/_base/components/layout/box.tsx index 62e709efc6..fbff9366fe 100644 --- a/web/app/components/workflow/nodes/_base/components/layout/box.tsx +++ b/web/app/components/workflow/nodes/_base/components/layout/box.tsx @@ -6,17 +6,20 @@ export type BoxProps = { className?: string children?: ReactNode withBorderBottom?: boolean + withBorderTop?: boolean } export const Box = memo(({ className, children, withBorderBottom, + withBorderTop, }: BoxProps) => { return (
diff --git a/web/app/components/workflow/nodes/_base/components/layout/field-title.tsx b/web/app/components/workflow/nodes/_base/components/layout/field-title.tsx index e5e8fe950d..2e581a0b9b 100644 --- a/web/app/components/workflow/nodes/_base/components/layout/field-title.tsx +++ b/web/app/components/workflow/nodes/_base/components/layout/field-title.tsx @@ -9,6 +9,7 @@ import { cn } from '@/utils/classnames' export type FieldTitleProps = { title?: string + className?: string operation?: ReactNode subTitle?: string | ReactNode tooltip?: string @@ -19,6 +20,7 @@ export type FieldTitleProps = { } export const FieldTitle = memo(({ title, + className, operation, subTitle, tooltip, @@ -31,7 +33,7 @@ export const FieldTitle = memo(({ const collapsedMerged = collapsed !== undefined ? collapsed : collapsedLocal return ( -
+
{ diff --git a/web/app/components/workflow/nodes/_base/components/layout/group.tsx b/web/app/components/workflow/nodes/_base/components/layout/group.tsx index 6e35cb7b69..7cca898b44 100644 --- a/web/app/components/workflow/nodes/_base/components/layout/group.tsx +++ b/web/app/components/workflow/nodes/_base/components/layout/group.tsx @@ -6,17 +6,20 @@ export type GroupProps = { className?: string children?: ReactNode withBorderBottom?: boolean + withBorderTop?: boolean } export const Group = memo(({ className, children, withBorderBottom, + withBorderTop, }: GroupProps) => { return (
diff --git a/web/app/components/workflow/nodes/_base/hooks/use-node-crud.ts b/web/app/components/workflow/nodes/_base/hooks/use-node-crud.ts index d1741f0bbb..cb3a898387 100644 --- a/web/app/components/workflow/nodes/_base/hooks/use-node-crud.ts +++ b/web/app/components/workflow/nodes/_base/hooks/use-node-crud.ts @@ -1,4 +1,6 @@ import type { CommonNodeType } from '@/app/components/workflow/types' +import { useCallback } from 'react' +import { useStoreApi } from 'reactflow' import { useNodeDataUpdate } from '@/app/components/workflow/hooks' const useNodeCrud = (id: string, data: CommonNodeType) => { @@ -18,3 +20,27 @@ const useNodeCrud = (id: string, data: CommonNodeType) => { } export default useNodeCrud + +export const useNodeCurdKit = (id: string) => { + const store = useStoreApi() + const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate() + + const getNodeData = useCallback(() => { + const { getNodes } = store.getState() + const nodes = getNodes() + + return nodes.find(node => node.id === id) + }, [store, id]) + + const handleNodeDataUpdate = useCallback((data: Partial>) => { + handleNodeDataUpdateWithSyncDraft({ + id, + data, + }) + }, [id, handleNodeDataUpdateWithSyncDraft]) + + return { + getNodeData, + handleNodeDataUpdate, + } +} diff --git a/web/app/components/workflow/nodes/llm/components/tools/index.tsx b/web/app/components/workflow/nodes/llm/components/tools/index.tsx index 4b6fc3960d..a6e5fefc75 100644 --- a/web/app/components/workflow/nodes/llm/components/tools/index.tsx +++ b/web/app/components/workflow/nodes/llm/components/tools/index.tsx @@ -1,28 +1,54 @@ +import type { ToolValue } from '@/app/components/workflow/block-selector/types' import { memo } from 'react' import { useTranslation } from 'react-i18next' -import Tooltip from '@/app/components/base/tooltip' -import Field from '@/app/components/workflow/nodes/_base/components/field' +import MultipleToolSelector from '@/app/components/plugins/plugin-detail-panel/multiple-tool-selector' +import { BoxGroup } from '@/app/components/workflow/nodes/_base/components/layout' +import MaxIterations from './max-iterations' +import { useNodeTools } from './use-node-tools' const i18nPrefix = 'workflow.nodes.llm' -const Tools = () => { +type ToolsProps = { + nodeId: string + tools?: ToolValue[] + maxIterations?: number +} +const Tools = ({ + nodeId, + tools = [], + maxIterations = 10, +}: ToolsProps) => { const { t } = useTranslation() + const { + handleToolsChange, + handleMaxIterationsChange, + } = useNodeTools(nodeId) return ( - - - )} + -
-
Tools
-
-
+ + + ) } diff --git a/web/app/components/workflow/nodes/llm/components/tools/max-iterations.tsx b/web/app/components/workflow/nodes/llm/components/tools/max-iterations.tsx new file mode 100644 index 0000000000..3f1a4b7130 --- /dev/null +++ b/web/app/components/workflow/nodes/llm/components/tools/max-iterations.tsx @@ -0,0 +1,40 @@ +import { memo } from 'react' +import { InputNumber } from '@/app/components/base/input-number' +import Slider from '@/app/components/base/slider' +import Tooltip from '@/app/components/base/tooltip' + +type MaxIterationsProps = { + value?: number + onChange?: (value: number) => void +} +const MaxIterations = ({ value = 10, onChange }: MaxIterationsProps) => { + return ( +
+
Max Iterations
+ +
+ {})} + min={1} + max={99} + step={1} + /> +
+ {})} + min={1} + max={99} + step={1} + /> +
+ ) +} + +export default memo(MaxIterations) diff --git a/web/app/components/workflow/nodes/llm/components/tools/use-node-tools.ts b/web/app/components/workflow/nodes/llm/components/tools/use-node-tools.ts new file mode 100644 index 0000000000..37b2e8f252 --- /dev/null +++ b/web/app/components/workflow/nodes/llm/components/tools/use-node-tools.ts @@ -0,0 +1,23 @@ +import type { LLMNodeType } from '../../types' +import type { ToolValue } from '@/app/components/workflow/block-selector/types' +import { useNodeCurdKit } from '@/app/components/workflow/nodes/_base/hooks/use-node-crud' + +export const useNodeTools = (nodeId: string) => { + const { handleNodeDataUpdate } = useNodeCurdKit(nodeId) + + const handleToolsChange = (tools: ToolValue[]) => { + handleNodeDataUpdate({ + tools, + }) + } + const handleMaxIterationsChange = (maxIterations: number) => { + handleNodeDataUpdate({ + max_iterations: maxIterations, + }) + } + + return { + handleToolsChange, + handleMaxIterationsChange, + } +} diff --git a/web/app/components/workflow/nodes/llm/panel.tsx b/web/app/components/workflow/nodes/llm/panel.tsx index fd20b1a2bb..0f9d51cf5b 100644 --- a/web/app/components/workflow/nodes/llm/panel.tsx +++ b/web/app/components/workflow/nodes/llm/panel.tsx @@ -22,6 +22,7 @@ import VarReferencePicker from '../_base/components/variable/var-reference-picke import ConfigPrompt from './components/config-prompt' import ReasoningFormatConfig from './components/reasoning-format-config' import StructureOutput from './components/structure-output' +import Tools from './components/tools' import useConfig from './use-config' const i18nPrefix = 'workflow.nodes.llm' @@ -233,6 +234,12 @@ const Panel: FC> = ({ )} + + {/* Vision: GPT4-vision and so on */} + settings?: Record + extra?: Record +} + export type LLMNodeType = CommonNodeType & { model: ModelConfig prompt_template: PromptItem[] | PromptItem @@ -18,6 +31,8 @@ export type LLMNodeType = CommonNodeType & { structured_output_enabled?: boolean structured_output?: StructuredOutput reasoning_format?: 'tagged' | 'separated' + tools?: ToolValue[] + max_iterations?: number } export enum Type { From bbd11c9e89fa8b7036a9a3bac5d78cb3c2caca7b Mon Sep 17 00:00:00 2001 From: zxhlyh Date: Tue, 30 Dec 2025 10:40:01 +0800 Subject: [PATCH 3/6] feat: llm node support tools --- .../components/workflow/run/llm-log/index.tsx | 2 + .../workflow/run/llm-log/llm-log-trigger.tsx | 41 +++++++++++++++++ .../workflow/run/llm-log/llm-result-panel.tsx | 46 +++++++++++++++++++ web/app/components/workflow/run/node.tsx | 10 ++++ .../components/workflow/run/result-panel.tsx | 12 +++++ .../workflow/run/special-result-panel.tsx | 17 +++++++ web/types/workflow.ts | 18 ++++++++ 7 files changed, 146 insertions(+) create mode 100644 web/app/components/workflow/run/llm-log/index.tsx create mode 100644 web/app/components/workflow/run/llm-log/llm-log-trigger.tsx create mode 100644 web/app/components/workflow/run/llm-log/llm-result-panel.tsx diff --git a/web/app/components/workflow/run/llm-log/index.tsx b/web/app/components/workflow/run/llm-log/index.tsx new file mode 100644 index 0000000000..26851aa68a --- /dev/null +++ b/web/app/components/workflow/run/llm-log/index.tsx @@ -0,0 +1,2 @@ +export { default as LLMLogTrigger } from './llm-log-trigger' +export { default as LLMResultPanel } from './llm-result-panel' diff --git a/web/app/components/workflow/run/llm-log/llm-log-trigger.tsx b/web/app/components/workflow/run/llm-log/llm-log-trigger.tsx new file mode 100644 index 0000000000..4714fe6330 --- /dev/null +++ b/web/app/components/workflow/run/llm-log/llm-log-trigger.tsx @@ -0,0 +1,41 @@ +import type { NodeTracing } from '@/types/workflow' +import { + RiArrowRightSLine, + RiRestartFill, +} from '@remixicon/react' +import { useTranslation } from 'react-i18next' +import Button from '@/app/components/base/button' + +type LLMLogTriggerProps = { + nodeInfo: NodeTracing + onShowLLMDetail: (detail: NodeTracing[]) => void +} +const LLMLogTrigger = ({ + nodeInfo, + onShowLLMDetail, +}: LLMLogTriggerProps) => { + const { t } = useTranslation() + const { retryDetail } = nodeInfo + + const handleShowRetryResultList = (e: React.MouseEvent) => { + e.stopPropagation() + e.nativeEvent.stopImmediatePropagation() + onShowLLMDetail(retryDetail || []) + } + + return ( + + ) +} + +export default LLMLogTrigger diff --git a/web/app/components/workflow/run/llm-log/llm-result-panel.tsx b/web/app/components/workflow/run/llm-log/llm-result-panel.tsx new file mode 100644 index 0000000000..8f42486df1 --- /dev/null +++ b/web/app/components/workflow/run/llm-log/llm-result-panel.tsx @@ -0,0 +1,46 @@ +'use client' + +import type { FC } from 'react' +import type { NodeTracing } from '@/types/workflow' +import { + RiArrowLeftLine, +} from '@remixicon/react' +import { memo } from 'react' +import { useTranslation } from 'react-i18next' +import TracingPanel from '../tracing-panel' + +type Props = { + list: NodeTracing[] + onBack: () => void +} + +const LLMResultPanel: FC = ({ + list, + onBack, +}) => { + const { t } = useTranslation() + + return ( +
+
{ + e.stopPropagation() + e.nativeEvent.stopImmediatePropagation() + onBack() + }} + > + + {t('singleRun.back', { ns: 'workflow' })} +
+ ({ + ...item, + title: `${t('nodes.common.retry.retry', { ns: 'workflow' })} ${index + 1}`, + }))} + className="bg-background-section-burn" + /> +
+ ) +} +export default memo(LLMResultPanel) diff --git a/web/app/components/workflow/run/node.tsx b/web/app/components/workflow/run/node.tsx index 8611a98d3b..d4316f285f 100644 --- a/web/app/components/workflow/run/node.tsx +++ b/web/app/components/workflow/run/node.tsx @@ -29,6 +29,7 @@ import { BlockEnum } from '../types' import LargeDataAlert from '../variable-inspect/large-data-alert' import { AgentLogTrigger } from './agent-log' import { IterationLogTrigger } from './iteration-log' +import { LLMLogTrigger } from './llm-log' import { LoopLogTrigger } from './loop-log' import { RetryLogTrigger } from './retry-log' @@ -43,6 +44,7 @@ type Props = { onShowLoopDetail?: (detail: NodeTracing[][], loopDurationMap: LoopDurationMap, loopVariableMap: LoopVariableMap) => void onShowRetryDetail?: (detail: NodeTracing[]) => void onShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void + onShowLLMDetail?: (detail: NodeTracing[]) => void notShowIterationNav?: boolean notShowLoopNav?: boolean } @@ -58,6 +60,7 @@ const NodePanel: FC = ({ onShowLoopDetail, onShowRetryDetail, onShowAgentOrToolLog, + onShowLLMDetail, notShowIterationNav, notShowLoopNav, }) => { @@ -96,6 +99,7 @@ const NodePanel: FC = ({ const isRetryNode = hasRetryNode(nodeInfo.node_type) && !!nodeInfo.retryDetail?.length const isAgentNode = nodeInfo.node_type === BlockEnum.Agent && !!nodeInfo.agentLog?.length const isToolNode = nodeInfo.node_type === BlockEnum.Tool && !!nodeInfo.agentLog?.length + const isLLMNode = nodeInfo.node_type === BlockEnum.LLM && !!nodeInfo.generation_detail const inputsTitle = useMemo(() => { let text = t('common.input', { ns: 'workflow' }) @@ -193,6 +197,12 @@ const NodePanel: FC = ({ onShowRetryResultList={onShowRetryDetail} /> )} + {isLLMNode && onShowLLMDetail && ( + + )} { (isAgentNode || isToolNode) && onShowAgentOrToolLog && ( void onShowRetryDetail?: (detail: NodeTracing[]) => void handleShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void + onShowLLMDetail?: (detail: NodeTracing[]) => void } const ResultPanel: FC = ({ @@ -71,6 +73,7 @@ const ResultPanel: FC = ({ handleShowLoopResultList, onShowRetryDetail, handleShowAgentOrToolLog, + onShowLLMDetail, }) => { const { t } = useTranslation() const isIterationNode = nodeInfo?.node_type === BlockEnum.Iteration && !!nodeInfo?.details?.length @@ -78,6 +81,7 @@ const ResultPanel: FC = ({ const isRetryNode = hasRetryNode(nodeInfo?.node_type) && !!nodeInfo?.retryDetail?.length const isAgentNode = nodeInfo?.node_type === BlockEnum.Agent && !!nodeInfo?.agentLog?.length const isToolNode = nodeInfo?.node_type === BlockEnum.Tool && !!nodeInfo?.agentLog?.length + const isLLMNode = nodeInfo?.node_type === BlockEnum.LLM && !!nodeInfo?.generation_detail return (
@@ -116,6 +120,14 @@ const ResultPanel: FC = ({ /> ) } + { + isLLMNode && onShowLLMDetail && ( + + ) + } { (isAgentNode || isToolNode) && handleShowAgentOrToolLog && ( handleShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void + + showLLMDetail?: boolean + setShowLLMDetailFalse?: () => void + llmResultList?: NodeTracing[] } const SpecialResultPanel = ({ showRetryDetail, @@ -49,6 +54,10 @@ const SpecialResultPanel = ({ agentOrToolLogItemStack, agentOrToolLogListMap, handleShowAgentOrToolLog, + + showLLMDetail, + setShowLLMDetailFalse, + llmResultList, }: SpecialResultPanelProps) => { return (
{ @@ -64,6 +73,14 @@ const SpecialResultPanel = ({ /> ) } + { + !!showLLMDetail && !!llmResultList?.length && setShowLLMDetailFalse && ( + + ) + } { showIteratingDetail && !!iterationResultList?.length && setShowIteratingDetailFalse && ( Date: Sun, 4 Jan 2026 18:03:47 +0800 Subject: [PATCH 4/6] feat: llm node support tools --- .../base/chat/chat/answer/index.tsx | 7 ++ .../chat/chat/answer/tool-calls/index.tsx | 19 +++++ .../base/chat/chat/answer/tool-calls/item.tsx | 75 +++++++++++++++++++ web/app/components/base/chat/chat/type.ts | 12 +++ .../workflow/panel/debug-and-preview/hooks.ts | 46 +++++++++++- web/service/base.ts | 14 ++++ 6 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 web/app/components/base/chat/chat/answer/tool-calls/index.tsx create mode 100644 web/app/components/base/chat/chat/answer/tool-calls/item.tsx diff --git a/web/app/components/base/chat/chat/answer/index.tsx b/web/app/components/base/chat/chat/answer/index.tsx index 04b884388e..f6a9e896f9 100644 --- a/web/app/components/base/chat/chat/answer/index.tsx +++ b/web/app/components/base/chat/chat/answer/index.tsx @@ -21,6 +21,7 @@ import BasicContent from './basic-content' import More from './more' import Operation from './operation' import SuggestedQuestions from './suggested-questions' +import ToolCalls from './tool-calls' import WorkflowProcessItem from './workflow-process' type AnswerProps = { @@ -61,6 +62,7 @@ const Answer: FC = ({ workflowProcess, allFiles, message_files, + toolCalls, } = item const hasAgentThoughts = !!agent_thoughts?.length @@ -154,6 +156,11 @@ const Answer: FC = ({ /> ) } + { + !!toolCalls?.length && ( + + ) + } { responding && contentIsEmpty && !hasAgentThoughts && (
diff --git a/web/app/components/base/chat/chat/answer/tool-calls/index.tsx b/web/app/components/base/chat/chat/answer/tool-calls/index.tsx new file mode 100644 index 0000000000..9de41233aa --- /dev/null +++ b/web/app/components/base/chat/chat/answer/tool-calls/index.tsx @@ -0,0 +1,19 @@ +import type { ToolCallItem } from '../../type' +import ToolCallsItem from './item' + +type ToolCallsProps = { + toolCalls: ToolCallItem[] +} +const ToolCalls = ({ + toolCalls, +}: ToolCallsProps) => { + return ( +
+ {toolCalls.map((toolCall: ToolCallItem) => ( + + ))} +
+ ) +} + +export default ToolCalls diff --git a/web/app/components/base/chat/chat/answer/tool-calls/item.tsx b/web/app/components/base/chat/chat/answer/tool-calls/item.tsx new file mode 100644 index 0000000000..539f1a83ba --- /dev/null +++ b/web/app/components/base/chat/chat/answer/tool-calls/item.tsx @@ -0,0 +1,75 @@ +import type { ToolCallItem } from '../../type' +import { + RiArrowDownSLine, +} from '@remixicon/react' +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' +import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' + +type ToolCallsItemProps = { + payload: ToolCallItem +} +const ToolCallsItem = ({ + payload, +}: ToolCallsItemProps) => { + const { t } = useTranslation() + const [expand, setExpand] = useState(false) + return ( +
+
setExpand(!expand)}> +
{payload.tool_name}
+ { + !!payload.tool_elapsed_time && ( +
+ {payload.tool_elapsed_time?.toFixed(3)} + s +
+ ) + } + +
+ { + expand && ( +
+
+ { + payload.is_thought && ( +
{payload.tool_output}
+ ) + } + { + !payload.is_thought && ( + {t('common.input', { ns: 'workflow' })}
} + language={CodeLanguage.json} + value={JSON.parse(payload.tool_arguments || '{}')} + isJSONStringifyBeauty + /> + ) + } + { + !payload.is_thought && ( + {t('common.output', { ns: 'workflow' })}
} + language={CodeLanguage.json} + value={{ + answer: payload.tool_output, + }} + isJSONStringifyBeauty + /> + ) + } +
+ ) + } +
+ ) +} + +export default ToolCallsItem diff --git a/web/app/components/base/chat/chat/type.ts b/web/app/components/base/chat/chat/type.ts index 291b0ae064..56ffdc7e06 100644 --- a/web/app/components/base/chat/chat/type.ts +++ b/web/app/components/base/chat/chat/type.ts @@ -64,6 +64,17 @@ export type CitationItem = { word_count: number } +export type ToolCallItem = { + is_thought?: boolean + tool_call_id?: string + tool_name?: string + tool_arguments?: string + tool_files?: string[] + tool_error?: string + tool_output?: string + tool_elapsed_time?: number +} + export type IChatItem = { id: string content: string @@ -104,6 +115,7 @@ export type IChatItem = { siblingIndex?: number prevSibling?: string nextSibling?: string + toolCalls?: ToolCallItem[] } export type Metadata = { diff --git a/web/app/components/workflow/panel/debug-and-preview/hooks.ts b/web/app/components/workflow/panel/debug-and-preview/hooks.ts index 2c46833df8..068be40023 100644 --- a/web/app/components/workflow/panel/debug-and-preview/hooks.ts +++ b/web/app/components/workflow/panel/debug-and-preview/hooks.ts @@ -270,9 +270,53 @@ export const useChat = ( handleRun( bodyParams, { - onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId, taskId }: any) => { + onData: (message: string, isFirstMessage: boolean, { + conversationId: newConversationId, + messageId, + taskId, + chunk_type, + tool_call_id, + tool_name, + tool_arguments, + tool_files, + tool_error, + tool_elapsed_time, + }: any) => { responseItem.content = responseItem.content + message + if (chunk_type === 'tool_call') { + if (!responseItem.toolCalls) + responseItem.toolCalls = [] + responseItem.toolCalls?.push({ + tool_call_id, + tool_name, + tool_arguments, + tool_files, + tool_error, + tool_elapsed_time, + }) + } + + if (chunk_type === 'tool_result') { + const currentToolCallIndex = responseItem.toolCalls?.findIndex(item => item.tool_call_id === tool_call_id) ?? -1 + + if (currentToolCallIndex > -1) + responseItem.toolCalls![currentToolCallIndex].tool_output = message + } + + if (chunk_type === 'thought_start') { + responseItem.toolCalls?.push({ + is_thought: true, + tool_elapsed_time, + }) + } + + if (chunk_type === 'thought') { + const currentThoughtIndex = responseItem.toolCalls?.findIndex(item => item.is_thought) ?? -1 + if (currentThoughtIndex > -1) + responseItem.toolCalls![currentThoughtIndex].tool_output = message + } + if (messageId && !hasSetResponseId) { questionItem.id = `question-${messageId}` responseItem.id = messageId diff --git a/web/service/base.ts b/web/service/base.ts index d9f3dba53a..bbb05cf357 100644 --- a/web/service/base.ts +++ b/web/service/base.ts @@ -40,6 +40,13 @@ export type IOnDataMoreInfo = { messageId: string errorMessage?: string errorCode?: string + chunk_type?: 'text' | 'tool_call' | 'tool_result' | 'thought' | 'thought_start' + tool_call_id?: string + tool_name?: string + tool_arguments?: string + tool_files?: string[] + tool_error?: string + tool_elapsed_time?: number } export type IOnData = (message: string, isFirstMessage: boolean, moreInfo: IOnDataMoreInfo) => void @@ -234,6 +241,13 @@ export const handleStream = ( conversationId: bufferObj.conversation_id, taskId: bufferObj.task_id, messageId: bufferObj.id, + chunk_type: bufferObj.chunk_type, + tool_call_id: bufferObj.tool_call_id, + tool_name: bufferObj.tool_name, + tool_arguments: bufferObj.tool_arguments, + tool_files: bufferObj.tool_files, + tool_error: bufferObj.tool_error, + tool_elapsed_time: bufferObj.tool_elapsed_time, }) isFirstMessage = false } From 1d93f41fcf77437dff1b0548648c6448cd691da6 Mon Sep 17 00:00:00 2001 From: zxhlyh Date: Wed, 7 Jan 2026 16:28:41 +0800 Subject: [PATCH 5/6] feat: llm node support tools --- .../chat/chat/answer/tool-calls/index.tsx | 12 +++-- .../chat/chat/answer/workflow-process.tsx | 2 +- web/app/components/base/chat/chat/type.ts | 14 ++--- .../icons/assets/vender/workflow/thinking.svg | 4 ++ .../icons/src/vender/workflow/Thinking.json | 35 +++++++++++++ .../icons/src/vender/workflow/Thinking.tsx | 20 ++++++++ .../base/icons/src/vender/workflow/index.ts | 1 + .../model-selector/feature-icon.tsx | 39 ++++++++++---- .../model-selector/popup-item.tsx | 10 +++- .../workflow/panel/debug-and-preview/hooks.ts | 19 +++++-- web/app/components/workflow/run/hooks.ts | 20 +++++++- web/app/components/workflow/run/index.tsx | 4 +- .../workflow/run/llm-log/llm-log-trigger.tsx | 18 +++---- .../workflow/run/llm-log/llm-result-panel.tsx | 35 +++++++++---- .../run/llm-log/tool-call-item.tsx} | 51 +++++++++++++------ web/app/components/workflow/run/node.tsx | 5 +- .../components/workflow/run/result-panel.tsx | 5 +- .../workflow/run/special-result-panel.tsx | 3 +- .../components/workflow/run/tracing-panel.tsx | 10 ++++ web/i18n/en-US/workflow.json | 1 + web/i18n/zh-Hans/workflow.json | 1 + web/service/base.ts | 12 ++++- web/types/workflow.ts | 37 +++++++++++++- 23 files changed, 282 insertions(+), 76 deletions(-) create mode 100644 web/app/components/base/icons/assets/vender/workflow/thinking.svg create mode 100644 web/app/components/base/icons/src/vender/workflow/Thinking.json create mode 100644 web/app/components/base/icons/src/vender/workflow/Thinking.tsx rename web/app/components/{base/chat/chat/answer/tool-calls/item.tsx => workflow/run/llm-log/tool-call-item.tsx} (55%) diff --git a/web/app/components/base/chat/chat/answer/tool-calls/index.tsx b/web/app/components/base/chat/chat/answer/tool-calls/index.tsx index 9de41233aa..f51daa7497 100644 --- a/web/app/components/base/chat/chat/answer/tool-calls/index.tsx +++ b/web/app/components/base/chat/chat/answer/tool-calls/index.tsx @@ -1,5 +1,5 @@ -import type { ToolCallItem } from '../../type' -import ToolCallsItem from './item' +import type { ToolCallItem } from '@/types/workflow' +import ToolCallItemComponent from '@/app/components/workflow/run/llm-log/tool-call-item' type ToolCallsProps = { toolCalls: ToolCallItem[] @@ -8,9 +8,13 @@ const ToolCalls = ({ toolCalls, }: ToolCallsProps) => { return ( -
+
{toolCalls.map((toolCall: ToolCallItem) => ( - + ))}
) diff --git a/web/app/components/base/chat/chat/answer/workflow-process.tsx b/web/app/components/base/chat/chat/answer/workflow-process.tsx index fd8c1daf3e..12f0001bd4 100644 --- a/web/app/components/base/chat/chat/answer/workflow-process.tsx +++ b/web/app/components/base/chat/chat/answer/workflow-process.tsx @@ -45,7 +45,7 @@ const WorkflowProcessItem = ({ return (
+ + + diff --git a/web/app/components/base/icons/src/vender/workflow/Thinking.json b/web/app/components/base/icons/src/vender/workflow/Thinking.json new file mode 100644 index 0000000000..6fe807775d --- /dev/null +++ b/web/app/components/base/icons/src/vender/workflow/Thinking.json @@ -0,0 +1,35 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "12", + "height": "14", + "viewBox": "0 0 12 14", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M2 9.49479C0.782372 8.51826 0 7.01768 0 5.33333C0 2.38782 2.38782 0 5.33333 0C8.20841 0 10.5503 2.27504 10.6608 5.12305L11.888 6.96354C12.0843 7.25794 12.0161 7.65424 11.7331 7.86654L10.6667 8.66602V10C10.6667 10.7364 10.0697 11.3333 9.33333 11.3333H8V13.3333H6.66667V10.6667C6.66667 10.2985 6.96514 10 7.33333 10H9.33333V8.33333C9.33333 8.12349 9.43239 7.92603 9.60026 7.80013L10.4284 7.17838L9.44531 5.70312C9.3723 5.59361 9.33333 5.46495 9.33333 5.33333C9.33333 3.1242 7.54248 1.33333 5.33333 1.33333C3.1242 1.33333 1.33333 3.1242 1.33333 5.33333C1.33333 6.69202 2.0103 7.89261 3.04818 8.61654C3.2269 8.74119 3.33329 8.94552 3.33333 9.16341V13.3333H2V9.49479Z", + "fill": "currentColor" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M6.04367 4.24012L5.6504 3.21778C5.59993 3.08657 5.47393 3 5.33333 3C5.19273 3 5.06673 3.08657 5.01627 3.21778L4.62303 4.24012C4.55531 4.41618 4.41618 4.55531 4.24012 4.62303L3.21778 5.01624C3.08657 5.0667 3 5.19276 3 5.33333C3 5.47393 3.08657 5.59993 3.21778 5.6504L4.24012 6.04367C4.41618 6.11133 4.55531 6.25047 4.62303 6.42653L5.01627 7.44887C5.06673 7.58007 5.19273 7.66667 5.33333 7.66667C5.47393 7.66667 5.59993 7.58007 5.6504 7.44887L6.04367 6.42653C6.11133 6.25047 6.25047 6.11133 6.42653 6.04367L7.44887 5.6504C7.58007 5.59993 7.66667 5.47393 7.66667 5.33333C7.66667 5.19276 7.58007 5.0667 7.44887 5.01624L6.42653 4.62303C6.25047 4.55531 6.11133 4.41618 6.04367 4.24012Z", + "fill": "currentColor" + }, + "children": [] + } + ] + }, + "name": "Thinking" +} diff --git a/web/app/components/base/icons/src/vender/workflow/Thinking.tsx b/web/app/components/base/icons/src/vender/workflow/Thinking.tsx new file mode 100644 index 0000000000..dbe3716a24 --- /dev/null +++ b/web/app/components/base/icons/src/vender/workflow/Thinking.tsx @@ -0,0 +1,20 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import type { IconData } from '@/app/components/base/icons/IconBase' +import * as React from 'react' +import IconBase from '@/app/components/base/icons/IconBase' +import data from './Thinking.json' + +const Icon = ( + { + ref, + ...props + }: React.SVGProps & { + ref?: React.RefObject> + }, +) => + +Icon.displayName = 'Thinking' + +export default Icon diff --git a/web/app/components/base/icons/src/vender/workflow/index.ts b/web/app/components/base/icons/src/vender/workflow/index.ts index ec8dce100d..b559d9c6aa 100644 --- a/web/app/components/base/icons/src/vender/workflow/index.ts +++ b/web/app/components/base/icons/src/vender/workflow/index.ts @@ -24,6 +24,7 @@ export { default as ParameterExtractor } from './ParameterExtractor' export { default as QuestionClassifier } from './QuestionClassifier' export { default as Schedule } from './Schedule' export { default as TemplatingTransform } from './TemplatingTransform' +export { default as Thinking } from './Thinking' export { default as TriggerAll } from './TriggerAll' export { default as VariableX } from './VariableX' export { default as WebhookLine } from './WebhookLine' diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/feature-icon.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/feature-icon.tsx index 8e6e4567ff..5a3ada80ba 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-selector/feature-icon.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-selector/feature-icon.tsx @@ -2,6 +2,7 @@ import type { FC } from 'react' import { RiFileTextLine, RiFilmAiLine, + RiHammerLine, RiImageCircleAiLine, RiVoiceAiFill, } from '@remixicon/react' @@ -38,17 +39,33 @@ const FeatureIcon: FC = ({ // ) // } - // if (feature === ModelFeatureEnum.toolCall) { - // return ( - // - // - // - // - // - // ) - // } + if (feature === ModelFeatureEnum.toolCall) { + if (showFeaturesLabel) { + return ( + + + {ModelFeatureTextEnum.toolCall} + + ) + } + + return ( + +
+ + + +
+
+ ) + } // if (feature === ModelFeatureEnum.multiToolCall) { // return ( diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx index bfa6f8d867..afda9846de 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx @@ -96,6 +96,14 @@ const PopupItem: FC = ({
{currentProvider?.description?.[language] || currentProvider?.description?.en_US}
)} */}
+ { + modelItem.features?.includes(ModelFeatureEnum.toolCall) && ( + + ) + } {modelItem.model_type && ( {modelTypeFormat(modelItem.model_type)} @@ -118,7 +126,7 @@ const PopupItem: FC = ({
{t('model.capabilities', { ns: 'common' })}
- {modelItem.features?.map(feature => ( + {modelItem.features?.filter(feature => feature !== ModelFeatureEnum.toolCall).map(feature => ( item.is_thought) ?? -1 + // if (currentThoughtIndex > -1) + // responseItem.toolCalls![currentThoughtIndex].tool_output = message + } + if (chunk_type === 'thought') { - const currentThoughtIndex = responseItem.toolCalls?.findIndex(item => item.is_thought) ?? -1 - if (currentThoughtIndex > -1) - responseItem.toolCalls![currentThoughtIndex].tool_output = message + console.log(message, 'xx3') } if (messageId && !hasSetResponseId) { diff --git a/web/app/components/workflow/run/hooks.ts b/web/app/components/workflow/run/hooks.ts index 593836f5b3..9aa6d253a1 100644 --- a/web/app/components/workflow/run/hooks.ts +++ b/web/app/components/workflow/run/hooks.ts @@ -1,6 +1,7 @@ import type { AgentLogItemWithChildren, IterationDurationMap, + LLMTraceItem, LoopDurationMap, LoopVariableMap, NodeTracing, @@ -79,8 +80,18 @@ export const useLogs = () => { } }, [setAgentOrToolLogItemStack, setAgentOrToolLogListMap]) + const [showLLMDetail, { + setTrue: setShowLLMDetailTrue, + setFalse: setShowLLMDetailFalse, + }] = useBoolean(false) + const [llmResultList, setLLMResultList] = useState([]) + const handleShowLLMDetail = useCallback((detail: LLMTraceItem[]) => { + setShowLLMDetailTrue() + setLLMResultList(detail) + }, [setShowLLMDetailTrue, setLLMResultList]) + return { - showSpecialResultPanel: showRetryDetail || showIteratingDetail || showLoopingDetail || !!agentOrToolLogItemStack.length, + showSpecialResultPanel: showRetryDetail || showIteratingDetail || showLoopingDetail || !!agentOrToolLogItemStack.length || showLLMDetail, showRetryDetail, setShowRetryDetailTrue, setShowRetryDetailFalse, @@ -111,5 +122,12 @@ export const useLogs = () => { agentOrToolLogItemStack, agentOrToolLogListMap, handleShowAgentOrToolLog, + + showLLMDetail, + setShowLLMDetailTrue, + setShowLLMDetailFalse, + llmResultList, + setLLMResultList, + handleShowLLMDetail, } } diff --git a/web/app/components/workflow/run/index.tsx b/web/app/components/workflow/run/index.tsx index 51f641f265..9590dc24d7 100644 --- a/web/app/components/workflow/run/index.tsx +++ b/web/app/components/workflow/run/index.tsx @@ -153,7 +153,7 @@ const RunPanel: FC = ({
{/* panel detail */} -
+
{loading && (
@@ -192,7 +192,7 @@ const RunPanel: FC = ({ )} {!loading && currentTab === 'TRACING' && ( )} diff --git a/web/app/components/workflow/run/llm-log/llm-log-trigger.tsx b/web/app/components/workflow/run/llm-log/llm-log-trigger.tsx index 4714fe6330..1d65f754b4 100644 --- a/web/app/components/workflow/run/llm-log/llm-log-trigger.tsx +++ b/web/app/components/workflow/run/llm-log/llm-log-trigger.tsx @@ -1,37 +1,37 @@ -import type { NodeTracing } from '@/types/workflow' +import type { LLMTraceItem, NodeTracing } from '@/types/workflow' import { RiArrowRightSLine, - RiRestartFill, } from '@remixicon/react' import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' +import { Thinking } from '@/app/components/base/icons/src/vender/workflow' type LLMLogTriggerProps = { nodeInfo: NodeTracing - onShowLLMDetail: (detail: NodeTracing[]) => void + onShowLLMDetail: (detail: LLMTraceItem[]) => void } const LLMLogTrigger = ({ nodeInfo, onShowLLMDetail, }: LLMLogTriggerProps) => { const { t } = useTranslation() - const { retryDetail } = nodeInfo + const llmTrace = nodeInfo?.execution_metadata?.llm_trace || [] - const handleShowRetryResultList = (e: React.MouseEvent) => { + const handleShowLLMDetail = (e: React.MouseEvent) => { e.stopPropagation() e.nativeEvent.stopImmediatePropagation() - onShowLLMDetail(retryDetail || []) + onShowLLMDetail(llmTrace || []) } return ( diff --git a/web/app/components/workflow/run/llm-log/llm-result-panel.tsx b/web/app/components/workflow/run/llm-log/llm-result-panel.tsx index 8f42486df1..0223a4cdec 100644 --- a/web/app/components/workflow/run/llm-log/llm-result-panel.tsx +++ b/web/app/components/workflow/run/llm-log/llm-result-panel.tsx @@ -1,16 +1,19 @@ 'use client' import type { FC } from 'react' -import type { NodeTracing } from '@/types/workflow' +import type { + LLMTraceItem, + ToolCallItem, +} from '@/types/workflow' import { RiArrowLeftLine, } from '@remixicon/react' import { memo } from 'react' import { useTranslation } from 'react-i18next' -import TracingPanel from '../tracing-panel' +import ToolCallItemComponent from '@/app/components/workflow/run/llm-log/tool-call-item' type Props = { - list: NodeTracing[] + list: LLMTraceItem[] onBack: () => void } @@ -19,6 +22,18 @@ const LLMResultPanel: FC = ({ onBack, }) => { const { t } = useTranslation() + const formattedList = list.map(item => ({ + type: item.type, + tool_call_id: item.provider, + tool_name: item.name, + tool_arguments: item.type === 'tool' ? item.output.arguments : undefined, + tool_icon: item.icon, + tool_icon_dark: item.icon_dark, + tool_files: [], + tool_error: item.error, + tool_output: item.type === 'tool' ? item.output.output : item.output, + tool_elapsed_time: item.duration, + })) return (
@@ -33,13 +48,13 @@ const LLMResultPanel: FC = ({ {t('singleRun.back', { ns: 'workflow' })}
- ({ - ...item, - title: `${t('nodes.common.retry.retry', { ns: 'workflow' })} ${index + 1}`, - }))} - className="bg-background-section-burn" - /> +
+ { + formattedList.map((item, index) => ( + + )) + } +
) } diff --git a/web/app/components/base/chat/chat/answer/tool-calls/item.tsx b/web/app/components/workflow/run/llm-log/tool-call-item.tsx similarity index 55% rename from web/app/components/base/chat/chat/answer/tool-calls/item.tsx rename to web/app/components/workflow/run/llm-log/tool-call-item.tsx index 539f1a83ba..026f85df9a 100644 --- a/web/app/components/base/chat/chat/answer/tool-calls/item.tsx +++ b/web/app/components/workflow/run/llm-log/tool-call-item.tsx @@ -1,30 +1,40 @@ -import type { ToolCallItem } from '../../type' +import type { ToolCallItem } from '@/types/workflow' import { RiArrowDownSLine, } from '@remixicon/react' import { useState } from 'react' import { useTranslation } from 'react-i18next' +import BlockIcon from '@/app/components/workflow/block-icon' import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' +import { BlockEnum } from '@/app/components/workflow/types' +import { cn } from '@/utils/classnames' -type ToolCallsItemProps = { +type ToolCallItemComponentProps = { + className?: string payload: ToolCallItem } -const ToolCallsItem = ({ +const ToolCallItemComponent = ({ + className, payload, -}: ToolCallsItemProps) => { +}: ToolCallItemComponentProps) => { const { t } = useTranslation() const [expand, setExpand] = useState(false) return (
setExpand(!expand)}> -
{payload.tool_name}
+ +
{payload.tool_name}
{ !!payload.tool_elapsed_time && (
- {payload.tool_elapsed_time?.toFixed(3)} + {payload.tool_elapsed_time?.toFixed(1)} s
) @@ -36,31 +46,40 @@ const ToolCallsItem = ({
{ - payload.is_thought && ( + payload.type === 'thought' && typeof payload.tool_output === 'string' && (
{payload.tool_output}
) } { - !payload.is_thought && ( + payload.type === 'model' && ( {t('common.input', { ns: 'workflow' })}
} + title={
{t('common.data', { ns: 'workflow' })}
} language={CodeLanguage.json} - value={JSON.parse(payload.tool_arguments || '{}')} + value={payload.tool_output} isJSONStringifyBeauty /> ) } { - !payload.is_thought && ( + payload.type === 'tool' && ( + {t('common.input', { ns: 'workflow' })}
} + language={CodeLanguage.json} + value={payload.tool_arguments} + isJSONStringifyBeauty + /> + ) + } + { + payload.type === 'tool' && ( {t('common.output', { ns: 'workflow' })}
} language={CodeLanguage.json} - value={{ - answer: payload.tool_output, - }} + value={payload.tool_output} isJSONStringifyBeauty /> ) @@ -72,4 +91,4 @@ const ToolCallsItem = ({ ) } -export default ToolCallsItem +export default ToolCallItemComponent diff --git a/web/app/components/workflow/run/node.tsx b/web/app/components/workflow/run/node.tsx index d4316f285f..ffec4392b6 100644 --- a/web/app/components/workflow/run/node.tsx +++ b/web/app/components/workflow/run/node.tsx @@ -3,6 +3,7 @@ import type { FC } from 'react' import type { AgentLogItemWithChildren, IterationDurationMap, + LLMTraceItem, LoopDurationMap, LoopVariableMap, NodeTracing, @@ -44,7 +45,7 @@ type Props = { onShowLoopDetail?: (detail: NodeTracing[][], loopDurationMap: LoopDurationMap, loopVariableMap: LoopVariableMap) => void onShowRetryDetail?: (detail: NodeTracing[]) => void onShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void - onShowLLMDetail?: (detail: NodeTracing[]) => void + onShowLLMDetail?: (detail: LLMTraceItem[]) => void notShowIterationNav?: boolean notShowLoopNav?: boolean } @@ -99,7 +100,7 @@ const NodePanel: FC = ({ const isRetryNode = hasRetryNode(nodeInfo.node_type) && !!nodeInfo.retryDetail?.length const isAgentNode = nodeInfo.node_type === BlockEnum.Agent && !!nodeInfo.agentLog?.length const isToolNode = nodeInfo.node_type === BlockEnum.Tool && !!nodeInfo.agentLog?.length - const isLLMNode = nodeInfo.node_type === BlockEnum.LLM && !!nodeInfo.generation_detail + const isLLMNode = nodeInfo.node_type === BlockEnum.LLM && !!nodeInfo.execution_metadata?.llm_trace?.length const inputsTitle = useMemo(() => { let text = t('common.input', { ns: 'workflow' }) diff --git a/web/app/components/workflow/run/result-panel.tsx b/web/app/components/workflow/run/result-panel.tsx index 8bc55a504b..00e8b028d1 100644 --- a/web/app/components/workflow/run/result-panel.tsx +++ b/web/app/components/workflow/run/result-panel.tsx @@ -2,6 +2,7 @@ import type { FC } from 'react' import type { AgentLogItemWithChildren, + LLMTraceItem, NodeTracing, } from '@/types/workflow' import { useTranslation } from 'react-i18next' @@ -46,7 +47,7 @@ export type ResultPanelProps = { handleShowLoopResultList?: (detail: NodeTracing[][], loopDurationMap: any) => void onShowRetryDetail?: (detail: NodeTracing[]) => void handleShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void - onShowLLMDetail?: (detail: NodeTracing[]) => void + onShowLLMDetail?: (detail: LLMTraceItem[]) => void } const ResultPanel: FC = ({ @@ -81,7 +82,7 @@ const ResultPanel: FC = ({ const isRetryNode = hasRetryNode(nodeInfo?.node_type) && !!nodeInfo?.retryDetail?.length const isAgentNode = nodeInfo?.node_type === BlockEnum.Agent && !!nodeInfo?.agentLog?.length const isToolNode = nodeInfo?.node_type === BlockEnum.Tool && !!nodeInfo?.agentLog?.length - const isLLMNode = nodeInfo?.node_type === BlockEnum.LLM && !!nodeInfo?.generation_detail + const isLLMNode = nodeInfo?.node_type === BlockEnum.LLM && !!nodeInfo?.execution_metadata?.llm_trace?.length return (
diff --git a/web/app/components/workflow/run/special-result-panel.tsx b/web/app/components/workflow/run/special-result-panel.tsx index f12d51ef20..4ccc832417 100644 --- a/web/app/components/workflow/run/special-result-panel.tsx +++ b/web/app/components/workflow/run/special-result-panel.tsx @@ -1,6 +1,7 @@ import type { AgentLogItemWithChildren, IterationDurationMap, + LLMTraceItem, LoopDurationMap, LoopVariableMap, NodeTracing, @@ -33,7 +34,7 @@ export type SpecialResultPanelProps = { showLLMDetail?: boolean setShowLLMDetailFalse?: () => void - llmResultList?: NodeTracing[] + llmResultList?: LLMTraceItem[] } const SpecialResultPanel = ({ showRetryDetail, diff --git a/web/app/components/workflow/run/tracing-panel.tsx b/web/app/components/workflow/run/tracing-panel.tsx index 8931c8f7fe..51686bd839 100644 --- a/web/app/components/workflow/run/tracing-panel.tsx +++ b/web/app/components/workflow/run/tracing-panel.tsx @@ -91,6 +91,11 @@ const TracingPanel: FC = ({ agentOrToolLogItemStack, agentOrToolLogListMap, handleShowAgentOrToolLog, + + showLLMDetail, + setShowLLMDetailFalse, + llmResultList, + handleShowLLMDetail, } = useLogs() const renderNode = (node: NodeTracing) => { @@ -153,6 +158,7 @@ const TracingPanel: FC = ({ onShowLoopDetail={handleShowLoopResultList} onShowRetryDetail={handleShowRetryResultList} onShowAgentOrToolLog={handleShowAgentOrToolLog} + onShowLLMDetail={handleShowLLMDetail} hideInfo={hideNodeInfo} hideProcessDetail={hideNodeProcessDetail} /> @@ -182,6 +188,10 @@ const TracingPanel: FC = ({ agentOrToolLogItemStack={agentOrToolLogItemStack} agentOrToolLogListMap={agentOrToolLogListMap} handleShowAgentOrToolLog={handleShowAgentOrToolLog} + + showLLMDetail={showLLMDetail} + setShowLLMDetailFalse={setShowLLMDetailFalse} + llmResultList={llmResultList} /> ) } diff --git a/web/i18n/en-US/workflow.json b/web/i18n/en-US/workflow.json index 9dc21e79e6..371487856d 100644 --- a/web/i18n/en-US/workflow.json +++ b/web/i18n/en-US/workflow.json @@ -126,6 +126,7 @@ "common.currentDraftUnpublished": "Current Draft Unpublished", "common.currentView": "Current View", "common.currentWorkflow": "Current Workflow", + "common.data": "Data", "common.debugAndPreview": "Preview", "common.disconnect": "Disconnect", "common.duplicate": "Duplicate", diff --git a/web/i18n/zh-Hans/workflow.json b/web/i18n/zh-Hans/workflow.json index 7787c9db4b..50a0801706 100644 --- a/web/i18n/zh-Hans/workflow.json +++ b/web/i18n/zh-Hans/workflow.json @@ -126,6 +126,7 @@ "common.currentDraftUnpublished": "当前草稿未发布", "common.currentView": "当前视图", "common.currentWorkflow": "整个工作流", + "common.data": "数据", "common.debugAndPreview": "预览", "common.disconnect": "断开连接", "common.duplicate": "复制", diff --git a/web/service/base.ts b/web/service/base.ts index bbb05cf357..cc421b0f75 100644 --- a/web/service/base.ts +++ b/web/service/base.ts @@ -34,16 +34,24 @@ import { getWebAppPassport } from './webapp-auth' const TIME_OUT = 100000 +export type IconObject = { + background: string + content: string +} + export type IOnDataMoreInfo = { conversationId?: string taskId?: string messageId: string errorMessage?: string errorCode?: string - chunk_type?: 'text' | 'tool_call' | 'tool_result' | 'thought' | 'thought_start' + chunk_type?: 'text' | 'tool_call' | 'tool_result' | 'thought' | 'thought_start' | 'thought_end' tool_call_id?: string tool_name?: string tool_arguments?: string + tool_icon?: string | IconObject + tool_icon_dark?: string | IconObject + tool_files?: string[] tool_error?: string tool_elapsed_time?: number @@ -245,6 +253,8 @@ export const handleStream = ( tool_call_id: bufferObj.tool_call_id, tool_name: bufferObj.tool_name, tool_arguments: bufferObj.tool_arguments, + tool_icon: bufferObj.tool_icon, + tool_icon_dark: bufferObj.tool_icon_dark, tool_files: bufferObj.tool_files, tool_error: bufferObj.tool_error, tool_elapsed_time: bufferObj.tool_elapsed_time, diff --git a/web/types/workflow.ts b/web/types/workflow.ts index 3f7ea304b3..3241ff0732 100644 --- a/web/types/workflow.ts +++ b/web/types/workflow.ts @@ -28,11 +28,33 @@ export type AgentLogItemWithChildren = AgentLogItem & { children: AgentLogItemWithChildren[] } +export type IconObject = { + background: string + content: string +} + +export type ToolCallItem = { + type: 'model' | 'tool' | 'thought' + tool_call_id?: string + tool_name?: string + tool_arguments?: string + tool_icon?: string | IconObject + tool_icon_dark?: string | IconObject + tool_files?: string[] + tool_error?: string + tool_output?: Record | string + tool_elapsed_time?: number +} + export type ToolCallDetail = { id: string name: string arguments: string - result: string + output: string + files: string[] + error: string + elapsed_time?: number + status: string } export type SequenceSegment = | { type: 'context', start: number, end: number } @@ -45,6 +67,18 @@ export type LLMLogItem = { sequence: SequenceSegment[] } +export type LLMTraceItem = { + type: 'model' | 'tool' + duration: number + output: Record + provider?: string + name: string + icon?: string | IconObject + icon_dark?: string | IconObject + error?: string + status?: 'success' | 'error' +} + export type NodeTracing = { id: string index: number @@ -89,6 +123,7 @@ export type NodeTracing = { icon?: string } loop_variable_map?: Record + llm_trace?: LLMTraceItem[] } metadata: { iterator_length: number From c3230281795a4ac88faa0259b03c3299c2569821 Mon Sep 17 00:00:00 2001 From: zxhlyh Date: Thu, 8 Jan 2026 14:27:37 +0800 Subject: [PATCH 6/6] feat: llm node support tools --- .../chat/chat/answer/tool-calls/index.tsx | 4 +- web/app/components/base/chat/chat/hooks.ts | 68 ++++++++++++++- web/app/components/base/chat/chat/type.ts | 5 -- web/app/components/workflow/constants.ts | 4 + .../components/workflow/nodes/llm/panel.tsx | 5 ++ .../workflow/panel/debug-and-preview/hooks.ts | 55 +++++++----- .../workflow/run/llm-log/llm-result-panel.tsx | 36 +++++--- .../workflow/run/llm-log/tool-call-item.tsx | 86 ++++++++++++++++--- web/i18n/en-US/workflow.json | 1 + web/i18n/zh-Hans/workflow.json | 1 + web/types/workflow.ts | 29 +++++-- 11 files changed, 229 insertions(+), 65 deletions(-) diff --git a/web/app/components/base/chat/chat/answer/tool-calls/index.tsx b/web/app/components/base/chat/chat/answer/tool-calls/index.tsx index f51daa7497..66118006fe 100644 --- a/web/app/components/base/chat/chat/answer/tool-calls/index.tsx +++ b/web/app/components/base/chat/chat/answer/tool-calls/index.tsx @@ -9,9 +9,9 @@ const ToolCalls = ({ }: ToolCallsProps) => { return (
- {toolCalls.map((toolCall: ToolCallItem) => ( + {toolCalls.map((toolCall: ToolCallItem, index: number) => ( diff --git a/web/app/components/base/chat/chat/hooks.ts b/web/app/components/base/chat/chat/hooks.ts index 9b8a9b11dc..c9b9a29f34 100644 --- a/web/app/components/base/chat/chat/hooks.ts +++ b/web/app/components/base/chat/chat/hooks.ts @@ -319,6 +319,9 @@ export const useChat = ( return player } + let toolCallId = '' + let thoughtId = '' + ssePost( url, { @@ -326,7 +329,19 @@ export const useChat = ( }, { isPublicAPI, - onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId, taskId }: any) => { + onData: (message: string, isFirstMessage: boolean, { + conversationId: newConversationId, + messageId, + taskId, + chunk_type, + tool_icon, + tool_icon_dark, + tool_name, + tool_arguments, + tool_files, + tool_error, + tool_elapsed_time, + }: any) => { if (!isAgentMode) { responseItem.content = responseItem.content + message } @@ -336,6 +351,57 @@ export const useChat = ( lastThought.thought = lastThought.thought + message // need immer setAutoFreeze } + if (chunk_type === 'tool_call') { + if (!responseItem.toolCalls) + responseItem.toolCalls = [] + toolCallId = uuidV4() + responseItem.toolCalls?.push({ + id: toolCallId, + type: 'tool', + toolName: tool_name, + toolArguments: tool_arguments, + toolIcon: tool_icon, + toolIconDark: tool_icon_dark, + }) + } + + if (chunk_type === 'tool_result') { + const currentToolCallIndex = responseItem.toolCalls?.findIndex(item => item.id === toolCallId) ?? -1 + + if (currentToolCallIndex > -1) { + responseItem.toolCalls![currentToolCallIndex].toolError = tool_error + responseItem.toolCalls![currentToolCallIndex].toolDuration = tool_elapsed_time + responseItem.toolCalls![currentToolCallIndex].toolFiles = tool_files + responseItem.toolCalls![currentToolCallIndex].toolOutput = message + } + } + + if (chunk_type === 'thought_start') { + if (!responseItem.toolCalls) + responseItem.toolCalls = [] + thoughtId = uuidV4() + responseItem.toolCalls.push({ + id: thoughtId, + type: 'thought', + thoughtOutput: '', + }) + } + + if (chunk_type === 'thought') { + const currentThoughtIndex = responseItem.toolCalls?.findIndex(item => item.id === thoughtId) ?? -1 + if (currentThoughtIndex > -1) { + responseItem.toolCalls![currentThoughtIndex].thoughtOutput += message + } + } + + if (chunk_type === 'thought_end') { + const currentThoughtIndex = responseItem.toolCalls?.findIndex(item => item.id === thoughtId) ?? -1 + if (currentThoughtIndex > -1) { + responseItem.toolCalls![currentThoughtIndex].thoughtOutput += message + responseItem.toolCalls![currentThoughtIndex].thoughtCompleted = true + } + } + if (messageId && !hasSetResponseId) { questionItem.id = `question-${messageId}` responseItem.id = messageId diff --git a/web/app/components/base/chat/chat/type.ts b/web/app/components/base/chat/chat/type.ts index 1a6ff44f2d..5f2740dce2 100644 --- a/web/app/components/base/chat/chat/type.ts +++ b/web/app/components/base/chat/chat/type.ts @@ -64,11 +64,6 @@ export type CitationItem = { word_count: number } -export type IconObject = { - background: string - content: string -} - export type IChatItem = { id: string content: string diff --git a/web/app/components/workflow/constants.ts b/web/app/components/workflow/constants.ts index 4d95db7fcf..b06e194485 100644 --- a/web/app/components/workflow/constants.ts +++ b/web/app/components/workflow/constants.ts @@ -150,6 +150,10 @@ export const LLM_OUTPUT_STRUCT: Var[] = [ variable: 'usage', type: VarType.object, }, + { + variable: 'generation', + type: VarType.object, + }, ] export const KNOWLEDGE_RETRIEVAL_OUTPUT_STRUCT: Var[] = [ diff --git a/web/app/components/workflow/nodes/llm/panel.tsx b/web/app/components/workflow/nodes/llm/panel.tsx index 9d336d25b4..9ddd3ecd88 100644 --- a/web/app/components/workflow/nodes/llm/panel.tsx +++ b/web/app/components/workflow/nodes/llm/panel.tsx @@ -315,6 +315,11 @@ const Panel: FC> = ({ type="object" description={t(`${i18nPrefix}.outputVars.usage`, { ns: 'workflow' })} /> + {inputs.structured_output_enabled && ( <> diff --git a/web/app/components/workflow/panel/debug-and-preview/hooks.ts b/web/app/components/workflow/panel/debug-and-preview/hooks.ts index 6cdb1c194a..7b82f10630 100644 --- a/web/app/components/workflow/panel/debug-and-preview/hooks.ts +++ b/web/app/components/workflow/panel/debug-and-preview/hooks.ts @@ -15,6 +15,7 @@ import { useState, } from 'react' import { useTranslation } from 'react-i18next' +import { v4 as uuidV4 } from 'uuid' import { getProcessedInputs, processOpeningStatement, @@ -266,6 +267,8 @@ export const useChat = ( } let hasSetResponseId = false + let toolCallId = '' + let thoughtId = '' handleRun( bodyParams, @@ -277,7 +280,6 @@ export const useChat = ( chunk_type, tool_icon, tool_icon_dark, - tool_call_id, tool_name, tool_arguments, tool_files, @@ -289,43 +291,52 @@ export const useChat = ( if (chunk_type === 'tool_call') { if (!responseItem.toolCalls) responseItem.toolCalls = [] + toolCallId = uuidV4() responseItem.toolCalls?.push({ + id: toolCallId, type: 'tool', - tool_call_id, - tool_name, - tool_arguments, - tool_icon, - tool_icon_dark, - tool_files, - tool_error, - tool_elapsed_time, + toolName: tool_name, + toolArguments: tool_arguments, + toolIcon: tool_icon, + toolIconDark: tool_icon_dark, }) } if (chunk_type === 'tool_result') { - const currentToolCallIndex = responseItem.toolCalls?.findIndex(item => item.tool_call_id === tool_call_id) ?? -1 + const currentToolCallIndex = responseItem.toolCalls?.findIndex(item => item.id === toolCallId) ?? -1 - if (currentToolCallIndex > -1) - responseItem.toolCalls![currentToolCallIndex].tool_output = message + if (currentToolCallIndex > -1) { + responseItem.toolCalls![currentToolCallIndex].toolError = tool_error + responseItem.toolCalls![currentToolCallIndex].toolDuration = tool_elapsed_time + responseItem.toolCalls![currentToolCallIndex].toolFiles = tool_files + responseItem.toolCalls![currentToolCallIndex].toolOutput = message + } } if (chunk_type === 'thought_start') { - console.log(message, 'xx1') - responseItem.toolCalls?.push({ + if (!responseItem.toolCalls) + responseItem.toolCalls = [] + thoughtId = uuidV4() + responseItem.toolCalls.push({ + id: thoughtId, type: 'thought', - tool_elapsed_time, + thoughtOutput: '', }) } - if (chunk_type === 'thought_end') { - console.log(message, 'xx2') - // const currentThoughtIndex = responseItem.toolCalls?.findIndex(item => item.is_thought) ?? -1 - // if (currentThoughtIndex > -1) - // responseItem.toolCalls![currentThoughtIndex].tool_output = message + if (chunk_type === 'thought') { + const currentThoughtIndex = responseItem.toolCalls?.findIndex(item => item.id === thoughtId) ?? -1 + if (currentThoughtIndex > -1) { + responseItem.toolCalls![currentThoughtIndex].thoughtOutput += message + } } - if (chunk_type === 'thought') { - console.log(message, 'xx3') + if (chunk_type === 'thought_end') { + const currentThoughtIndex = responseItem.toolCalls?.findIndex(item => item.id === thoughtId) ?? -1 + if (currentThoughtIndex > -1) { + responseItem.toolCalls![currentThoughtIndex].thoughtOutput += message + responseItem.toolCalls![currentThoughtIndex].thoughtCompleted = true + } } if (messageId && !hasSetResponseId) { diff --git a/web/app/components/workflow/run/llm-log/llm-result-panel.tsx b/web/app/components/workflow/run/llm-log/llm-result-panel.tsx index 0223a4cdec..56687050a9 100644 --- a/web/app/components/workflow/run/llm-log/llm-result-panel.tsx +++ b/web/app/components/workflow/run/llm-log/llm-result-panel.tsx @@ -22,18 +22,30 @@ const LLMResultPanel: FC = ({ onBack, }) => { const { t } = useTranslation() - const formattedList = list.map(item => ({ - type: item.type, - tool_call_id: item.provider, - tool_name: item.name, - tool_arguments: item.type === 'tool' ? item.output.arguments : undefined, - tool_icon: item.icon, - tool_icon_dark: item.icon_dark, - tool_files: [], - tool_error: item.error, - tool_output: item.type === 'tool' ? item.output.output : item.output, - tool_elapsed_time: item.duration, - })) + const formattedList = list.map((item) => { + if (item.type === 'tool') { + return { + type: 'tool', + toolName: item.name, + toolProvider: item.provider, + toolIcon: item.icon, + toolIconDark: item.icon_dark, + toolArguments: item.output.arguments, + toolOutput: item.output.output, + toolDuration: item.duration, + } + } + + return { + type: 'model', + modelName: item.name, + modelProvider: item.provider, + modelIcon: item.icon, + modelIconDark: item.icon_dark, + modelOutput: item.output, + modelDuration: item.duration, + } + }) return (
diff --git a/web/app/components/workflow/run/llm-log/tool-call-item.tsx b/web/app/components/workflow/run/llm-log/tool-call-item.tsx index 026f85df9a..e3e5802655 100644 --- a/web/app/components/workflow/run/llm-log/tool-call-item.tsx +++ b/web/app/components/workflow/run/llm-log/tool-call-item.tsx @@ -4,6 +4,8 @@ import { } from '@remixicon/react' import { useState } from 'react' import { useTranslation } from 'react-i18next' +import AppIcon from '@/app/components/base/app-icon' +import { Thinking } from '@/app/components/base/icons/src/vender/workflow' import BlockIcon from '@/app/components/workflow/block-icon' import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' @@ -24,17 +26,73 @@ const ToolCallItemComponent = ({
-
setExpand(!expand)}> - -
{payload.tool_name}
+
{ + setExpand(!expand) + }} + > { - !!payload.tool_elapsed_time && ( + payload.type === 'thought' && ( + + ) + } + { + payload.type === 'tool' && ( + + ) + } + { + payload.type === 'model' && ( + + ) + } + { + payload.type === 'thought' && ( +
+ { + payload.thoughtCompleted && !expand && (payload.thoughtOutput || '') as string + } + { + payload.thoughtCompleted && expand && 'THOUGHT' + } + { + !payload.thoughtCompleted && 'THINKING...' + } +
+ ) + } + { + payload.type === 'tool' && ( +
{payload.toolName}
+ ) + } + { + payload.type === 'model' && ( +
{payload.modelName}
+ ) + } + { + !!payload.toolDuration && (
- {payload.tool_elapsed_time?.toFixed(1)} + {payload.toolDuration?.toFixed(1)} + s +
+ ) + } + { + !!payload.modelDuration && ( +
+ {payload.modelDuration?.toFixed(1)} s
) @@ -46,8 +104,8 @@ const ToolCallItemComponent = ({
{ - payload.type === 'thought' && typeof payload.tool_output === 'string' && ( -
{payload.tool_output}
+ payload.type === 'thought' && typeof payload.thoughtOutput === 'string' && ( +
{payload.thoughtOutput}
) } { @@ -56,7 +114,7 @@ const ToolCallItemComponent = ({ readOnly title={
{t('common.data', { ns: 'workflow' })}
} language={CodeLanguage.json} - value={payload.tool_output} + value={payload.modelOutput} isJSONStringifyBeauty /> ) @@ -67,7 +125,7 @@ const ToolCallItemComponent = ({ readOnly title={
{t('common.input', { ns: 'workflow' })}
} language={CodeLanguage.json} - value={payload.tool_arguments} + value={payload.toolArguments} isJSONStringifyBeauty /> ) @@ -79,7 +137,7 @@ const ToolCallItemComponent = ({ className="mt-1" title={
{t('common.output', { ns: 'workflow' })}
} language={CodeLanguage.json} - value={payload.tool_output} + value={payload.toolOutput} isJSONStringifyBeauty /> ) diff --git a/web/i18n/en-US/workflow.json b/web/i18n/en-US/workflow.json index 371487856d..198a1d0061 100644 --- a/web/i18n/en-US/workflow.json +++ b/web/i18n/en-US/workflow.json @@ -651,6 +651,7 @@ "nodes.llm.jsonSchema.warningTips.saveSchema": "Please finish editing the current field before saving the schema", "nodes.llm.model": "model", "nodes.llm.notSetContextInPromptTip": "To enable the context feature, please fill in the context variable in PROMPT.", + "nodes.llm.outputVars.generation": "Generation Information", "nodes.llm.outputVars.output": "Generate content", "nodes.llm.outputVars.reasoning_content": "Reasoning Content", "nodes.llm.outputVars.usage": "Model Usage Information", diff --git a/web/i18n/zh-Hans/workflow.json b/web/i18n/zh-Hans/workflow.json index 50a0801706..a133c3234d 100644 --- a/web/i18n/zh-Hans/workflow.json +++ b/web/i18n/zh-Hans/workflow.json @@ -651,6 +651,7 @@ "nodes.llm.jsonSchema.warningTips.saveSchema": "请先完成当前字段的编辑", "nodes.llm.model": "模型", "nodes.llm.notSetContextInPromptTip": "要启用上下文功能,请在提示中填写上下文变量。", + "nodes.llm.outputVars.generation": "生成信息", "nodes.llm.outputVars.output": "生成内容", "nodes.llm.outputVars.reasoning_content": "推理内容", "nodes.llm.outputVars.usage": "模型用量信息", diff --git a/web/types/workflow.ts b/web/types/workflow.ts index 3241ff0732..8de0df840e 100644 --- a/web/types/workflow.ts +++ b/web/types/workflow.ts @@ -34,16 +34,27 @@ export type IconObject = { } export type ToolCallItem = { + id: string type: 'model' | 'tool' | 'thought' - tool_call_id?: string - tool_name?: string - tool_arguments?: string - tool_icon?: string | IconObject - tool_icon_dark?: string | IconObject - tool_files?: string[] - tool_error?: string - tool_output?: Record | string - tool_elapsed_time?: number + thoughtCompleted?: boolean + thoughtOutput?: string + + toolName?: string + toolProvider?: string + toolIcon?: string | IconObject + toolIconDark?: string | IconObject + toolArguments?: string + toolOutput?: Record | string + toolFiles?: string[] + toolError?: string + toolDuration?: number + + modelName?: string + modelProvider?: string + modelOutput?: Record | string + modelDuration?: number + modelIcon?: string | IconObject + modelIconDark?: string | IconObject } export type ToolCallDetail = {