Variable assign (#2346)

* feat: Support variable assign

* feat: Workfloat support variable assign(#2114)

* feat: Support variable assign save input output value

* feat: Execution detail support variable assign(#2114)

* feat: Support variable assign dict array value config

* chore: rename package

---------

Co-authored-by: wangdan-fit2cloud <dan.wang@fit2cloud.com>
This commit is contained in:
刘瑞斌 2025-02-21 11:00:56 +08:00 committed by GitHub
parent bb557fd187
commit df940686e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 420 additions and 32 deletions

View File

@ -24,11 +24,14 @@ from .search_dataset_node import *
from .speech_to_text_step_node import BaseSpeechToTextNode
from .start_node import *
from .text_to_speech_step_node.impl.base_text_to_speech_node import BaseTextToSpeechNode
from .variable_assign_node import BaseVariableAssignNode
node_list = [BaseStartStepNode, BaseChatNode, BaseSearchDatasetNode, BaseQuestionNode, BaseConditionNode, BaseReplyNode,
node_list = [BaseStartStepNode, BaseChatNode, BaseSearchDatasetNode, BaseQuestionNode,
BaseConditionNode, BaseReplyNode,
BaseFunctionNodeNode, BaseFunctionLibNodeNode, BaseRerankerNode, BaseApplicationNode,
BaseDocumentExtractNode,
BaseImageUnderstandNode, BaseFormNode, BaseSpeechToTextNode, BaseTextToSpeechNode,BaseImageGenerateNode]
BaseImageUnderstandNode, BaseFormNode, BaseSpeechToTextNode, BaseTextToSpeechNode,
BaseImageGenerateNode, BaseVariableAssignNode]
def get_node(node_type):

View File

@ -0,0 +1,3 @@
# coding=utf-8
from .impl import *

View File

@ -0,0 +1,27 @@
# 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
from common.util.field_message import ErrMessage
class VariableAssignNodeParamsSerializer(serializers.Serializer):
variable_list = serializers.ListField(required=True,
error_messages=ErrMessage.list(_("Reference Field")))
class IVariableAssignNode(INode):
type = 'variable-assign-node'
def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
return VariableAssignNodeParamsSerializer
def _run(self):
return self.execute(**self.node_params_serializer.data, **self.flow_params_serializer.data)
def execute(self, variable_list, stream, **kwargs) -> NodeResult:
pass

View File

@ -0,0 +1,9 @@
# coding=utf-8
"""
@project: maxkb
@Author
@file __init__.py
@date2024/6/11 17:49
@desc:
"""
from .base_variable_assign_node import *

View File

@ -0,0 +1,59 @@
# coding=utf-8
import json
from typing import List
from application.flow.i_step_node import NodeResult
from application.flow.step_node.variable_assign_node.i_variable_assign_node import IVariableAssignNode
class BaseVariableAssignNode(IVariableAssignNode):
def save_context(self, details, workflow_manage):
self.context['variable_list'] = details.get('variable_list')
self.context['result_list'] = details.get('result_list')
def execute(self, variable_list, stream, **kwargs) -> NodeResult:
#
result_list = []
for variable in variable_list:
if 'fields' not in variable:
continue
if 'global' == variable['fields'][0]:
result = {
'name': variable['name'],
'input_value': self.get_reference_content(variable['fields']),
}
if variable['source'] == 'custom':
if variable['type'] in ['dict', 'array']:
if isinstance(variable['value'], dict) or isinstance(variable['value'], list):
val = variable['value']
else:
val = json.loads(variable['value'])
self.workflow_manage.context[variable['fields'][1]] = val
result['output_value'] = variable['value'] = val
else:
self.workflow_manage.context[variable['fields'][1]] = variable['value']
result['output_value'] = variable['value']
else:
reference = self.get_reference_content(variable['reference'])
self.workflow_manage.context[variable['fields'][1]] = reference
result['output_value'] = reference
result_list.append(result)
return NodeResult({'variable_list': variable_list, 'result_list': result_list}, {})
def get_reference_content(self, fields: List[str]):
return str(self.workflow_manage.get_reference_field(
fields[0],
fields[1:]))
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,
'variable_list': self.context.get('variable_list'),
'result_list': self.context.get('result_list'),
'status': self.status,
'err_message': self.err_message
}

View File

@ -0,0 +1,5 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.98262 2.70361H5.98155C5.17745 2.70361 4.55609 2.95232 4.11749 3.46751C3.71543 3.9294 3.51441 4.56895 3.51441 5.36838V8.69045C3.51441 9.18787 3.40476 9.56094 3.18545 9.80965C2.97469 10.0292 2.6275 10.1764 2.14389 10.2413C1.95529 10.2666 1.81483 10.4233 1.81482 10.6084V11.4085C1.81481 11.5847 1.94968 11.7334 2.12951 11.7554C2.62062 11.8156 2.97261 11.9537 3.18545 12.1902C3.40476 12.4211 3.51441 12.7942 3.51441 13.3094V16.6492C3.51441 17.4309 3.71543 18.0704 4.11749 18.5323C4.55609 19.0297 5.17745 19.2962 5.98155 19.2962H6.98262C7.20691 19.2962 7.38874 19.1195 7.38874 18.9014V18.252C7.38874 18.034 7.20691 17.8572 6.98262 17.8572H6.3105C5.61605 17.8572 5.2871 17.4664 5.2871 16.7203V13.2205C5.2871 12.1546 4.75712 11.4263 3.71543 10.9999C4.75712 10.6268 5.2871 9.88071 5.2871 8.77927V5.29732C5.2871 4.51565 5.61605 4.14259 6.3105 4.14259H6.98262C7.20691 4.14259 7.38874 3.96584 7.38874 3.74781V3.09839C7.38874 2.88036 7.20691 2.70361 6.98262 2.70361ZM16.0185 2.70361H15.0174C14.7931 2.70361 14.6113 2.88036 14.6113 3.09839V3.74781C14.6113 3.96584 14.7931 4.14259 15.0174 4.14259H15.6895C16.3657 4.14259 16.7129 4.51565 16.7129 5.29732V8.77927C16.7129 9.88071 17.2246 10.6268 18.2846 10.9999C17.2246 11.4263 16.7129 12.1546 16.7129 13.2205V16.7203C16.7129 17.4664 16.3657 17.8572 15.6895 17.8572H15.0174C14.7931 17.8572 14.6113 18.034 14.6113 18.252V18.9014C14.6113 19.1195 14.7931 19.2962 15.0174 19.2962H16.0185C16.8226 19.2962 17.4439 19.0297 17.8825 18.5323C18.2846 18.0704 18.4856 17.4309 18.4856 16.6492V13.3094C18.4856 12.7942 18.5953 12.4211 18.8146 12.1902C19.0209 11.961 19.3579 11.8242 19.8257 11.7612L19.8331 11.7602C20.0347 11.7339 20.1852 11.5667 20.1852 11.3689V10.6149C20.1852 10.4263 20.0421 10.2665 19.8499 10.2405C19.3701 10.1756 19.0277 10.0314 18.8328 9.82741C18.5953 9.5787 18.4856 9.18787 18.4856 8.69045V5.36838C18.4856 4.56895 18.2846 3.9294 17.8825 3.46751C17.4439 2.95232 16.8226 2.70361 16.0185 2.70361Z" fill="white"/>
<path d="M7.44443 8.68878C7.44443 8.49241 7.60362 8.33323 7.79999 8.33323H14.2C14.3964 8.33323 14.5555 8.49241 14.5555 8.68878V9.75545C14.5555 9.95182 14.3964 10.111 14.2 10.111H7.79999C7.60362 10.111 7.44443 9.95182 7.44443 9.75545V8.68878Z" fill="white"/>
<path d="M7.44443 12.2443C7.44443 12.048 7.60362 11.8888 7.79999 11.8888H14.2C14.3964 11.8888 14.5555 12.048 14.5555 12.2443V13.311C14.5555 13.5074 14.3964 13.6666 14.2 13.6666H7.79999C7.60362 13.6666 7.44443 13.5074 7.44443 13.311V12.2443Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -59,13 +59,16 @@
<h5 class="p-8-12">
{{ $t('common.param.inputParam') }}
</h5>
<div class="p-8-12 border-t-dashed lighter">
<div class="mb-8">
<span class="color-secondary">
{{ $t('chat.paragraphSource.question') }}:</span
>
{{ item.question || '-' }}
</div>
<div v-for="(f, i) in item.global_fields" :key="i" class="mb-8">
<span class="color-secondary">{{ f.label }}:</span> {{ f.value }}
</div>
@ -604,6 +607,30 @@
</div>
</div>
</template>
<!-- 变量赋值 -->
<template v-if="item.type === WorkflowType.VariableAssignNode">
<div class="card-never border-r-4">
<h5 class="p-8-12">
{{ $t('common.param.inputParam') }}
</h5>
<div class="p-8-12 border-t-dashed lighter">
<div v-for="(f, i) in item.result_list" :key="i" class="mb-8">
<span class="color-secondary">{{ f.name }}:</span> {{ f.input_value }}
</div>
</div>
</div>
<div class="card-never border-r-4">
<h5 class="p-8-12">
{{ $t('common.param.outputParam') }}
</h5>
<div class="p-8-12 border-t-dashed lighter">
<div v-for="(f, i) in item.result_list" :key="i" class="mb-8">
<span class="color-secondary">{{ f.name }}:</span> {{ f.output_value }}
</div>
</div>
</div>
</template>
</template>
<template v-else>
<div class="card-never border-r-4">

View File

@ -12,8 +12,9 @@ export enum WorkflowType {
Application = 'application-node',
DocumentExtractNode = 'document-extract-node',
ImageUnderstandNode = 'image-understand-node',
VariableAssignNode = 'variable-assign-node',
FormNode = 'form-node',
TextToSpeechNode = 'text-to-speech-node',
SpeechToTextNode = 'speech-to-text-node',
ImageGenerateNode = 'image-generate-node'
ImageGenerateNode = 'image-generate-node'
}

View File

@ -19,7 +19,7 @@ export default {
autoSave: 'Auto Save',
latestRelease: 'Latest Release',
copyParam: 'Copy Parameters',
debug: 'Run',
debug: 'Run'
},
tip: {
publicSuccess: 'Published successfully',
@ -48,12 +48,13 @@ export default {
beautify: 'Auto-Arrange'
},
variable: {
label: 'Variable',
global: 'Global Variable',
Referencing: 'Referenced Variable',
ReferencingRequired: 'Referenced variable is required',
ReferencingError: 'Invalid referenced variable',
NoReferencing: 'Referenced variable does not exist',
fieldMessage: 'Please select a variable'
placeholder: 'Please select a variable'
},
condition: {
title: 'Execution Condition',
@ -82,12 +83,11 @@ export default {
baseNode: {
label: 'Base Information',
appName: {
label: 'App Name',
label: 'App Name'
},
appDescription: {
label: 'App Description',
},
label: 'App Description'
},
fileUpload: {
label: 'File Upload',
tooltip: 'When enabled, the Q&A page will display a file upload button.'
@ -114,7 +114,7 @@ export default {
If you want the user to see the output of this node, please turn on the switch.`
},
defaultPrompt: 'Known Information',
think: 'Thinking Process',
think: 'Thinking Process'
},
searchDatasetNode: {
label: 'Knowledge Retrieval',
@ -188,8 +188,7 @@ export default {
label: 'Form Output Content',
requiredMessage:
'Please set the output content of this node, { form } is a placeholder for the form.',
tooltip:
'Define the output content of this node. { form } is a placeholder for the form'
tooltip: 'Define the output content of this node. { form } is a placeholder for the form'
},
formAllContent: 'All Form Content',
formSetting: 'Form Configuration'
@ -212,6 +211,11 @@ export default {
requiredMessage: 'Please select an image'
}
},
variableAssignNode: {
label: 'Variable Assign',
text: 'Update the value of the global variable',
assign: 'Set Value'
},
imageGenerateNode: {
label: 'Image Generation',
text: 'Generate images based on provided text content',
@ -222,13 +226,11 @@ export default {
},
prompt: {
label: 'Positive Prompt',
tooltip:
'Describe elements and visual features you want in the generated image'
tooltip: 'Describe elements and visual features you want in the generated image'
},
negative_prompt: {
label: 'Negative Prompt',
tooltip:
'Describe elements you want to exclude from the generated image',
tooltip: 'Describe elements you want to exclude from the generated image',
placeholder:
'Please describe content you do not want to generate, such as color, bloody content'
}

View File

@ -19,7 +19,7 @@ export default {
autoSave: '自动保存',
latestRelease: '最近发布',
copyParam: '复制参数',
debug: '调试',
debug: '调试'
},
tip: {
publicSuccess: '发布成功',
@ -48,12 +48,13 @@ export default {
beautify: '一键美化'
},
variable: {
label: '变量',
global: '全局变量',
Referencing: '引用变量',
ReferencingRequired: '引用变量必填',
ReferencingError: '引用变量错误',
NoReferencing: '不存在的引用变量',
fieldMessage: '请选择变量'
placeholder: '请选择变量'
},
condition: {
title: '执行条件',
@ -83,12 +84,11 @@ export default {
baseNode: {
label: '基本信息',
appName: {
label: '应用名称',
label: '应用名称'
},
appDescription: {
label: '应用描述',
},
label: '应用描述'
},
fileUpload: {
label: '文件上传',
tooltip: '开启后,问答页面会显示上传文件的按钮。'
@ -115,7 +115,7 @@ export default {
`
},
defaultPrompt: '已知信息',
think: '思考过程',
think: '思考过程'
},
searchDatasetNode: {
label: '知识库检索',
@ -211,6 +211,11 @@ export default {
requiredMessage: '请选择图片'
}
},
variableAssignNode: {
label: '变量赋值',
text: '更新全局变量的值',
assign: '赋值'
},
imageGenerateNode: {
label: '图片生成',
text: '根据提供的文本内容生成图片',

View File

@ -48,12 +48,13 @@ export default {
beautify: '一鍵美化'
},
variable: {
label: '變量',
global: '全局變量',
Referencing: '引用變量',
ReferencingRequired: '引用變量必填',
ReferencingError: '引用變量錯誤',
NoReferencing: '不存在的引用變量',
fieldMessage: '請選擇變量'
placeholder: '請選擇變量'
},
condition: {
title: '執行條件',
@ -114,7 +115,7 @@ export default {
`
},
defaultPrompt: '已知信息',
think: '思考過程',
think: '思考過程'
},
searchDatasetNode: {
label: '知識庫檢索',
@ -210,6 +211,11 @@ export default {
requiredMessage: '請選擇圖片'
}
},
variableAssignNode: {
label: '變數賦值',
text: '更新全域變數的值',
assign: '賦值'
},
imageGenerateNode: {
label: '圖片生成',
text: '根據提供的文本內容生成圖片',

View File

@ -25,6 +25,7 @@ import { t } from '@/locales'
const props = defineProps<{
nodeModel: any
modelValue: Array<any>
global?: Boolean
}>()
const emit = defineEmits(['update:modelValue'])
const data = computed({
@ -49,7 +50,9 @@ const wheel = (e: any) => {
function visibleChange(bool: boolean) {
if (bool) {
options.value = props.nodeModel.get_up_node_field_list(false, true)
options.value = props.global
? props.nodeModel.get_up_node_field_list(false, true).filter((v) => v.value === 'global')
: props.nodeModel.get_up_node_field_list(false, true)
}
}
@ -76,7 +79,9 @@ const validate = () => {
}
defineExpose({ validate })
onMounted(() => {
options.value = props.nodeModel.get_up_node_field_list(false, true)
options.value = props.global
? props.nodeModel.get_up_node_field_list(false, true).filter((v) => v.value === 'global')
: props.nodeModel.get_up_node_field_list(false, true)
})
</script>
<style scoped></style>

View File

@ -239,6 +239,19 @@ export const imageUnderstandNode = {
}
}
export const variableAssignNode = {
type: WorkflowType.VariableAssignNode,
text: t('views.applicationWorkflow.nodes.variableAssignNode.text'),
label: t('views.applicationWorkflow.nodes.variableAssignNode.label'),
height: 252,
properties: {
stepName: t('views.applicationWorkflow.nodes.variableAssignNode.label'),
config: {
}
}
}
export const imageGenerateNode = {
type: WorkflowType.ImageGenerateNode,
text: t('views.applicationWorkflow.nodes.imageGenerateNode.text'),
@ -307,7 +320,8 @@ export const menuNodes = [
questionNode,
documentExtractNode,
speechToTextNode,
textToSpeechNode
textToSpeechNode,
variableAssignNode
]
/**
@ -400,7 +414,8 @@ export const nodeDict: any = {
[WorkflowType.ImageUnderstandNode]: imageUnderstandNode,
[WorkflowType.TextToSpeechNode]: textToSpeechNode,
[WorkflowType.SpeechToTextNode]: speechToTextNode,
[WorkflowType.ImageGenerateNode]: imageGenerateNode
[WorkflowType.ImageGenerateNode]: imageGenerateNode,
[WorkflowType.VariableAssignNode]: variableAssignNode,
}
export function isWorkFlow(type: string | undefined) {
return type === 'WORK_FLOW'

View File

@ -0,0 +1,6 @@
<template>
<AppAvatar shape="square">
<img src="@/assets/icon_assigner.svg" style="width: 65%" alt="" />
</AppAvatar>
</template>
<script setup lang="ts"></script>

View File

@ -48,7 +48,7 @@
:rules="{
type: 'array',
required: true,
message: $t('views.applicationWorkflow.variable.fieldMessage'),
message: $t('views.applicationWorkflow.variable.placeholder'),
trigger: 'change'
}"
>
@ -57,7 +57,7 @@
:nodeModel="nodeModel"
class="w-full"
:placeholder="
$t('views.applicationWorkflow.variable.fieldMessage')
$t('views.applicationWorkflow.variable.placeholder')
"
v-model="condition.field"
/>

View File

@ -45,7 +45,7 @@
:rules="{
type: 'array',
required: true,
message: $t('views.applicationWorkflow.variable.fieldMessage'),
message: $t('views.applicationWorkflow.variable.placeholder'),
trigger: 'change'
}"
>

View File

@ -0,0 +1,14 @@
import VariableAssignNodeVue from './index.vue'
import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
class VariableAssignNode extends AppNode {
constructor(props: any) {
super(props, VariableAssignNodeVue)
}
}
export default {
type: 'variable-assign-node',
model: AppNodeModel,
view: VariableAssignNode
}

View File

@ -0,0 +1,201 @@
<template>
<NodeContainer :nodeModel="nodeModel">
<el-form
@submit.prevent
:model="form_data"
label-position="top"
require-asterisk-position="right"
label-width="auto"
ref="replyNodeFormRef"
>
<template v-for="(item, index) in form_data.variable_list" :key="item.id">
<el-card shadow="never" class="card-never mb-8" style="--el-card-padding: 12px">
<el-form-item :label="$t('views.applicationWorkflow.variable.label')">
<template #label>
<div class="flex-between">
<div>
{{ $t('views.applicationWorkflow.variable.label') }}
<span class="danger">*</span>
</div>
<el-button text @click="deleteVariable(index)" v-if="index !== 0">
<el-icon>
<Delete />
</el-icon>
</el-button>
</div>
</template>
<NodeCascader
ref="nodeCascaderRef"
:nodeModel="nodeModel"
class="w-full"
:placeholder="$t('views.applicationWorkflow.variable.placeholder')"
v-model="item.fields"
:global="true"
@change="variableChange(item)"
/>
</el-form-item>
<el-form-item>
<template #label>
<div class="flex-between">
<div>
<span
>{{ $t('views.applicationWorkflow.nodes.variableAssignNode.assign')
}}<span class="danger">*</span></span
>
</div>
<el-select
:teleported="false"
v-model="item.source"
size="small"
style="width: 85px"
>
<el-option
:label="$t('views.applicationWorkflow.nodes.replyNode.replyContent.reference')"
value="referencing"
/>
<el-option
:label="$t('views.applicationWorkflow.nodes.replyNode.replyContent.custom')"
value="custom"
/>
</el-select>
</div>
</template>
<div v-if="item.source === 'custom'" class="flex">
<el-select v-model="item.type" style="width: 130px;">
<el-option v-for="item in typeOptions" :key="item" :label="item" :value="item" />
</el-select>
<el-input
class="ml-4"
v-model="item.value"
:placeholder="$t('common.inputPlaceholder')"
show-word-limit
clearable
@wheel="wheel"
></el-input>
</div>
<NodeCascader
v-else
ref="nodeCascaderRef2"
:nodeModel="nodeModel"
class="w-full"
:placeholder="$t('views.applicationWorkflow.variable.placeholder')"
v-model="item.reference"
/>
</el-form-item>
</el-card>
</template>
<el-button link type="primary" @click="addVariable">
<el-icon class="mr-4">
<Plus />
</el-icon>
{{ $t('common.add') }}
</el-button>
</el-form>
</NodeContainer>
</template>
<script setup lang="ts">
import { cloneDeep, set } from 'lodash'
import NodeContainer from '@/workflow/common/NodeContainer.vue'
import NodeCascader from '@/workflow/common/NodeCascader.vue'
import { computed, onMounted, ref } from 'vue'
import { isLastNode } from '@/workflow/common/data'
import { randomId } from '@/utils/utils'
const props = defineProps<{ nodeModel: any }>()
const typeOptions = ['string', 'int', 'dict', 'array', 'float']
const wheel = (e: any) => {
if (e.ctrlKey === true) {
e.preventDefault()
return true
} else {
e.stopPropagation()
return true
}
}
const form = {
variable_list: [
{
id: randomId(),
fields: [],
value: null,
reference: [],
type: 'string',
source: 'custom',
name: ''
}
]
}
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)
}
})
function submitDialog(val: string) {
set(props.nodeModel.properties.node_data, 'content', val)
}
const replyNodeFormRef = ref()
const nodeCascaderRef = ref()
const validate = async () => {
return Promise.all([replyNodeFormRef.value?.validate()]).catch((err: any) => {
return Promise.reject({ node: props.nodeModel, errMessage: err })
})
}
function addVariable() {
const list = cloneDeep(props.nodeModel.properties.node_data.variable_list)
const obj = {
id: randomId(),
fields: [],
value: null,
reference: [],
type: 'string',
source: 'custom',
name: ''
}
list.push(obj)
set(props.nodeModel.properties.node_data, 'variable_list', list)
}
function deleteVariable(index: number) {
const list = cloneDeep(props.nodeModel.properties.node_data.variable_list)
list.splice(index, 1)
set(props.nodeModel.properties.node_data, 'variable_list', list)
}
function variableChange(item: any) {
props.nodeModel.graphModel.nodes.map((node: any) => {
if (node.id === 'start-node') {
node.properties.config.globalFields.forEach((field: any) => {
if (field.value === item.fields[1]) {
item.name = field.label
}
})
}
})
}
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)
})
</script>
<style lang="scss" scoped></style>