feat: Support parameter extraction nodes (#4208)

v3.2
shaohuzhang1 2025-10-17 15:18:33 +08:00 committed by GitHub
parent a4330e30a8
commit c25a950650
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 823 additions and 23 deletions

View File

@ -21,6 +21,7 @@ from .loop_continue_node import BaseLoopContinueNode
from .loop_node import *
from .loop_start_node import *
from .mcp_node import BaseMcpNode
from .parameter_extraction_node import BaseParameterExtractionNode
from .question_node import *
from .reranker_node import *
from .search_document_node import BaseSearchDocumentNode
@ -44,7 +45,7 @@ node_list = [BaseStartStepNode, BaseChatNode, BaseSearchKnowledgeNode, BaseSearc
BaseVideoUnderstandNode,
BaseIntentNode, BaseLoopNode, BaseLoopStartStepNode,
BaseLoopContinueNode,
BaseLoopBreakNode, BaseVariableSplittingNode]
BaseLoopBreakNode, BaseVariableSplittingNode, BaseParameterExtractionNode]
def get_node(node_type):

View File

@ -0,0 +1,9 @@
# coding=utf-8
"""
@project: MaxKB
@Author虎虎
@file __init__.py.py
@date2025/10/13 14:56
@desc:
"""
from .impl import *

View File

@ -0,0 +1,39 @@
# 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"))
model_params_setting = serializers.DictField(required=False,
label=_("Model parameter settings"))
model_id = serializers.CharField(required=True, label=_("Model id"))
class IParameterExtractionNode(INode):
type = 'parameter-extraction-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'],
self.node_params_serializer.data['model_params_setting'],
self.node_params_serializer.data['model_id'])
def execute(self, input_variable, variable_list, model_params_setting, model_id, **kwargs) -> NodeResult:
pass

View File

@ -0,0 +1,9 @@
# coding=utf-8
"""
@project: MaxKB
@Author虎虎
@file __init__.py.py
@date2025/10/13 15:01
@desc:
"""
from .base_parameter_extraction_node import *

View File

@ -0,0 +1,112 @@
# coding=utf-8
"""
@project: MaxKB
@Author虎虎
@file base_variable_splitting_node.py
@date2025/10/13 15:02
@desc:
"""
import json
import re
from django.db.models import QuerySet
from langchain_core.messages import HumanMessage
from langchain_core.prompts import PromptTemplate
from application.flow.i_step_node import NodeResult
from application.flow.step_node.parameter_extraction_node.i_parameter_extraction_node import IParameterExtractionNode
from models_provider.models import Model
from models_provider.tools import get_model_instance_by_model_workspace_id, get_model_credential
prompt = """
Please strictly process the text according to the following requirements:
**Task**:
Extract specified field information from given text
**Enter text**:
{{question}}
**Extract configuration**:
{{properties}}
**Rule**:
- Strictly follow the data and field of Extract configuration
- If not found, use null value
- Only return pure JSON without additional text
- Keep the string format neat
"""
def get_default_model_params_setting(model_id):
model = QuerySet(Model).filter(id=model_id).first()
credential = get_model_credential(model.provider, model.model_type, model.model_name)
model_params_setting = credential.get_model_params_setting_form(
model.model_name).get_default_form_data()
return model_params_setting
def generate_properties(variable_list):
return {variable['field']: {'type': variable['parameter_type'], 'description': variable['desc'],
'title': variable['label']} for variable in
variable_list}
def generate_example(variable_list):
return {variable['field']: None for variable in variable_list}
def generate_content(input_variable, variable_list):
properties = generate_properties(variable_list)
prompt_template = PromptTemplate.from_template(prompt, template_format='jinja2')
value = prompt_template.format(properties=properties, question=input_variable)
return value
def json_loads(response, expected_fields):
if not response or not isinstance(response, str):
return {field: None for field in expected_fields}
cleaned = response.strip()
extraction_strategies = [
lambda: json.loads(cleaned),
lambda: json.loads(re.search(r'```(?:json)?\s*(\{.*?\})\s*```', cleaned, re.DOTALL).group(1)),
lambda: json.loads(re.search(r'(\{[\s\S]*\})', cleaned).group(1)),
]
for strategy in extraction_strategies:
try:
result = strategy()
return result
except:
continue
return generate_example(expected_fields)
class BaseParameterExtractionNode(IParameterExtractionNode):
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, model_params_setting, model_id, **kwargs) -> NodeResult:
if model_params_setting is None:
model_params_setting = get_default_model_params_setting(model_id)
workspace_id = self.workflow_manage.get_body().get('workspace_id')
chat_model = get_model_instance_by_model_workspace_id(model_id, workspace_id,
**model_params_setting)
content = generate_content(input_variable, variable_list)
response = chat_model.invoke([HumanMessage(content=content)])
result = json_loads(response.content, variable_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

@ -0,0 +1,11 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_5700_5715)">
<path d="M0 10C0 5.28595 0 2.92893 1.46447 1.46447C2.92893 0 5.28595 0 10 0C14.714 0 17.0711 0 18.5355 1.46447C20 2.92893 20 5.28595 20 10C20 14.714 20 17.0711 18.5355 18.5355C17.0711 20 14.714 20 10 20C5.28595 20 2.92893 20 1.46447 18.5355C0 17.0711 0 14.714 0 10Z" fill="#3370FF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.79163 6.61442C4.79163 5.60765 5.60777 4.7915 6.61454 4.7915C7.44032 4.7915 8.13787 5.3406 8.36197 6.09359H14.9479C15.0917 6.09359 15.2083 6.21018 15.2083 6.354V6.87484C15.2083 7.01866 15.0917 7.13525 14.9479 7.13525H8.36197C8.13787 7.88824 7.44032 8.43734 6.61454 8.43734C5.60777 8.43734 4.79163 7.62118 4.79163 6.61442ZM5.05204 10.5207C4.90822 10.5207 4.79163 10.4041 4.79163 10.2603V9.73942C4.79163 9.5956 4.90822 9.479 5.05204 9.479H8.25253C8.47664 8.72602 9.17418 8.17692 9.99996 8.17692C10.8257 8.17692 11.5233 8.72602 11.7474 9.479H14.9479C15.0917 9.479 15.2083 9.5956 15.2083 9.73942V10.2603C15.2083 10.4041 15.0917 10.5207 14.9479 10.5207H11.7474C11.5233 11.2737 10.8257 11.8228 9.99996 11.8228C9.17418 11.8228 8.47664 11.2737 8.25253 10.5207H5.05204ZM9.99996 10.7811C10.4314 10.7811 10.7812 10.4313 10.7812 9.99984C10.7812 9.56837 10.4314 9.21859 9.99996 9.21859C9.56849 9.21859 9.21871 9.56837 9.21871 9.99984C9.21871 10.4313 9.56849 10.7811 9.99996 10.7811ZM5.05204 13.9061C4.90822 13.9061 4.79163 13.7895 4.79163 13.6457V13.1248C4.79163 12.981 4.90822 12.8644 5.05204 12.8644H11.6379C11.8621 12.1114 12.5596 11.5623 13.3854 11.5623C14.3921 11.5623 15.2083 12.3785 15.2083 13.3853C15.2083 14.392 14.3921 15.2082 13.3854 15.2082C12.5596 15.2082 11.8621 14.6591 11.6379 13.9061H5.05204ZM13.3854 12.604C12.9539 12.604 12.6041 12.9538 12.6041 13.3853C12.6041 13.8167 12.9539 14.1665 13.3854 14.1665C13.8168 14.1665 14.1666 13.8167 14.1666 13.3853C14.1666 12.9538 13.8168 12.604 13.3854 12.604ZM6.61454 7.39567C7.04601 7.39567 7.39579 7.04589 7.39579 6.61442C7.39579 6.18295 7.04601 5.83317 6.61454 5.83317C6.18307 5.83317 5.83329 6.18295 5.83329 6.61442C5.83329 7.04589 6.18307 7.39567 6.61454 7.39567Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_5700_5715">
<rect width="20" height="20" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -35,6 +35,7 @@ export enum WorkflowType {
LoopBreakNode = 'loop-break-node',
VariableSplittingNode = 'variable-splitting-node',
VideoUnderstandNode = 'video-understand-node',
ParameterExtractionNode = 'parameter-extraction-node',
}
export enum WorkflowMode {
// 应用工作流

View File

@ -429,6 +429,20 @@ You are a master of problem optimization, adept at accurately inferring user int
placeholder: 'Please enter expression',
},
},
parameterExtractionNode: {
label: '參數提取',
text: '利用 AI 模型提取結構化參數',
result: '結果',
selectVariables: {
label: '選擇變數',
placeholder: '請選擇變數',
},
extractParameters: {
label: '提取參數',
desc: '描述',
parameterType: '參數類型',
},
},
},
compare: {
is_null: 'Is null',

View File

@ -442,6 +442,20 @@ export default {
placeholder: '请输入表达式',
},
},
parameterExtractionNode: {
label: '参数提取',
text: '利用 AI 模型提取结构化参数',
result: '结果',
selectVariables: {
label: '选择变量',
placeholder: '请选择变量',
},
extractParameters: {
label: '提取参数',
desc: '描述',
parameterType: '参数类型',
},
},
},
compare: {
is_null: '为空',

View File

@ -415,6 +415,20 @@ export default {
placeholder: '請輸入表達式',
},
},
parameterExtractionNode: {
label: 'Parameter Extraction',
text: 'Extract structured parameters using AI model',
result: 'Result',
selectVariables: {
label: 'Select Variables',
placeholder: 'Please select variables',
},
extractParameters: {
label: 'Extract Parameters',
desc: 'Description',
parameterType: 'Parameter Type',
},
},
},
compare: {
is_null: '為空',

View File

@ -17,7 +17,7 @@ export const startNode = {
},
],
globalFields: [
{label: t('views.applicationWorkflow.nodes.startNode.currentTime'), value: 'time'},
{ label: t('views.applicationWorkflow.nodes.startNode.currentTime'), value: 'time' },
{
label: t('views.application.form.historyRecord.label'),
value: 'history_context',
@ -28,9 +28,9 @@ export const startNode = {
},
],
},
fields: [{label: t('views.applicationWorkflow.nodes.startNode.question'), value: 'question'}],
fields: [{ label: t('views.applicationWorkflow.nodes.startNode.question'), value: 'question' }],
globalFields: [
{label: t('views.applicationWorkflow.nodes.startNode.currentTime'), value: 'time'},
{ label: t('views.applicationWorkflow.nodes.startNode.currentTime'), value: 'time' },
],
showNode: true,
},
@ -53,7 +53,7 @@ export const baseNode = {
},
config: {},
showNode: true,
user_input_config: {title: t('chat.userInput')},
user_input_config: { title: t('chat.userInput') },
user_input_field_list: [],
},
}
@ -181,6 +181,24 @@ export const variableSplittingNode = {
},
}
export const parameterExtractionNode = {
type: WorkflowType.ParameterExtractionNode,
text: t('views.applicationWorkflow.nodes.parameterExtractionNode.text', '变量拆分'),
label: t('views.applicationWorkflow.nodes.parameterExtractionNode.label', '变量拆分'),
height: 345,
properties: {
stepName: t('views.applicationWorkflow.nodes.parameterExtractionNode.label', '变量拆分'),
config: {
fields: [
{
label: t('views.applicationWorkflow.nodes.parameterExtractionNode.result', '结果'),
value: 'result',
},
],
},
},
}
export const conditionNode = {
type: WorkflowType.Condition,
text: t('views.applicationWorkflow.nodes.conditionNode.text'),
@ -315,7 +333,6 @@ export const videoUnderstandNode = {
},
}
export const variableAssignNode = {
type: WorkflowType.VariableAssignNode,
text: t('views.applicationWorkflow.nodes.variableAssignNode.text'),
@ -609,7 +626,7 @@ export const menuNodes = [
},
{
label: t('views.applicationWorkflow.nodes.classify.dataProcessing', '数据处理'),
list: [variableSplittingNode],
list: [variableSplittingNode, parameterExtractionNode],
},
{
label: t('views.applicationWorkflow.nodes.classify.other'),
@ -699,22 +716,22 @@ export const applicationNode = {
}
export const compareList = [
{value: 'is_null', label: t('views.applicationWorkflow.compare.is_null')},
{value: 'is_not_null', label: t('views.applicationWorkflow.compare.is_not_null')},
{value: 'contain', label: t('views.applicationWorkflow.compare.contain')},
{value: 'not_contain', label: t('views.applicationWorkflow.compare.not_contain')},
{value: 'eq', label: t('views.applicationWorkflow.compare.eq')},
{value: 'ge', label: t('views.applicationWorkflow.compare.ge')},
{value: 'gt', label: t('views.applicationWorkflow.compare.gt')},
{value: 'le', label: t('views.applicationWorkflow.compare.le')},
{value: 'lt', label: t('views.applicationWorkflow.compare.lt')},
{value: 'len_eq', label: t('views.applicationWorkflow.compare.len_eq')},
{value: 'len_ge', label: t('views.applicationWorkflow.compare.len_ge')},
{value: 'len_gt', label: t('views.applicationWorkflow.compare.len_gt')},
{value: 'len_le', label: t('views.applicationWorkflow.compare.len_le')},
{value: 'len_lt', label: t('views.applicationWorkflow.compare.len_lt')},
{value: 'is_true', label: t('views.applicationWorkflow.compare.is_true')},
{value: 'is_not_true', label: t('views.applicationWorkflow.compare.is_not_true')},
{ value: 'is_null', label: t('views.applicationWorkflow.compare.is_null') },
{ value: 'is_not_null', label: t('views.applicationWorkflow.compare.is_not_null') },
{ value: 'contain', label: t('views.applicationWorkflow.compare.contain') },
{ value: 'not_contain', label: t('views.applicationWorkflow.compare.not_contain') },
{ value: 'eq', label: t('views.applicationWorkflow.compare.eq') },
{ value: 'ge', label: t('views.applicationWorkflow.compare.ge') },
{ value: 'gt', label: t('views.applicationWorkflow.compare.gt') },
{ value: 'le', label: t('views.applicationWorkflow.compare.le') },
{ value: 'lt', label: t('views.applicationWorkflow.compare.lt') },
{ value: 'len_eq', label: t('views.applicationWorkflow.compare.len_eq') },
{ value: 'len_ge', label: t('views.applicationWorkflow.compare.len_ge') },
{ value: 'len_gt', label: t('views.applicationWorkflow.compare.len_gt') },
{ value: 'len_le', label: t('views.applicationWorkflow.compare.len_le') },
{ value: 'len_lt', label: t('views.applicationWorkflow.compare.len_lt') },
{ value: 'is_true', label: t('views.applicationWorkflow.compare.is_true') },
{ value: 'is_not_true', label: t('views.applicationWorkflow.compare.is_not_true') },
]
export const nodeDict: any = {
@ -748,6 +765,7 @@ export const nodeDict: any = {
[WorkflowType.LoopContinueNode]: loopContinueNode,
[WorkflowType.VariableSplittingNode]: variableSplittingNode,
[WorkflowType.VideoUnderstandNode]: videoUnderstandNode,
[WorkflowType.ParameterExtractionNode]: parameterExtractionNode,
}
export function isWorkFlow(type: string | undefined) {

View File

@ -0,0 +1,6 @@
<template>
<el-avatar shape="square" class="avatar-blue">
<img src="@/assets/workflow/icon_parameter_extraction.svg" style="width: 100%" alt="" />
</el-avatar>
</template>
<script setup lang="ts"></script>

View File

@ -0,0 +1,192 @@
<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-item
:label="
$t(
'views.applicationWorkflow.nodes.parameterExtractionNode.extractParameters.parameterType',
)
"
:required="true"
prop="parameter_type"
:rules="rules.label"
>
<el-select
v-model="form.parameter_type"
:placeholder="
$t('common.selectPlaceholder') +
$t(
'views.applicationWorkflow.nodes.parameterExtractionNode.extractParameters.parameterType',
)
"
style="width: 100%"
>
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item
:label="
$t(
'views.applicationWorkflow.nodes.parameterExtractionNode.extractParameters.desc',
'描述',
)
"
prop="desc"
>
<el-input
v-model="form.desc"
style="width: 100%"
:rows="2"
type="textarea"
:placeholder="
$t('common.inputPlaceholder') +
$t(
'views.applicationWorkflow.nodes.parameterExtractionNode.extractParameters.desc',
'描述',
)
"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click.prevent="close"> {{ $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 options = [
{
value: 'string',
label: 'String',
},
{
value: 'number',
label: 'Number',
},
{
value: 'object',
label: 'Object',
},
{
value: 'boolean',
label: 'Boolean',
},
{
value: 'array',
label: 'Array',
},
]
const fieldFormRef = ref()
const loading = ref<boolean>(false)
const isEdit = ref(false)
const currentIndex = ref(null)
const form = ref<any>({
field: '',
label: '',
parameter_type: '',
desc: '',
})
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,153 @@
<template>
<div class="flex-between mb-16">
<h5 class="break-all ellipsis lighter" style="max-width: 80%">
{{
$t(
'views.applicationWorkflow.nodes.parameterExtractionNode.extractParameters.label',
'提取参数',
)
}}
</h5>
<div>
<span class="ml-4">
<el-button link type="primary" @click="openAddDialog()">
<AppIcon iconName="app-add-outlined" class="mr-4"></AppIcon>
{{ $t('common.add') }}
</el-button>
</span>
</div>
</div>
<el-table
v-if="props.nodeModel.properties.node_data.variable_list?.length > 0"
:data="props.nodeModel.properties.node_data.variable_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
prop="label"
:label="
$t(
'views.applicationWorkflow.nodes.parameterExtractionNode.extractParameters.parameterType',
)
"
>
<template #default="{ row }">
<span>
<span :title="row.parameter_type" class="ellipsis-1">
{{ row.parameter_type }}
</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)">
<AppIcon iconName="app-edit"></AppIcon>
</el-button>
</el-tooltip>
</span>
<el-tooltip effect="dark" :content="$t('common.delete')" placement="top">
<el-button type="primary" text @click="deleteField($index)">
<AppIcon iconName="app-delete"></AppIcon>
</el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<ParametersFieldDialog
ref="ParametersFieldDialogRef"
@refresh="refreshFieldList"
></ParametersFieldDialog>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { set, cloneDeep } from 'lodash'
import ParametersFieldDialog from './ParametersFieldDialog.vue'
import { MsgError } from '@/utils/message'
import { t } from '@/locales'
const props = defineProps<{ nodeModel: any }>()
const tableRef = ref()
const ParametersFieldDialogRef = ref()
const inputFieldList = ref<any[]>([])
function openAddDialog(data?: any, index?: any) {
ParametersFieldDialogRef.value.open(data, index)
}
function deleteField(index: any) {
inputFieldList.value.splice(index, 1)
const fields = [
{
label: t('views.applicationWorkflow.nodes.variableSplittingNode.result', '结果'),
value: 'result',
},
...inputFieldList.value.map((item) => ({ label: item.label, value: item.field })),
]
set(props.nodeModel.properties.config, 'fields', fields)
}
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
}
}
if ([undefined, null].includes(index)) {
inputFieldList.value.push(data)
} else {
inputFieldList.value.splice(index, 1, data)
}
ParametersFieldDialogRef.value.close()
const fields = [
{
label: t('views.applicationWorkflow.nodes.variableSplittingNode.result', '结果'),
value: 'result',
},
...inputFieldList.value.map((item) => ({ label: item.label, value: item.field })),
]
set(props.nodeModel.properties.config, 'fields', fields)
}
onMounted(() => {
if (props.nodeModel.properties.node_data.variable_list) {
inputFieldList.value = cloneDeep(props.nodeModel.properties.node_data.variable_list)
}
set(props.nodeModel.properties.node_data, 'variable_list', inputFieldList)
const fields = [
{
label: t('views.applicationWorkflow.nodes.variableSplittingNode.result', '结果'),
value: 'result',
},
...inputFieldList.value.map((item) => ({ label: item.label, value: item.field })),
]
set(props.nodeModel.properties.config, 'fields', fields)
})
</script>
<style scoped lang="scss"></style>

View File

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

View File

@ -0,0 +1,193 @@
<template>
<NodeContainer :nodeModel="nodeModel">
<el-form
@submit.prevent
:model="form_data"
label-position="top"
require-asterisk-position="right"
label-width="auto"
ref="VariableSplittingRef"
hide-required-asterisk
>
<el-form-item
:label="$t('views.application.form.aiModel.label')"
prop="model_id"
:rules="{
required: true,
message: $t('views.application.form.aiModel.placeholder'),
trigger: 'change',
}"
>
<template #label>
<div class="flex-between w-full">
<div>
<span
>{{ $t('views.application.form.aiModel.label')
}}<span class="color-danger">*</span></span
>
</div>
<el-button
:disabled="!form_data.model_id"
type="primary"
link
@click="openAIParamSettingDialog(form_data.model_id)"
@refreshForm="refreshParam"
>
<AppIcon iconName="app-setting"></AppIcon>
</el-button>
</div>
</template>
<ModelSelect
@change="model_change"
@wheel="wheel"
:teleported="false"
v-model="form_data.model_id"
:placeholder="$t('views.application.form.aiModel.placeholder')"
:options="modelOptions"
@submitModel="getSelectModel"
showFooter
:model-type="'LLM'"
></ModelSelect>
</el-form-item>
<el-form-item
:label="
$t(
'views.applicationWorkflow.nodes.parameterExtractionNode.selectVariables.label',
'选择变量',
)
"
>
<template #label>
<div class="flex-between">
<div>
{{
$t(
'views.applicationWorkflow.nodes.parameterExtractionNode.selectVariables.label',
'选择变量',
)
}}
<span class="color-danger">*</span>
</div>
</div>
</template>
<NodeCascader
ref="nodeCascaderRef"
:nodeModel="nodeModel"
class="w-full"
:placeholder="
$t(
'views.applicationWorkflow.nodes.parameterExtractionNode.selectVariables.placeholder',
)
"
v-model="form_data.input_variable"
/>
</el-form-item>
<ParametersFieldTable
ref="ParametersFieldTableRef"
:node-model="nodeModel"
></ParametersFieldTable>
</el-form>
<AIModeParamSettingDialog ref="AIModeParamSettingDialogRef" @refresh="refreshParam" />
</NodeContainer>
</template>
<script setup lang="ts">
import { computed, onMounted, ref, inject } from 'vue'
import NodeContainer from '@/workflow/common/NodeContainer.vue'
import NodeCascader from '@/workflow/common/NodeCascader.vue'
import VariableFieldTable from '@/workflow/nodes/variable-splitting/component/VariableFieldTable.vue'
import AIModeParamSettingDialog from '@/views/application/component/AIModeParamSettingDialog.vue'
import ParametersFieldTable from '@/workflow/nodes/parameter-extraction-node/component/ParametersFieldTable.vue'
import { useRoute } from 'vue-router'
import { loadSharedApi } from '@/utils/dynamics-api/shared-api'
import { set, groupBy } from 'lodash'
const getApplicationDetail = inject('getApplicationDetail') as any
const props = defineProps<{ nodeModel: any }>()
const AIModeParamSettingDialogRef = ref<InstanceType<typeof AIModeParamSettingDialog>>()
const route = useRoute()
const {
params: { id },
} = route as any
const openAIParamSettingDialog = (modelId: string) => {
if (modelId) {
AIModeParamSettingDialogRef.value?.open(modelId, id, form_data.value.model_params_setting)
}
}
function refreshParam(data: any) {
set(props.nodeModel.properties.node_data, 'model_params_setting', data)
}
const application = getApplicationDetail()
const modelOptions = ref<any>(null)
const wheel = (e: any) => {
if (e.ctrlKey === true) {
e.preventDefault()
return true
} else {
e.stopPropagation()
return true
}
}
const apiType = computed(() => {
if (route.path.includes('resource-management')) {
return 'systemManage'
} else {
return 'workspace'
}
})
function getSelectModel() {
const obj =
apiType.value === 'systemManage'
? {
model_type: 'LLM',
workspace_id: application.value?.workspace_id,
}
: {
model_type: 'LLM',
}
loadSharedApi({ type: 'model', systemType: apiType.value })
.getSelectModelList(obj)
.then((res: any) => {
modelOptions.value = groupBy(res?.data, 'provider')
})
}
const form = {
input_variable: [],
model_params_setting: {},
model_id: '',
variable_list: [],
}
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 model_change = (model_id?: string) => {
if (model_id) {
AIModeParamSettingDialogRef.value?.reset_default(model_id, id)
} else {
refreshParam({})
}
}
const VariableSplittingRef = ref()
const validate = async () => {
return VariableSplittingRef.value.validate()
}
onMounted(() => {
getSelectModel()
set(props.nodeModel, 'validate', validate)
})
</script>
<style lang="scss" scoped></style>