diff --git a/web/app/components/base/chat/chat/answer/index.tsx b/web/app/components/base/chat/chat/answer/index.tsx index 7420b84ede7..d680a144777 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..66118006fef --- /dev/null +++ b/web/app/components/base/chat/chat/answer/tool-calls/index.tsx @@ -0,0 +1,23 @@ +import type { ToolCallItem } from '@/types/workflow' +import ToolCallItemComponent from '@/app/components/workflow/run/llm-log/tool-call-item' + +type ToolCallsProps = { + toolCalls: ToolCallItem[] +} +const ToolCalls = ({ + toolCalls, +}: ToolCallsProps) => { + return ( +
+ {toolCalls.map((toolCall: ToolCallItem, index: number) => ( + + ))} +
+ ) +} + +export default ToolCalls 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 (
{ + 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 291b0ae0646..5f2740dce23 100644 --- a/web/app/components/base/chat/chat/type.ts +++ b/web/app/components/base/chat/chat/type.ts @@ -2,7 +2,7 @@ import type { FileEntity } from '@/app/components/base/file-uploader/types' import type { TypeWithI18N } from '@/app/components/header/account-setting/model-provider-page/declarations' import type { InputVarType } from '@/app/components/workflow/types' import type { Annotation, MessageRating } from '@/models/log' -import type { FileResponse } from '@/types/workflow' +import type { FileResponse, ToolCallItem } from '@/types/workflow' export type MessageMore = { time: string @@ -104,6 +104,7 @@ export type IChatItem = { siblingIndex?: number prevSibling?: string nextSibling?: string + toolCalls?: ToolCallItem[] } export type Metadata = { diff --git a/web/app/components/base/icons/assets/vender/workflow/thinking.svg b/web/app/components/base/icons/assets/vender/workflow/thinking.svg new file mode 100644 index 00000000000..2ec614932fd --- /dev/null +++ b/web/app/components/base/icons/assets/vender/workflow/thinking.svg @@ -0,0 +1,4 @@ + + + + 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 => ( > = ({ 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 2c46833df8e..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,13 +267,78 @@ export const useChat = ( } let hasSetResponseId = false + let toolCallId = '' + let thoughtId = '' handleRun( bodyParams, { - 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) => { responseItem.content = responseItem.content + message + 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/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/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..1d65f754b43 --- /dev/null +++ b/web/app/components/workflow/run/llm-log/llm-log-trigger.tsx @@ -0,0 +1,41 @@ +import type { LLMTraceItem, NodeTracing } from '@/types/workflow' +import { + RiArrowRightSLine, +} 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: LLMTraceItem[]) => void +} +const LLMLogTrigger = ({ + nodeInfo, + onShowLLMDetail, +}: LLMLogTriggerProps) => { + const { t } = useTranslation() + const llmTrace = nodeInfo?.execution_metadata?.llm_trace || [] + + const handleShowLLMDetail = (e: React.MouseEvent) => { + e.stopPropagation() + e.nativeEvent.stopImmediatePropagation() + onShowLLMDetail(llmTrace || []) + } + + 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..56687050a98 --- /dev/null +++ b/web/app/components/workflow/run/llm-log/llm-result-panel.tsx @@ -0,0 +1,73 @@ +'use client' + +import type { FC } from 'react' +import type { + LLMTraceItem, + ToolCallItem, +} from '@/types/workflow' +import { + RiArrowLeftLine, +} from '@remixicon/react' +import { memo } from 'react' +import { useTranslation } from 'react-i18next' +import ToolCallItemComponent from '@/app/components/workflow/run/llm-log/tool-call-item' + +type Props = { + list: LLMTraceItem[] + onBack: () => void +} + +const LLMResultPanel: FC = ({ + list, + onBack, +}) => { + const { t } = useTranslation() + 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 ( +
+
{ + e.stopPropagation() + e.nativeEvent.stopImmediatePropagation() + onBack() + }} + > + + {t('singleRun.back', { ns: 'workflow' })} +
+
+ { + formattedList.map((item, index) => ( + + )) + } +
+
+ ) +} +export default memo(LLMResultPanel) 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 new file mode 100644 index 00000000000..e3e58026559 --- /dev/null +++ b/web/app/components/workflow/run/llm-log/tool-call-item.tsx @@ -0,0 +1,152 @@ +import type { ToolCallItem } from '@/types/workflow' +import { + RiArrowDownSLine, +} 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' +import { BlockEnum } from '@/app/components/workflow/types' +import { cn } from '@/utils/classnames' + +type ToolCallItemComponentProps = { + className?: string + payload: ToolCallItem +} +const ToolCallItemComponent = ({ + className, + payload, +}: ToolCallItemComponentProps) => { + const { t } = useTranslation() + const [expand, setExpand] = useState(false) + return ( +
+
{ + setExpand(!expand) + }} + > + { + 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.toolDuration?.toFixed(1)} + s +
+ ) + } + { + !!payload.modelDuration && ( +
+ {payload.modelDuration?.toFixed(1)} + s +
+ ) + } + +
+ { + expand && ( +
+
+ { + payload.type === 'thought' && typeof payload.thoughtOutput === 'string' && ( +
{payload.thoughtOutput}
+ ) + } + { + payload.type === 'model' && ( + {t('common.data', { ns: 'workflow' })}
} + language={CodeLanguage.json} + value={payload.modelOutput} + isJSONStringifyBeauty + /> + ) + } + { + payload.type === 'tool' && ( + {t('common.input', { ns: 'workflow' })}
} + language={CodeLanguage.json} + value={payload.toolArguments} + isJSONStringifyBeauty + /> + ) + } + { + payload.type === 'tool' && ( + {t('common.output', { ns: 'workflow' })}
} + language={CodeLanguage.json} + value={payload.toolOutput} + isJSONStringifyBeauty + /> + ) + } +
+ ) + } +
+ ) +} + +export default ToolCallItemComponent diff --git a/web/app/components/workflow/run/node.tsx b/web/app/components/workflow/run/node.tsx index 8611a98d3bd..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, @@ -29,6 +30,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 +45,7 @@ type Props = { onShowLoopDetail?: (detail: NodeTracing[][], loopDurationMap: LoopDurationMap, loopVariableMap: LoopVariableMap) => void onShowRetryDetail?: (detail: NodeTracing[]) => void onShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void + onShowLLMDetail?: (detail: LLMTraceItem[]) => void notShowIterationNav?: boolean notShowLoopNav?: boolean } @@ -58,6 +61,7 @@ const NodePanel: FC = ({ onShowLoopDetail, onShowRetryDetail, onShowAgentOrToolLog, + onShowLLMDetail, notShowIterationNav, notShowLoopNav, }) => { @@ -96,6 +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.execution_metadata?.llm_trace?.length const inputsTitle = useMemo(() => { let text = t('common.input', { ns: 'workflow' }) @@ -193,6 +198,12 @@ const NodePanel: FC = ({ onShowRetryResultList={onShowRetryDetail} /> )} + {isLLMNode && onShowLLMDetail && ( + + )} { (isAgentNode || isToolNode) && onShowAgentOrToolLog && ( void onShowRetryDetail?: (detail: NodeTracing[]) => void handleShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void + onShowLLMDetail?: (detail: LLMTraceItem[]) => void } const ResultPanel: FC = ({ @@ -71,6 +74,7 @@ const ResultPanel: FC = ({ handleShowLoopResultList, onShowRetryDetail, handleShowAgentOrToolLog, + onShowLLMDetail, }) => { const { t } = useTranslation() const isIterationNode = nodeInfo?.node_type === BlockEnum.Iteration && !!nodeInfo?.details?.length @@ -78,6 +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?.execution_metadata?.llm_trace?.length return (
@@ -116,6 +121,14 @@ const ResultPanel: FC = ({ /> ) } + { + isLLMNode && onShowLLMDetail && ( + + ) + } { (isAgentNode || isToolNode) && handleShowAgentOrToolLog && ( handleShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void + + showLLMDetail?: boolean + setShowLLMDetailFalse?: () => void + llmResultList?: LLMTraceItem[] } const SpecialResultPanel = ({ showRetryDetail, @@ -49,6 +55,10 @@ const SpecialResultPanel = ({ agentOrToolLogItemStack, agentOrToolLogListMap, handleShowAgentOrToolLog, + + showLLMDetail, + setShowLLMDetailFalse, + llmResultList, }: SpecialResultPanelProps) => { return (
{ @@ -64,6 +74,14 @@ const SpecialResultPanel = ({ /> ) } + { + !!showLLMDetail && !!llmResultList?.length && setShowLLMDetailFalse && ( + + ) + } { showIteratingDetail && !!iterationResultList?.length && setShowIteratingDetailFalse && ( = ({ 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 4a04f5ac630..b7af41b534f 100644 --- a/web/i18n/en-US/workflow.json +++ b/web/i18n/en-US/workflow.json @@ -130,6 +130,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", @@ -671,6 +672,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 356aea5656c..512c9c765eb 100644 --- a/web/i18n/zh-Hans/workflow.json +++ b/web/i18n/zh-Hans/workflow.json @@ -128,6 +128,7 @@ "common.currentDraftUnpublished": "当前草稿未发布", "common.currentView": "当前视图", "common.currentWorkflow": "整个工作流", + "common.data": "数据", "common.debugAndPreview": "预览", "common.disconnect": "断开连接", "common.duplicate": "复制", @@ -664,6 +665,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/service/base.ts b/web/service/base.ts index 956886bea98..d83eedb2498 100644 --- a/web/service/base.ts +++ b/web/service/base.ts @@ -34,12 +34,27 @@ 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' | '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 } export type IOnData = (message: string, isFirstMessage: boolean, moreInfo: IOnDataMoreInfo) => void @@ -250,6 +265,15 @@ 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_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, }) isFirstMessage = false } diff --git a/web/types/workflow.ts b/web/types/workflow.ts index 5f74ef2c129..8de0df840ec 100644 --- a/web/types/workflow.ts +++ b/web/types/workflow.ts @@ -28,6 +28,68 @@ export type AgentLogItemWithChildren = AgentLogItem & { children: AgentLogItemWithChildren[] } +export type IconObject = { + background: string + content: string +} + +export type ToolCallItem = { + id: string + type: 'model' | 'tool' | 'thought' + 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 = { + id: string + name: string + arguments: string + output: string + files: string[] + error: string + elapsed_time?: number + status: string +} +export type SequenceSegment + = | { type: 'context', start: number, end: number } + | { type: 'reasoning', index: number } + | { type: 'tool_call', index: number } + +export type LLMLogItem = { + reasoning_content: string[] + tool_calls: ToolCallDetail[] + 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 @@ -72,6 +134,7 @@ export type NodeTracing = { icon?: string } loop_variable_map?: Record + llm_trace?: LLMTraceItem[] } metadata: { iterator_length: number @@ -104,6 +167,7 @@ export type NodeTracing = { parent_parallel_id?: string parent_parallel_start_node_id?: string agentLog?: AgentLogItemWithChildren[] // agent log + generation_detail?: LLMLogItem } export type FetchWorkflowDraftResponse = {