Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/application/flow/step_node/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
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,
Expand All @@ -40,7 +41,7 @@
BaseImageGenerateNode, BaseVariableAssignNode, BaseMcpNode, BaseTextToVideoNode, BaseImageToVideoNode,
BaseIntentNode, BaseLoopNode, BaseLoopStartStepNode,
BaseLoopContinueNode,
BaseLoopBreakNode]
BaseLoopBreakNode, BaseVariableSplittingNode]


def get_node(node_type):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# coding=utf-8
"""
@project: MaxKB
@Author:虎虎
@file: __init__.py.py
@date:2025/10/13 14:56
@desc:
"""
from .impl import *
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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 *
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ dependencies = [
"websockets==15.0.1",
"pylint==3.3.7",
"cohere==5.17.0",
"jsonpath-ng==1.7.0"
]

[tool.uv]
Expand Down
11 changes: 11 additions & 0 deletions ui/src/assets/workflow/icon_variable_splitting.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions ui/src/enums/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
// 应用工作流
Expand Down
10 changes: 10 additions & 0 deletions ui/src/locales/lang/en-US/views/application-workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
3 changes: 3 additions & 0 deletions ui/src/locales/lang/zh-CN/dynamics-form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ export default {
placeholder: '请输入关键字搜索',
},
paramForm: {
variable: {
label: '变量',
},
field: {
label: '参数',
placeholder: '请输入参数',
Expand Down
10 changes: 10 additions & 0 deletions ui/src/locales/lang/zh-CN/views/application-workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,16 @@ export default {
text: '终止当前循环,跳出循环体',
isBreak: 'Break',
},
variableSplittingNode: {
label: '变量拆分',
text: '用于拆分变量',
result: '结果',
splitVariables: '拆分变量',
expression: {
label: '表达式',
placeholder: '请输入表达式',
},
},
},
compare: {
is_null: '为空',
Expand Down
10 changes: 10 additions & 0 deletions ui/src/locales/lang/zh-Hant/views/application-workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '為空',
Expand Down
3 changes: 2 additions & 1 deletion ui/src/workflow/common/app-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -243,6 +243,7 @@ class AppNode extends HtmlResize.view {
return {
getNode: () => model,
getGraph: () => graphModel,
workflowMode: WorkflowMode.Application,
}
},
})
Expand Down
27 changes: 27 additions & 0 deletions ui/src/workflow/common/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down Expand Up @@ -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],
Expand All @@ -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],
Expand Down Expand Up @@ -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'
Expand Down
12 changes: 12 additions & 0 deletions ui/src/workflow/common/teleport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions ui/src/workflow/icons/variable-splitting-node-icon.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<template>
<el-avatar shape="square" class="avatar-blue">
<img src="@/assets/workflow/icon_variable_splitting.svg" style="width: 65%" alt="" />
</el-avatar>
</template>
<script setup lang="ts"></script>
17 changes: 13 additions & 4 deletions ui/src/workflow/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@
</template>
<script setup lang="ts">
import LogicFlow from '@logicflow/core'
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted, onUnmounted } from 'vue'
import AppEdge from './common/edge'
import loopEdge from './common/loopEdge'
import Control from './common/NodeControl.vue'
import { baseNodes } from '@/workflow/common/data'
import '@logicflow/extension/lib/style/index.css'
import '@logicflow/core/dist/style/index.css'
import { initDefaultShortcut } from '@/workflow/common/shortcut'
import Dagre from '@/workflow/plugins/dagre'
import { getTeleport } from '@/workflow/common/teleport'
import { disconnectAll, getTeleport } from '@/workflow/common/teleport'
import { WorkflowMode } from '@/enums/application'
const nodes: any = import.meta.glob('./nodes/**/index.ts', { eager: true })

defineOptions({ name: 'WorkFlow' })
Expand All @@ -40,6 +40,9 @@ const lf = ref()
onMounted(() => {
renderGraphData()
})
onUnmounted(() => {
disconnectAll()
})
const render = (data: any) => {
lf.value.render(data)
}
Expand Down Expand Up @@ -88,7 +91,13 @@ const renderGraphData = (data?: any) => {
lf.value.setDefaultEdgeType('app-edge')

lf.value.render(data ? data : {})

lf.value.graphModel.get_provide = (node: any, graph: any) => {
return {
getNode: () => node,
getGraph: () => graph,
workflowMode: WorkflowMode.Application,
}
}
lf.value.graphModel.eventCenter.on('delete_edge', (id_list: Array<string>) => {
id_list.forEach((id: string) => {
lf.value.deleteEdge(id)
Expand Down
11 changes: 9 additions & 2 deletions ui/src/workflow/nodes/loop-body-node/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@
<script setup lang="ts">
import { set, cloneDeep } from 'lodash'
import AppEdge from '@/workflow/common/edge'
import { ref, onMounted } from 'vue'
import { ref, onMounted, onUnmounted } from 'vue'
import LogicFlow from '@logicflow/core'
import Dagre from '@/workflow/plugins/dagre'
import { initDefaultShortcut } from '@/workflow/common/shortcut'
import LoopBodyContainer from '@/workflow/nodes/loop-body-node/LoopBodyContainer.vue'
import { WorkflowMode } from '@/enums/application'
import { WorkFlowInstance } from '@/workflow/common/validate'
import { t } from '@/locales'
import { disconnectByFlow } from '@/workflow/common/teleport'
const nodes: any = import.meta.glob('@/workflow/nodes/**/index.ts', { eager: true })
const props = defineProps<{ nodeModel: any }>()
const containerRef = ref()
Expand Down Expand Up @@ -62,7 +63,9 @@ const set_loop_body = () => {
const refresh_loop_fields = (fields: Array<any>) => {
const loop_node_id = props.nodeModel.properties.loop_node_id
const loop_node = props.nodeModel.graphModel.getNodeModelById(loop_node_id)
loop_node.properties.config.fields = fields
if (loop_node) {
loop_node.properties.config.fields = fields
}
}

const lf = ref()
Expand Down Expand Up @@ -158,5 +161,9 @@ onMounted(() => {
set(props.nodeModel, 'validate', validate)
set(props.nodeModel, 'set_loop_body', set_loop_body)
})
onUnmounted(() => {
disconnectByFlow(lf.value.graphModel.flowId)
lf.value = null
})
</script>
<style lang="scss" scoped></style>
Loading
Loading