Compare commits

...

13 Commits

Author SHA1 Message Date
zxhlyh
4d3d8b35d9 Merge branch 'main' into feat/llm-node-support-tools 2026-01-08 14:28:13 +08:00
zxhlyh
c323028179 feat: llm node support tools 2026-01-08 14:27:37 +08:00
zxhlyh
70149ea05e Merge branch 'main' into feat/llm-node-support-tools 2026-01-07 16:29:47 +08:00
zxhlyh
1d93f41fcf feat: llm node support tools 2026-01-07 16:28:41 +08:00
zxhlyh
04f40303fd Merge branch 'main' into feat/llm-node-support-tools 2026-01-04 18:04:42 +08:00
zxhlyh
ececc5ec2c feat: llm node support tools 2026-01-04 18:03:47 +08:00
zxhlyh
e83635ee5a Merge branch 'main' into feat/llm-node-support-tools 2025-12-30 11:47:54 +08:00
zxhlyh
d79372a46d Merge branch 'main' into feat/llm-node-support-tools 2025-12-30 11:47:26 +08:00
zxhlyh
bbd11c9e89 feat: llm node support tools 2025-12-30 10:40:01 +08:00
zxhlyh
d132abcdb4 merge main 2025-12-29 15:55:45 +08:00
zxhlyh
d60348572e feat: llm node support tools 2025-12-29 14:55:26 +08:00
zxhlyh
0cff94d90e Merge branch 'main' into feat/llm-node-support-tools 2025-12-25 13:45:49 +08:00
zxhlyh
a7859de625 feat: llm node support tools 2025-12-24 14:15:55 +08:00
36 changed files with 880 additions and 20 deletions

View File

@@ -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<AnswerProps> = ({
workflowProcess,
allFiles,
message_files,
toolCalls,
} = item
const hasAgentThoughts = !!agent_thoughts?.length
@@ -154,6 +156,11 @@ const Answer: FC<AnswerProps> = ({
/>
)
}
{
!!toolCalls?.length && (
<ToolCalls toolCalls={toolCalls} />
)
}
{
responding && contentIsEmpty && !hasAgentThoughts && (
<div className="flex h-5 w-6 items-center justify-center">

View File

@@ -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 (
<div className="my-1 space-y-1">
{toolCalls.map((toolCall: ToolCallItem, index: number) => (
<ToolCallItemComponent
key={index}
payload={toolCall}
className="bg-background-gradient-bg-fill-chat-bubble-bg-2 shadow-none"
/>
))}
</div>
)
}
export default ToolCalls

View File

@@ -45,7 +45,7 @@ const WorkflowProcessItem = ({
return (
<div
className={cn(
'-mx-1 rounded-xl px-2.5',
'rounded-xl px-2.5',
collapse ? 'border-l-[0.25px] border-components-panel-border py-[7px]' : 'border-[0.5px] border-components-panel-border-subtle px-1 pb-1 pt-[7px]',
running && !collapse && 'bg-background-section-burn',
succeeded && !collapse && 'bg-state-success-hover',

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
<svg width="12" height="14" viewBox="0 0 12 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path 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="#354052"/>
<path 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="#354052"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

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

View File

@@ -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<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'Thinking'
export default Icon

View File

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

View File

@@ -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<FeatureIconProps> = ({
// )
// }
// if (feature === ModelFeatureEnum.toolCall) {
// return (
// <Tooltip
// popupContent={t('common.modelProvider.featureSupported', { feature: ModelFeatureTextEnum.toolCall })}
// >
// <ModelBadge className={`mr-0.5 !px-0 w-[18px] justify-center text-gray-500 ${className}`}>
// <MagicWand className='w-3 h-3' />
// </ModelBadge>
// </Tooltip>
// )
// }
if (feature === ModelFeatureEnum.toolCall) {
if (showFeaturesLabel) {
return (
<ModelBadge className={cn('gap-x-0.5', className)}>
<RiHammerLine className="size-3" />
<span>{ModelFeatureTextEnum.toolCall}</span>
</ModelBadge>
)
}
return (
<Tooltip
popupContent={t('modelProvider.featureSupported', { ns: 'common', feature: ModelFeatureTextEnum.toolCall })}
>
<div className="inline-block cursor-help">
<ModelBadge
className={cn(
'w-[18px] justify-center !px-0',
className,
)}
>
<RiHammerLine className="size-3" />
</ModelBadge>
</div>
</Tooltip>
)
}
// if (feature === ModelFeatureEnum.multiToolCall) {
// return (

View File

@@ -96,6 +96,14 @@ const PopupItem: FC<PopupItemProps> = ({
<div className='text-text-tertiary system-xs-regular'>{currentProvider?.description?.[language] || currentProvider?.description?.en_US}</div>
)} */}
<div className="flex flex-wrap gap-1">
{
modelItem.features?.includes(ModelFeatureEnum.toolCall) && (
<FeatureIcon
feature={ModelFeatureEnum.toolCall}
showFeaturesLabel
/>
)
}
{modelItem.model_type && (
<ModelBadge>
{modelTypeFormat(modelItem.model_type)}
@@ -118,7 +126,7 @@ const PopupItem: FC<PopupItemProps> = ({
<div className="pt-2">
<div className="system-2xs-medium-uppercase mb-1 text-text-tertiary">{t('model.capabilities', { ns: 'common' })}</div>
<div className="flex flex-wrap gap-1">
{modelItem.features?.map(feature => (
{modelItem.features?.filter(feature => feature !== ModelFeatureEnum.toolCall).map(feature => (
<FeatureIcon
key={feature}
feature={feature}

View File

@@ -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[] = [

View File

@@ -6,17 +6,20 @@ export type BoxProps = {
className?: string
children?: ReactNode
withBorderBottom?: boolean
withBorderTop?: boolean
}
export const Box = memo(({
className,
children,
withBorderBottom,
withBorderTop,
}: BoxProps) => {
return (
<div
className={cn(
'py-2',
withBorderBottom && 'border-b border-divider-subtle',
withBorderTop && 'border-t border-divider-subtle',
className,
)}
>

View File

@@ -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 (
<div className={cn('mb-0.5', !!subTitle && 'mb-1')}>
<div className={cn('mb-0.5', !!subTitle && 'mb-1', className)}>
<div
className="group/collapse flex items-center justify-between py-1"
onClick={() => {

View File

@@ -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 (
<div
className={cn(
'px-4 py-2',
withBorderBottom && 'border-b border-divider-subtle',
withBorderTop && 'border-t border-divider-subtle',
className,
)}
>

View File

@@ -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 = <T>(id: string, data: CommonNodeType<T>) => {
@@ -18,3 +20,27 @@ const useNodeCrud = <T>(id: string, data: CommonNodeType<T>) => {
}
export default useNodeCrud
export const useNodeCurdKit = <T>(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<CommonNodeType<T>>) => {
handleNodeDataUpdateWithSyncDraft({
id,
data,
})
}, [id, handleNodeDataUpdateWithSyncDraft])
return {
getNodeData,
handleNodeDataUpdate,
}
}

View File

@@ -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 (
<BoxGroup
boxProps={{
withBorderBottom: true,
withBorderTop: true,
}}
groupProps={{
className: 'px-0',
}}
>
<MultipleToolSelector
nodeId={nodeId}
nodeOutputVars={[]}
availableNodes={[]}
value={tools}
label={t(`nodes.llm.tools.title`, { ns: 'workflow' })}
tooltip={t(`nodes.llm.tools.title`, { ns: 'workflow' })}
onChange={handleToolsChange}
supportCollapse
/>
<MaxIterations
value={maxIterations}
onChange={handleMaxIterationsChange}
/>
</BoxGroup>
)
}
export default memo(Tools)

View File

@@ -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 (
<div className="mt-3 flex h-10 items-center">
<div className="system-sm-semibold mr-0.5 truncate uppercase text-text-secondary">Max Iterations</div>
<Tooltip
popupContent="Max Iterations is the maximum number of iterations to run the tool."
triggerClassName="shrink-0 w-4 h-4"
/>
<div className="mr-3 flex grow items-center justify-end">
<Slider
className="w-[124px]"
value={value}
onChange={onChange ?? (() => {})}
min={1}
max={99}
step={1}
/>
</div>
<InputNumber
className="w-10 shrink-0"
value={value}
onChange={onChange ?? (() => {})}
min={1}
max={99}
step={1}
/>
</div>
)
}
export default memo(MaxIterations)

View File

@@ -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<LLMNodeType>(nodeId)
const handleToolsChange = (tools: ToolValue[]) => {
handleNodeDataUpdate({
tools,
})
}
const handleMaxIterationsChange = (maxIterations: number) => {
handleNodeDataUpdate({
max_iterations: maxIterations,
})
}
return {
handleToolsChange,
handleMaxIterationsChange,
}
}

View File

@@ -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<NodePanelProps<LLMNodeType>> = ({
</>
)}
<Tools
nodeId={id}
tools={inputs.tools}
maxIterations={inputs.max_iterations}
/>
{/* Vision: GPT4-vision and so on */}
<ConfigVision
nodeId={id}
@@ -308,6 +315,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

@@ -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<string, any>
settings?: Record<string, any>
extra?: Record<string, any>
}
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 {

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}
/>
)
}

View File

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

View File

@@ -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": "模型用量信息",

View File

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

View File

@@ -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, any> | string
toolFiles?: string[]
toolError?: string
toolDuration?: number
modelName?: string
modelProvider?: string
modelOutput?: Record<string, any> | 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<string, any>
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<string, any>
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 = {