Merge branch 'feat/llm-support-tools' into feat/support-agent-sandbox

This commit is contained in:
Novice
2026-01-20 10:27:42 +08:00
28 changed files with 706 additions and 19 deletions

View File

@ -161,6 +161,10 @@ export const LLM_OUTPUT_STRUCT: Var[] = [
variable: 'usage',
type: VarType.object,
},
{
variable: 'generation',
type: VarType.object,
},
]
export const KNOWLEDGE_RETRIEVAL_OUTPUT_STRUCT: Var[] = [

View File

@ -336,6 +336,11 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
type="object"
description={t(`${i18nPrefix}.outputVars.usage`, { ns: 'workflow' })}
/>
<VarItem
name="generation"
type="object"
description={t(`${i18nPrefix}.outputVars.generation`, { ns: 'workflow' })}
/>
{inputs.structured_output_enabled && (
<>
<Split className="mt-3" />

View File

@ -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

View File

@ -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<LLMTraceItem[]>([])
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,
}
}

View File

@ -153,7 +153,7 @@ const RunPanel: FC<RunProps> = ({
</div>
</div>
{/* panel detail */}
<div ref={ref} className={cn('relative h-0 grow overflow-y-auto rounded-b-xl bg-components-panel-bg')}>
<div ref={ref} className={cn('relative h-0 grow overflow-y-auto rounded-b-xl bg-background-section')}>
{loading && (
<div className="flex h-full items-center justify-center bg-components-panel-bg">
<Loading />
@ -192,7 +192,7 @@ const RunPanel: FC<RunProps> = ({
)}
{!loading && currentTab === 'TRACING' && (
<TracingPanel
className="bg-background-section-burn"
className="bg-background-section"
list={list}
/>
)}

View File

@ -0,0 +1,2 @@
export { default as LLMLogTrigger } from './llm-log-trigger'
export { default as LLMResultPanel } from './llm-result-panel'

View File

@ -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<HTMLButtonElement>) => {
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
onShowLLMDetail(llmTrace || [])
}
return (
<Button
className="mb-1 flex w-full items-center justify-between"
variant="tertiary"
onClick={handleShowLLMDetail}
>
<div className="flex items-center">
<Thinking className="mr-[5px] h-4 w-4 shrink-0 text-components-button-tertiary-text" />
{t('detail', { ns: 'runLog' })}
</div>
<RiArrowRightSLine className="h-4 w-4 shrink-0 text-components-button-tertiary-text" />
</Button>
)
}
export default LLMLogTrigger

View File

@ -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<Props> = ({
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 (
<div>
<div
className="system-sm-medium flex h-8 cursor-pointer items-center bg-components-panel-bg px-4 text-text-accent-secondary"
onClick={(e) => {
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
onBack()
}}
>
<RiArrowLeftLine className="mr-1 h-4 w-4" />
{t('singleRun.back', { ns: 'workflow' })}
</div>
<div className="space-y-1 p-2">
{
formattedList.map((item, index) => (
<ToolCallItemComponent key={index} payload={item as ToolCallItem} />
))
}
</div>
</div>
)
}
export default memo(LLMResultPanel)

View File

@ -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 (
<div
className={cn('rounded-[10px] border-[0.5px] border-components-panel-border bg-background-default-subtle px-2 pb-1 pt-2 shadow-xs', className)}
>
<div
className="mb-1 flex cursor-pointer items-center hover:bg-background-gradient-bg-fill-chat-bubble-bg-2"
onClick={() => {
setExpand(!expand)
}}
>
{
payload.type === 'thought' && (
<Thinking className="mr-1 h-4 w-4 shrink-0" />
)
}
{
payload.type === 'tool' && (
<BlockIcon
type={BlockEnum.Tool}
toolIcon={payload.toolIcon}
className="mr-1 h-4 w-4 shrink-0"
/>
)
}
{
payload.type === 'model' && (
<AppIcon
iconType={typeof payload.modelIcon === 'string' ? 'image' : undefined}
imageUrl={typeof payload.modelIcon === 'string' ? payload.modelIcon : undefined}
background={typeof payload.modelIcon === 'object' ? payload.modelIcon.background : undefined}
className="mr-1 h-4 w-4 shrink-0"
/>
)
}
{
payload.type === 'thought' && (
<div className="system-xs-medium mr-1 grow truncate text-text-secondary" title={payload.thoughtOutput}>
{
payload.thoughtCompleted && !expand && (payload.thoughtOutput || '') as string
}
{
payload.thoughtCompleted && expand && 'THOUGHT'
}
{
!payload.thoughtCompleted && 'THINKING...'
}
</div>
)
}
{
payload.type === 'tool' && (
<div className="system-xs-medium mr-1 grow truncate text-text-secondary" title={payload.toolName}>{payload.toolName}</div>
)
}
{
payload.type === 'model' && (
<div className="system-xs-medium mr-1 grow truncate text-text-secondary" title={payload.modelName}>{payload.modelName}</div>
)
}
{
!!payload.toolDuration && (
<div className="system-xs-regular mr-1 shrink-0 text-text-tertiary">
{payload.toolDuration?.toFixed(1)}
s
</div>
)
}
{
!!payload.modelDuration && (
<div className="system-xs-regular mr-1 shrink-0 text-text-tertiary">
{payload.modelDuration?.toFixed(1)}
s
</div>
)
}
<RiArrowDownSLine className="h-4 w-4 shrink-0" />
</div>
{
expand && (
<div className="relative px-2 pl-9">
<div className="absolute bottom-1 left-2 top-1 w-[1px] bg-divider-regular"></div>
{
payload.type === 'thought' && typeof payload.thoughtOutput === 'string' && (
<div className="body-sm-medium text-text-tertiary">{payload.thoughtOutput}</div>
)
}
{
payload.type === 'model' && (
<CodeEditor
readOnly
title={<div>{t('common.data', { ns: 'workflow' })}</div>}
language={CodeLanguage.json}
value={payload.modelOutput}
isJSONStringifyBeauty
/>
)
}
{
payload.type === 'tool' && (
<CodeEditor
readOnly
title={<div>{t('common.input', { ns: 'workflow' })}</div>}
language={CodeLanguage.json}
value={payload.toolArguments}
isJSONStringifyBeauty
/>
)
}
{
payload.type === 'tool' && (
<CodeEditor
readOnly
className="mt-1"
title={<div>{t('common.output', { ns: 'workflow' })}</div>}
language={CodeLanguage.json}
value={payload.toolOutput}
isJSONStringifyBeauty
/>
)
}
</div>
)
}
</div>
)
}
export default ToolCallItemComponent

View File

@ -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<Props> = ({
onShowLoopDetail,
onShowRetryDetail,
onShowAgentOrToolLog,
onShowLLMDetail,
notShowIterationNav,
notShowLoopNav,
}) => {
@ -96,6 +100,7 @@ const NodePanel: FC<Props> = ({
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<Props> = ({
onShowRetryResultList={onShowRetryDetail}
/>
)}
{isLLMNode && onShowLLMDetail && (
<LLMLogTrigger
nodeInfo={nodeInfo}
onShowLLMDetail={onShowLLMDetail}
/>
)}
{
(isAgentNode || isToolNode) && onShowAgentOrToolLog && (
<AgentLogTrigger

View File

@ -2,6 +2,7 @@
import type { FC } from 'react'
import type {
AgentLogItemWithChildren,
LLMTraceItem,
NodeTracing,
} from '@/types/workflow'
import { useTranslation } from 'react-i18next'
@ -15,6 +16,7 @@ import { RetryLogTrigger } from '@/app/components/workflow/run/retry-log'
import { BlockEnum } from '@/app/components/workflow/types'
import { hasRetryNode } from '@/app/components/workflow/utils'
import LargeDataAlert from '../variable-inspect/large-data-alert'
import { LLMLogTrigger } from './llm-log'
import MetaData from './meta'
import StatusPanel from './status'
@ -45,6 +47,7 @@ export type ResultPanelProps = {
handleShowLoopResultList?: (detail: NodeTracing[][], loopDurationMap: any) => void
onShowRetryDetail?: (detail: NodeTracing[]) => void
handleShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void
onShowLLMDetail?: (detail: LLMTraceItem[]) => void
}
const ResultPanel: FC<ResultPanelProps> = ({
@ -71,6 +74,7 @@ const ResultPanel: FC<ResultPanelProps> = ({
handleShowLoopResultList,
onShowRetryDetail,
handleShowAgentOrToolLog,
onShowLLMDetail,
}) => {
const { t } = useTranslation()
const isIterationNode = nodeInfo?.node_type === BlockEnum.Iteration && !!nodeInfo?.details?.length
@ -78,6 +82,7 @@ const ResultPanel: FC<ResultPanelProps> = ({
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 (
<div className="bg-components-panel-bg py-2">
@ -116,6 +121,14 @@ const ResultPanel: FC<ResultPanelProps> = ({
/>
)
}
{
isLLMNode && onShowLLMDetail && (
<LLMLogTrigger
nodeInfo={nodeInfo}
onShowLLMDetail={onShowLLMDetail}
/>
)
}
{
(isAgentNode || isToolNode) && handleShowAgentOrToolLog && (
<AgentLogTrigger

View File

@ -1,12 +1,14 @@
import type {
AgentLogItemWithChildren,
IterationDurationMap,
LLMTraceItem,
LoopDurationMap,
LoopVariableMap,
NodeTracing,
} from '@/types/workflow'
import { AgentResultPanel } from './agent-log'
import { IterationResultPanel } from './iteration-log'
import { LLMResultPanel } from './llm-log'
import { LoopResultPanel } from './loop-log'
import { RetryResultPanel } from './retry-log'
@ -29,6 +31,10 @@ export type SpecialResultPanelProps = {
agentOrToolLogItemStack?: AgentLogItemWithChildren[]
agentOrToolLogListMap?: Record<string, AgentLogItemWithChildren[]>
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 (
<div onClick={(e) => {
@ -64,6 +74,14 @@ const SpecialResultPanel = ({
/>
)
}
{
!!showLLMDetail && !!llmResultList?.length && setShowLLMDetailFalse && (
<LLMResultPanel
list={llmResultList}
onBack={setShowLLMDetailFalse}
/>
)
}
{
showIteratingDetail && !!iterationResultList?.length && setShowIteratingDetailFalse && (
<IterationResultPanel

View File

@ -91,6 +91,11 @@ const TracingPanel: FC<TracingPanelProps> = ({
agentOrToolLogItemStack,
agentOrToolLogListMap,
handleShowAgentOrToolLog,
showLLMDetail,
setShowLLMDetailFalse,
llmResultList,
handleShowLLMDetail,
} = useLogs()
const renderNode = (node: NodeTracing) => {
@ -153,6 +158,7 @@ const TracingPanel: FC<TracingPanelProps> = ({
onShowLoopDetail={handleShowLoopResultList}
onShowRetryDetail={handleShowRetryResultList}
onShowAgentOrToolLog={handleShowAgentOrToolLog}
onShowLLMDetail={handleShowLLMDetail}
hideInfo={hideNodeInfo}
hideProcessDetail={hideNodeProcessDetail}
/>
@ -182,6 +188,10 @@ const TracingPanel: FC<TracingPanelProps> = ({
agentOrToolLogItemStack={agentOrToolLogItemStack}
agentOrToolLogListMap={agentOrToolLogListMap}
handleShowAgentOrToolLog={handleShowAgentOrToolLog}
showLLMDetail={showLLMDetail}
setShowLLMDetailFalse={setShowLLMDetailFalse}
llmResultList={llmResultList}
/>
)
}