From bbd11c9e89fa8b7036a9a3bac5d78cb3c2caca7b Mon Sep 17 00:00:00 2001 From: zxhlyh Date: Tue, 30 Dec 2025 10:40:01 +0800 Subject: [PATCH 1/4] 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 00000000000..26851aa68ac --- /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 00000000000..4714fe63307 --- /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 00000000000..8f42486df17 --- /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 8611a98d3bd..d4316f285f8 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 2/4] 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 04b884388e7..f6a9e896f96 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 00000000000..9de41233aac --- /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 00000000000..539f1a83ba8 --- /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 291b0ae0646..56ffdc7e069 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 2c46833df8e..068be400230 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 d9f3dba53a7..bbb05cf3571 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 3/4] 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 9de41233aac..f51daa74978 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 fd8c1daf3e7..12f0001bd44 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 00000000000..6fe807775de --- /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 00000000000..dbe3716a242 --- /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 ec8dce100da..b559d9c6aae 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 8e6e4567ff4..5a3ada80ba0 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 bfa6f8d8679..afda9846de1 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 593836f5b38..9aa6d253a15 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 51f641f265c..9590dc24d70 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 4714fe63307..1d65f754b43 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 8f42486df17..0223a4cdec0 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 539f1a83ba8..026f85df9ad 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 d4316f285f8..ffec4392b6a 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 8bc55a504b4..00e8b028d1c 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 f12d51ef206..4ccc8324173 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 8931c8f7fe5..51686bd8394 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 9dc21e79e66..371487856d9 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 7787c9db4b6..50a08017068 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 bbb05cf3571..cc421b0f750 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 3f7ea304b30..3241ff0732b 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 4/4] 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 f51daa74978..66118006fef 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 9b8a9b11dc8..c9b9a29f34a 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 1a6ff44f2d5..5f2740dce23 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 4d95db7fcfe..b06e1944859 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 9d336d25b47..9ddd3ecd885 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 6cdb1c194a0..7b82f106301 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 0223a4cdec0..56687050a98 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 026f85df9ad..e3e58026559 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 371487856d9..198a1d00618 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 50a08017068..a133c3234d6 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 3241ff0732b..8de0df840ec 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 = {