feat: Variable aggregation node

This commit is contained in:
zhangzhanwei 2025-10-24 11:43:35 +08:00 committed by zhanweizhang7
parent 4393db9f05
commit a66e636aa4
10 changed files with 414 additions and 11 deletions

View File

@ -32,6 +32,7 @@ from .text_to_speech_step_node.impl.base_text_to_speech_node import BaseTextToSp
from .text_to_video_step_node.impl.base_text_to_video_node import BaseTextToVideoNode
from .tool_lib_node import *
from .tool_node import *
from .variable_aggregation_node.impl.base_variable_aggregation_node import BaseVariableAggregationNode
from .variable_assign_node import BaseVariableAssignNode
from .variable_splitting_node import BaseVariableSplittingNode
from .video_understand_step_node import BaseVideoUnderstandNode
@ -45,7 +46,7 @@ node_list = [BaseStartStepNode, BaseChatNode, BaseSearchKnowledgeNode, BaseSearc
BaseVideoUnderstandNode,
BaseIntentNode, BaseLoopNode, BaseLoopStartStepNode,
BaseLoopContinueNode,
BaseLoopBreakNode, BaseVariableSplittingNode, BaseParameterExtractionNode]
BaseLoopBreakNode, BaseVariableSplittingNode, BaseParameterExtractionNode, BaseVariableAggregationNode]
def get_node(node_type):

View File

@ -0,0 +1,42 @@
# 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 VariableListSerializer(serializers.Serializer):
v_id = serializers.CharField(required=True, label=_("Variable id"))
variable = serializers.ListField(required=True, label=_("Variable"))
class VariableGroupSerializer(serializers.Serializer):
id = serializers.CharField(required=True, label=_("Group id"))
group_name = serializers.CharField(required=True, label=_("group_name"))
variable_list = VariableListSerializer(many=True)
class VariableAggregationNodeSerializer(serializers.Serializer):
strategy = serializers.CharField(required=True, label=_("Strategy"))
group_list = VariableGroupSerializer(many=True)
class IVariableAggregation(INode):
type = 'variable-aggregation-node'
def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
return VariableAggregationNodeSerializer
def _run(self):
return self.execute(**self.node_params_serializer.data, **self.flow_params_serializer.data)
def execute(self,strategy,group_list,**kwargs) -> NodeResult:
pass

View File

@ -0,0 +1,54 @@
#coding=utf-8
"""
@project: MaxKB
@Author虎²
@file base_variable_aggregation_node.py
@date2025/10/23 17:42
@desc:
"""
from application.flow.i_step_node import NodeResult
from application.flow.step_node.variable_aggregation_node.i_variable_aggregation_node import IVariableAggregation
class BaseVariableAggregationNode(IVariableAggregation):
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 get_first_non_null(self, variable_list):
for variable in variable_list:
v = self.workflow_manage.get_reference_field(
variable.get('variable')[0],
variable.get('variable')[1:])
if v is not None:
return v
return None
def set_variable_to_json(self, variable_list):
return {variable.get('variable')[1:][0]: self.workflow_manage.get_reference_field(
variable.get('variable')[0],
variable.get('variable')[1:]) for variable in variable_list}
def execute(self,strategy,group_list,**kwargs) -> NodeResult:
strategy_map = {'first_non_null':self.get_first_non_null,
'variable_to_json': self.set_variable_to_json,
}
result = { item.get('group_name'):strategy_map[strategy](item.get('variable_list')) for item in group_list}
return NodeResult({'result': result,**result},{})
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
}

View File

@ -263,6 +263,15 @@ You are a master of problem optimization, adept at accurately inferring user int
variableAggregationNode: {
label: 'Variable Aggregation',
text: 'Perform aggregation processing on the outputs of multiple branches',
Strategy: 'Aggregation Strategy',
placeholder: 'Return the first non-null value of each group',
placeholder1: 'Structurally aggregate each group of variables',
group: {
placeholder: 'Please select a variable',
noneError: 'Name cannot be empty',
dupError: 'Name cannot be duplicated',
},
add: 'Add Group',
},
mcpNode: {
label: 'MCP Node',

View File

@ -264,6 +264,15 @@ export default {
variableAggregationNode: {
label: '变量聚合',
text: '对多个分支的输出进行聚合处理',
Strategy: '聚合策略',
placeholder: '返回每组的第一个非空值',
placeholder1: '结构化聚合每组变量',
group: {
placeholder: '请选择变量',
noneError: '名称不能为空',
dupError: '名称不能重复',
},
add: '添加分组',
},
variableAssignNode: {
label: '变量赋值',

View File

@ -263,6 +263,15 @@ export default {
variableAggregationNode: {
label: '變量聚合',
text: '對多個分支的輸出進行聚合處理',
Strategy: '聚合策略',
placeholder: '返回每組的第一個非空值',
placeholder1: '結構化聚合每組變量',
group: {
placeholder: '請選擇變量',
noneError: '名稱不能為空',
dupError: '名稱不能重複',
},
add: '新增分組',
},
mcpNode: {
label: 'MCP 調用',

View File

@ -345,7 +345,9 @@ export const variableAggregationNode = {
height: 252,
properties: {
stepName: t('views.applicationWorkflow.nodes.variableAggregationNode.label'),
config: {},
config: {
fields: [],
},
},
}

View File

@ -1,20 +1,297 @@
<template>
<div>
<span>变量聚合</span>
</div>
<NodeContainer :nodeModel="nodeModel">
<h5 class="title-decoration-1 mb-8">{{ $t('views.applicationWorkflow.nodeSetting') }}</h5>
<el-form
@submit.prevent
:model="form_data"
label-position="top"
require-asterisk-position="right"
label-width="auto"
ref="VariableAggregationRef"
hide-required-asterisk
>
<el-form-item
:label="$t('views.applicationWorkflow.nodes.variableAggregationNode.Strategy')"
:rules="{
required: true,
trigger: 'change',
}"
>
<template #label>
<div class="flex-between">
<div>
<span>{{ $t('views.applicationWorkflow.nodes.variableAggregationNode.Strategy') }}
<span class="color-danger">*</span>
</span>
</div>
</div>
</template>
<el-select
v-model="form_data.strategy"
>
<el-option
:label="t('views.applicationWorkflow.nodes.variableAggregationNode.placeholder')"
value="first_non_null"
/>
<el-option
:label="t('views.applicationWorkflow.nodes.variableAggregationNode.placeholder1')"
value="variable_to_json"
/>
</el-select>
</el-form-item>
<div v-for="(group_list, gIndex) in form_data.group_list" :key="group_list.id" class="mb-8">
<el-card shadow="never" class="card-never" style="--el-card-padding: 12px">
<div class="flex-between mb-12">
<el-form-item
v-if="editingGroupIndex === gIndex"
:prop="`group_list.${gIndex}.group_name`"
:rules="groupNameRules(gIndex)"
style="margin-bottom: 0; flex: 1;"
>
<el-input
v-model="form_data.group_list[gIndex].group_name"
@blur="finishEditGroupName(gIndex)"
@input="validateGroupNameField(gIndex)"
ref="groupNameInputRef"
size="small"
style="width: 200px; font-weight: bold;"
>
</el-input>
</el-form-item>
<span v-else class="font-bold">{{ group_list.group_name }}</span>
<div class="flex align-center">
<el-button @click="editGroupName(gIndex)" size="large" link>
<el-icon><EditPen /></el-icon>
</el-button>
<el-button @click="deleteGroup(gIndex)" size="large" link :disabled="form_data.group_list.length <= 1">
<AppIcon iconName="app-delete"></AppIcon>
</el-button>
</div>
</div>
<div v-for="(item, vIndex) in group_list.variable_list" :key="item.v_id" class="mb-4">
<el-row :gutter="8">
<el-col :span="21">
<el-form-item
:prop="`group_list.${gIndex}.variable_list.${vIndex}.variable`"
:rules="{
type: 'array',
required: true,
message: $t('views.applicationWorkflow.nodes.variableAggregationNode.group.placeholder'),
trigger: 'change',
}"
>
<NodeCascader
ref="nodeCascaderRef"
:nodeModel="nodeModel"
class="w-full"
:placeholder="$t('views.applicationWorkflow.nodes.variableAggregationNode.group.placeholder')"
v-model="item.variable"
/>
</el-form-item>
</el-col>
<el-col :span="3" style="text-align: center;">
<el-button
link
size="large"
class="mt-4"
:disabled="group_list.variable_list.length <= 1"
@click="deleteVariable(gIndex, vIndex)"
>
<AppIcon iconName="app-delete"></AppIcon>
</el-button>
</el-col>
</el-row>
</div>
<el-button @click="addVariable(gIndex)" type="primary" size="large" link>
<AppIcon iconName="app-add-outlined" class="mr-4"/>
{{ $t('common.add') }}
</el-button>
</el-card>
</div>
<el-button @click="addGroup" type="primary" size="large" link>
<AppIcon iconName="app-add-outlined" class="mr-4"/>
{{ $t('views.applicationWorkflow.nodes.variableAggregationNode.add') }}
</el-button>
</el-form>
</NodeContainer>
</template>
<script setup lang="ts">
import { set, groupBy, cloneDeep } from 'lodash'
import { set, cloneDeep, debounce } from 'lodash'
import NodeCascader from '@/workflow/common/NodeCascader.vue'
import NodeContainer from '@/workflow/common/NodeContainer.vue'
import AIModeParamSettingDialog from '@/views/application/component/AIModeParamSettingDialog.vue'
import type { FormInstance } from 'element-plus'
import { ref, computed, onMounted, inject } from 'vue'
import { ref, computed, onMounted, nextTick } from 'vue'
import { isLastNode } from '@/workflow/common/data'
import { t } from '@/locales'
import { useRoute } from 'vue-router'
import { randomId } from '@/utils/common'
import { loadSharedApi } from '@/utils/dynamics-api/shared-api'
const props = defineProps<{ nodeModel: any }>()
const VariableAggregationRef = ref()
const nodeCascaderRef = ref()
const editingGroupIndex = ref<number | null>(null)
const groupNameInputRef = ref()
const form = {
strategy: 'first_non_null',
group_list: [
{
id: randomId(),
group_name: 'Group1',
variable_list: [
{
v_id: randomId(),
variable: []
}
]
},
]
}
const form_data = computed({
get: () => {
if (props.nodeModel.properties.node_data) {
return props.nodeModel.properties.node_data
} else {
set(props.nodeModel.properties, 'node_data', form)
}
return props.nodeModel.properties.node_data
},
set: (value) => {
set(props.nodeModel.properties, 'node_data', value)
}
})
const isGroupNameValid = ref<boolean>(true)
const groupNameErrMsg = ref('')
const tempGroupName = ref('')
const editGroupName = async (gIndex: number) => {
editingGroupIndex.value = gIndex
tempGroupName.value = form_data.value.group_list[gIndex].group_name
isGroupNameValid.value = true
groupNameErrMsg.value = ''
await nextTick()
if (groupNameInputRef.value) {
groupNameInputRef.value.focus()
}
}
const groupNameRules = (gIndex: number) => [
{
required: true,
message: t('views.applicationWorkflow.nodes.variableAggregationNode.group.noneError'),
trigger: 'blur'
},
{
validator: (rule: any, value: string, callback: any) => {
const trimmedValue = value?.trim() || ''
const hasDuplicate = form_data.value.group_list.some((item: any, index: number) =>
index !== gIndex && item.group_name.trim() === trimmedValue
)
if (hasDuplicate) {
callback(new Error(t('views.applicationWorkflow.nodes.variableAggregationNode.group.dupError')))
} else {
callback()
}
},
trigger: 'change' //
}
]
const validateGroupNameField = debounce((gIndex: number) => {
VariableAggregationRef.value?.validateField(`group_list.${gIndex}.group_name`)
}, 500)
const finishEditGroupName = async (gIndex: number) => {
try {
await VariableAggregationRef.value?.validateField(`group_list.${gIndex}.group_name`)
const c_group_list = cloneDeep(form_data.value.group_list)
const fields = c_group_list.map((item:any) => ({ label: item.group_name, value: item.group_name}))
set(props.nodeModel.properties.config, 'fields', fields)
editingGroupIndex.value = null
} catch (error) {
form_data.value.group_list[gIndex].group_name = tempGroupName.value
editingGroupIndex.value = null
}
}
const deleteGroup = (gIndex: number) => {
const c_group_list = cloneDeep(form_data.value.group_list)
c_group_list.splice(gIndex,1)
form_data.value.group_list = c_group_list
const fields = c_group_list.map((item:any) => ({ label: item.group_name, value: item.group_name}))
set(props.nodeModel.properties.config, 'fields', fields)
}
const addVariable = (gIndex: number) => {
const c_group_list = cloneDeep(form_data.value.group_list)
c_group_list[gIndex].variable_list.push({
v_id: randomId(),
variable: []
})
form_data.value.group_list = c_group_list
}
const deleteVariable = (gIndex: number,vIndex: number) => {
const c_group_list = cloneDeep(form_data.value.group_list)
c_group_list[gIndex].variable_list.splice(vIndex, 1)
form_data.value.group_list = c_group_list
}
const addGroup = () => {
let group_number = form_data.value.group_list.length + 1
let group_name = `Group${group_number}`
while (form_data.value.group_list.some((item: any) => item.group_name === group_name)) {
group_number++
group_name = `Group${group_number}`
}
const c_group_list = cloneDeep(form_data.value.group_list)
c_group_list.push({
id: randomId(),
group_name: group_name,
variable_list: [{
v_id: randomId(),
variable: []
}]
})
form_data.value.group_list = c_group_list
const fields = c_group_list.map((item:any) => ({ label: item.group_name, value: item.group_name}))
set(props.nodeModel.properties.config, 'fields', fields)
}
const validate = async () => {
const validate_list = [
...nodeCascaderRef.value.map((item:any)=>item.validate()),
VariableAggregationRef.value?.validate(),
]
return Promise.all(validate_list).catch((err) => {
return Promise.reject({node: props.nodeModel, errMessage: err})
})
}
onMounted(() => {
if (typeof props.nodeModel.properties.node_data?.is_result === 'undefined') {
if (isLastNode(props.nodeModel)) {
set(props.nodeModel.properties.node_data, 'is_result', true)
}
}
set(props.nodeModel, 'validate', validate)
const fields = form_data.value.group_list.map((item:any) => ({ label: item.group_name, value: item.group_name}))
set(props.nodeModel.properties.config, 'fields', fields)
})
</script>
<style lang="scss" scoped></style>