diff --git a/apps/application/flow/step_node/__init__.py b/apps/application/flow/step_node/__init__.py index 85e4c98f4..e5582f1cf 100644 --- a/apps/application/flow/step_node/__init__.py +++ b/apps/application/flow/step_node/__init__.py @@ -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): diff --git a/apps/application/flow/step_node/parameter_extraction_node/__init__.py b/apps/application/flow/step_node/parameter_extraction_node/__init__.py new file mode 100644 index 000000000..c93d71e9e --- /dev/null +++ b/apps/application/flow/step_node/parameter_extraction_node/__init__.py @@ -0,0 +1,9 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: __init__.py.py + @date:2025/10/13 14:56 + @desc: +""" +from .impl import * diff --git a/apps/application/flow/step_node/parameter_extraction_node/i_parameter_extraction_node.py b/apps/application/flow/step_node/parameter_extraction_node/i_parameter_extraction_node.py new file mode 100644 index 000000000..6ff670057 --- /dev/null +++ b/apps/application/flow/step_node/parameter_extraction_node/i_parameter_extraction_node.py @@ -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 diff --git a/apps/application/flow/step_node/parameter_extraction_node/impl/__init__.py b/apps/application/flow/step_node/parameter_extraction_node/impl/__init__.py new file mode 100644 index 000000000..a0d23a104 --- /dev/null +++ b/apps/application/flow/step_node/parameter_extraction_node/impl/__init__.py @@ -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 * diff --git a/apps/application/flow/step_node/parameter_extraction_node/impl/base_parameter_extraction_node.py b/apps/application/flow/step_node/parameter_extraction_node/impl/base_parameter_extraction_node.py new file mode 100644 index 000000000..fa950cf0a --- /dev/null +++ b/apps/application/flow/step_node/parameter_extraction_node/impl/base_parameter_extraction_node.py @@ -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 + } diff --git a/ui/src/assets/workflow/icon_parameter_extraction.svg b/ui/src/assets/workflow/icon_parameter_extraction.svg new file mode 100644 index 000000000..ffca8e1d4 --- /dev/null +++ b/ui/src/assets/workflow/icon_parameter_extraction.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/ui/src/enums/application.ts b/ui/src/enums/application.ts index dea4e72e9..0be70d07f 100644 --- a/ui/src/enums/application.ts +++ b/ui/src/enums/application.ts @@ -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 { // 应用工作流 diff --git a/ui/src/locales/lang/en-US/views/application-workflow.ts b/ui/src/locales/lang/en-US/views/application-workflow.ts index bd9bbf6ca..01b931d9b 100644 --- a/ui/src/locales/lang/en-US/views/application-workflow.ts +++ b/ui/src/locales/lang/en-US/views/application-workflow.ts @@ -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', diff --git a/ui/src/locales/lang/zh-CN/views/application-workflow.ts b/ui/src/locales/lang/zh-CN/views/application-workflow.ts index 2b32889d3..a29deab74 100644 --- a/ui/src/locales/lang/zh-CN/views/application-workflow.ts +++ b/ui/src/locales/lang/zh-CN/views/application-workflow.ts @@ -442,6 +442,20 @@ export default { placeholder: '请输入表达式', }, }, + parameterExtractionNode: { + label: '参数提取', + text: '利用 AI 模型提取结构化参数', + result: '结果', + selectVariables: { + label: '选择变量', + placeholder: '请选择变量', + }, + extractParameters: { + label: '提取参数', + desc: '描述', + parameterType: '参数类型', + }, + }, }, compare: { is_null: '为空', diff --git a/ui/src/locales/lang/zh-Hant/views/application-workflow.ts b/ui/src/locales/lang/zh-Hant/views/application-workflow.ts index 70866540a..c94df5ed0 100644 --- a/ui/src/locales/lang/zh-Hant/views/application-workflow.ts +++ b/ui/src/locales/lang/zh-Hant/views/application-workflow.ts @@ -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: '為空', diff --git a/ui/src/workflow/common/data.ts b/ui/src/workflow/common/data.ts index 1baadace3..7c0bacfd4 100644 --- a/ui/src/workflow/common/data.ts +++ b/ui/src/workflow/common/data.ts @@ -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) { diff --git a/ui/src/workflow/icons/parameter-extraction-node-icon.vue b/ui/src/workflow/icons/parameter-extraction-node-icon.vue new file mode 100644 index 000000000..84a49e959 --- /dev/null +++ b/ui/src/workflow/icons/parameter-extraction-node-icon.vue @@ -0,0 +1,6 @@ + + diff --git a/ui/src/workflow/nodes/parameter-extraction-node/component/ParametersFieldDialog.vue b/ui/src/workflow/nodes/parameter-extraction-node/component/ParametersFieldDialog.vue new file mode 100644 index 000000000..3e24107e2 --- /dev/null +++ b/ui/src/workflow/nodes/parameter-extraction-node/component/ParametersFieldDialog.vue @@ -0,0 +1,192 @@ + + + diff --git a/ui/src/workflow/nodes/parameter-extraction-node/component/ParametersFieldTable.vue b/ui/src/workflow/nodes/parameter-extraction-node/component/ParametersFieldTable.vue new file mode 100644 index 000000000..8c1d38147 --- /dev/null +++ b/ui/src/workflow/nodes/parameter-extraction-node/component/ParametersFieldTable.vue @@ -0,0 +1,153 @@ + + + + + diff --git a/ui/src/workflow/nodes/parameter-extraction-node/index.ts b/ui/src/workflow/nodes/parameter-extraction-node/index.ts new file mode 100644 index 000000000..41b707e82 --- /dev/null +++ b/ui/src/workflow/nodes/parameter-extraction-node/index.ts @@ -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, +} diff --git a/ui/src/workflow/nodes/parameter-extraction-node/index.vue b/ui/src/workflow/nodes/parameter-extraction-node/index.vue new file mode 100644 index 000000000..c0d7bec11 --- /dev/null +++ b/ui/src/workflow/nodes/parameter-extraction-node/index.vue @@ -0,0 +1,193 @@ + + +