feat: Support session variables (#3792)
Some checks are pending
sync2gitee / repo-sync (push) Waiting to run

This commit is contained in:
shaohuzhang1 2025-08-01 17:11:36 +08:00 committed by GitHub
parent 0e78245bfb
commit 83a1ffb891
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 404 additions and 48 deletions

View File

@ -23,6 +23,7 @@ def get_default_global_variable(input_field_list: List):
if item.get('default_value', None) is not None
}
def get_global_variable(node):
body = node.workflow_manage.get_body()
history_chat_record = node.flow_params_serializer.data.get('history_chat_record', [])
@ -74,6 +75,7 @@ class BaseStartStepNode(IStarNode):
'other': self.workflow_manage.other_list,
}
self.workflow_manage.chat_context = self.workflow_manage.get_chat_info().get_chat_variable()
return NodeResult(node_variable, workflow_variable)
def get_details(self, index: int, **kwargs):

View File

@ -2,8 +2,11 @@
import json
from typing import List
from django.db.models import QuerySet
from application.flow.i_step_node import NodeResult
from application.flow.step_node.variable_assign_node.i_variable_assign_node import IVariableAssignNode
from application.models import Chat
class BaseVariableAssignNode(IVariableAssignNode):
@ -11,40 +14,56 @@ class BaseVariableAssignNode(IVariableAssignNode):
self.context['variable_list'] = details.get('variable_list')
self.context['result_list'] = details.get('result_list')
def global_evaluation(self, variable, value):
self.workflow_manage.context[variable['fields'][1]] = value
def chat_evaluation(self, variable, value):
self.workflow_manage.chat_context[variable['fields'][1]] = value
def handle(self, variable, evaluation):
result = {
'name': variable['name'],
'input_value': self.get_reference_content(variable['fields']),
}
if variable['source'] == 'custom':
if variable['type'] == 'json':
if isinstance(variable['value'], dict) or isinstance(variable['value'], list):
val = variable['value']
else:
val = json.loads(variable['value'])
evaluation(variable, val)
result['output_value'] = variable['value'] = val
elif variable['type'] == 'string':
# 变量解析 例如:{{global.xxx}}
val = self.workflow_manage.generate_prompt(variable['value'])
evaluation(variable, val)
result['output_value'] = val
else:
val = variable['value']
evaluation(variable, val)
result['output_value'] = val
else:
reference = self.get_reference_content(variable['reference'])
evaluation(variable, reference)
result['output_value'] = reference
return result
def execute(self, variable_list, stream, **kwargs) -> NodeResult:
#
result_list = []
is_chat = False
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'] == 'json':
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
elif variable['type'] == 'string':
# 变量解析 例如:{{global.xxx}}
val = self.workflow_manage.generate_prompt(variable['value'])
self.workflow_manage.context[variable['fields'][1]] = val
result['output_value'] = val
else:
val = variable['value']
self.workflow_manage.context[variable['fields'][1]] = val
result['output_value'] = val
else:
reference = self.get_reference_content(variable['reference'])
self.workflow_manage.context[variable['fields'][1]] = reference
result['output_value'] = reference
result = self.handle(variable, self.global_evaluation)
result_list.append(result)
if 'chat' == variable['fields'][0]:
result = self.handle(variable, self.chat_evaluation)
result_list.append(result)
is_chat = True
if is_chat:
self.workflow_manage.get_chat_info().set_chat_variable(self.workflow_manage.chat_context)
return NodeResult({'variable_list': variable_list, 'result_list': result_list}, {})
def get_reference_content(self, fields: List[str]):

View File

@ -117,6 +117,7 @@ class WorkflowManage:
self.params = params
self.flow = flow
self.context = {}
self.chat_context = {}
self.node_chunk_manage = NodeChunkManage(self)
self.work_flow_post_handler = work_flow_post_handler
self.current_node = None
@ -131,6 +132,7 @@ class WorkflowManage:
self.lock = threading.Lock()
self.field_list = []
self.global_field_list = []
self.chat_field_list = []
self.init_fields()
if start_node_id is not None:
self.load_node(chat_record, start_node_id, start_node_data)
@ -140,6 +142,7 @@ class WorkflowManage:
def init_fields(self):
field_list = []
global_field_list = []
chat_field_list = []
for node in self.flow.nodes:
properties = node.properties
node_name = properties.get('stepName')
@ -154,10 +157,16 @@ class WorkflowManage:
if global_fields is not None:
for global_field in global_fields:
global_field_list.append({**global_field, 'node_id': node_id, 'node_name': node_name})
chat_fields = node_config.get('chatFields')
if chat_fields is not None:
for chat_field in chat_fields:
chat_field_list.append({**chat_field, 'node_id': node_id, 'node_name': node_name})
field_list.sort(key=lambda f: len(f.get('node_name') + f.get('value')), reverse=True)
global_field_list.sort(key=lambda f: len(f.get('node_name') + f.get('value')), reverse=True)
chat_field_list.sort(key=lambda f: len(f.get('node_name') + f.get('value')), reverse=True)
self.field_list = field_list
self.global_field_list = global_field_list
self.chat_field_list = chat_field_list
def append_answer(self, content):
self.answer += content
@ -445,6 +454,9 @@ class WorkflowManage:
return current_node.node_params.get('is_result', not self._has_next_node(
current_node, current_node_result)) if current_node.node_params is not None else False
def get_chat_info(self):
return self.work_flow_post_handler.chat_info
def get_chunk_content(self, chunk, is_end=False):
return 'data: ' + json.dumps(
{'chat_id': self.params['chat_id'], 'id': self.params['chat_record_id'], 'operate': True,
@ -587,12 +599,15 @@ class WorkflowManage:
"""
if node_id == 'global':
return INode.get_field(self.context, fields)
elif node_id == 'chat':
return INode.get_field(self.chat_context, fields)
else:
return self.get_node_by_id(node_id).get_reference_field(fields)
def get_workflow_content(self):
context = {
'global': self.context,
'chat': self.chat_context
}
for node in self.node_context:
@ -610,6 +625,10 @@ class WorkflowManage:
globeLabelNew = f"global.{field.get('value')}"
globeValue = f"context.get('global').get('{field.get('value', '')}','')"
prompt = prompt.replace(globeLabel, globeValue).replace(globeLabelNew, globeValue)
for field in self.chat_field_list:
chatLabel = f"chat.{field.get('value')}"
chatValue = f"context.get('chat').get('{field.get('value', '')}','')"
prompt = prompt.replace(chatLabel, chatValue)
return prompt
def generate_prompt(self, prompt: str):

View File

@ -166,6 +166,34 @@ class ChatInfo:
'exclude_paragraph_id_list': exclude_paragraph_id_list, 'stream': stream, 'chat_user_id': chat_user_id,
'chat_user_type': chat_user_type, 'form_data': form_data}
def set_chat(self, question):
if not self.debug:
if not QuerySet(Chat).filter(id=self.chat_id).exists():
Chat(id=self.chat_id, application_id=self.application_id, abstract=question[0:1024],
chat_user_id=self.chat_user_id, chat_user_type=self.chat_user_type,
asker=self.get_chat_user()).save()
def set_chat_variable(self, chat_context):
if not self.debug:
chat = QuerySet(Chat).filter(id=self.chat_id).first()
if chat:
chat.meta = {**(chat.meta if isinstance(chat.meta, dict) else {}), **chat_context}
chat.save()
else:
cache.set(Cache_Version.CHAT_VARIABLE.get_key(key=self.chat_id), chat_context,
version=Cache_Version.CHAT_VARIABLE.get_version(),
timeout=60 * 30)
def get_chat_variable(self):
if not self.debug:
chat = QuerySet(Chat).filter(id=self.chat_id).first()
if chat:
return chat.meta
return {}
else:
return cache.get(Cache_Version.CHAT_VARIABLE.get_key(key=self.chat_id),
version=Cache_Version.CHAT_VARIABLE.get_version()) or {}
def append_chat_record(self, chat_record: ChatRecord):
chat_record.problem_text = chat_record.problem_text[0:10240] if chat_record.problem_text is not None else ""
chat_record.answer_text = chat_record.answer_text[0:40960] if chat_record.problem_text is not None else ""

View File

@ -253,6 +253,7 @@ class ChatSerializers(serializers.Serializer):
# 构建运行参数
params = chat_info.to_pipeline_manage_params(message, get_post_handler(chat_info), exclude_paragraph_id_list,
chat_user_id, chat_user_type, stream, form_data)
chat_info.set_chat(message)
# 运行流水线作业
pipeline_message.run(params)
return pipeline_message.context['chat_result']
@ -307,6 +308,7 @@ class ChatSerializers(serializers.Serializer):
other_list,
instance.get('runtime_node_id'),
instance.get('node_data'), chat_record, instance.get('child_node'))
chat_info.set_chat(message)
r = work_flow_manage.run()
return r

View File

@ -29,6 +29,9 @@ class Cache_Version(Enum):
# 对话
CHAT = "CHAT", lambda key: key
CHAT_VARIABLE = "CHAT_VARIABLE", lambda key: key
# 应用API KEY
APPLICATION_API_KEY = "APPLICATION_API_KEY", lambda secret_key, use_get_data: secret_key

View File

@ -52,6 +52,7 @@ export default {
variable: {
label: '变量',
global: '全局变量',
chat: '会话变量',
Referencing: '引用变量',
ReferencingRequired: '引用变量必填',
ReferencingError: '引用变量错误',

View File

@ -51,7 +51,9 @@ const wheel = (e: any) => {
function visibleChange(bool: boolean) {
if (bool) {
options.value = props.global
? props.nodeModel.get_up_node_field_list(false, true).filter((v: any) => v.value === 'global')
? props.nodeModel
.get_up_node_field_list(false, true)
.filter((v: any) => ['global', 'chat'].includes(v.value))
: props.nodeModel.get_up_node_field_list(false, true)
}
}

View File

@ -34,7 +34,7 @@ class AppNode extends HtmlResize.view {
} else {
const filterNodes = props.graphModel.nodes.filter((v: any) => v.type === props.model.type)
const filterNameSameNodes = filterNodes.filter(
(v: any) => v.properties.stepName === props.model.properties.stepName
(v: any) => v.properties.stepName === props.model.properties.stepName,
)
if (filterNameSameNodes.length - 1 > 0) {
getNodesName(filterNameSameNodes.length - 1)
@ -61,14 +61,20 @@ class AppNode extends HtmlResize.view {
value: 'global',
label: t('views.applicationWorkflow.variable.global'),
type: 'global',
children: this.props.model.properties?.config?.globalFields || []
children: this.props.model.properties?.config?.globalFields || [],
})
result.push({
value: 'chat',
label: t('views.applicationWorkflow.variable.chat'),
type: 'chat',
children: this.props.model.properties?.config?.chatFields || [],
})
}
result.push({
value: this.props.model.id,
label: this.props.model.properties.stepName,
type: this.props.model.type,
children: this.props.model.properties?.config?.fields || []
children: this.props.model.properties?.config?.fields || [],
})
return result
}
@ -83,7 +89,7 @@ class AppNode extends HtmlResize.view {
if (contain_self) {
return {
...this.up_node_field_dict,
[this.props.model.id]: this.get_node_field_list()
[this.props.model.id]: this.get_node_field_list(),
}
}
return this.up_node_field_dict ? this.up_node_field_dict : {}
@ -92,7 +98,7 @@ class AppNode extends HtmlResize.view {
get_up_node_field_list(contain_self: boolean, use_cache: boolean) {
const result = Object.values(this.get_up_node_field_dict(contain_self, use_cache)).reduce(
(pre, next) => [...pre, ...next],
[]
[],
)
const start_node_field_list = this.props.graphModel
.getNodeModelById('start-node')
@ -126,7 +132,7 @@ class AppNode extends HtmlResize.view {
x: x - 10,
y: y - 12,
width: 30,
height: 30
height: 30,
},
[
lh('div', {
@ -174,10 +180,10 @@ class AppNode extends HtmlResize.view {
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_5199_166905" result="shape"/>
</filter>
</defs>
</svg>`
}
})
]
</svg>`,
},
}),
],
)
}
@ -214,7 +220,7 @@ class AppNode extends HtmlResize.view {
} else {
this.r = h(this.component, {
properties: this.props.model.properties,
nodeModel: this.props.model
nodeModel: this.props.model,
})
this.app = createApp({
render() {
@ -223,13 +229,13 @@ class AppNode extends HtmlResize.view {
provide() {
return {
getNode: () => model,
getGraph: () => graphModel
getGraph: () => graphModel,
}
}
},
})
this.app.use(ElementPlus, {
locale: zhCn
locale: zhCn,
})
this.app.use(Components)
this.app.use(directives)
@ -295,7 +301,7 @@ class AppNodeModel extends HtmlResize.model {
}
getNodeStyle() {
return {
overflow: 'visible'
overflow: 'visible',
}
}
getOutlineStyle() {
@ -361,13 +367,13 @@ class AppNodeModel extends HtmlResize.model {
message: t('views.applicationWorkflow.tip.onlyRight'),
validate: (sourceNode: any, targetNode: any, sourceAnchor: any) => {
return sourceAnchor.type === 'right'
}
},
}
this.sourceRules.push({
message: t('views.applicationWorkflow.tip.notRecyclable'),
validate: (sourceNode: any, targetNode: any, sourceAnchor: any, targetAnchor: any) => {
return !isLoop(sourceNode.id, targetNode.id)
}
},
})
this.sourceRules.push(circleOnlyAsTarget)
@ -375,7 +381,7 @@ class AppNodeModel extends HtmlResize.model {
message: t('views.applicationWorkflow.tip.onlyLeft'),
validate: (sourceNode: any, targetNode: any, sourceAnchor: any, targetAnchor: any) => {
return targetAnchor.type === 'left'
}
},
})
}
getDefaultAnchor() {
@ -390,14 +396,14 @@ class AppNodeModel extends HtmlResize.model {
y: showNode ? y : y - 15,
id: `${id}_left`,
edgeAddable: false,
type: 'left'
type: 'left',
})
}
anchors.push({
x: x + width / 2 - 10,
y: showNode ? y : y - 15,
id: `${id}_right`,
type: 'right'
type: 'right',
})
}

View File

@ -0,0 +1,4 @@
<template>
<img src="@/assets/workflow/icon_globe_color.svg" style="width: 18px" alt="" />
</template>
<script setup lang="ts"></script>

View File

@ -0,0 +1,118 @@
<template>
<el-dialog
:title="isEdit ? $t('common.param.editParam') : $t('common.param.addParam')"
v-model="dialogVisible"
:close-on-click-modal="false"
:close-on-press-escape="false"
:destroy-on-close="true"
:before-close="close"
append-to-body
>
<el-form
label-position="top"
ref="fieldFormRef"
:rules="rules"
:model="form"
require-asterisk-position="right"
>
<el-form-item
:label="$t('dynamicsForm.paramForm.field.label')"
:required="true"
prop="field"
:rules="rules.field"
>
<el-input
v-model="form.field"
:maxlength="64"
:placeholder="$t('dynamicsForm.paramForm.field.placeholder')"
show-word-limit
/>
</el-form-item>
<el-form-item
:label="$t('dynamicsForm.paramForm.name.label')"
:required="true"
prop="label"
:rules="rules.label"
>
<el-input
v-model="form.label"
:maxlength="64"
show-word-limit
:placeholder="$t('dynamicsForm.paramForm.name.placeholder')"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click.prevent="dialogVisible = false"> {{ $t('common.cancel') }} </el-button>
<el-button type="primary" @click="submit(fieldFormRef)" :loading="loading">
{{ isEdit ? $t('common.save') : $t('common.add') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import type { FormInstance } from 'element-plus'
import { cloneDeep } from 'lodash'
import { t } from '@/locales'
const emit = defineEmits(['refresh'])
const fieldFormRef = ref()
const loading = ref<boolean>(false)
const isEdit = ref(false)
const currentIndex = ref(null)
const form = ref<any>({
field: '',
label: '',
})
const rules = reactive({
label: [
{ required: true, message: t('dynamicsForm.paramForm.name.requiredMessage'), trigger: 'blur' },
],
field: [
{ required: true, message: t('dynamicsForm.paramForm.field.requiredMessage'), trigger: 'blur' },
{
pattern: /^[a-zA-Z0-9_]+$/,
message: t('dynamicsForm.paramForm.field.requiredMessage2'),
trigger: 'blur',
},
],
})
const dialogVisible = ref<boolean>(false)
const open = (row: any, index?: any) => {
if (row) {
form.value = cloneDeep(row)
isEdit.value = true
currentIndex.value = index
}
dialogVisible.value = true
}
const close = () => {
dialogVisible.value = false
isEdit.value = false
currentIndex.value = null
form.value = {
field: '',
label: '',
}
}
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid) => {
if (valid) {
emit('refresh', form.value, currentIndex.value)
}
})
}
defineExpose({ open, close })
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,110 @@
<template>
<div class="flex-between mb-16">
<h5 class="break-all ellipsis lighter" style="max-width: 80%">
{{ $t('views.applicationWorkflow.variable.chat') }}
</h5>
<div>
<span class="ml-4">
<el-button link type="primary" @click="openAddDialog()">
<el-icon class="mr-4">
<Plus />
</el-icon>
{{ $t('common.add') }}
</el-button>
</span>
</div>
</div>
<el-table
v-if="props.nodeModel.properties.chat_input_field_list?.length > 0"
:data="props.nodeModel.properties.chat_input_field_list"
class="mb-16"
ref="tableRef"
row-key="field"
>
<el-table-column prop="field" :label="$t('dynamicsForm.paramForm.field.label')" width="95">
<template #default="{ row }">
<span :title="row.field" class="ellipsis-1">{{ row.field }}</span>
</template>
</el-table-column>
<el-table-column prop="label" :label="$t('dynamicsForm.paramForm.name.label')">
<template #default="{ row }">
<span>
<span :title="row.label" class="ellipsis-1">
{{ row.label }}
</span></span
>
</template>
</el-table-column>
<el-table-column :label="$t('common.operation')" align="left" width="90">
<template #default="{ row, $index }">
<span class="mr-4">
<el-tooltip effect="dark" :content="$t('common.modify')" placement="top">
<el-button type="primary" text @click.stop="openAddDialog(row, $index)">
<el-icon><EditPen /></el-icon>
</el-button>
</el-tooltip>
</span>
<el-tooltip effect="dark" :content="$t('common.delete')" placement="top">
<el-button type="primary" text @click="deleteField($index)">
<el-icon>
<Delete />
</el-icon>
</el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<ChatFieldDialog ref="ChatFieldDialogRef" @refresh="refreshFieldList"></ChatFieldDialog>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { set, cloneDeep } from 'lodash'
import ChatFieldDialog from './ChatFieldDialog.vue'
import { MsgError } from '@/utils/message'
import { t } from '@/locales'
const props = defineProps<{ nodeModel: any }>()
const tableRef = ref()
const ChatFieldDialogRef = ref()
const inputFieldList = ref<any[]>([])
function openAddDialog(data?: any, index?: any) {
ChatFieldDialogRef.value.open(data, index)
}
function deleteField(index: any) {
inputFieldList.value.splice(index, 1)
props.nodeModel.graphModel.eventCenter.emit('chatFieldList')
}
function refreshFieldList(data: any, index: any) {
for (let i = 0; i < inputFieldList.value.length; i++) {
if (inputFieldList.value[i].field === data.field && index !== i) {
MsgError(t('views.applicationWorkflow.tip.paramErrorMessage') + data.field)
return
}
}
console.log(index)
if (index) {
inputFieldList.value.splice(index, 1, data)
} else {
inputFieldList.value.push(data)
}
ChatFieldDialogRef.value.close()
props.nodeModel.graphModel.eventCenter.emit('chatFieldList')
}
onMounted(() => {
if (props.nodeModel.properties.chat_input_field_list) {
inputFieldList.value = cloneDeep(props.nodeModel.properties.chat_input_field_list)
}
set(props.nodeModel.properties, 'chat_input_field_list', inputFieldList)
})
</script>
<style scoped lang="scss"></style>

View File

@ -83,6 +83,7 @@
</el-form-item>
<UserInputFieldTable ref="UserInputFieldTableFef" :node-model="nodeModel" />
<ApiInputFieldTable ref="ApiInputFieldTableFef" :node-model="nodeModel" />
<ChatFieldTable ref="ChatFieldTeble" :node-model="nodeModel"></ChatFieldTable>
<el-form-item>
<template #label>
<div class="flex-between">
@ -177,6 +178,7 @@ import TTSModeParamSettingDialog from '@/views/application/component/TTSModePara
import ApiInputFieldTable from './component/ApiInputFieldTable.vue'
import UserInputFieldTable from './component/UserInputFieldTable.vue'
import FileUploadSettingDialog from '@/workflow/nodes/base-node/component/FileUploadSettingDialog.vue'
import ChatFieldTable from './component/ChatFieldTable.vue'
import { useRoute } from 'vue-router'
import { loadSharedApi } from '@/utils/dynamics-api/shared-api'
const getApplicationDetail = inject('getApplicationDetail') as any

View File

@ -20,6 +20,30 @@
</el-button>
</el-tooltip>
</div>
<template v-if="nodeModel.properties.config.chatFields">
<h5 class="title-decoration-1 mb-8">{{ $t('views.applicationWorkflow.variable.chat') }}</h5>
<div
v-for="(item, index) in nodeModel.properties.config.chatFields
? nodeModel.properties.config.chatFields
: []"
:key="index"
class="flex-between border-r-6 p-8-12 mb-8 layout-bg lighter"
@mouseenter="showicon = true"
@mouseleave="showicon = false"
>
<span class="break-all">{{ item.label }} {{ '{' + item.value + '}' }}</span>
<el-tooltip
effect="dark"
:content="$t('views.applicationWorkflow.setting.copyParam')"
placement="top"
v-if="showicon === true"
>
<el-button link @click="copyClick(`{{chat.${item.value}}}`)" style="padding: 0">
<AppIcon iconName="app-copy"></AppIcon>
</el-button>
</el-tooltip>
</div>
</template>
</NodeContainer>
</template>
<script setup lang="ts">
@ -75,8 +99,18 @@ const refreshFieldList = () => {
const refreshFieldList = getRefreshFieldList()
set(props.nodeModel.properties.config, 'globalFields', [...globalFields, ...refreshFieldList])
}
props.nodeModel.graphModel.eventCenter.on('refreshFieldList', refreshFieldList)
const refreshChatFieldList = () => {
const chatFieldList = props.nodeModel.graphModel.nodes
.filter((v: any) => v.id === 'base-node')
.map((v: any) => cloneDeep(v.properties.chat_input_field_list))
.reduce((x: any, y: any) => [...x, ...y], [])
.map((i: any) => ({ label: i.label, value: i.field }))
set(props.nodeModel.properties.config, 'chatFields', chatFieldList)
}
props.nodeModel.graphModel.eventCenter.on('refreshFieldList', refreshFieldList)
props.nodeModel.graphModel.eventCenter.on('chatFieldList', refreshChatFieldList)
const refreshFileUploadConfig = () => {
let fields = cloneDeep(props.nodeModel.properties.config.fields)
const form_data = props.nodeModel.graphModel.nodes
@ -120,6 +154,7 @@ const refreshFileUploadConfig = () => {
props.nodeModel.graphModel.eventCenter.on('refreshFileUploadConfig', refreshFileUploadConfig)
onMounted(() => {
refreshChatFieldList()
refreshFieldList()
refreshFileUploadConfig()
})

View File

@ -245,6 +245,11 @@ function variableChange(item: any) {
item.name = field.label
}
})
node.properties.config.chatFields.forEach((field: any) => {
if (field.value === item.fields[1]) {
item.name = field.label
}
})
}
})
}