diff --git a/apps/application/flow/step_node/__init__.py b/apps/application/flow/step_node/__init__.py index 70e9f93bf..e95f16cfa 100644 --- a/apps/application/flow/step_node/__init__.py +++ b/apps/application/flow/step_node/__init__.py @@ -31,6 +31,7 @@ from .text_to_video_step_node.impl.base_text_to_video_node import BaseTextToVide from .tool_lib_node import * from .tool_node import * from .variable_assign_node import BaseVariableAssignNode +from .variable_splitting_node import BaseVariableSplittingNode node_list = [BaseStartStepNode, BaseChatNode, BaseSearchKnowledgeNode, BaseQuestionNode, BaseConditionNode, BaseReplyNode, @@ -40,7 +41,7 @@ node_list = [BaseStartStepNode, BaseChatNode, BaseSearchKnowledgeNode, BaseQuest BaseImageGenerateNode, BaseVariableAssignNode, BaseMcpNode, BaseTextToVideoNode, BaseImageToVideoNode, BaseIntentNode, BaseLoopNode, BaseLoopStartStepNode, BaseLoopContinueNode, - BaseLoopBreakNode] + BaseLoopBreakNode, BaseVariableSplittingNode] def get_node(node_type): diff --git a/apps/application/flow/step_node/variable_splitting_node/__init__.py b/apps/application/flow/step_node/variable_splitting_node/__init__.py new file mode 100644 index 000000000..c93d71e9e --- /dev/null +++ b/apps/application/flow/step_node/variable_splitting_node/__init__.py @@ -0,0 +1,9 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: __init__.py.py + @date:2025/10/13 14:56 + @desc: +""" +from .impl import * diff --git a/apps/application/flow/step_node/variable_splitting_node/i_variable_splitting_node.py b/apps/application/flow/step_node/variable_splitting_node/i_variable_splitting_node.py new file mode 100644 index 000000000..52cff8eb2 --- /dev/null +++ b/apps/application/flow/step_node/variable_splitting_node/i_variable_splitting_node.py @@ -0,0 +1,32 @@ +# coding=utf-8 + +from typing import Type + +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers + +from application.flow.i_step_node import INode, NodeResult + + +class VariableSplittingNodeParamsSerializer(serializers.Serializer): + input_variable = serializers.ListField(required=True, + label=_("input variable")) + + variable_list = serializers.ListField(required=True, + label=_("Split variables")) + + +class IVariableSplittingNode(INode): + type = 'variable-splitting-node' + + def get_node_params_serializer_class(self) -> Type[serializers.Serializer]: + return VariableSplittingNodeParamsSerializer + + def _run(self): + input_variable = self.workflow_manage.get_reference_field( + self.node_params_serializer.data.get('input_variable')[0], + self.node_params_serializer.data.get('input_variable')[1:]) + return self.execute(input_variable, self.node_params_serializer.data['variable_list']) + + def execute(self, input_variable, variable_list, **kwargs) -> NodeResult: + pass diff --git a/apps/application/flow/step_node/variable_splitting_node/impl/__init__.py b/apps/application/flow/step_node/variable_splitting_node/impl/__init__.py new file mode 100644 index 000000000..1ef0d7ac5 --- /dev/null +++ b/apps/application/flow/step_node/variable_splitting_node/impl/__init__.py @@ -0,0 +1,9 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: __init__.py.py + @date:2025/10/13 15:01 + @desc: +""" +from .base_variable_splitting_node import * \ No newline at end of file diff --git a/apps/application/flow/step_node/variable_splitting_node/impl/base_variable_splitting_node.py b/apps/application/flow/step_node/variable_splitting_node/impl/base_variable_splitting_node.py new file mode 100644 index 000000000..0d2cc964c --- /dev/null +++ b/apps/application/flow/step_node/variable_splitting_node/impl/base_variable_splitting_node.py @@ -0,0 +1,53 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: base_variable_splitting_node.py + @date:2025/10/13 15:02 + @desc: +""" +from jsonpath_ng import parse + +from application.flow.i_step_node import NodeResult +from application.flow.step_node.variable_splitting_node.i_variable_splitting_node import IVariableSplittingNode + + +def smart_jsonpath_search(data: dict, path: str): + """ + 智能JSON Path搜索 + 返回: + - 单个匹配: 直接返回值 + - 多个匹配: 返回值的列表 + - 无匹配: 返回None + """ + jsonpath_expr = parse(path) + matches = jsonpath_expr.find(data) + + if not matches: + return None + elif len(matches) == 1: + return matches[0].value + else: + return [match.value for match in matches] + + +class BaseVariableSplittingNode(IVariableSplittingNode): + def save_context(self, details, workflow_manage): + for key, value in details.get('result').items(): + self.context['key'] = value + self.context['result'] = details.get('result') + + def execute(self, input_variable, variable_list, **kwargs) -> NodeResult: + response = {v['field']: smart_jsonpath_search(input_variable, v['expression']) for v in variable_list} + return NodeResult({'result': response, **response}, {}) + + def get_details(self, index: int, **kwargs): + return { + 'name': self.node.properties.get('stepName'), + "index": index, + 'run_time': self.context.get('run_time'), + 'type': self.node.type, + 'result': self.context.get('result'), + 'status': self.status, + 'err_message': self.err_message + } diff --git a/pyproject.toml b/pyproject.toml index 50069cb89..0319e0d09 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,6 +57,7 @@ dependencies = [ "websockets==15.0.1", "pylint==3.3.7", "cohere==5.17.0", + "jsonpath-ng==1.7.0" ] [tool.uv] diff --git a/ui/src/assets/workflow/icon_variable_splitting.svg b/ui/src/assets/workflow/icon_variable_splitting.svg new file mode 100644 index 000000000..ff46b58d7 --- /dev/null +++ b/ui/src/assets/workflow/icon_variable_splitting.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/ui/src/enums/application.ts b/ui/src/enums/application.ts index bbd32508e..423b7944c 100644 --- a/ui/src/enums/application.ts +++ b/ui/src/enums/application.ts @@ -32,6 +32,7 @@ export enum WorkflowType { LoopStartNode = 'loop-start-node', LoopContinueNode = 'loop-continue-node', LoopBreakNode = 'loop-break-node', + VariableSplittingNode = 'variable-splitting-node', } export enum WorkflowMode { // 应用工作流 diff --git a/ui/src/locales/lang/en-US/views/application-workflow.ts b/ui/src/locales/lang/en-US/views/application-workflow.ts index dfd354557..727e53658 100644 --- a/ui/src/locales/lang/en-US/views/application-workflow.ts +++ b/ui/src/locales/lang/en-US/views/application-workflow.ts @@ -396,6 +396,16 @@ You are a master of problem optimization, adept at accurately inferring user int text: 'Terminate the current loop and exit the loop body', isBreak: 'Break', }, + variableSplittingNode: { + label: 'Variable Splitting', + text: 'Used to split variables', + result: 'Result', + splitVariables: 'Split Variables', + expression: { + label: 'Expression', + placeholder: 'Please enter expression', + }, + }, }, compare: { is_null: 'Is null', diff --git a/ui/src/locales/lang/zh-CN/dynamics-form.ts b/ui/src/locales/lang/zh-CN/dynamics-form.ts index 0dc6c73bb..b8df5669e 100644 --- a/ui/src/locales/lang/zh-CN/dynamics-form.ts +++ b/ui/src/locales/lang/zh-CN/dynamics-form.ts @@ -28,6 +28,9 @@ export default { placeholder: '请输入关键字搜索', }, paramForm: { + variable: { + label: '变量', + }, field: { label: '参数', placeholder: '请输入参数', diff --git a/ui/src/locales/lang/zh-CN/views/application-workflow.ts b/ui/src/locales/lang/zh-CN/views/application-workflow.ts index 5bf4a25cb..af85a80ea 100644 --- a/ui/src/locales/lang/zh-CN/views/application-workflow.ts +++ b/ui/src/locales/lang/zh-CN/views/application-workflow.ts @@ -396,6 +396,16 @@ export default { text: '终止当前循环,跳出循环体', isBreak: 'Break', }, + variableSplittingNode: { + label: '变量拆分', + text: '用于拆分变量', + result: '结果', + splitVariables: '拆分变量', + expression: { + label: '表达式', + placeholder: '请输入表达式', + }, + }, }, compare: { is_null: '为空', diff --git a/ui/src/locales/lang/zh-Hant/views/application-workflow.ts b/ui/src/locales/lang/zh-Hant/views/application-workflow.ts index d9ffc97a2..b9398ee30 100644 --- a/ui/src/locales/lang/zh-Hant/views/application-workflow.ts +++ b/ui/src/locales/lang/zh-Hant/views/application-workflow.ts @@ -382,6 +382,16 @@ export default { isContinue: 'Continue', }, loopBreakNode: { label: 'Break', text: '終止當前循環,跳出循環體', isBreak: 'Break' }, + variableSplittingNode: { + label: '變量拆分', + text: '用於拆分變量', + result: '結果', + splitVariables: '拆分變量', + expression: { + label: '表達式', + placeholder: '請輸入表達式', + }, + }, }, compare: { is_null: '為空', diff --git a/ui/src/workflow/common/app-node.ts b/ui/src/workflow/common/app-node.ts index 5354c93b0..d8922318d 100644 --- a/ui/src/workflow/common/app-node.ts +++ b/ui/src/workflow/common/app-node.ts @@ -7,7 +7,7 @@ import { h as lh } from '@logicflow/core' import { createApp, h } from 'vue' import directives from '@/directives' import i18n from '@/locales' -import { WorkflowType } from '@/enums/application' +import { WorkflowMode, WorkflowType } from '@/enums/application' import { nodeDict } from '@/workflow/common/data' import { isActive, connect, disconnect } from './teleport' import { t } from '@/locales' @@ -243,6 +243,7 @@ class AppNode extends HtmlResize.view { return { getNode: () => model, getGraph: () => graphModel, + workflowMode: WorkflowMode.Application, } }, }) diff --git a/ui/src/workflow/common/data.ts b/ui/src/workflow/common/data.ts index dabdf3b32..4a118fac3 100644 --- a/ui/src/workflow/common/data.ts +++ b/ui/src/workflow/common/data.ts @@ -137,6 +137,24 @@ export const questionNode = { }, }, } +export const variableSplittingNode = { + type: WorkflowType.VariableSplittingNode, + text: t('views.applicationWorkflow.nodes.variableSplittingNode.text', '变量拆分'), + label: t('views.applicationWorkflow.nodes.variableSplittingNode.label', '变量拆分'), + height: 345, + properties: { + stepName: t('views.applicationWorkflow.nodes.variableSplittingNode.label', '变量拆分'), + config: { + fields: [ + { + label: t('views.applicationWorkflow.nodes.variableSplittingNode.result', '结果'), + value: 'result', + }, + ], + }, + }, +} + export const conditionNode = { type: WorkflowType.Condition, text: t('views.applicationWorkflow.nodes.conditionNode.text'), @@ -540,6 +558,10 @@ export const menuNodes = [ label: t('views.applicationWorkflow.nodes.classify.businessLogic'), list: [conditionNode, formNode, variableAssignNode, replyNode, loopNode], }, + { + label: t('views.applicationWorkflow.nodes.classify.dataProcessing', '数据处理'), + list: [variableSplittingNode], + }, { label: t('views.applicationWorkflow.nodes.classify.other'), list: [mcpNode, documentExtractNode, toolNode], @@ -565,6 +587,10 @@ export const applicationLoopMenuNodes = [ label: t('views.applicationWorkflow.nodes.classify.businessLogic'), list: [conditionNode, formNode, variableAssignNode, replyNode, loopContinueNode, loopBreakNode], }, + { + label: t('views.applicationWorkflow.nodes.classify.dataProcessing', '数据处理'), + list: [variableSplittingNode], + }, { label: t('views.applicationWorkflow.nodes.classify.other'), list: [mcpNode, documentExtractNode, toolNode], @@ -666,6 +692,7 @@ export const nodeDict: any = { [WorkflowType.LoopStartNode]: loopStartNode, [WorkflowType.LoopBreakNode]: loopBodyNode, [WorkflowType.LoopContinueNode]: loopContinueNode, + [WorkflowType.VariableSplittingNode]: variableSplittingNode, } export function isWorkFlow(type: string | undefined) { return type === 'WORK_FLOW' diff --git a/ui/src/workflow/common/teleport.ts b/ui/src/workflow/common/teleport.ts index eb863f166..1637684c6 100644 --- a/ui/src/workflow/common/teleport.ts +++ b/ui/src/workflow/common/teleport.ts @@ -39,6 +39,18 @@ export function disconnect(id: string) { delete items[id] } } +export function disconnectByFlow(flowId: string) { + Object.keys(items).forEach((key) => { + if (key.startsWith(flowId)) { + delete items[key] + } + }) +} +export function disconnectAll() { + Object.keys(items).forEach((key) => { + delete items[key] + }) +} export function isActive() { return active diff --git a/ui/src/workflow/icons/variable-splitting-node-icon.vue b/ui/src/workflow/icons/variable-splitting-node-icon.vue new file mode 100644 index 000000000..c435fd8fc --- /dev/null +++ b/ui/src/workflow/icons/variable-splitting-node-icon.vue @@ -0,0 +1,6 @@ + + diff --git a/ui/src/workflow/index.vue b/ui/src/workflow/index.vue index e78fd5282..30cc3b040 100644 --- a/ui/src/workflow/index.vue +++ b/ui/src/workflow/index.vue @@ -6,16 +6,16 @@ diff --git a/ui/src/workflow/nodes/loop-node/index.vue b/ui/src/workflow/nodes/loop-node/index.vue index d05896e7f..8f528ae17 100644 --- a/ui/src/workflow/nodes/loop-node/index.vue +++ b/ui/src/workflow/nodes/loop-node/index.vue @@ -73,7 +73,7 @@ + diff --git a/ui/src/workflow/nodes/variable-splitting/component/VariableFieldTable.vue b/ui/src/workflow/nodes/variable-splitting/component/VariableFieldTable.vue new file mode 100644 index 000000000..e399e4158 --- /dev/null +++ b/ui/src/workflow/nodes/variable-splitting/component/VariableFieldTable.vue @@ -0,0 +1,132 @@ + + + + + diff --git a/ui/src/workflow/nodes/variable-splitting/index.ts b/ui/src/workflow/nodes/variable-splitting/index.ts new file mode 100644 index 000000000..5e20ad653 --- /dev/null +++ b/ui/src/workflow/nodes/variable-splitting/index.ts @@ -0,0 +1,14 @@ +import VariableSplittingNodeVue from './index.vue' +import { AppNode, AppNodeModel } from '@/workflow/common/app-node' + +class VariableSplittingNode extends AppNode { + constructor(props: any) { + super(props, VariableSplittingNodeVue) + } +} + +export default { + type: 'variable-splitting-node', + model: AppNodeModel, + view: VariableSplittingNode, +} diff --git a/ui/src/workflow/nodes/variable-splitting/index.vue b/ui/src/workflow/nodes/variable-splitting/index.vue new file mode 100644 index 000000000..90aeb0fa4 --- /dev/null +++ b/ui/src/workflow/nodes/variable-splitting/index.vue @@ -0,0 +1,69 @@ + + +