From 7c097fe6ebdc8872d66d9d9b8b99c9f78331957d Mon Sep 17 00:00:00 2001 From: Mminamiyama Date: Thu, 26 Jun 2025 13:18:11 +0800 Subject: [PATCH] feat(env-panel): add description field to environment variables Add description field to environment variable modal and display it in the env item component. This allows users to provide additional context for each environment variable. --- .../nodes/_base/components/variable/utils.ts | 2993 +++++++++-------- .../workflow/panel/env-panel/env-item.tsx | 109 +- .../panel/env-panel/variable-modal.tsx | 349 +- 3 files changed, 1735 insertions(+), 1716 deletions(-) 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 428c204dd3..a79b6e98f8 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/utils.ts +++ b/web/app/components/workflow/nodes/_base/components/variable/utils.ts @@ -1,1496 +1,1497 @@ -import produce from 'immer' -import { isArray, uniq } from 'lodash-es' -import type { CodeNodeType } from '../../../code/types' -import type { EndNodeType } from '../../../end/types' -import type { AnswerNodeType } from '../../../answer/types' -import { type LLMNodeType, type StructuredOutput, Type } from '../../../llm/types' -import type { KnowledgeRetrievalNodeType } from '../../../knowledge-retrieval/types' -import type { IfElseNodeType } from '../../../if-else/types' -import type { TemplateTransformNodeType } from '../../../template-transform/types' -import type { QuestionClassifierNodeType } from '../../../question-classifier/types' -import type { HttpNodeType } from '../../../http/types' -import { VarType as ToolVarType } from '../../../tool/types' -import type { ToolNodeType } from '../../../tool/types' -import type { ParameterExtractorNodeType } from '../../../parameter-extractor/types' -import type { IterationNodeType } from '../../../iteration/types' -import type { LoopNodeType } from '../../../loop/types' -import type { ListFilterNodeType } from '../../../list-operator/types' -import { OUTPUT_FILE_SUB_VARIABLES } from '../../../constants' -import type { DocExtractorNodeType } from '../../../document-extractor/types' -import { BlockEnum, InputVarType, VarType } from '@/app/components/workflow/types' -import type { StartNodeType } from '@/app/components/workflow/nodes/start/types' -import type { ConversationVariable, EnvironmentVariable, Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types' -import type { VariableAssignerNodeType } from '@/app/components/workflow/nodes/variable-assigner/types' -import type { Field as StructField } from '@/app/components/workflow/nodes/llm/types' - -import { - HTTP_REQUEST_OUTPUT_STRUCT, - KNOWLEDGE_RETRIEVAL_OUTPUT_STRUCT, - LLM_OUTPUT_STRUCT, - PARAMETER_EXTRACTOR_COMMON_STRUCT, - QUESTION_CLASSIFIER_OUTPUT_STRUCT, - SUPPORT_OUTPUT_VARS_NODE, - TEMPLATE_TRANSFORM_OUTPUT_STRUCT, - TOOL_OUTPUT_STRUCT, -} from '@/app/components/workflow/constants' -import type { PromptItem } from '@/models/debug' -import { VAR_REGEX } from '@/config' -import type { AgentNodeType } from '../../../agent/types' - -export const isSystemVar = (valueSelector: ValueSelector) => { - return valueSelector[0] === 'sys' || valueSelector[1] === 'sys' -} - -export const isENV = (valueSelector: ValueSelector) => { - return valueSelector[0] === 'env' -} - -export const isConversationVar = (valueSelector: ValueSelector) => { - return valueSelector[0] === 'conversation' -} - -const inputVarTypeToVarType = (type: InputVarType): VarType => { - return ({ - [InputVarType.number]: VarType.number, - [InputVarType.singleFile]: VarType.file, - [InputVarType.multiFiles]: VarType.arrayFile, - } as any)[type] || VarType.string -} - -const structTypeToVarType = (type: Type, isArray?: boolean): VarType => { - if (isArray) { - return ({ - [Type.string]: VarType.arrayString, - [Type.number]: VarType.arrayNumber, - [Type.object]: VarType.arrayObject, - } as any)[type] || VarType.string - } - return ({ - [Type.string]: VarType.string, - [Type.number]: VarType.number, - [Type.boolean]: VarType.boolean, - [Type.object]: VarType.object, - [Type.array]: VarType.array, - } as any)[type] || VarType.string -} - -export const varTypeToStructType = (type: VarType): Type => { - return ({ - [VarType.string]: Type.string, - [VarType.number]: Type.number, - [VarType.boolean]: Type.boolean, - [VarType.object]: Type.object, - [VarType.array]: Type.array, - } as any)[type] || Type.string -} - -const findExceptVarInStructuredProperties = (properties: Record, filterVar: (payload: Var, selector: ValueSelector) => boolean): Record => { - const res = produce(properties, (draft) => { - Object.keys(properties).forEach((key) => { - const item = properties[key] - const isObj = item.type === Type.object - const isArray = item.type === Type.array - const arrayType = item.items?.type - - if (!isObj && !filterVar({ - variable: key, - type: structTypeToVarType(isArray ? arrayType! : item.type, isArray), - }, [key])) { - delete properties[key] - return - } - if (item.type === Type.object && item.properties) - item.properties = findExceptVarInStructuredProperties(item.properties, filterVar) - }) - return draft - }) - return res -} - -const findExceptVarInStructuredOutput = (structuredOutput: StructuredOutput, filterVar: (payload: Var, selector: ValueSelector) => boolean): StructuredOutput => { - const res = produce(structuredOutput, (draft) => { - const properties = draft.schema.properties - Object.keys(properties).forEach((key) => { - const item = properties[key] - const isObj = item.type === Type.object - const isArray = item.type === Type.array - const arrayType = item.items?.type - if (!isObj && !filterVar({ - variable: key, - type: structTypeToVarType(isArray ? arrayType! : item.type, isArray), - }, [key])) { - delete properties[key] - return - } - if (item.type === Type.object && item.properties) - item.properties = findExceptVarInStructuredProperties(item.properties, filterVar) - }) - return draft - }) - return res -} - -const findExceptVarInObject = (obj: any, filterVar: (payload: Var, selector: ValueSelector) => boolean, value_selector: ValueSelector, isFile?: boolean): Var => { - const { children } = obj - const isStructuredOutput = !!(children as StructuredOutput)?.schema?.properties - - let childrenResult: Var[] | StructuredOutput | undefined - - if (isStructuredOutput) { - childrenResult = findExceptVarInStructuredOutput(children, filterVar) - } - else if (Array.isArray(children)) { - childrenResult = children.filter((item: Var) => { - const { children: itemChildren } = item - const currSelector = [...value_selector, item.variable] - - if (!itemChildren) - return filterVar(item, currSelector) - - const filteredObj = findExceptVarInObject(item, filterVar, currSelector, false) // File doesn't contain file children - return filteredObj.children && (filteredObj.children as Var[])?.length > 0 - }) - } - else { - childrenResult = [] - } - - const res: Var = { - variable: obj.variable, - type: isFile ? VarType.file : VarType.object, - children: childrenResult, - } - - return res -} - -const formatItem = ( - item: any, - isChatMode: boolean, - filterVar: (payload: Var, selector: ValueSelector) => boolean, -): NodeOutPutVar => { - const { id, data } = item - - const res: NodeOutPutVar = { - nodeId: id, - title: data.title, - vars: [], - } - switch (data.type) { - case BlockEnum.Start: { - const { - variables, - } = data as StartNodeType - res.vars = variables.map((v) => { - return { - variable: v.variable, - type: inputVarTypeToVarType(v.type), - isParagraph: v.type === InputVarType.paragraph, - isSelect: v.type === InputVarType.select, - options: v.options, - required: v.required, - } - }) - if (isChatMode) { - res.vars.push({ - variable: 'sys.query', - type: VarType.string, - }) - res.vars.push({ - variable: 'sys.dialogue_count', - type: VarType.number, - }) - res.vars.push({ - variable: 'sys.conversation_id', - type: VarType.string, - }) - } - res.vars.push({ - variable: 'sys.user_id', - type: VarType.string, - }) - res.vars.push({ - variable: 'sys.files', - type: VarType.arrayFile, - }) - res.vars.push({ - variable: 'sys.app_id', - type: VarType.string, - }) - res.vars.push({ - variable: 'sys.workflow_id', - type: VarType.string, - }) - res.vars.push({ - variable: 'sys.workflow_run_id', - type: VarType.string, - }) - - break - } - - 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, - }) - } - - break - } - case BlockEnum.KnowledgeRetrieval: { - res.vars = KNOWLEDGE_RETRIEVAL_OUTPUT_STRUCT - break - } - - case BlockEnum.Code: { - const { - outputs, - } = data as CodeNodeType - res.vars = outputs - ? Object.keys(outputs).map((key) => { - return { - variable: key, - type: outputs[key].type, - } - }) - : [] - break - } - - case BlockEnum.TemplateTransform: { - res.vars = TEMPLATE_TRANSFORM_OUTPUT_STRUCT - break - } - - case BlockEnum.QuestionClassifier: { - res.vars = QUESTION_CLASSIFIER_OUTPUT_STRUCT - break - } - - case BlockEnum.HttpRequest: { - res.vars = HTTP_REQUEST_OUTPUT_STRUCT - break - } - - case BlockEnum.VariableAssigner: { - const { - output_type, - advanced_settings, - } = data as VariableAssignerNodeType - const isGroup = !!advanced_settings?.group_enabled - if (!isGroup) { - res.vars = [ - { - variable: 'output', - type: output_type, - }, - ] - } - else { - res.vars = advanced_settings?.groups.map((group) => { - return { - variable: group.group_name, - type: VarType.object, - children: [{ - variable: 'output', - type: group.output_type, - }], - } - }) - } - break - } - - // eslint-disable-next-line sonarjs/no-duplicated-branches - case BlockEnum.VariableAggregator: { - const { - output_type, - advanced_settings, - } = data as VariableAssignerNodeType - const isGroup = !!advanced_settings?.group_enabled - if (!isGroup) { - res.vars = [ - { - variable: 'output', - type: output_type, - }, - ] - } - else { - res.vars = advanced_settings?.groups.map((group) => { - return { - variable: group.group_name, - type: VarType.object, - children: [{ - variable: 'output', - type: group.output_type, - }], - } - }) - } - break - } - - case BlockEnum.Tool: { - const { - output_schema, - } = data as ToolNodeType - if (!output_schema) { - res.vars = TOOL_OUTPUT_STRUCT - } - else { - const outputSchema: any[] = [] - Object.keys(output_schema.properties).forEach((outputKey) => { - const output = output_schema.properties[outputKey] - const dataType = output.type - outputSchema.push({ - variable: outputKey, - type: dataType === 'array' - ? `array[${output.items?.type.slice(0, 1).toLocaleLowerCase()}${output.items?.type.slice(1)}]` - : `${output.type.slice(0, 1).toLocaleLowerCase()}${output.type.slice(1)}`, - description: output.description, - children: output.type === 'object' ? { - schema: { - type: 'object', - properties: output.properties, - }, - } : undefined, - }) - }) - res.vars = [ - ...TOOL_OUTPUT_STRUCT, - ...outputSchema, - ] - } - break - } - - case BlockEnum.ParameterExtractor: { - res.vars = [ - ...((data as ParameterExtractorNodeType).parameters || []).map((p) => { - return { - variable: p.name, - type: p.type as unknown as VarType, - } - }), - ...PARAMETER_EXTRACTOR_COMMON_STRUCT, - ] - break - } - - case BlockEnum.Iteration: { - res.vars = [ - { - variable: 'output', - type: (data as IterationNodeType).output_type || VarType.arrayString, - }, - ] - break - } - - case BlockEnum.Loop: { - const { loop_variables } = data as LoopNodeType - res.isLoop = true - res.vars = loop_variables?.map((v) => { - return { - variable: v.label, - type: v.var_type, - isLoopVariable: true, - nodeId: res.nodeId, - } - }) || [] - - break - } - - case BlockEnum.DocExtractor: { - res.vars = [ - { - variable: 'text', - type: (data as DocExtractorNodeType).is_array_file ? VarType.arrayString : VarType.string, - }, - ] - break - } - - case BlockEnum.ListFilter: { - if (!(data as ListFilterNodeType).var_type) - break - - res.vars = [ - { - variable: 'result', - type: (data as ListFilterNodeType).var_type, - }, - { - variable: 'first_record', - type: (data as ListFilterNodeType).item_var_type, - }, - { - variable: 'last_record', - type: (data as ListFilterNodeType).item_var_type, - }, - ] - break - } - - case BlockEnum.Agent: { - const payload = data as AgentNodeType - const outputs: Var[] = [] - Object.keys(payload.output_schema?.properties || {}).forEach((outputKey) => { - const output = payload.output_schema.properties[outputKey] - outputs.push({ - variable: outputKey, - type: output.type === 'array' - ? `Array[${output.items?.type.slice(0, 1).toLocaleUpperCase()}${output.items?.type.slice(1)}]` as VarType - : `${output.type.slice(0, 1).toLocaleUpperCase()}${output.type.slice(1)}` as VarType, - }) - }) - res.vars = [ - ...outputs, - ...TOOL_OUTPUT_STRUCT, - ] - break - } - - case 'env': { - res.vars = data.envList.map((env: EnvironmentVariable) => { - return { - variable: `env.${env.name}`, - type: env.value_type, - } - }) as Var[] - break - } - - case 'conversation': { - res.vars = data.chatVarList.map((chatVar: ConversationVariable) => { - return { - variable: `conversation.${chatVar.name}`, - type: chatVar.value_type, - des: chatVar.description, - } - }) as Var[] - break - } - } - - const { error_strategy } = data - - if (error_strategy) { - res.vars = [ - ...res.vars, - { - variable: 'error_message', - type: VarType.string, - isException: true, - }, - { - variable: 'error_type', - type: VarType.string, - isException: true, - }, - ] - } - - const selector = [id] - res.vars = res.vars.filter((v) => { - const isCurrentMatched = filterVar(v, (() => { - const variableArr = v.variable.split('.') - const [first] = variableArr - if (first === 'sys' || first === 'env' || first === 'conversation') - return variableArr - - return [...selector, ...variableArr] - })()) - if (isCurrentMatched) - return true - - const isFile = v.type === VarType.file - const children = (() => { - if (isFile) { - return OUTPUT_FILE_SUB_VARIABLES.map((key) => { - return { - variable: key, - type: key === 'size' ? VarType.number : VarType.string, - } - }) - } - return v.children - })() - if (!children) - return false - - const obj = findExceptVarInObject(isFile ? { ...v, children } : v, filterVar, selector, isFile) - return obj?.children && ((obj?.children as Var[]).length > 0 || Object.keys((obj?.children as StructuredOutput)?.schema?.properties || {}).length > 0) - }).map((v) => { - const isFile = v.type === VarType.file - - const { children } = (() => { - if (isFile) { - return { - children: OUTPUT_FILE_SUB_VARIABLES.map((key) => { - return { - variable: key, - type: key === 'size' ? VarType.number : VarType.string, - } - }), - } - } - return v - })() - - if (!children) - return v - - return findExceptVarInObject(isFile ? { ...v, children } : v, filterVar, selector, isFile) - }) - - return res -} -export const toNodeOutputVars = ( - nodes: any[], - isChatMode: boolean, - filterVar = (_payload: Var, _selector: ValueSelector) => true, - environmentVariables: EnvironmentVariable[] = [], - conversationVariables: ConversationVariable[] = [], -): NodeOutPutVar[] => { - // ENV_NODE data format - const ENV_NODE = { - id: 'env', - data: { - title: 'ENVIRONMENT', - type: 'env', - envList: environmentVariables, - }, - } - // CHAT_VAR_NODE data format - const CHAT_VAR_NODE = { - id: 'conversation', - data: { - title: 'CONVERSATION', - type: 'conversation', - chatVarList: conversationVariables, - }, - } - // Sort nodes in reverse chronological order (most recent first) - const sortedNodes = [...nodes].sort((a, b) => { - if (a.data.type === BlockEnum.Start) return 1 - if (b.data.type === BlockEnum.Start) return -1 - if (a.data.type === 'env') return 1 - if (b.data.type === 'env') return -1 - if (a.data.type === 'conversation') return 1 - if (b.data.type === 'conversation') return -1 - // sort nodes by x position - return (b.position?.x || 0) - (a.position?.x || 0) - }) - - const res = [ - ...sortedNodes.filter(node => SUPPORT_OUTPUT_VARS_NODE.includes(node?.data?.type)), - ...(environmentVariables.length > 0 ? [ENV_NODE] : []), - ...((isChatMode && conversationVariables.length > 0) ? [CHAT_VAR_NODE] : []), - ].map((node) => { - return { - ...formatItem(node, isChatMode, filterVar), - isStartNode: node.data.type === BlockEnum.Start, - } - }).filter(item => item.vars.length > 0) - return res -} - -const getIterationItemType = ({ - valueSelector, - beforeNodesOutputVars, -}: { - valueSelector: ValueSelector - beforeNodesOutputVars: NodeOutPutVar[] -}): VarType => { - const outputVarNodeId = valueSelector[0] - const isSystem = isSystemVar(valueSelector) - - const targetVar = isSystem ? beforeNodesOutputVars.find(v => v.isStartNode) : beforeNodesOutputVars.find(v => v.nodeId === outputVarNodeId) - - if (!targetVar) - return VarType.string - - let arrayType: VarType = VarType.string - - let curr: any = targetVar.vars - if (isSystem) { - arrayType = curr.find((v: any) => v.variable === (valueSelector).join('.'))?.type - } - else { - for (let i = 1; i < valueSelector.length; i++) { - const key = valueSelector[i] - const isLast = i === valueSelector.length - 1 - curr = Array.isArray(curr) ? curr.find(v => v.variable === key) : [] - - if (isLast) - arrayType = curr?.type - else if (curr?.type === VarType.object || curr?.type === VarType.file) - curr = curr.children || [] - } - } - - switch (arrayType as VarType) { - case VarType.arrayString: - return VarType.string - case VarType.arrayNumber: - return VarType.number - case VarType.arrayObject: - return VarType.object - case VarType.array: - return VarType.any - case VarType.arrayFile: - return VarType.file - default: - return VarType.string - } -} - -const getLoopItemType = ({ - valueSelector, - beforeNodesOutputVars, -}: { - valueSelector: ValueSelector - beforeNodesOutputVars: NodeOutPutVar[] - -}): VarType => { - const outputVarNodeId = valueSelector[0] - const isSystem = isSystemVar(valueSelector) - - const targetVar = isSystem ? beforeNodesOutputVars.find(v => v.isStartNode) : beforeNodesOutputVars.find(v => v.nodeId === outputVarNodeId) - if (!targetVar) - return VarType.string - - let arrayType: VarType = VarType.string - - let curr: any = targetVar.vars - if (isSystem) { - arrayType = curr.find((v: any) => v.variable === (valueSelector).join('.'))?.type - } - else { - (valueSelector).slice(1).forEach((key, i) => { - const isLast = i === valueSelector.length - 2 - curr = curr?.find((v: any) => v.variable === key) - if (isLast) { - arrayType = curr?.type - } - else { - if (curr?.type === VarType.object || curr?.type === VarType.file) - curr = curr.children - } - }) - } - - switch (arrayType as VarType) { - case VarType.arrayString: - return VarType.string - case VarType.arrayNumber: - return VarType.number - case VarType.arrayObject: - return VarType.object - case VarType.array: - return VarType.any - case VarType.arrayFile: - return VarType.file - default: - return VarType.string - } -} - -export const getVarType = ({ - parentNode, - valueSelector, - isIterationItem, - isLoopItem, - availableNodes, - isChatMode, - isConstant, - environmentVariables = [], - conversationVariables = [], -}: { - valueSelector: ValueSelector - parentNode?: Node | null - isIterationItem?: boolean - isLoopItem?: boolean - availableNodes: any[] - isChatMode: boolean - isConstant?: boolean - environmentVariables?: EnvironmentVariable[] - conversationVariables?: ConversationVariable[] -}): VarType => { - if (isConstant) - return VarType.string - - const beforeNodesOutputVars = toNodeOutputVars( - availableNodes, - isChatMode, - undefined, - environmentVariables, - conversationVariables, - ) - - const isIterationInnerVar = parentNode?.data.type === BlockEnum.Iteration - if (isIterationItem) { - return getIterationItemType({ - valueSelector, - beforeNodesOutputVars, - }) - } - if (isIterationInnerVar) { - if (valueSelector[1] === 'item') { - const itemType = getIterationItemType({ - valueSelector: (parentNode?.data as any).iterator_selector || [], - beforeNodesOutputVars, - }) - return itemType - } - if (valueSelector[1] === 'index') - return VarType.number - } - - const isLoopInnerVar = parentNode?.data.type === BlockEnum.Loop - if (isLoopItem) { - return getLoopItemType({ - valueSelector, - beforeNodesOutputVars, - }) - } - if (isLoopInnerVar) { - if (valueSelector[1] === 'item') { - const itemType = getLoopItemType({ - valueSelector: (parentNode?.data as any).iterator_selector || [], - beforeNodesOutputVars, - }) - return itemType - } - if (valueSelector[1] === 'index') - return VarType.number - } - - const isSystem = isSystemVar(valueSelector) - const isEnv = isENV(valueSelector) - const isChatVar = isConversationVar(valueSelector) - const startNode = availableNodes.find((node: any) => { - return node?.data.type === BlockEnum.Start - }) - - const targetVarNodeId = isSystem ? startNode?.id : valueSelector[0] - const targetVar = beforeNodesOutputVars.find(v => v.nodeId === targetVarNodeId) - - if (!targetVar) - return VarType.string - - let type: VarType = VarType.string - let curr: any = targetVar.vars - - if (isSystem || isEnv || isChatVar) { - return curr.find((v: any) => v.variable === (valueSelector as ValueSelector).join('.'))?.type - } - else { - const targetVar = curr.find((v: any) => v.variable === valueSelector[1]) - if (!targetVar) - return VarType.string - - const isStructuredOutputVar = !!targetVar.children?.schema?.properties - if (isStructuredOutputVar) { - if (valueSelector.length === 2) { // root - return VarType.object - } - let currProperties = targetVar.children.schema; - (valueSelector as ValueSelector).slice(2).forEach((key, i) => { - const isLast = i === valueSelector.length - 3 - if (!currProperties) - return - - currProperties = currProperties.properties[key] - if (isLast) - type = structTypeToVarType(currProperties?.type) - }) - return type - } - - (valueSelector as ValueSelector).slice(1).forEach((key, i) => { - const isLast = i === valueSelector.length - 2 - if (Array.isArray(curr)) - curr = curr?.find((v: any) => v.variable === key) - - if (isLast) { - type = curr?.type - } - else { - if (curr?.type === VarType.object || curr?.type === VarType.file) - curr = curr.children - } - }) - return type - } -} - -// node output vars + parent inner vars(if in iteration or other wrap node) -export const toNodeAvailableVars = ({ - parentNode, - t, - beforeNodes, - isChatMode, - environmentVariables, - conversationVariables, - filterVar, -}: { - parentNode?: Node | null - t?: any - // to get those nodes output vars - beforeNodes: Node[] - isChatMode: boolean - // env - environmentVariables?: EnvironmentVariable[] - // chat var - conversationVariables?: ConversationVariable[] - filterVar: (payload: Var, selector: ValueSelector) => boolean -}): NodeOutPutVar[] => { - const beforeNodesOutputVars = toNodeOutputVars( - beforeNodes, - isChatMode, - filterVar, - environmentVariables, - conversationVariables, - ) - const isInIteration = parentNode?.data.type === BlockEnum.Iteration - if (isInIteration) { - const iterationNode: any = parentNode - const itemType = getVarType({ - parentNode: iterationNode, - isIterationItem: true, - valueSelector: iterationNode?.data.iterator_selector || [], - availableNodes: beforeNodes, - isChatMode, - environmentVariables, - conversationVariables, - }) - const itemChildren = itemType === VarType.file - ? { - children: OUTPUT_FILE_SUB_VARIABLES.map((key) => { - return { - variable: key, - type: key === 'size' ? VarType.number : VarType.string, - } - }), - } - : {} - const iterationVar = { - nodeId: iterationNode?.id, - title: t('workflow.nodes.iteration.currentIteration'), - vars: [ - { - variable: 'item', - type: itemType, - ...itemChildren, - }, - { - variable: 'index', - type: VarType.number, - }, - ], - } - const iterationIndex = beforeNodesOutputVars.findIndex(v => v.nodeId === iterationNode?.id) - if (iterationIndex > -1) - beforeNodesOutputVars.splice(iterationIndex, 1) - beforeNodesOutputVars.unshift(iterationVar) - } - return beforeNodesOutputVars -} - -export const getNodeInfoById = (nodes: any, id: string) => { - if (!isArray(nodes)) - return - return nodes.find((node: any) => node.id === id) -} - -const matchNotSystemVars = (prompts: string[]) => { - if (!prompts) - return [] - - const allVars: string[] = [] - prompts.forEach((prompt) => { - VAR_REGEX.lastIndex = 0 - if (typeof prompt !== 'string') - return - allVars.push(...(prompt.match(VAR_REGEX) || [])) - }) - const uniqVars = uniq(allVars).map(v => v.replaceAll('{{#', '').replace('#}}', '').split('.')) - return uniqVars -} - -const replaceOldVarInText = (text: string, oldVar: ValueSelector, newVar: ValueSelector) => { - if (!text || typeof text !== 'string') - return text - - if (!newVar || newVar.length === 0) - return text - - return text.replaceAll(`{{#${oldVar.join('.')}#}}`, `{{#${newVar.join('.')}#}}`) -} - -export const getNodeUsedVars = (node: Node): ValueSelector[] => { - const { data } = node - const { type } = data - let res: ValueSelector[] = [] - switch (type) { - case BlockEnum.End: { - res = (data as EndNodeType).outputs?.map((output) => { - return output.value_selector - }) - break - } - case BlockEnum.Answer: { - res = (data as AnswerNodeType).variables?.map((v) => { - return v.value_selector - }) - break - } - case BlockEnum.LLM: { - const payload = data as LLMNodeType - const isChatModel = payload.model?.mode === 'chat' - let prompts: string[] = [] - if (isChatModel) { - prompts = (payload.prompt_template as PromptItem[])?.map(p => p.text) || [] - if (payload.memory?.query_prompt_template) - prompts.push(payload.memory.query_prompt_template) - } - else { prompts = [(payload.prompt_template as PromptItem).text] } - - const inputVars: ValueSelector[] = matchNotSystemVars(prompts) - const contextVar = (data as LLMNodeType).context?.variable_selector ? [(data as LLMNodeType).context?.variable_selector] : [] - res = [...inputVars, ...contextVar] - break - } - case BlockEnum.KnowledgeRetrieval: { - res = [(data as KnowledgeRetrievalNodeType).query_variable_selector] - break - } - case BlockEnum.IfElse: { - res = (data as IfElseNodeType).conditions?.map((c) => { - return c.variable_selector || [] - }) || [] - break - } - case BlockEnum.Code: { - res = (data as CodeNodeType).variables?.map((v) => { - return v.value_selector - }) - break - } - case BlockEnum.TemplateTransform: { - res = (data as TemplateTransformNodeType).variables?.map((v: any) => { - return v.value_selector - }) - break - } - case BlockEnum.QuestionClassifier: { - const payload = data as QuestionClassifierNodeType - res = [payload.query_variable_selector] - const varInInstructions = matchNotSystemVars([payload.instruction || '']) - res.push(...varInInstructions) - break - } - case BlockEnum.HttpRequest: { - const payload = data as HttpNodeType - res = matchNotSystemVars([payload.url, payload.headers, payload.params, typeof payload.body.data === 'string' ? payload.body.data : payload.body.data.map(d => d.value).join('')]) - break - } - case BlockEnum.Tool: { - const payload = data as ToolNodeType - const mixVars = matchNotSystemVars(Object.keys(payload.tool_parameters)?.filter(key => payload.tool_parameters[key].type === ToolVarType.mixed).map(key => payload.tool_parameters[key].value) as string[]) - const vars = Object.keys(payload.tool_parameters).filter(key => payload.tool_parameters[key].type === ToolVarType.variable).map(key => payload.tool_parameters[key].value as string) || [] - res = [...(mixVars as ValueSelector[]), ...(vars as any)] - break - } - - case BlockEnum.VariableAssigner: { - res = (data as VariableAssignerNodeType)?.variables - break - } - - case BlockEnum.VariableAggregator: { - res = (data as VariableAssignerNodeType)?.variables - break - } - - case BlockEnum.ParameterExtractor: { - const payload = data as ParameterExtractorNodeType - res = [payload.query] - const varInInstructions = matchNotSystemVars([payload.instruction || '']) - res.push(...varInInstructions) - break - } - - case BlockEnum.Iteration: { - res = [(data as IterationNodeType).iterator_selector] - break - } - - case BlockEnum.Loop: { - const payload = data as LoopNodeType - res = payload.break_conditions?.map((c) => { - return c.variable_selector || [] - }) || [] - break - } - - case BlockEnum.ListFilter: { - res = [(data as ListFilterNodeType).variable] - break - } - - case BlockEnum.Agent: { - const payload = data as AgentNodeType - const valueSelectors: ValueSelector[] = [] - if (!payload.agent_parameters) - break - - Object.keys(payload.agent_parameters || {}).forEach((key) => { - const { value } = payload.agent_parameters![key] - if (typeof value === 'string') - valueSelectors.push(...matchNotSystemVars([value])) - }) - res = valueSelectors - break - } - } - return res || [] -} - -// can be used in iteration node -export const getNodeUsedVarPassToServerKey = (node: Node, valueSelector: ValueSelector): string | string[] => { - const { data } = node - const { type } = data - let res: string | string[] = '' - switch (type) { - case BlockEnum.LLM: { - const payload = data as LLMNodeType - res = [`#${valueSelector.join('.')}#`] - if (payload.context?.variable_selector.join('.') === valueSelector.join('.')) - res.push('#context#') - - break - } - case BlockEnum.KnowledgeRetrieval: { - res = 'query' - break - } - case BlockEnum.IfElse: { - const targetVar = (data as IfElseNodeType).conditions?.find(c => c.variable_selector?.join('.') === valueSelector.join('.')) - if (targetVar) - res = `#${valueSelector.join('.')}#` - break - } - case BlockEnum.Code: { - const targetVar = (data as CodeNodeType).variables?.find(v => v.value_selector.join('.') === valueSelector.join('.')) - if (targetVar) - res = targetVar.variable - break - } - case BlockEnum.TemplateTransform: { - const targetVar = (data as TemplateTransformNodeType).variables?.find(v => v.value_selector.join('.') === valueSelector.join('.')) - if (targetVar) - res = targetVar.variable - break - } - case BlockEnum.QuestionClassifier: { - res = 'query' - break - } - case BlockEnum.HttpRequest: { - res = `#${valueSelector.join('.')}#` - break - } - - case BlockEnum.Tool: { - res = `#${valueSelector.join('.')}#` - break - } - - case BlockEnum.VariableAssigner: { - res = `#${valueSelector.join('.')}#` - break - } - - case BlockEnum.VariableAggregator: { - res = `#${valueSelector.join('.')}#` - break - } - - case BlockEnum.ParameterExtractor: { - res = 'query' - break - } - } - return res -} - -export const findUsedVarNodes = (varSelector: ValueSelector, availableNodes: Node[]): Node[] => { - const res: Node[] = [] - availableNodes.forEach((node) => { - const vars = getNodeUsedVars(node) - if (vars.find(v => v.join('.') === varSelector.join('.'))) - res.push(node) - }) - return res -} - -export const updateNodeVars = (oldNode: Node, oldVarSelector: ValueSelector, newVarSelector: ValueSelector): Node => { - const newNode = produce(oldNode, (draft: any) => { - const { data } = draft - const { type } = data - - switch (type) { - case BlockEnum.End: { - const payload = data as EndNodeType - if (payload.outputs) { - payload.outputs = payload.outputs.map((output) => { - if (output.value_selector.join('.') === oldVarSelector.join('.')) - output.value_selector = newVarSelector - return output - }) - } - break - } - case BlockEnum.Answer: { - const payload = data as AnswerNodeType - if (payload.variables) { - payload.variables = payload.variables.map((v) => { - if (v.value_selector.join('.') === oldVarSelector.join('.')) - v.value_selector = newVarSelector - return v - }) - } - break - } - case BlockEnum.LLM: { - const payload = data as LLMNodeType - const isChatModel = payload.model?.mode === 'chat' - if (isChatModel) { - payload.prompt_template = (payload.prompt_template as PromptItem[]).map((prompt) => { - return { - ...prompt, - text: replaceOldVarInText(prompt.text, oldVarSelector, newVarSelector), - } - }) - if (payload.memory?.query_prompt_template) - payload.memory.query_prompt_template = replaceOldVarInText(payload.memory.query_prompt_template, oldVarSelector, newVarSelector) - } - else { - payload.prompt_template = { - ...payload.prompt_template, - text: replaceOldVarInText((payload.prompt_template as PromptItem).text, oldVarSelector, newVarSelector), - } - } - if (payload.context?.variable_selector?.join('.') === oldVarSelector.join('.')) - payload.context.variable_selector = newVarSelector - - break - } - case BlockEnum.KnowledgeRetrieval: { - const payload = data as KnowledgeRetrievalNodeType - if (payload.query_variable_selector.join('.') === oldVarSelector.join('.')) - payload.query_variable_selector = newVarSelector - break - } - case BlockEnum.IfElse: { - const payload = data as IfElseNodeType - if (payload.conditions) { - payload.conditions = payload.conditions.map((c) => { - if (c.variable_selector?.join('.') === oldVarSelector.join('.')) - c.variable_selector = newVarSelector - return c - }) - } - break - } - case BlockEnum.Code: { - const payload = data as CodeNodeType - if (payload.variables) { - payload.variables = payload.variables.map((v) => { - if (v.value_selector.join('.') === oldVarSelector.join('.')) - v.value_selector = newVarSelector - return v - }) - } - break - } - case BlockEnum.TemplateTransform: { - const payload = data as TemplateTransformNodeType - if (payload.variables) { - payload.variables = payload.variables.map((v: any) => { - if (v.value_selector.join('.') === oldVarSelector.join('.')) - v.value_selector = newVarSelector - return v - }) - } - break - } - case BlockEnum.QuestionClassifier: { - const payload = data as QuestionClassifierNodeType - if (payload.query_variable_selector.join('.') === oldVarSelector.join('.')) - payload.query_variable_selector = newVarSelector - payload.instruction = replaceOldVarInText(payload.instruction, oldVarSelector, newVarSelector) - break - } - case BlockEnum.HttpRequest: { - const payload = data as HttpNodeType - payload.url = replaceOldVarInText(payload.url, oldVarSelector, newVarSelector) - payload.headers = replaceOldVarInText(payload.headers, oldVarSelector, newVarSelector) - payload.params = replaceOldVarInText(payload.params, oldVarSelector, newVarSelector) - if (typeof payload.body.data === 'string') { - payload.body.data = replaceOldVarInText(payload.body.data, oldVarSelector, newVarSelector) - } - else { - payload.body.data = payload.body.data.map((d) => { - return { - ...d, - value: replaceOldVarInText(d.value || '', oldVarSelector, newVarSelector), - } - }) - } - break - } - case BlockEnum.Tool: { - const payload = data as ToolNodeType - const hasShouldRenameVar = Object.keys(payload.tool_parameters)?.filter(key => payload.tool_parameters[key].type !== ToolVarType.constant) - if (hasShouldRenameVar) { - Object.keys(payload.tool_parameters).forEach((key) => { - const value = payload.tool_parameters[key] - const { type } = value - if (type === ToolVarType.variable) { - payload.tool_parameters[key] = { - ...value, - value: newVarSelector, - } - } - - if (type === ToolVarType.mixed) { - payload.tool_parameters[key] = { - ...value, - value: replaceOldVarInText(payload.tool_parameters[key].value as string, oldVarSelector, newVarSelector), - } - } - }) - } - break - } - case BlockEnum.VariableAssigner: { - const payload = data as VariableAssignerNodeType - if (payload.variables) { - payload.variables = payload.variables.map((v) => { - if (v.join('.') === oldVarSelector.join('.')) - v = newVarSelector - return v - }) - } - break - } - // eslint-disable-next-line sonarjs/no-duplicated-branches - case BlockEnum.VariableAggregator: { - const payload = data as VariableAssignerNodeType - if (payload.variables) { - payload.variables = payload.variables.map((v) => { - if (v.join('.') === oldVarSelector.join('.')) - v = newVarSelector - return v - }) - } - break - } - case BlockEnum.ParameterExtractor: { - const payload = data as ParameterExtractorNodeType - if (payload.query.join('.') === oldVarSelector.join('.')) - payload.query = newVarSelector - payload.instruction = replaceOldVarInText(payload.instruction, oldVarSelector, newVarSelector) - break - } - case BlockEnum.Iteration: { - const payload = data as IterationNodeType - if (payload.iterator_selector.join('.') === oldVarSelector.join('.')) - payload.iterator_selector = newVarSelector - - break - } - case BlockEnum.Loop: { - const payload = data as LoopNodeType - if (payload.break_conditions) { - payload.break_conditions = payload.break_conditions.map((c) => { - if (c.variable_selector?.join('.') === oldVarSelector.join('.')) - c.variable_selector = newVarSelector - return c - }) - } - break - } - case BlockEnum.ListFilter: { - const payload = data as ListFilterNodeType - if (payload.variable.join('.') === oldVarSelector.join('.')) - payload.variable = newVarSelector - break - } - } - }) - return newNode -} - -const varToValueSelectorList = (v: Var, parentValueSelector: ValueSelector, res: ValueSelector[]) => { - if (!v.variable) - return - - res.push([...parentValueSelector, v.variable]) - const isStructuredOutput = !!(v.children as StructuredOutput)?.schema?.properties - - if ((v.children as Var[])?.length > 0) { - (v.children as Var[]).forEach((child) => { - varToValueSelectorList(child, [...parentValueSelector, v.variable], res) - }) - } - if (isStructuredOutput) { - Object.keys((v.children as StructuredOutput)?.schema?.properties || {}).forEach((key) => { - const type = (v.children as StructuredOutput)?.schema?.properties[key].type - const isArray = type === Type.array - const arrayType = (v.children as StructuredOutput)?.schema?.properties[key].items?.type - varToValueSelectorList({ - variable: key, - type: structTypeToVarType(isArray ? arrayType! : type, isArray), - }, [...parentValueSelector, v.variable], res) - }) - } -} - -const varsToValueSelectorList = (vars: Var | Var[], parentValueSelector: ValueSelector, res: ValueSelector[]) => { - if (Array.isArray(vars)) { - vars.forEach((v) => { - varToValueSelectorList(v, parentValueSelector, res) - }) - } - varToValueSelectorList(vars as Var, parentValueSelector, res) -} - -export const getNodeOutputVars = (node: Node, isChatMode: boolean): ValueSelector[] => { - const { data, id } = node - const { type } = data - let res: ValueSelector[] = [] - - switch (type) { - case BlockEnum.Start: { - const { - variables, - } = data as StartNodeType - res = variables.map((v) => { - return [id, v.variable] - }) - - if (isChatMode) { - res.push([id, 'sys', 'query']) - res.push([id, 'sys', 'files']) - } - break - } - - 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, - }) - } - varsToValueSelectorList(vars, [id], res) - break - } - - case BlockEnum.KnowledgeRetrieval: { - varsToValueSelectorList(KNOWLEDGE_RETRIEVAL_OUTPUT_STRUCT, [id], res) - break - } - - case BlockEnum.Code: { - const { - outputs, - } = data as CodeNodeType - Object.keys(outputs).forEach((key) => { - res.push([id, key]) - }) - break - } - - case BlockEnum.TemplateTransform: { - varsToValueSelectorList(TEMPLATE_TRANSFORM_OUTPUT_STRUCT, [id], res) - break - } - - case BlockEnum.QuestionClassifier: { - varsToValueSelectorList(QUESTION_CLASSIFIER_OUTPUT_STRUCT, [id], res) - break - } - - case BlockEnum.HttpRequest: { - varsToValueSelectorList(HTTP_REQUEST_OUTPUT_STRUCT, [id], res) - break - } - - case BlockEnum.VariableAssigner: { - res.push([id, 'output']) - break - } - - case BlockEnum.VariableAggregator: { - res.push([id, 'output']) - break - } - - case BlockEnum.Tool: { - varsToValueSelectorList(TOOL_OUTPUT_STRUCT, [id], res) - break - } - - case BlockEnum.ParameterExtractor: { - const { - parameters, - } = data as ParameterExtractorNodeType - if (parameters?.length > 0) { - parameters.forEach((p) => { - res.push([id, p.name]) - }) - } - - break - } - - case BlockEnum.Iteration: { - res.push([id, 'output']) - break - } - - case BlockEnum.Loop: { - res.push([id, 'output']) - break - } - - case BlockEnum.DocExtractor: { - res.push([id, 'text']) - break - } - - case BlockEnum.ListFilter: { - res.push([id, 'result']) - res.push([id, 'first_record']) - res.push([id, 'last_record']) - break - } - } - - return res -} +import produce from 'immer' +import { isArray, uniq } from 'lodash-es' +import type { CodeNodeType } from '../../../code/types' +import type { EndNodeType } from '../../../end/types' +import type { AnswerNodeType } from '../../../answer/types' +import { type LLMNodeType, type StructuredOutput, Type } from '../../../llm/types' +import type { KnowledgeRetrievalNodeType } from '../../../knowledge-retrieval/types' +import type { IfElseNodeType } from '../../../if-else/types' +import type { TemplateTransformNodeType } from '../../../template-transform/types' +import type { QuestionClassifierNodeType } from '../../../question-classifier/types' +import type { HttpNodeType } from '../../../http/types' +import { VarType as ToolVarType } from '../../../tool/types' +import type { ToolNodeType } from '../../../tool/types' +import type { ParameterExtractorNodeType } from '../../../parameter-extractor/types' +import type { IterationNodeType } from '../../../iteration/types' +import type { LoopNodeType } from '../../../loop/types' +import type { ListFilterNodeType } from '../../../list-operator/types' +import { OUTPUT_FILE_SUB_VARIABLES } from '../../../constants' +import type { DocExtractorNodeType } from '../../../document-extractor/types' +import { BlockEnum, InputVarType, VarType } from '@/app/components/workflow/types' +import type { StartNodeType } from '@/app/components/workflow/nodes/start/types' +import type { ConversationVariable, EnvironmentVariable, Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types' +import type { VariableAssignerNodeType } from '@/app/components/workflow/nodes/variable-assigner/types' +import type { Field as StructField } from '@/app/components/workflow/nodes/llm/types' + +import { + HTTP_REQUEST_OUTPUT_STRUCT, + KNOWLEDGE_RETRIEVAL_OUTPUT_STRUCT, + LLM_OUTPUT_STRUCT, + PARAMETER_EXTRACTOR_COMMON_STRUCT, + QUESTION_CLASSIFIER_OUTPUT_STRUCT, + SUPPORT_OUTPUT_VARS_NODE, + TEMPLATE_TRANSFORM_OUTPUT_STRUCT, + TOOL_OUTPUT_STRUCT, +} from '@/app/components/workflow/constants' +import type { PromptItem } from '@/models/debug' +import { VAR_REGEX } from '@/config' +import type { AgentNodeType } from '../../../agent/types' + +export const isSystemVar = (valueSelector: ValueSelector) => { + return valueSelector[0] === 'sys' || valueSelector[1] === 'sys' +} + +export const isENV = (valueSelector: ValueSelector) => { + return valueSelector[0] === 'env' +} + +export const isConversationVar = (valueSelector: ValueSelector) => { + return valueSelector[0] === 'conversation' +} + +const inputVarTypeToVarType = (type: InputVarType): VarType => { + return ({ + [InputVarType.number]: VarType.number, + [InputVarType.singleFile]: VarType.file, + [InputVarType.multiFiles]: VarType.arrayFile, + } as any)[type] || VarType.string +} + +const structTypeToVarType = (type: Type, isArray?: boolean): VarType => { + if (isArray) { + return ({ + [Type.string]: VarType.arrayString, + [Type.number]: VarType.arrayNumber, + [Type.object]: VarType.arrayObject, + } as any)[type] || VarType.string + } + return ({ + [Type.string]: VarType.string, + [Type.number]: VarType.number, + [Type.boolean]: VarType.boolean, + [Type.object]: VarType.object, + [Type.array]: VarType.array, + } as any)[type] || VarType.string +} + +export const varTypeToStructType = (type: VarType): Type => { + return ({ + [VarType.string]: Type.string, + [VarType.number]: Type.number, + [VarType.boolean]: Type.boolean, + [VarType.object]: Type.object, + [VarType.array]: Type.array, + } as any)[type] || Type.string +} + +const findExceptVarInStructuredProperties = (properties: Record, filterVar: (payload: Var, selector: ValueSelector) => boolean): Record => { + const res = produce(properties, (draft) => { + Object.keys(properties).forEach((key) => { + const item = properties[key] + const isObj = item.type === Type.object + const isArray = item.type === Type.array + const arrayType = item.items?.type + + if (!isObj && !filterVar({ + variable: key, + type: structTypeToVarType(isArray ? arrayType! : item.type, isArray), + }, [key])) { + delete properties[key] + return + } + if (item.type === Type.object && item.properties) + item.properties = findExceptVarInStructuredProperties(item.properties, filterVar) + }) + return draft + }) + return res +} + +const findExceptVarInStructuredOutput = (structuredOutput: StructuredOutput, filterVar: (payload: Var, selector: ValueSelector) => boolean): StructuredOutput => { + const res = produce(structuredOutput, (draft) => { + const properties = draft.schema.properties + Object.keys(properties).forEach((key) => { + const item = properties[key] + const isObj = item.type === Type.object + const isArray = item.type === Type.array + const arrayType = item.items?.type + if (!isObj && !filterVar({ + variable: key, + type: structTypeToVarType(isArray ? arrayType! : item.type, isArray), + }, [key])) { + delete properties[key] + return + } + if (item.type === Type.object && item.properties) + item.properties = findExceptVarInStructuredProperties(item.properties, filterVar) + }) + return draft + }) + return res +} + +const findExceptVarInObject = (obj: any, filterVar: (payload: Var, selector: ValueSelector) => boolean, value_selector: ValueSelector, isFile?: boolean): Var => { + const { children } = obj + const isStructuredOutput = !!(children as StructuredOutput)?.schema?.properties + + let childrenResult: Var[] | StructuredOutput | undefined + + if (isStructuredOutput) { + childrenResult = findExceptVarInStructuredOutput(children, filterVar) + } + else if (Array.isArray(children)) { + childrenResult = children.filter((item: Var) => { + const { children: itemChildren } = item + const currSelector = [...value_selector, item.variable] + + if (!itemChildren) + return filterVar(item, currSelector) + + const filteredObj = findExceptVarInObject(item, filterVar, currSelector, false) // File doesn't contain file children + return filteredObj.children && (filteredObj.children as Var[])?.length > 0 + }) + } + else { + childrenResult = [] + } + + const res: Var = { + variable: obj.variable, + type: isFile ? VarType.file : VarType.object, + children: childrenResult, + } + + return res +} + +const formatItem = ( + item: any, + isChatMode: boolean, + filterVar: (payload: Var, selector: ValueSelector) => boolean, +): NodeOutPutVar => { + const { id, data } = item + + const res: NodeOutPutVar = { + nodeId: id, + title: data.title, + vars: [], + } + switch (data.type) { + case BlockEnum.Start: { + const { + variables, + } = data as StartNodeType + res.vars = variables.map((v) => { + return { + variable: v.variable, + type: inputVarTypeToVarType(v.type), + isParagraph: v.type === InputVarType.paragraph, + isSelect: v.type === InputVarType.select, + options: v.options, + required: v.required, + } + }) + if (isChatMode) { + res.vars.push({ + variable: 'sys.query', + type: VarType.string, + }) + res.vars.push({ + variable: 'sys.dialogue_count', + type: VarType.number, + }) + res.vars.push({ + variable: 'sys.conversation_id', + type: VarType.string, + }) + } + res.vars.push({ + variable: 'sys.user_id', + type: VarType.string, + }) + res.vars.push({ + variable: 'sys.files', + type: VarType.arrayFile, + }) + res.vars.push({ + variable: 'sys.app_id', + type: VarType.string, + }) + res.vars.push({ + variable: 'sys.workflow_id', + type: VarType.string, + }) + res.vars.push({ + variable: 'sys.workflow_run_id', + type: VarType.string, + }) + + break + } + + 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, + }) + } + + break + } + case BlockEnum.KnowledgeRetrieval: { + res.vars = KNOWLEDGE_RETRIEVAL_OUTPUT_STRUCT + break + } + + case BlockEnum.Code: { + const { + outputs, + } = data as CodeNodeType + res.vars = outputs + ? Object.keys(outputs).map((key) => { + return { + variable: key, + type: outputs[key].type, + } + }) + : [] + break + } + + case BlockEnum.TemplateTransform: { + res.vars = TEMPLATE_TRANSFORM_OUTPUT_STRUCT + break + } + + case BlockEnum.QuestionClassifier: { + res.vars = QUESTION_CLASSIFIER_OUTPUT_STRUCT + break + } + + case BlockEnum.HttpRequest: { + res.vars = HTTP_REQUEST_OUTPUT_STRUCT + break + } + + case BlockEnum.VariableAssigner: { + const { + output_type, + advanced_settings, + } = data as VariableAssignerNodeType + const isGroup = !!advanced_settings?.group_enabled + if (!isGroup) { + res.vars = [ + { + variable: 'output', + type: output_type, + }, + ] + } + else { + res.vars = advanced_settings?.groups.map((group) => { + return { + variable: group.group_name, + type: VarType.object, + children: [{ + variable: 'output', + type: group.output_type, + }], + } + }) + } + break + } + + // eslint-disable-next-line sonarjs/no-duplicated-branches + case BlockEnum.VariableAggregator: { + const { + output_type, + advanced_settings, + } = data as VariableAssignerNodeType + const isGroup = !!advanced_settings?.group_enabled + if (!isGroup) { + res.vars = [ + { + variable: 'output', + type: output_type, + }, + ] + } + else { + res.vars = advanced_settings?.groups.map((group) => { + return { + variable: group.group_name, + type: VarType.object, + children: [{ + variable: 'output', + type: group.output_type, + }], + } + }) + } + break + } + + case BlockEnum.Tool: { + const { + output_schema, + } = data as ToolNodeType + if (!output_schema) { + res.vars = TOOL_OUTPUT_STRUCT + } + else { + const outputSchema: any[] = [] + Object.keys(output_schema.properties).forEach((outputKey) => { + const output = output_schema.properties[outputKey] + const dataType = output.type + outputSchema.push({ + variable: outputKey, + type: dataType === 'array' + ? `array[${output.items?.type.slice(0, 1).toLocaleLowerCase()}${output.items?.type.slice(1)}]` + : `${output.type.slice(0, 1).toLocaleLowerCase()}${output.type.slice(1)}`, + description: output.description, + children: output.type === 'object' ? { + schema: { + type: 'object', + properties: output.properties, + }, + } : undefined, + }) + }) + res.vars = [ + ...TOOL_OUTPUT_STRUCT, + ...outputSchema, + ] + } + break + } + + case BlockEnum.ParameterExtractor: { + res.vars = [ + ...((data as ParameterExtractorNodeType).parameters || []).map((p) => { + return { + variable: p.name, + type: p.type as unknown as VarType, + } + }), + ...PARAMETER_EXTRACTOR_COMMON_STRUCT, + ] + break + } + + case BlockEnum.Iteration: { + res.vars = [ + { + variable: 'output', + type: (data as IterationNodeType).output_type || VarType.arrayString, + }, + ] + break + } + + case BlockEnum.Loop: { + const { loop_variables } = data as LoopNodeType + res.isLoop = true + res.vars = loop_variables?.map((v) => { + return { + variable: v.label, + type: v.var_type, + isLoopVariable: true, + nodeId: res.nodeId, + } + }) || [] + + break + } + + case BlockEnum.DocExtractor: { + res.vars = [ + { + variable: 'text', + type: (data as DocExtractorNodeType).is_array_file ? VarType.arrayString : VarType.string, + }, + ] + break + } + + case BlockEnum.ListFilter: { + if (!(data as ListFilterNodeType).var_type) + break + + res.vars = [ + { + variable: 'result', + type: (data as ListFilterNodeType).var_type, + }, + { + variable: 'first_record', + type: (data as ListFilterNodeType).item_var_type, + }, + { + variable: 'last_record', + type: (data as ListFilterNodeType).item_var_type, + }, + ] + break + } + + case BlockEnum.Agent: { + const payload = data as AgentNodeType + const outputs: Var[] = [] + Object.keys(payload.output_schema?.properties || {}).forEach((outputKey) => { + const output = payload.output_schema.properties[outputKey] + outputs.push({ + variable: outputKey, + type: output.type === 'array' + ? `Array[${output.items?.type.slice(0, 1).toLocaleUpperCase()}${output.items?.type.slice(1)}]` as VarType + : `${output.type.slice(0, 1).toLocaleUpperCase()}${output.type.slice(1)}` as VarType, + }) + }) + res.vars = [ + ...outputs, + ...TOOL_OUTPUT_STRUCT, + ] + break + } + + case 'env': { + res.vars = data.envList.map((env: EnvironmentVariable) => { + return { + variable: `env.${env.name}`, + type: env.value_type, + des: env.description, + } + }) as Var[] + break + } + + case 'conversation': { + res.vars = data.chatVarList.map((chatVar: ConversationVariable) => { + return { + variable: `conversation.${chatVar.name}`, + type: chatVar.value_type, + des: chatVar.description, + } + }) as Var[] + break + } + } + + const { error_strategy } = data + + if (error_strategy) { + res.vars = [ + ...res.vars, + { + variable: 'error_message', + type: VarType.string, + isException: true, + }, + { + variable: 'error_type', + type: VarType.string, + isException: true, + }, + ] + } + + const selector = [id] + res.vars = res.vars.filter((v) => { + const isCurrentMatched = filterVar(v, (() => { + const variableArr = v.variable.split('.') + const [first] = variableArr + if (first === 'sys' || first === 'env' || first === 'conversation') + return variableArr + + return [...selector, ...variableArr] + })()) + if (isCurrentMatched) + return true + + const isFile = v.type === VarType.file + const children = (() => { + if (isFile) { + return OUTPUT_FILE_SUB_VARIABLES.map((key) => { + return { + variable: key, + type: key === 'size' ? VarType.number : VarType.string, + } + }) + } + return v.children + })() + if (!children) + return false + + const obj = findExceptVarInObject(isFile ? { ...v, children } : v, filterVar, selector, isFile) + return obj?.children && ((obj?.children as Var[]).length > 0 || Object.keys((obj?.children as StructuredOutput)?.schema?.properties || {}).length > 0) + }).map((v) => { + const isFile = v.type === VarType.file + + const { children } = (() => { + if (isFile) { + return { + children: OUTPUT_FILE_SUB_VARIABLES.map((key) => { + return { + variable: key, + type: key === 'size' ? VarType.number : VarType.string, + } + }), + } + } + return v + })() + + if (!children) + return v + + return findExceptVarInObject(isFile ? { ...v, children } : v, filterVar, selector, isFile) + }) + + return res +} +export const toNodeOutputVars = ( + nodes: any[], + isChatMode: boolean, + filterVar = (_payload: Var, _selector: ValueSelector) => true, + environmentVariables: EnvironmentVariable[] = [], + conversationVariables: ConversationVariable[] = [], +): NodeOutPutVar[] => { + // ENV_NODE data format + const ENV_NODE = { + id: 'env', + data: { + title: 'ENVIRONMENT', + type: 'env', + envList: environmentVariables, + }, + } + // CHAT_VAR_NODE data format + const CHAT_VAR_NODE = { + id: 'conversation', + data: { + title: 'CONVERSATION', + type: 'conversation', + chatVarList: conversationVariables, + }, + } + // Sort nodes in reverse chronological order (most recent first) + const sortedNodes = [...nodes].sort((a, b) => { + if (a.data.type === BlockEnum.Start) return 1 + if (b.data.type === BlockEnum.Start) return -1 + if (a.data.type === 'env') return 1 + if (b.data.type === 'env') return -1 + if (a.data.type === 'conversation') return 1 + if (b.data.type === 'conversation') return -1 + // sort nodes by x position + return (b.position?.x || 0) - (a.position?.x || 0) + }) + + const res = [ + ...sortedNodes.filter(node => SUPPORT_OUTPUT_VARS_NODE.includes(node?.data?.type)), + ...(environmentVariables.length > 0 ? [ENV_NODE] : []), + ...((isChatMode && conversationVariables.length > 0) ? [CHAT_VAR_NODE] : []), + ].map((node) => { + return { + ...formatItem(node, isChatMode, filterVar), + isStartNode: node.data.type === BlockEnum.Start, + } + }).filter(item => item.vars.length > 0) + return res +} + +const getIterationItemType = ({ + valueSelector, + beforeNodesOutputVars, +}: { + valueSelector: ValueSelector + beforeNodesOutputVars: NodeOutPutVar[] +}): VarType => { + const outputVarNodeId = valueSelector[0] + const isSystem = isSystemVar(valueSelector) + + const targetVar = isSystem ? beforeNodesOutputVars.find(v => v.isStartNode) : beforeNodesOutputVars.find(v => v.nodeId === outputVarNodeId) + + if (!targetVar) + return VarType.string + + let arrayType: VarType = VarType.string + + let curr: any = targetVar.vars + if (isSystem) { + arrayType = curr.find((v: any) => v.variable === (valueSelector).join('.'))?.type + } + else { + for (let i = 1; i < valueSelector.length; i++) { + const key = valueSelector[i] + const isLast = i === valueSelector.length - 1 + curr = Array.isArray(curr) ? curr.find(v => v.variable === key) : [] + + if (isLast) + arrayType = curr?.type + else if (curr?.type === VarType.object || curr?.type === VarType.file) + curr = curr.children || [] + } + } + + switch (arrayType as VarType) { + case VarType.arrayString: + return VarType.string + case VarType.arrayNumber: + return VarType.number + case VarType.arrayObject: + return VarType.object + case VarType.array: + return VarType.any + case VarType.arrayFile: + return VarType.file + default: + return VarType.string + } +} + +const getLoopItemType = ({ + valueSelector, + beforeNodesOutputVars, +}: { + valueSelector: ValueSelector + beforeNodesOutputVars: NodeOutPutVar[] + +}): VarType => { + const outputVarNodeId = valueSelector[0] + const isSystem = isSystemVar(valueSelector) + + const targetVar = isSystem ? beforeNodesOutputVars.find(v => v.isStartNode) : beforeNodesOutputVars.find(v => v.nodeId === outputVarNodeId) + if (!targetVar) + return VarType.string + + let arrayType: VarType = VarType.string + + let curr: any = targetVar.vars + if (isSystem) { + arrayType = curr.find((v: any) => v.variable === (valueSelector).join('.'))?.type + } + else { + (valueSelector).slice(1).forEach((key, i) => { + const isLast = i === valueSelector.length - 2 + curr = curr?.find((v: any) => v.variable === key) + if (isLast) { + arrayType = curr?.type + } + else { + if (curr?.type === VarType.object || curr?.type === VarType.file) + curr = curr.children + } + }) + } + + switch (arrayType as VarType) { + case VarType.arrayString: + return VarType.string + case VarType.arrayNumber: + return VarType.number + case VarType.arrayObject: + return VarType.object + case VarType.array: + return VarType.any + case VarType.arrayFile: + return VarType.file + default: + return VarType.string + } +} + +export const getVarType = ({ + parentNode, + valueSelector, + isIterationItem, + isLoopItem, + availableNodes, + isChatMode, + isConstant, + environmentVariables = [], + conversationVariables = [], +}: { + valueSelector: ValueSelector + parentNode?: Node | null + isIterationItem?: boolean + isLoopItem?: boolean + availableNodes: any[] + isChatMode: boolean + isConstant?: boolean + environmentVariables?: EnvironmentVariable[] + conversationVariables?: ConversationVariable[] +}): VarType => { + if (isConstant) + return VarType.string + + const beforeNodesOutputVars = toNodeOutputVars( + availableNodes, + isChatMode, + undefined, + environmentVariables, + conversationVariables, + ) + + const isIterationInnerVar = parentNode?.data.type === BlockEnum.Iteration + if (isIterationItem) { + return getIterationItemType({ + valueSelector, + beforeNodesOutputVars, + }) + } + if (isIterationInnerVar) { + if (valueSelector[1] === 'item') { + const itemType = getIterationItemType({ + valueSelector: (parentNode?.data as any).iterator_selector || [], + beforeNodesOutputVars, + }) + return itemType + } + if (valueSelector[1] === 'index') + return VarType.number + } + + const isLoopInnerVar = parentNode?.data.type === BlockEnum.Loop + if (isLoopItem) { + return getLoopItemType({ + valueSelector, + beforeNodesOutputVars, + }) + } + if (isLoopInnerVar) { + if (valueSelector[1] === 'item') { + const itemType = getLoopItemType({ + valueSelector: (parentNode?.data as any).iterator_selector || [], + beforeNodesOutputVars, + }) + return itemType + } + if (valueSelector[1] === 'index') + return VarType.number + } + + const isSystem = isSystemVar(valueSelector) + const isEnv = isENV(valueSelector) + const isChatVar = isConversationVar(valueSelector) + const startNode = availableNodes.find((node: any) => { + return node?.data.type === BlockEnum.Start + }) + + const targetVarNodeId = isSystem ? startNode?.id : valueSelector[0] + const targetVar = beforeNodesOutputVars.find(v => v.nodeId === targetVarNodeId) + + if (!targetVar) + return VarType.string + + let type: VarType = VarType.string + let curr: any = targetVar.vars + + if (isSystem || isEnv || isChatVar) { + return curr.find((v: any) => v.variable === (valueSelector as ValueSelector).join('.'))?.type + } + else { + const targetVar = curr.find((v: any) => v.variable === valueSelector[1]) + if (!targetVar) + return VarType.string + + const isStructuredOutputVar = !!targetVar.children?.schema?.properties + if (isStructuredOutputVar) { + if (valueSelector.length === 2) { // root + return VarType.object + } + let currProperties = targetVar.children.schema; + (valueSelector as ValueSelector).slice(2).forEach((key, i) => { + const isLast = i === valueSelector.length - 3 + if (!currProperties) + return + + currProperties = currProperties.properties[key] + if (isLast) + type = structTypeToVarType(currProperties?.type) + }) + return type + } + + (valueSelector as ValueSelector).slice(1).forEach((key, i) => { + const isLast = i === valueSelector.length - 2 + if (Array.isArray(curr)) + curr = curr?.find((v: any) => v.variable === key) + + if (isLast) { + type = curr?.type + } + else { + if (curr?.type === VarType.object || curr?.type === VarType.file) + curr = curr.children + } + }) + return type + } +} + +// node output vars + parent inner vars(if in iteration or other wrap node) +export const toNodeAvailableVars = ({ + parentNode, + t, + beforeNodes, + isChatMode, + environmentVariables, + conversationVariables, + filterVar, +}: { + parentNode?: Node | null + t?: any + // to get those nodes output vars + beforeNodes: Node[] + isChatMode: boolean + // env + environmentVariables?: EnvironmentVariable[] + // chat var + conversationVariables?: ConversationVariable[] + filterVar: (payload: Var, selector: ValueSelector) => boolean +}): NodeOutPutVar[] => { + const beforeNodesOutputVars = toNodeOutputVars( + beforeNodes, + isChatMode, + filterVar, + environmentVariables, + conversationVariables, + ) + const isInIteration = parentNode?.data.type === BlockEnum.Iteration + if (isInIteration) { + const iterationNode: any = parentNode + const itemType = getVarType({ + parentNode: iterationNode, + isIterationItem: true, + valueSelector: iterationNode?.data.iterator_selector || [], + availableNodes: beforeNodes, + isChatMode, + environmentVariables, + conversationVariables, + }) + const itemChildren = itemType === VarType.file + ? { + children: OUTPUT_FILE_SUB_VARIABLES.map((key) => { + return { + variable: key, + type: key === 'size' ? VarType.number : VarType.string, + } + }), + } + : {} + const iterationVar = { + nodeId: iterationNode?.id, + title: t('workflow.nodes.iteration.currentIteration'), + vars: [ + { + variable: 'item', + type: itemType, + ...itemChildren, + }, + { + variable: 'index', + type: VarType.number, + }, + ], + } + const iterationIndex = beforeNodesOutputVars.findIndex(v => v.nodeId === iterationNode?.id) + if (iterationIndex > -1) + beforeNodesOutputVars.splice(iterationIndex, 1) + beforeNodesOutputVars.unshift(iterationVar) + } + return beforeNodesOutputVars +} + +export const getNodeInfoById = (nodes: any, id: string) => { + if (!isArray(nodes)) + return + return nodes.find((node: any) => node.id === id) +} + +const matchNotSystemVars = (prompts: string[]) => { + if (!prompts) + return [] + + const allVars: string[] = [] + prompts.forEach((prompt) => { + VAR_REGEX.lastIndex = 0 + if (typeof prompt !== 'string') + return + allVars.push(...(prompt.match(VAR_REGEX) || [])) + }) + const uniqVars = uniq(allVars).map(v => v.replaceAll('{{#', '').replace('#}}', '').split('.')) + return uniqVars +} + +const replaceOldVarInText = (text: string, oldVar: ValueSelector, newVar: ValueSelector) => { + if (!text || typeof text !== 'string') + return text + + if (!newVar || newVar.length === 0) + return text + + return text.replaceAll(`{{#${oldVar.join('.')}#}}`, `{{#${newVar.join('.')}#}}`) +} + +export const getNodeUsedVars = (node: Node): ValueSelector[] => { + const { data } = node + const { type } = data + let res: ValueSelector[] = [] + switch (type) { + case BlockEnum.End: { + res = (data as EndNodeType).outputs?.map((output) => { + return output.value_selector + }) + break + } + case BlockEnum.Answer: { + res = (data as AnswerNodeType).variables?.map((v) => { + return v.value_selector + }) + break + } + case BlockEnum.LLM: { + const payload = data as LLMNodeType + const isChatModel = payload.model?.mode === 'chat' + let prompts: string[] = [] + if (isChatModel) { + prompts = (payload.prompt_template as PromptItem[])?.map(p => p.text) || [] + if (payload.memory?.query_prompt_template) + prompts.push(payload.memory.query_prompt_template) + } + else { prompts = [(payload.prompt_template as PromptItem).text] } + + const inputVars: ValueSelector[] = matchNotSystemVars(prompts) + const contextVar = (data as LLMNodeType).context?.variable_selector ? [(data as LLMNodeType).context?.variable_selector] : [] + res = [...inputVars, ...contextVar] + break + } + case BlockEnum.KnowledgeRetrieval: { + res = [(data as KnowledgeRetrievalNodeType).query_variable_selector] + break + } + case BlockEnum.IfElse: { + res = (data as IfElseNodeType).conditions?.map((c) => { + return c.variable_selector || [] + }) || [] + break + } + case BlockEnum.Code: { + res = (data as CodeNodeType).variables?.map((v) => { + return v.value_selector + }) + break + } + case BlockEnum.TemplateTransform: { + res = (data as TemplateTransformNodeType).variables?.map((v: any) => { + return v.value_selector + }) + break + } + case BlockEnum.QuestionClassifier: { + const payload = data as QuestionClassifierNodeType + res = [payload.query_variable_selector] + const varInInstructions = matchNotSystemVars([payload.instruction || '']) + res.push(...varInInstructions) + break + } + case BlockEnum.HttpRequest: { + const payload = data as HttpNodeType + res = matchNotSystemVars([payload.url, payload.headers, payload.params, typeof payload.body.data === 'string' ? payload.body.data : payload.body.data.map(d => d.value).join('')]) + break + } + case BlockEnum.Tool: { + const payload = data as ToolNodeType + const mixVars = matchNotSystemVars(Object.keys(payload.tool_parameters)?.filter(key => payload.tool_parameters[key].type === ToolVarType.mixed).map(key => payload.tool_parameters[key].value) as string[]) + const vars = Object.keys(payload.tool_parameters).filter(key => payload.tool_parameters[key].type === ToolVarType.variable).map(key => payload.tool_parameters[key].value as string) || [] + res = [...(mixVars as ValueSelector[]), ...(vars as any)] + break + } + + case BlockEnum.VariableAssigner: { + res = (data as VariableAssignerNodeType)?.variables + break + } + + case BlockEnum.VariableAggregator: { + res = (data as VariableAssignerNodeType)?.variables + break + } + + case BlockEnum.ParameterExtractor: { + const payload = data as ParameterExtractorNodeType + res = [payload.query] + const varInInstructions = matchNotSystemVars([payload.instruction || '']) + res.push(...varInInstructions) + break + } + + case BlockEnum.Iteration: { + res = [(data as IterationNodeType).iterator_selector] + break + } + + case BlockEnum.Loop: { + const payload = data as LoopNodeType + res = payload.break_conditions?.map((c) => { + return c.variable_selector || [] + }) || [] + break + } + + case BlockEnum.ListFilter: { + res = [(data as ListFilterNodeType).variable] + break + } + + case BlockEnum.Agent: { + const payload = data as AgentNodeType + const valueSelectors: ValueSelector[] = [] + if (!payload.agent_parameters) + break + + Object.keys(payload.agent_parameters || {}).forEach((key) => { + const { value } = payload.agent_parameters![key] + if (typeof value === 'string') + valueSelectors.push(...matchNotSystemVars([value])) + }) + res = valueSelectors + break + } + } + return res || [] +} + +// can be used in iteration node +export const getNodeUsedVarPassToServerKey = (node: Node, valueSelector: ValueSelector): string | string[] => { + const { data } = node + const { type } = data + let res: string | string[] = '' + switch (type) { + case BlockEnum.LLM: { + const payload = data as LLMNodeType + res = [`#${valueSelector.join('.')}#`] + if (payload.context?.variable_selector.join('.') === valueSelector.join('.')) + res.push('#context#') + + break + } + case BlockEnum.KnowledgeRetrieval: { + res = 'query' + break + } + case BlockEnum.IfElse: { + const targetVar = (data as IfElseNodeType).conditions?.find(c => c.variable_selector?.join('.') === valueSelector.join('.')) + if (targetVar) + res = `#${valueSelector.join('.')}#` + break + } + case BlockEnum.Code: { + const targetVar = (data as CodeNodeType).variables?.find(v => v.value_selector.join('.') === valueSelector.join('.')) + if (targetVar) + res = targetVar.variable + break + } + case BlockEnum.TemplateTransform: { + const targetVar = (data as TemplateTransformNodeType).variables?.find(v => v.value_selector.join('.') === valueSelector.join('.')) + if (targetVar) + res = targetVar.variable + break + } + case BlockEnum.QuestionClassifier: { + res = 'query' + break + } + case BlockEnum.HttpRequest: { + res = `#${valueSelector.join('.')}#` + break + } + + case BlockEnum.Tool: { + res = `#${valueSelector.join('.')}#` + break + } + + case BlockEnum.VariableAssigner: { + res = `#${valueSelector.join('.')}#` + break + } + + case BlockEnum.VariableAggregator: { + res = `#${valueSelector.join('.')}#` + break + } + + case BlockEnum.ParameterExtractor: { + res = 'query' + break + } + } + return res +} + +export const findUsedVarNodes = (varSelector: ValueSelector, availableNodes: Node[]): Node[] => { + const res: Node[] = [] + availableNodes.forEach((node) => { + const vars = getNodeUsedVars(node) + if (vars.find(v => v.join('.') === varSelector.join('.'))) + res.push(node) + }) + return res +} + +export const updateNodeVars = (oldNode: Node, oldVarSelector: ValueSelector, newVarSelector: ValueSelector): Node => { + const newNode = produce(oldNode, (draft: any) => { + const { data } = draft + const { type } = data + + switch (type) { + case BlockEnum.End: { + const payload = data as EndNodeType + if (payload.outputs) { + payload.outputs = payload.outputs.map((output) => { + if (output.value_selector.join('.') === oldVarSelector.join('.')) + output.value_selector = newVarSelector + return output + }) + } + break + } + case BlockEnum.Answer: { + const payload = data as AnswerNodeType + if (payload.variables) { + payload.variables = payload.variables.map((v) => { + if (v.value_selector.join('.') === oldVarSelector.join('.')) + v.value_selector = newVarSelector + return v + }) + } + break + } + case BlockEnum.LLM: { + const payload = data as LLMNodeType + const isChatModel = payload.model?.mode === 'chat' + if (isChatModel) { + payload.prompt_template = (payload.prompt_template as PromptItem[]).map((prompt) => { + return { + ...prompt, + text: replaceOldVarInText(prompt.text, oldVarSelector, newVarSelector), + } + }) + if (payload.memory?.query_prompt_template) + payload.memory.query_prompt_template = replaceOldVarInText(payload.memory.query_prompt_template, oldVarSelector, newVarSelector) + } + else { + payload.prompt_template = { + ...payload.prompt_template, + text: replaceOldVarInText((payload.prompt_template as PromptItem).text, oldVarSelector, newVarSelector), + } + } + if (payload.context?.variable_selector?.join('.') === oldVarSelector.join('.')) + payload.context.variable_selector = newVarSelector + + break + } + case BlockEnum.KnowledgeRetrieval: { + const payload = data as KnowledgeRetrievalNodeType + if (payload.query_variable_selector.join('.') === oldVarSelector.join('.')) + payload.query_variable_selector = newVarSelector + break + } + case BlockEnum.IfElse: { + const payload = data as IfElseNodeType + if (payload.conditions) { + payload.conditions = payload.conditions.map((c) => { + if (c.variable_selector?.join('.') === oldVarSelector.join('.')) + c.variable_selector = newVarSelector + return c + }) + } + break + } + case BlockEnum.Code: { + const payload = data as CodeNodeType + if (payload.variables) { + payload.variables = payload.variables.map((v) => { + if (v.value_selector.join('.') === oldVarSelector.join('.')) + v.value_selector = newVarSelector + return v + }) + } + break + } + case BlockEnum.TemplateTransform: { + const payload = data as TemplateTransformNodeType + if (payload.variables) { + payload.variables = payload.variables.map((v: any) => { + if (v.value_selector.join('.') === oldVarSelector.join('.')) + v.value_selector = newVarSelector + return v + }) + } + break + } + case BlockEnum.QuestionClassifier: { + const payload = data as QuestionClassifierNodeType + if (payload.query_variable_selector.join('.') === oldVarSelector.join('.')) + payload.query_variable_selector = newVarSelector + payload.instruction = replaceOldVarInText(payload.instruction, oldVarSelector, newVarSelector) + break + } + case BlockEnum.HttpRequest: { + const payload = data as HttpNodeType + payload.url = replaceOldVarInText(payload.url, oldVarSelector, newVarSelector) + payload.headers = replaceOldVarInText(payload.headers, oldVarSelector, newVarSelector) + payload.params = replaceOldVarInText(payload.params, oldVarSelector, newVarSelector) + if (typeof payload.body.data === 'string') { + payload.body.data = replaceOldVarInText(payload.body.data, oldVarSelector, newVarSelector) + } + else { + payload.body.data = payload.body.data.map((d) => { + return { + ...d, + value: replaceOldVarInText(d.value || '', oldVarSelector, newVarSelector), + } + }) + } + break + } + case BlockEnum.Tool: { + const payload = data as ToolNodeType + const hasShouldRenameVar = Object.keys(payload.tool_parameters)?.filter(key => payload.tool_parameters[key].type !== ToolVarType.constant) + if (hasShouldRenameVar) { + Object.keys(payload.tool_parameters).forEach((key) => { + const value = payload.tool_parameters[key] + const { type } = value + if (type === ToolVarType.variable) { + payload.tool_parameters[key] = { + ...value, + value: newVarSelector, + } + } + + if (type === ToolVarType.mixed) { + payload.tool_parameters[key] = { + ...value, + value: replaceOldVarInText(payload.tool_parameters[key].value as string, oldVarSelector, newVarSelector), + } + } + }) + } + break + } + case BlockEnum.VariableAssigner: { + const payload = data as VariableAssignerNodeType + if (payload.variables) { + payload.variables = payload.variables.map((v) => { + if (v.join('.') === oldVarSelector.join('.')) + v = newVarSelector + return v + }) + } + break + } + // eslint-disable-next-line sonarjs/no-duplicated-branches + case BlockEnum.VariableAggregator: { + const payload = data as VariableAssignerNodeType + if (payload.variables) { + payload.variables = payload.variables.map((v) => { + if (v.join('.') === oldVarSelector.join('.')) + v = newVarSelector + return v + }) + } + break + } + case BlockEnum.ParameterExtractor: { + const payload = data as ParameterExtractorNodeType + if (payload.query.join('.') === oldVarSelector.join('.')) + payload.query = newVarSelector + payload.instruction = replaceOldVarInText(payload.instruction, oldVarSelector, newVarSelector) + break + } + case BlockEnum.Iteration: { + const payload = data as IterationNodeType + if (payload.iterator_selector.join('.') === oldVarSelector.join('.')) + payload.iterator_selector = newVarSelector + + break + } + case BlockEnum.Loop: { + const payload = data as LoopNodeType + if (payload.break_conditions) { + payload.break_conditions = payload.break_conditions.map((c) => { + if (c.variable_selector?.join('.') === oldVarSelector.join('.')) + c.variable_selector = newVarSelector + return c + }) + } + break + } + case BlockEnum.ListFilter: { + const payload = data as ListFilterNodeType + if (payload.variable.join('.') === oldVarSelector.join('.')) + payload.variable = newVarSelector + break + } + } + }) + return newNode +} + +const varToValueSelectorList = (v: Var, parentValueSelector: ValueSelector, res: ValueSelector[]) => { + if (!v.variable) + return + + res.push([...parentValueSelector, v.variable]) + const isStructuredOutput = !!(v.children as StructuredOutput)?.schema?.properties + + if ((v.children as Var[])?.length > 0) { + (v.children as Var[]).forEach((child) => { + varToValueSelectorList(child, [...parentValueSelector, v.variable], res) + }) + } + if (isStructuredOutput) { + Object.keys((v.children as StructuredOutput)?.schema?.properties || {}).forEach((key) => { + const type = (v.children as StructuredOutput)?.schema?.properties[key].type + const isArray = type === Type.array + const arrayType = (v.children as StructuredOutput)?.schema?.properties[key].items?.type + varToValueSelectorList({ + variable: key, + type: structTypeToVarType(isArray ? arrayType! : type, isArray), + }, [...parentValueSelector, v.variable], res) + }) + } +} + +const varsToValueSelectorList = (vars: Var | Var[], parentValueSelector: ValueSelector, res: ValueSelector[]) => { + if (Array.isArray(vars)) { + vars.forEach((v) => { + varToValueSelectorList(v, parentValueSelector, res) + }) + } + varToValueSelectorList(vars as Var, parentValueSelector, res) +} + +export const getNodeOutputVars = (node: Node, isChatMode: boolean): ValueSelector[] => { + const { data, id } = node + const { type } = data + let res: ValueSelector[] = [] + + switch (type) { + case BlockEnum.Start: { + const { + variables, + } = data as StartNodeType + res = variables.map((v) => { + return [id, v.variable] + }) + + if (isChatMode) { + res.push([id, 'sys', 'query']) + res.push([id, 'sys', 'files']) + } + break + } + + 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, + }) + } + varsToValueSelectorList(vars, [id], res) + break + } + + case BlockEnum.KnowledgeRetrieval: { + varsToValueSelectorList(KNOWLEDGE_RETRIEVAL_OUTPUT_STRUCT, [id], res) + break + } + + case BlockEnum.Code: { + const { + outputs, + } = data as CodeNodeType + Object.keys(outputs).forEach((key) => { + res.push([id, key]) + }) + break + } + + case BlockEnum.TemplateTransform: { + varsToValueSelectorList(TEMPLATE_TRANSFORM_OUTPUT_STRUCT, [id], res) + break + } + + case BlockEnum.QuestionClassifier: { + varsToValueSelectorList(QUESTION_CLASSIFIER_OUTPUT_STRUCT, [id], res) + break + } + + case BlockEnum.HttpRequest: { + varsToValueSelectorList(HTTP_REQUEST_OUTPUT_STRUCT, [id], res) + break + } + + case BlockEnum.VariableAssigner: { + res.push([id, 'output']) + break + } + + case BlockEnum.VariableAggregator: { + res.push([id, 'output']) + break + } + + case BlockEnum.Tool: { + varsToValueSelectorList(TOOL_OUTPUT_STRUCT, [id], res) + break + } + + case BlockEnum.ParameterExtractor: { + const { + parameters, + } = data as ParameterExtractorNodeType + if (parameters?.length > 0) { + parameters.forEach((p) => { + res.push([id, p.name]) + }) + } + + break + } + + case BlockEnum.Iteration: { + res.push([id, 'output']) + break + } + + case BlockEnum.Loop: { + res.push([id, 'output']) + break + } + + case BlockEnum.DocExtractor: { + res.push([id, 'text']) + break + } + + case BlockEnum.ListFilter: { + res.push([id, 'result']) + res.push([id, 'first_record']) + res.push([id, 'last_record']) + break + } + } + + return res +} diff --git a/web/app/components/workflow/panel/env-panel/env-item.tsx b/web/app/components/workflow/panel/env-panel/env-item.tsx index 91abafa8f6..aeda81e120 100644 --- a/web/app/components/workflow/panel/env-panel/env-item.tsx +++ b/web/app/components/workflow/panel/env-panel/env-item.tsx @@ -1,53 +1,56 @@ -import { memo, useState } from 'react' -import { capitalize } from 'lodash-es' -import { RiDeleteBinLine, RiEditLine, RiLock2Line } from '@remixicon/react' -import { Env } from '@/app/components/base/icons/src/vender/line/others' -import { useStore } from '@/app/components/workflow/store' -import type { EnvironmentVariable } from '@/app/components/workflow/types' -import cn from '@/utils/classnames' - -type EnvItemProps = { - env: EnvironmentVariable - onEdit: (env: EnvironmentVariable) => void - onDelete: (env: EnvironmentVariable) => void -} - -const EnvItem = ({ - env, - onEdit, - onDelete, -}: EnvItemProps) => { - const envSecrets = useStore(s => s.envSecrets) - const [destructive, setDestructive] = useState(false) - - return ( -
-
-
- -
{env.name}
-
{capitalize(env.value_type)}
- {env.value_type === 'secret' && } -
-
-
- onEdit(env)}/> -
-
setDestructive(true)} - onMouseOut={() => setDestructive(false)} - > - onDelete(env)} /> -
-
-
-
{env.value_type === 'secret' ? envSecrets[env.id] : env.value}
-
- ) -} - -export default memo(EnvItem) +import { memo, useState } from 'react' +import { capitalize } from 'lodash-es' +import { RiDeleteBinLine, RiEditLine, RiLock2Line } from '@remixicon/react' +import { Env } from '@/app/components/base/icons/src/vender/line/others' +import { useStore } from '@/app/components/workflow/store' +import { useTranslation } from 'react-i18next' +import type { EnvironmentVariable } from '@/app/components/workflow/types' +import cn from '@/utils/classnames' + +type EnvItemProps = { + env: EnvironmentVariable + onEdit: (env: EnvironmentVariable) => void + onDelete: (env: EnvironmentVariable) => void +} + +const EnvItem = ({ + env, + onEdit, + onDelete, +}: EnvItemProps) => { + const { t } = useTranslation() + const envSecrets = useStore(s => s.envSecrets) + const [destructive, setDestructive] = useState(false) + + return ( +
+
+
+ +
{env.name}
+
{capitalize(env.value_type)}
+ {env.value_type === 'secret' && } +
+
+
+ onEdit(env)}/> +
+
setDestructive(true)} + onMouseOut={() => setDestructive(false)} + > + onDelete(env)} /> +
+
+
+
{env.description ? (`${t('workflow.env.modal.description')}: ${env.description}`) : ''}
+
{env.value_type === 'secret' ? envSecrets[env.id] : env.value}
+
+ ) +} + +export default memo(EnvItem) diff --git a/web/app/components/workflow/panel/env-panel/variable-modal.tsx b/web/app/components/workflow/panel/env-panel/variable-modal.tsx index c842554948..ff56c81d1f 100644 --- a/web/app/components/workflow/panel/env-panel/variable-modal.tsx +++ b/web/app/components/workflow/panel/env-panel/variable-modal.tsx @@ -1,167 +1,182 @@ -import React, { useEffect } from 'react' -import { useTranslation } from 'react-i18next' -import { v4 as uuid4 } from 'uuid' -import { RiCloseLine } from '@remixicon/react' -import { useContext } from 'use-context-selector' -import Button from '@/app/components/base/button' -import Input from '@/app/components/base/input' -import Tooltip from '@/app/components/base/tooltip' -import { ToastContext } from '@/app/components/base/toast' -import { useStore } from '@/app/components/workflow/store' -import type { EnvironmentVariable } from '@/app/components/workflow/types' -import cn from '@/utils/classnames' -import { checkKeys } from '@/utils/var' - -export type ModalPropsType = { - env?: EnvironmentVariable - onClose: () => void - onSave: (env: EnvironmentVariable) => void -} -const VariableModal = ({ - env, - onClose, - onSave, -}: ModalPropsType) => { - const { t } = useTranslation() - const { notify } = useContext(ToastContext) - const envList = useStore(s => s.environmentVariables) - const envSecrets = useStore(s => s.envSecrets) - const [type, setType] = React.useState<'string' | 'number' | 'secret'>('string') - const [name, setName] = React.useState('') - const [value, setValue] = React.useState() - - const checkVariableName = (value: string) => { - const { isValid, errorMessageKey } = checkKeys([value], false) - if (!isValid) { - notify({ - type: 'error', - message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: t('workflow.env.modal.name') }), - }) - return false - } - return true - } - - const handleSave = () => { - if (!checkVariableName(name)) - return - if (!value) - return notify({ type: 'error', message: 'value can not be empty' }) - - // Add check for duplicate name when editing - if (env && env.name !== name && envList.some(e => e.name === name)) - return notify({ type: 'error', message: 'name is existed' }) - // Original check for create new variable - if (!env && envList.some(e => e.name === name)) - return notify({ type: 'error', message: 'name is existed' }) - - onSave({ - id: env ? env.id : uuid4(), - value_type: type, - name, - value: type === 'number' ? Number(value) : value, - }) - onClose() - } - - useEffect(() => { - if (env) { - setType(env.value_type) - setName(env.name) - setValue(env.value_type === 'secret' ? envSecrets[env.id] : env.value) - } - }, [env, envSecrets]) - - return ( -
-
- {!env ? t('workflow.env.modal.title') : t('workflow.env.modal.editTitle')} -
-
- -
-
-
-
- {/* type */} -
-
{t('workflow.env.modal.type')}
-
-
setType('string')}>String
-
{ - setType('number') - if (!(/^\d$/).test(value)) - setValue('') - }}>Number
-
setType('secret')}> - Secret - - {t('workflow.env.modal.secretTip')} -
- } - triggerClassName='ml-0.5 w-3.5 h-3.5' - /> -
-
-
- {/* name */} -
-
{t('workflow.env.modal.name')}
-
- setName(e.target.value || '')} - onBlur={e => checkVariableName(e.target.value)} - type='text' - /> -
-
- {/* value */} -
-
{t('workflow.env.modal.value')}
-
- { - type !== 'number' ?