diff --git a/web/app/components/workflow/hooks/use-checklist.ts b/web/app/components/workflow/hooks/use-checklist.ts index 3612d2765e..1493d9bf26 100644 --- a/web/app/components/workflow/hooks/use-checklist.ts +++ b/web/app/components/workflow/hooks/use-checklist.ts @@ -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 } } diff --git a/web/app/components/workflow/hooks/use-fetch-workflow-inspect-vars.ts b/web/app/components/workflow/hooks/use-fetch-workflow-inspect-vars.ts index 5586d1ce78..6147ba5e8f 100644 --- a/web/app/components/workflow/hooks/use-fetch-workflow-inspect-vars.ts +++ b/web/app/components/workflow/hooks/use-fetch-workflow-inspect-vars.ts @@ -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, diff --git a/web/app/components/workflow/hooks/use-inspect-vars-crud-common.ts b/web/app/components/workflow/hooks/use-inspect-vars-crud-common.ts index 14dac2e09b..9653505b2d 100644 --- a/web/app/components/workflow/hooks/use-inspect-vars-crud-common.ts +++ b/web/app/components/workflow/hooks/use-inspect-vars-crud-common.ts @@ -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() diff --git a/web/app/components/workflow/nodes/_base/components/variable-tag.tsx b/web/app/components/workflow/nodes/_base/components/variable-tag.tsx index a72e9b1da1..3d1558c8d5 100644 --- a/web/app/components/workflow/nodes/_base/components/variable-tag.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable-tag.tsx @@ -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() 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) diff --git a/web/app/components/workflow/nodes/_base/components/variable/utils.ts b/web/app/components/workflow/nodes/_base/components/variable/utils.ts index 02b1076fcb..138c33ee0b 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/utils.ts +++ b/web/app/components/workflow/nodes/_base/components/variable/utils.ts @@ -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 | 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 } diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx index a5fe820a43..587c026b11 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx @@ -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 = ({ 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 = ({ 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 diff --git a/web/app/components/workflow/nodes/if-else/components/condition-list/condition-var-selector.tsx b/web/app/components/workflow/nodes/if-else/components/condition-list/condition-var-selector.tsx index a8146c353d..95b3edce59 100644 --- a/web/app/components/workflow/nodes/if-else/components/condition-list/condition-var-selector.tsx +++ b/web/app/components/workflow/nodes/if-else/components/condition-list/condition-var-selector.tsx @@ -38,6 +38,7 @@ const ConditionVarSelector = ({ valueSelector={valueSelector} varType={varType} availableNodes={availableNodes} + availableVars={nodesOutputVars} isShort /> diff --git a/web/app/components/workflow/nodes/if-else/components/condition-number-input.tsx b/web/app/components/workflow/nodes/if-else/components/condition-number-input.tsx index a47605b389..52ceccf3e5 100644 --- a/web/app/components/workflow/nodes/if-else/components/condition-number-input.tsx +++ b/web/app/components/workflow/nodes/if-else/components/condition-number-input.tsx @@ -121,6 +121,7 @@ const ConditionNumberInput = ({ ) diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-variable-selector.tsx b/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-variable-selector.tsx index 7d902594af..2a2b6b1f29 100644 --- a/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-variable-selector.tsx +++ b/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-variable-selector.tsx @@ -57,6 +57,7 @@ const ConditionVariableSelector = ({ valueSelector={valueSelector} varType={varType} availableNodes={availableNodes} + availableVars={nodesOutputVars} isShort /> ) diff --git a/web/app/components/workflow/nodes/llm/panel.tsx b/web/app/components/workflow/nodes/llm/panel.tsx index dc742c5122..f765d53c21 100644 --- a/web/app/components/workflow/nodes/llm/panel.tsx +++ b/web/app/components/workflow/nodes/llm/panel.tsx @@ -410,28 +410,30 @@ const Panel: FC> = ({ )} > <> - + {!!inputs.computer_use && ( + + )} diff --git a/web/app/components/workflow/nodes/loop/components/condition-number-input.tsx b/web/app/components/workflow/nodes/loop/components/condition-number-input.tsx index a47605b389..52ceccf3e5 100644 --- a/web/app/components/workflow/nodes/loop/components/condition-number-input.tsx +++ b/web/app/components/workflow/nodes/loop/components/condition-number-input.tsx @@ -121,6 +121,7 @@ const ConditionNumberInput = ({ )