mirror of
https://github.com/1Panel-dev/MaxKB.git
synced 2025-12-26 01:33:05 +00:00
feat: Variable aggregation node
This commit is contained in:
parent
4393db9f05
commit
a66e636aa4
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
#coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎²
|
||||
@file: base_variable_aggregation_node.py
|
||||
@date:2025/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
|
||||
}
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -264,6 +264,15 @@ export default {
|
|||
variableAggregationNode: {
|
||||
label: '变量聚合',
|
||||
text: '对多个分支的输出进行聚合处理',
|
||||
Strategy: '聚合策略',
|
||||
placeholder: '返回每组的第一个非空值',
|
||||
placeholder1: '结构化聚合每组变量',
|
||||
group: {
|
||||
placeholder: '请选择变量',
|
||||
noneError: '名称不能为空',
|
||||
dupError: '名称不能重复',
|
||||
},
|
||||
add: '添加分组',
|
||||
},
|
||||
variableAssignNode: {
|
||||
label: '变量赋值',
|
||||
|
|
|
|||
|
|
@ -263,6 +263,15 @@ export default {
|
|||
variableAggregationNode: {
|
||||
label: '變量聚合',
|
||||
text: '對多個分支的輸出進行聚合處理',
|
||||
Strategy: '聚合策略',
|
||||
placeholder: '返回每組的第一個非空值',
|
||||
placeholder1: '結構化聚合每組變量',
|
||||
group: {
|
||||
placeholder: '請選擇變量',
|
||||
noneError: '名稱不能為空',
|
||||
dupError: '名稱不能重複',
|
||||
},
|
||||
add: '新增分組',
|
||||
},
|
||||
mcpNode: {
|
||||
label: 'MCP 調用',
|
||||
|
|
|
|||
|
|
@ -345,7 +345,9 @@ export const variableAggregationNode = {
|
|||
height: 252,
|
||||
properties: {
|
||||
stepName: t('views.applicationWorkflow.nodes.variableAggregationNode.label'),
|
||||
config: {},
|
||||
config: {
|
||||
fields: [],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue