diff --git a/web/app/components/base/chat/chat/answer/index.tsx b/web/app/components/base/chat/chat/answer/index.tsx index 7420b84ede..d680a14477 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..66118006fe --- /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 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 (
{ + 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 291b0ae064..5f2740dce2 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 0000000000..2ec614932f --- /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 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 => ( { 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 new file mode 100644 index 0000000000..2eb4fd5869 --- /dev/null +++ b/web/app/components/workflow/nodes/llm/components/tools/index.tsx @@ -0,0 +1,53 @@ +import type { ToolValue } from '@/app/components/workflow/block-selector/types' +import { memo } from 'react' +import { useTranslation } from 'react-i18next' +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' + +type ToolsProps = { + nodeId: string + tools?: ToolValue[] + maxIterations?: number +} +const Tools = ({ + nodeId, + tools = [], + maxIterations = 10, +}: ToolsProps) => { + const { t } = useTranslation() + const { + handleToolsChange, + handleMaxIterationsChange, + } = useNodeTools(nodeId) + + return ( + + + + + ) +} + +export default memo(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 670d3149be..9ddd3ecd88 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 = 'nodes.llm' @@ -233,6 +234,12 @@ const Panel: FC> = ({ )} + + {/* Vision: GPT4-vision and so on */} > = ({ type="object" description={t(`${i18nPrefix}.outputVars.usage`, { ns: 'workflow' })} /> + {inputs.structured_output_enabled && ( <> diff --git a/web/app/components/workflow/nodes/llm/types.ts b/web/app/components/workflow/nodes/llm/types.ts index 70dc4d9cc7..7e95c05a29 100644 --- a/web/app/components/workflow/nodes/llm/types.ts +++ b/web/app/components/workflow/nodes/llm/types.ts @@ -1,5 +1,18 @@ +import type { ToolValue } from '@/app/components/workflow/block-selector/types' import type { CommonNodeType, Memory, ModelConfig, PromptItem, ValueSelector, Variable, VisionSetting } from '@/app/components/workflow/types' +export type Tool = { + enabled: boolean + type: string + provider_name: 'plugin' | 'builtin' | 'api' | 'workflow' | 'app' | 'dataset-retrieval' + tool_name: string + plugin_unique_identifier?: string + credential_id?: string + parameters?: Record + 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 { 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..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,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 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/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..1d65f754b4 --- /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 0000000000..56687050a9 --- /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 0000000000..e3e5802655 --- /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 8611a98d3b..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, @@ -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 107dad5b28..198a1d0061 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", @@ -650,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", @@ -666,6 +668,7 @@ "nodes.llm.roleDescription.user": "Provide instructions, queries, or any text-based input to the model", "nodes.llm.singleRun.variable": "Variable", "nodes.llm.sysQueryInUser": "sys.query in user message is required", + "nodes.llm.tools.title": "Tools", "nodes.llm.variables": "variables", "nodes.llm.vision": "vision", "nodes.loop.ErrorMethod.continueOnError": "Continue on Error", diff --git a/web/i18n/zh-Hans/workflow.json b/web/i18n/zh-Hans/workflow.json index 7787c9db4b..a133c3234d 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": "复制", @@ -650,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/service/base.ts b/web/service/base.ts index d9f3dba53a..cc421b0f75 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 @@ -234,6 +249,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 5f74ef2c12..8de0df840e 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 = {