feat: LLM node to only show generation output var when computer use is

enabled, matching the actual output structure.
This commit is contained in:
zhsama
2026-02-09 23:40:42 +08:00
parent a71f336ee0
commit 41b218f427
12 changed files with 222 additions and 95 deletions

View File

@@ -46,7 +46,7 @@ import {
useGetToolIcon,
useNodesMetaData,
} from '../hooks'
import { getNodeUsedVars, isSpecialVar } from '../nodes/_base/components/variable/utils'
import { getNodeUsedVars, isValueSelectorInNodeOutputVars } from '../nodes/_base/components/variable/utils'
import {
useStore,
useWorkflowStore,
@@ -186,18 +186,8 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
const availableVars = map[node.id].availableVars
for (const variable of usedVars) {
const isSpecialVars = isSpecialVar(variable[0])
if (!isSpecialVars) {
const usedNode = availableVars.find(v => v.nodeId === variable?.[0])
if (usedNode) {
const usedVar = usedNode.vars.find(v => v.variable === variable?.[1])
if (!usedVar)
errorMessage = t('errorMsg.invalidVariable', { ns: 'workflow' })
}
else {
errorMessage = t('errorMsg.invalidVariable', { ns: 'workflow' })
}
}
if (!isValueSelectorInNodeOutputVars(variable, availableVars))
errorMessage = t('errorMsg.invalidVariable', { ns: 'workflow' })
}
}
@@ -383,20 +373,9 @@ export const useChecklistBeforePublish = () => {
const availableVars = map[node.id].availableVars
for (const variable of usedVars) {
const isSpecialVars = isSpecialVar(variable[0])
if (!isSpecialVars) {
const usedNode = availableVars.find(v => v.nodeId === variable?.[0])
if (usedNode) {
const usedVar = usedNode.vars.find(v => v.variable === variable?.[1])
if (!usedVar) {
notify({ type: 'error', message: `[${node.data.title}] ${t('errorMsg.invalidVariable', { ns: 'workflow' })}` })
return false
}
}
else {
notify({ type: 'error', message: `[${node.data.title}] ${t('errorMsg.invalidVariable', { ns: 'workflow' })}` })
return false
}
if (!isValueSelectorInNodeOutputVars(variable, availableVars)) {
notify({ type: 'error', message: `[${node.data.title}] ${t('errorMsg.invalidVariable', { ns: 'workflow' })}` })
return false
}
}

View File

@@ -16,7 +16,7 @@ import {
import { useInvalidateConversationVarValues, useInvalidateSysVarValues } from '@/service/use-workflow'
import { fetchAllInspectVars } from '@/service/workflow'
import useMatchSchemaType from '../nodes/_base/components/variable/use-match-schema-type'
import { toNodeOutputVars } from '../nodes/_base/components/variable/utils'
import { isValueSelectorInNodeOutputVars, toNodeOutputVars } from '../nodes/_base/components/variable/utils'
import { applyAgentSubgraphInspectVars } from './inspect-vars-agent-alias'
type Params = {
@@ -90,10 +90,14 @@ export const useSetWorkflowVarsWithValue = ({
const nodesWithVars: NodeWithVar[] = withValueNodes.map((node) => {
const nodeId = node.id
const isParentNode = resolvedInteractionMode === InteractionMode.Subgraph && parentNodeIds.has(nodeId)
const varsUnderTheNode = inspectVars.filter((varItem) => {
return varItem.selector[0] === nodeId
})
const nodeVar = allNodesOutputVars.find(item => item.nodeId === nodeId)
const varsUnderTheNode = inspectVars.filter((varItem) => {
if (varItem.selector[0] !== nodeId)
return false
if (!nodeVar)
return false
return isValueSelectorInNodeOutputVars(varItem.selector, [nodeVar])
})
return {
nodeId,

View File

@@ -12,6 +12,7 @@ import {
isConversationVar,
isENV,
isSystemVar,
isValueSelectorInNodeOutputVars,
toNodeOutputVars,
} from '@/app/components/workflow/nodes/_base/components/variable/utils'
import { useWorkflowStore } from '@/app/components/workflow/store'
@@ -140,13 +141,15 @@ export const useInspectVarsCrudCommon = ({
}
const currentNodeOutputVars = toNodeOutputVars([currentNode], false, () => true, [], [], [], allPluginInfoList, schemaTypeDefinitions)
const vars = await fetchNodeInspectVars(flowType, flowId, nodeId)
const varsWithSchemaType = vars.map((varItem) => {
const schemaType = currentNodeOutputVars[0]?.vars.find(v => v.variable === varItem.name)?.schemaType || ''
return {
...varItem,
schemaType,
}
})
const varsWithSchemaType = vars
.filter(varItem => isValueSelectorInNodeOutputVars(varItem.selector, currentNodeOutputVars))
.map((varItem) => {
const schemaType = currentNodeOutputVars[0]?.vars.find(v => v.variable === varItem.name)?.schemaType || ''
return {
...varItem,
schemaType,
}
})
setNodeInspectVars(nodeId, varsWithSchemaType)
const resolvedInteractionMode = interactionMode ?? InteractionMode.Default
if (resolvedInteractionMode !== InteractionMode.Subgraph) {
@@ -154,16 +157,29 @@ export const useInspectVarsCrudCommon = ({
const nextNodes = applyAgentSubgraphInspectVars(nodesWithInspectVars, nodeArr)
setNodesWithInspectVars(nextNodes)
}
}, [workflowStore, flowType, flowId, invalidateSysVarValues, invalidateConversationVarValues, buildInTools, customTools, workflowTools, mcpTools, interactionMode])
}, [workflowStore, flowType, flowId, invalidateSysVarValues, invalidateConversationVarValues, buildInTools, customTools, workflowTools, mcpTools, interactionMode, store])
// after last run would call this
const appendNodeInspectVars = useCallback((nodeId: string, payload: VarInInspect[], allNodes: Node[]) => {
const { dataSourceList } = workflowStore.getState()
const nodeInfo = allNodes.find(node => node.id === nodeId)
const allPluginInfoList = {
buildInTools: buildInTools || [],
customTools: customTools || [],
workflowTools: workflowTools || [],
mcpTools: mcpTools || [],
dataSourceList: dataSourceList || [],
}
const currentNodeOutputVars = nodeInfo
? toNodeOutputVars([nodeInfo], false, () => true, [], [], [], allPluginInfoList)
: []
const validPayload = payload.filter(varItem => isValueSelectorInNodeOutputVars(varItem.selector, currentNodeOutputVars))
const {
nodesWithInspectVars,
setNodesWithInspectVars,
} = workflowStore.getState()
const nodes = produce(nodesWithInspectVars, (draft) => {
const nodeInfo = allNodes.find(node => node.id === nodeId)
if (nodeInfo) {
const index = draft.findIndex(node => node.nodeId === nodeId)
if (index === -1) {
@@ -171,12 +187,12 @@ export const useInspectVarsCrudCommon = ({
nodeId,
nodeType: nodeInfo.data.type,
title: nodeInfo.data.title,
vars: payload,
vars: validPayload,
nodePayload: nodeInfo.data,
})
}
else {
draft[index].vars = payload
draft[index].vars = validPayload
// put the node to the topAdd commentMore actions
draft.unshift(draft.splice(index, 1)[0])
}
@@ -187,7 +203,7 @@ export const useInspectVarsCrudCommon = ({
const nextNodes = shouldApplyAlias ? applyAgentSubgraphInspectVars(nodes, allNodes) : nodes
setNodesWithInspectVars(nextNodes)
handleCancelNodeSuccessStatus(nodeId)
}, [workflowStore, handleCancelNodeSuccessStatus, interactionMode])
}, [workflowStore, handleCancelNodeSuccessStatus, interactionMode, buildInTools, customTools, workflowTools, mcpTools])
const hasNodeInspectVar = useCallback((nodeId: string, varId: string) => {
const { nodesWithInspectVars } = workflowStore.getState()

View File

@@ -1,13 +1,14 @@
import type {
CommonNodeType,
Node,
NodeOutPutVar,
ValueSelector,
VarType,
} from '@/app/components/workflow/types'
import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useNodes, useReactFlow, useStoreApi } from 'reactflow'
import { getNodeInfoById, isConversationVar, isENV, isGlobalVar, isRagVariableVar, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import { getNodeInfoById, isConversationVar, isENV, isGlobalVar, isRagVariableVar, isSystemVar, isValueSelectorInNodeOutputVars } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import {
VariableLabelInSelect,
} from '@/app/components/workflow/nodes/_base/components/variable/variable-label'
@@ -19,12 +20,14 @@ type VariableTagProps = {
varType: VarType
isShort?: boolean
availableNodes?: Node[]
availableVars?: NodeOutPutVar[]
}
const VariableTag = ({
valueSelector,
varType,
isShort,
availableNodes,
availableVars,
}: VariableTagProps) => {
const nodes = useNodes<CommonNodeType>()
const isRagVar = isRagVariableVar(valueSelector)
@@ -40,7 +43,12 @@ const VariableTag = ({
const isEnv = isENV(valueSelector)
const isChatVar = isConversationVar(valueSelector)
const isGlobal = isGlobalVar(valueSelector)
const isValid = Boolean(node) || isEnv || isChatVar || isRagVar || isGlobal
const isValid = useMemo(() => {
if (availableVars)
return isValueSelectorInNodeOutputVars(valueSelector, availableVars)
return Boolean(node) || isEnv || isChatVar || isRagVar || isGlobal
}, [availableVars, valueSelector, node, isEnv, isChatVar, isRagVar, isGlobal])
const variableName = isSystemVar(valueSelector) ? valueSelector.slice(0).join('.') : valueSelector.slice(1).join('.')
const isException = isExceptionVariable(variableName, node?.data.type)

View File

@@ -340,6 +340,29 @@ const findExceptVarInObject = (
return res
}
const getLLMNodeOutputVars = (llmNodeData: LLMNodeType): Var[] => {
const isComputerUseEnabled = !!llmNodeData.computer_use
const vars = [...LLM_OUTPUT_STRUCT].filter((item) => {
if (isComputerUseEnabled)
return true
return item.variable !== 'generation'
})
if (
llmNodeData.structured_output_enabled
&& llmNodeData.structured_output?.schema?.properties
&& Object.keys(llmNodeData.structured_output.schema.properties).length > 0
) {
vars.push({
variable: 'structured_output',
type: VarType.object,
children: llmNodeData.structured_output,
})
}
return vars
}
const formatItem = (
item: any,
isChatMode: boolean,
@@ -415,18 +438,8 @@ const formatItem = (
}
case BlockEnum.LLM: {
res.vars = [...LLM_OUTPUT_STRUCT]
if (
data.structured_output_enabled
&& data.structured_output?.schema?.properties
&& Object.keys(data.structured_output.schema.properties).length > 0
) {
res.vars.push({
variable: 'structured_output',
type: VarType.object,
children: data.structured_output,
})
}
const llmNodeData = data as LLMNodeType
res.vars = getLLMNodeOutputVars(llmNodeData)
break
}
@@ -1304,6 +1317,104 @@ export const getNodeInfoById = (nodes: any, id: string) => {
return nodes.find((node: any) => node.id === id)
}
const normalizeSpecialValueSelector = (valueSelector: ValueSelector): ValueSelector => {
if (valueSelector.length > 1 && isSpecialVar(valueSelector[1]))
return valueSelector.slice(1)
return valueSelector
}
const getVarRootSelector = (nodeId: string, variable: string): ValueSelector => {
const path = variable.split('.')
if (path.length > 0 && isSpecialVar(path[0]))
return path
return [nodeId, ...path]
}
const isSelectorPathValidInStructuredProperties = (
properties: Record<string, StructField> | undefined,
selectorTail: ValueSelector,
): boolean => {
if (!properties)
return false
if (selectorTail.length === 0)
return true
const [currentKey, ...rest] = selectorTail
const property = properties[currentKey]
if (!property)
return false
if (rest.length === 0)
return true
if (property.type === Type.object)
return isSelectorPathValidInStructuredProperties(property.properties, rest)
return false
}
const isSelectorPathValidInVar = (
variable: Var,
selectorTail: ValueSelector,
): boolean => {
if (selectorTail.length === 0)
return true
if (!variable.children)
return false
const structuredProperties = (variable.children as StructuredOutput)?.schema?.properties
if (structuredProperties)
return isSelectorPathValidInStructuredProperties(structuredProperties, selectorTail)
if (!Array.isArray(variable.children))
return false
const [currentKey, ...rest] = selectorTail
const child = variable.children.find(item => item.variable === currentKey)
if (!child)
return false
return isSelectorPathValidInVar(child, rest)
}
const isValueSelectorMatchVar = (
valueSelector: ValueSelector,
nodeId: string,
variable: Var,
): boolean => {
const rootSelector = getVarRootSelector(nodeId, variable.variable)
if (valueSelector.length < rootSelector.length)
return false
const isRootMatched = rootSelector.every((segment, index) => {
return valueSelector[index] === segment
})
if (!isRootMatched)
return false
const selectorTail = valueSelector.slice(rootSelector.length)
return isSelectorPathValidInVar(variable, selectorTail)
}
export const isValueSelectorInNodeOutputVars = (
valueSelector: ValueSelector,
nodeOutputVars: NodeOutPutVar[],
): boolean => {
if (!Array.isArray(valueSelector) || valueSelector.length === 0)
return false
const normalizedSelector = normalizeSpecialValueSelector(valueSelector)
const selectorsToCheck = [valueSelector]
if (normalizedSelector.join('.') !== valueSelector.join('.'))
selectorsToCheck.push(normalizedSelector)
return selectorsToCheck.some((selector) => {
return nodeOutputVars.some((nodeOutputVar) => {
return nodeOutputVar.vars.some((variable) => {
return isValueSelectorMatchVar(selector, nodeOutputVar.nodeId, variable)
})
})
})
}
const matchNotSystemVars = (prompts: string[]) => {
if (!prompts)
return []
@@ -1371,7 +1482,10 @@ export const getNodeUsedVars = (node: Node): ValueSelector[] => {
const contextVar = (data as LLMNodeType).context?.variable_selector
? [(data as LLMNodeType).context?.variable_selector]
: []
res = [...inputVars, ...contextVar]
const jinja2VarSelectors = payload.prompt_config?.jinja2_variables
?.map(item => item.value_selector)
.filter(selector => Array.isArray(selector) && selector.length > 0) || []
res = [...inputVars, ...contextVar, ...jinja2VarSelectors]
break
}
case BlockEnum.KnowledgeRetrieval: {
@@ -1416,6 +1530,14 @@ export const getNodeUsedVars = (node: Node): ValueSelector[] => {
})
break
}
case BlockEnum.Command: {
const payload = data as CommandNodeType
res = matchNotSystemVars([
payload.command,
payload.working_directory,
])
break
}
case BlockEnum.QuestionClassifier: {
const payload = data as QuestionClassifierNodeType
res = [payload.query_variable_selector]
@@ -2081,19 +2203,8 @@ export const getNodeOutputVars = (
}
case BlockEnum.LLM: {
const vars = [...LLM_OUTPUT_STRUCT]
const llmNodeData = data as LLMNodeType
if (
llmNodeData.structured_output_enabled
&& llmNodeData.structured_output?.schema?.properties
&& Object.keys(llmNodeData.structured_output.schema.properties).length > 0
) {
vars.push({
variable: 'structured_output',
type: VarType.object,
children: llmNodeData.structured_output,
})
}
const vars = getLLMNodeOutputVars(llmNodeData)
varsToValueSelectorList(vars, [id], res)
break
}

View File

@@ -49,7 +49,7 @@ import { cn } from '@/utils/classnames'
import useAvailableVarList from '../../hooks/use-available-var-list'
import RemoveButton from '../remove-button'
import ConstantField from './constant-field'
import { getNodeInfoById, isConversationVar, isENV, isGlobalVar, isRagVariableVar, isSystemVar, removeFileVars, varTypeToStructType } from './utils'
import { getNodeInfoById, isConversationVar, isENV, isGlobalVar, isRagVariableVar, isSystemVar, isValueSelectorInNodeOutputVars, removeFileVars, varTypeToStructType } from './utils'
import VarFullPathPanel from './var-full-path-panel'
import VarReferencePopup from './var-reference-popup'
@@ -312,7 +312,9 @@ const VarReferencePicker: FC<Props> = ({
const isChatVar = isConversationVar(value as ValueSelector)
const isGlobal = isGlobalVar(value as ValueSelector)
const isRagVar = isRagVariableVar(value as ValueSelector)
const isValidVar = Boolean(outputVarNode) || isEnv || isChatVar || isGlobal || isRagVar
const isValidVar = !hasValue || !Array.isArray(value)
? true
: isValueSelectorInNodeOutputVars(value, outputVars)
const isException = isExceptionVariable(varName, outputVarNode?.type)
return {
isEnv,
@@ -322,7 +324,7 @@ const VarReferencePicker: FC<Props> = ({
isValidVar,
isException,
}
}, [value, outputVarNode, varName])
}, [value, hasValue, outputVarNode, outputVars, varName])
// 8(left/right-padding) + 14(icon) + 4 + 14 + 2 = 42 + 17 buff
const availableWidth = triggerWidth - 56

View File

@@ -38,6 +38,7 @@ const ConditionVarSelector = ({
valueSelector={valueSelector}
varType={varType}
availableNodes={availableNodes}
availableVars={nodesOutputVars}
isShort
/>
</div>

View File

@@ -121,6 +121,7 @@ const ConditionNumberInput = ({
<VariableTag
valueSelector={variableTransformer(value) as string[]}
varType={VarType.number}
availableVars={variables}
isShort={isShort}
/>
)

View File

@@ -57,6 +57,7 @@ const ConditionVariableSelector = ({
valueSelector={valueSelector}
varType={varType}
availableNodes={availableNodes}
availableVars={nodesOutputVars}
isShort
/>
)

View File

@@ -410,28 +410,30 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
)}
>
<>
<VarItem
name="generation"
type="object"
description={t(`${i18nPrefix}.outputVars.generation`, { ns: 'workflow' })}
subItems={[
{
name: 'content',
type: 'string',
description: '',
},
{
name: 'reasoning_content',
type: 'array[string]',
description: '',
},
{
name: 'tool_calls',
type: 'array[object]',
description: '',
},
]}
/>
{!!inputs.computer_use && (
<VarItem
name="generation"
type="object"
description={t(`${i18nPrefix}.outputVars.generation`, { ns: 'workflow' })}
subItems={[
{
name: 'content',
type: 'string',
description: '',
},
{
name: 'reasoning_content',
type: 'array[string]',
description: '',
},
{
name: 'tool_calls',
type: 'array[object]',
description: '',
},
]}
/>
)}
<VarItem
name="text"
type="string"

View File

@@ -38,6 +38,7 @@ const ConditionVarSelector = ({
valueSelector={valueSelector}
varType={varType}
availableNodes={availableNodes}
availableVars={nodesOutputVars}
isShort
/>
</div>

View File

@@ -121,6 +121,7 @@ const ConditionNumberInput = ({
<VariableTag
valueSelector={variableTransformer(value) as string[]}
varType={VarType.number}
availableVars={variables}
isShort={isShort}
/>
)