feat: Support parameter extraction nodes (#4208)
parent
a4330e30a8
commit
c25a950650
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎虎
|
||||
@file: __init__.py.py
|
||||
@date:2025/10/13 14:56
|
||||
@desc:
|
||||
"""
|
||||
from .impl import *
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎虎
|
||||
@file: __init__.py.py
|
||||
@date:2025/10/13 15:01
|
||||
@desc:
|
||||
"""
|
||||
from .base_parameter_extraction_node import *
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎虎
|
||||
@file: base_variable_splitting_node.py
|
||||
@date:2025/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
|
||||
}
|
||||
|
|
@ -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 |
|
|
@ -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 {
|
||||
// 应用工作流
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -442,6 +442,20 @@ export default {
|
|||
placeholder: '请输入表达式',
|
||||
},
|
||||
},
|
||||
parameterExtractionNode: {
|
||||
label: '参数提取',
|
||||
text: '利用 AI 模型提取结构化参数',
|
||||
result: '结果',
|
||||
selectVariables: {
|
||||
label: '选择变量',
|
||||
placeholder: '请选择变量',
|
||||
},
|
||||
extractParameters: {
|
||||
label: '提取参数',
|
||||
desc: '描述',
|
||||
parameterType: '参数类型',
|
||||
},
|
||||
},
|
||||
},
|
||||
compare: {
|
||||
is_null: '为空',
|
||||
|
|
|
|||
|
|
@ -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: '為空',
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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>
|
||||
Loading…
Reference in New Issue