feat: Intent classify

v3.2
zhangzhanwei 2025-09-12 11:53:10 +08:00 committed by zhanweizhang7
parent bd668e7e2a
commit ce6f801a35
18 changed files with 861 additions and 1 deletions

View File

@ -24,13 +24,14 @@ from .text_to_speech_step_node.impl.base_text_to_speech_node import BaseTextToSp
from .tool_lib_node import *
from .tool_node import *
from .variable_assign_node import BaseVariableAssignNode
from .intent_node import *
node_list = [BaseStartStepNode, BaseChatNode, BaseSearchKnowledgeNode, BaseQuestionNode,
BaseConditionNode, BaseReplyNode,
BaseToolNodeNode, BaseToolLibNodeNode, BaseRerankerNode, BaseApplicationNode,
BaseDocumentExtractNode,
BaseImageUnderstandNode, BaseFormNode, BaseSpeechToTextNode, BaseTextToSpeechNode,
BaseImageGenerateNode, BaseVariableAssignNode, BaseMcpNode]
BaseImageGenerateNode, BaseVariableAssignNode, BaseMcpNode,BaseIntentNode]
def get_node(node_type):

View File

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

View File

@ -0,0 +1,46 @@
# 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 IntentBranchSerializer(serializers.Serializer):
id = serializers.CharField(required=True, label=_("Branch id"))
content = serializers.CharField(required=True, label=_("content"))
isOther = serializers.BooleanField(required=True, label=_("Branch Type"))
class IntentNodeSerializer(serializers.Serializer):
model_id = serializers.CharField(required=True, label=_("Model id"))
content_list = serializers.ListField(required=True, label=_("Text content"))
dialogue_number = serializers.IntegerField(required=True, label=
_("Number of multi-round conversations"))
model_params_setting = serializers.DictField(required=False,
label=_("Model parameter settings"))
branch = IntentBranchSerializer(many=True)
class IIntentNode(INode):
type = 'intent-node'
def save_context(self, details, workflow_manage):
pass
def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
return IntentNodeSerializer
def _run(self):
question = self.workflow_manage.get_reference_field(
self.node_params_serializer.data.get('content_list')[0],
self.node_params_serializer.data.get('content_list')[1:],
)
return self.execute(**self.node_params_serializer.data, **self.flow_params_serializer.data, user_input=str(question))
def execute(self, model_id, dialogue_number, history_chat_record, user_input, branch,
model_params_setting=None, **kwargs) -> NodeResult:
pass

View File

@ -0,0 +1,3 @@
from .base_intent_node import BaseIntentNode

View File

@ -0,0 +1,242 @@
# coding=utf-8
import json
import re
import time
from typing import List, Dict, Any
from functools import reduce
from django.db.models import QuerySet
from langchain.schema import HumanMessage, SystemMessage
from application.flow.i_step_node import INode, NodeResult
from application.flow.step_node.intent_node.i_intent_node import IIntentNode
from models_provider.models import Model
from models_provider.tools import get_model_instance_by_model_workspace_id, get_model_credential
from .prompt_template import PROMPT_TEMPLATE
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 _write_context(node_variable: Dict, workflow_variable: Dict, node: INode, workflow, answer: str):
chat_model = node_variable.get('chat_model')
message_tokens = chat_model.get_num_tokens_from_messages(node_variable.get('message_list'))
answer_tokens = chat_model.get_num_tokens(answer)
node.context['message_tokens'] = message_tokens
node.context['answer_tokens'] = answer_tokens
node.context['answer'] = answer
node.context['history_message'] = node_variable['history_message']
node.context['user_input'] = node_variable['user_input']
node.context['branch_id'] = node_variable.get('branch_id')
node.context['reason'] = node_variable.get('reason')
node.context['category'] = node_variable.get('category')
node.context['run_time'] = time.time() - node.context['start_time']
def write_context(node_variable: Dict, workflow_variable: Dict, node: INode, workflow):
response = node_variable.get('result')
answer = response.content
_write_context(node_variable, workflow_variable, node, workflow, answer)
class BaseIntentNode(IIntentNode):
def save_context(self, details, workflow_manage):
self.context['branch_id'] = details.get('branch_id')
self.context['category'] = details.get('category')
def execute(self, model_id, dialogue_number, history_chat_record, user_input, branch,
model_params_setting=None, **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
)
# 获取历史对话
history_message = self.get_history_message(history_chat_record, dialogue_number)
self.context['history_message'] = history_message
# 保存问题到上下文
self.context['user_input'] = user_input
# 构建分类提示词
prompt = self.build_classification_prompt(user_input, branch)
# 生成消息列表
system = self.build_system_prompt()
message_list = self.generate_message_list(system, prompt, history_message)
self.context['message_list'] = message_list
# 调用模型进行分类
try:
r = chat_model.invoke(message_list)
classification_result = r.content.strip()
# 解析分类结果获取分支信息
matched_branch = self.parse_classification_result(classification_result, branch)
# 返回结果
return NodeResult({
'result': r,
'chat_model': chat_model,
'message_list': message_list,
'history_message': history_message,
'user_input': user_input,
'branch_id': matched_branch['id'],
'reason': json.loads(r.content).get('reason'),
'category': matched_branch.get('content', matched_branch['id'])
}, {}, _write_context=write_context)
except Exception as e:
# 错误处理:返回"其他"分支
other_branch = self.find_other_branch(branch)
if other_branch:
return NodeResult({
'branch_id': other_branch['id'],
'category': other_branch.get('content', other_branch['id']),
'error': str(e)
}, {})
else:
raise Exception(f"error: {str(e)}")
@staticmethod
def get_history_message(history_chat_record, dialogue_number):
"""获取历史消息"""
start_index = len(history_chat_record) - dialogue_number
history_message = reduce(lambda x, y: [*x, *y], [
[history_chat_record[index].get_human_message(), history_chat_record[index].get_ai_message()]
for index in
range(start_index if start_index > 0 else 0, len(history_chat_record))], [])
for message in history_message:
if isinstance(message.content, str):
message.content = re.sub('<form_rander>[\d\D]*?<\/form_rander>', '', message.content)
return history_message
def build_system_prompt(self) -> str:
"""构建系统提示词"""
return "你是一个专业的意图识别助手,请根据用户输入和意图选项,准确识别用户的真实意图。"
def build_classification_prompt(self, user_input: str, branch: List[Dict]) -> str:
"""构建分类提示词"""
classification_list = []
other_branch = self.find_other_branch(branch)
# 添加其他分支
if other_branch:
classification_list.append({
"classificationId": 0,
"content": other_branch.get('content')
})
# 添加正常分支
classification_id = 1
for b in branch:
if not b.get('isOther'):
classification_list.append({
"classificationId": classification_id,
"content": b['content']
})
classification_id += 1
return PROMPT_TEMPLATE.format(
classification_list=classification_list,
user_input=user_input
)
def generate_message_list(self, system: str, prompt: str, history_message):
"""生成消息列表"""
if system is None or len(system) == 0:
return [*history_message, HumanMessage(self.workflow_manage.generate_prompt(prompt))]
else:
return [SystemMessage(self.workflow_manage.generate_prompt(system)), *history_message,
HumanMessage(self.workflow_manage.generate_prompt(prompt))]
def parse_classification_result(self, result: str, branch: List[Dict]) -> Dict[str, Any]:
"""解析分类结果"""
other_branch = self.find_other_branch(branch)
normal_intents = [
b
for b in branch
if not b.get('isOther')
]
def get_branch_by_id(category_id: int):
if category_id == 0:
return other_branch
elif 1 <= category_id <= len(normal_intents):
return normal_intents[category_id - 1]
return None
try:
result_json = json.loads(result)
classification_id = result_json.get('classificationId', 0) # 0 兜底
# 如果是 0 ,返回其他分支
matched_branch = get_branch_by_id(classification_id)
if matched_branch:
return matched_branch
except Exception as e:
# json 解析失败re 提取
numbers = re.findall(r'"classificationId":\s*(\d+)', result)
if numbers:
classification_id = int(numbers[0])
matched_branch = get_branch_by_id(classification_id)
if matched_branch:
return matched_branch
# 如果都解析失败返回“other”
return other_branch or (normal_intents[0] if normal_intents else {'id': 'unknown', 'content': 'unknown'})
def find_other_branch(self, branch: List[Dict]) -> Dict[str, Any] | None:
"""查找其他分支"""
for b in branch:
if b.get('isOther'):
return b
return None
def get_details(self, index: int, **kwargs):
"""获取节点执行详情"""
return {
'name': self.node.properties.get('stepName'),
'index': index,
'run_time': self.context.get('run_time'),
'system': self.context.get('system'),
'history_message': [
{'content': message.content, 'role': message.type}
for message in (self.context.get('history_message') or [])
],
'user_input': self.context.get('user_input'),
'answer': self.context.get('answer'),
'branch_id': self.context.get('branch_id'),
'category': self.context.get('category'),
'type': self.node.type,
'message_tokens': self.context.get('message_tokens'),
'answer_tokens': self.context.get('answer_tokens'),
'status': self.status,
'err_message': self.err_message
}

View File

@ -0,0 +1,30 @@
PROMPT_TEMPLATE = """# Role
You are an intention classification expert, good at being able to judge which classification the user's input belongs to.
## Skills
Skill 1: Clearly determine which of the following intention classifications the user's input belongs to.
Intention classification list:
{classification_list}
Note:
- Please determine the match only between the user's input content and the Intention classification list content, without judging or categorizing the match with the classification ID.
## User Input
{user_input}
## Reply requirements
- The answer must be returned in JSON format.
- Strictly ensure that the output is in a valid JSON format.
- Do not add prefix ```json or suffix ```
- The answer needs to include the following fields such as:
{{
"classificationId": 0,
"reason": ""
}}
## Limit
- Please do not reply in text."""

View File

@ -0,0 +1,18 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_5119_2683)">
<path d="M0 16C0 8.45753 0 4.68629 2.34315 2.34315C4.68629 0 8.45753 0 16 0C23.5425 0 27.3137 0 29.6569 2.34315C32 4.68629 32 8.45753 32 16C32 23.5425 32 27.3137 29.6569 29.6569C27.3137 32 23.5425 32 16 32C8.45753 32 4.68629 32 2.34315 29.6569C0 27.3137 0 23.5425 0 16Z" fill="#7F3BF5"/>
<g clip-path="url(#clip1_5119_2683)">
<path d="M23.5 15.0333C23.5 13.7712 23.0468 12.1152 22.0034 10.7918C20.9834 9.49802 19.3861 8.50013 16.9994 8.5001C15.3464 8.5001 14.1501 8.86918 13.3177 9.51166C12.4958 10.1462 11.9282 11.1249 11.6779 12.552C11.6326 12.8103 11.4681 13.0322 11.2344 13.151C10.3373 13.6067 9.63875 13.9754 9.13397 14.2578C9.33393 14.402 9.55402 14.592 9.7199 14.8388C9.98776 15.2375 10.0951 15.754 9.87615 16.3093C9.75405 16.6191 9.57819 16.9772 9.42124 17.3046C9.25621 17.649 9.09832 17.9865 8.9769 18.3121C8.85406 18.6416 8.78589 18.915 8.77264 19.1308C8.75991 19.339 8.80119 19.4363 8.83774 19.4872C8.88218 19.5492 8.96334 19.6192 9.1657 19.6858C9.38446 19.7578 9.65857 19.7994 10.0389 19.8494C10.7162 19.9383 11.7694 20.0426 12.6016 20.7031C13.594 21.4908 14.2861 22.6482 14.728 24.0893C14.8629 24.5292 14.6161 24.9951 14.1763 25.1301C13.7363 25.2651 13.2695 25.0176 13.1346 24.5776C12.7599 23.3559 12.2204 22.5282 11.5656 22.0084C11.1624 21.6884 10.6166 21.6065 9.82244 21.5022C9.46711 21.4555 9.03667 21.3983 8.64487 21.2695C8.23686 21.1352 7.80081 20.9018 7.48276 20.4581C7.15704 20.0036 7.08041 19.489 7.10841 19.0299C7.13601 18.5783 7.26731 18.1282 7.41603 17.7294C7.56633 17.3264 7.75294 16.9291 7.91814 16.5844C8.0823 16.2419 8.21579 15.9668 8.30958 15.7356C8.26526 15.6875 8.1784 15.6138 8.02394 15.5175C7.9341 15.4615 7.83916 15.4089 7.73585 15.3515C7.64188 15.2993 7.52356 15.2335 7.42254 15.17C7.34204 15.1195 7.16747 15.0086 7.03598 14.8388C6.96335 14.745 6.85753 14.5752 6.8366 14.3399C6.8134 14.079 6.90438 13.8538 7.03029 13.693L7.14422 13.5685C7.26732 13.4502 7.41174 13.3519 7.51613 13.2828C7.69136 13.1668 7.91855 13.0307 8.18995 12.8767C8.66603 12.6067 9.31125 12.2654 10.1195 11.8513C10.4631 10.309 11.1725 9.06206 12.2988 8.19248C13.5139 7.25452 15.1119 6.83344 16.9994 6.83344C19.9231 6.83346 21.9927 8.08664 23.312 9.75987C24.608 11.4036 25.1667 13.4317 25.1667 15.0333C25.1665 18.1251 22.731 21.3111 19.2487 22.2989C19.4255 22.6451 19.7799 23.1328 20.3612 23.7735C20.6702 24.1144 20.6442 24.6411 20.3034 24.9503C19.9625 25.2595 19.4359 25.2341 19.1266 24.8933C18.5034 24.2062 18.0278 23.5823 17.7521 23.0338C17.5028 22.5377 17.2729 21.8067 17.7066 21.1751L17.7562 21.11C17.8789 20.9644 18.0485 20.8644 18.2372 20.8284C21.3655 20.2321 23.4998 17.4657 23.5 15.0333Z" fill="white"/>
<path d="M15.4006 10.1818C15.7362 10.1167 16.0421 10.0894 16.3194 10.1362C16.6496 10.192 16.8658 10.3399 17.016 10.4788C17.0325 10.494 17.0452 10.5079 17.0567 10.5187C17.068 10.5192 17.0817 10.5203 17.0974 10.5203C17.5965 10.5204 18.1473 10.59 18.6021 10.7425C18.8259 10.8176 19.0788 10.9302 19.2914 11.1047C19.4395 11.2262 19.6026 11.4119 19.6918 11.6605C19.7327 11.667 19.7783 11.6726 19.8269 11.6768C19.9181 11.6846 20.0041 11.6865 20.0678 11.6865C20.3422 11.6866 20.553 11.8022 20.6676 11.8786C20.7961 11.9642 20.9056 12.069 20.9947 12.1699C21.1745 12.3735 21.3376 12.6349 21.4708 12.9259C21.7387 13.5113 21.9272 14.3088 21.8281 15.1965C21.7521 15.8762 21.4305 16.509 20.7327 16.8119C20.3159 16.9926 19.8481 17.0128 19.372 16.9469C19.244 17.1139 19.091 17.2635 18.9098 17.3888C18.514 17.6625 18.0385 17.7982 17.5409 17.8494C17.2295 17.8815 16.8878 17.8535 16.5685 17.6932C16.2371 17.5268 16.0076 17.2566 15.8678 16.9478C15.7135 16.6069 15.6639 16.2092 15.6765 15.7897C14.9117 15.6141 14.2546 15.3235 13.7836 14.8604C13.0107 14.1003 13.0306 12.9493 13.3181 11.8338L13.3263 11.8021C13.6137 10.8434 14.4511 10.366 15.4006 10.1818Z" fill="white"/>
<path d="M18.2693 15.9171C18.6603 15.6745 19.1741 15.7946 19.4168 16.1856C19.6593 16.5766 19.539 17.0903 19.1482 17.3331C18.9633 17.4478 18.7396 17.6028 18.5395 17.7644C18.3271 17.9359 18.1916 18.0749 18.1358 18.1526C17.9757 18.376 17.8237 18.5765 17.6899 18.7532C17.5532 18.9337 17.44 19.0833 17.3432 19.2211C17.142 19.5075 17.0771 19.6552 17.0584 19.7493C16.9686 20.2007 16.5299 20.4942 16.0785 20.4044C15.6273 20.3146 15.3346 19.8758 15.4242 19.4246C15.5142 18.9725 15.752 18.5868 15.9793 18.2633C16.0966 18.0962 16.2291 17.9214 16.3609 17.7473C16.4957 17.5694 16.635 17.3843 16.7809 17.1809C16.97 16.9171 17.2475 16.6662 17.4929 16.468C17.7506 16.26 18.0305 16.0653 18.2693 15.9171Z" fill="white"/>
</g>
</g>
<defs>
<clipPath id="clip0_5119_2683">
<rect width="32" height="32" fill="white"/>
</clipPath>
<clipPath id="clip1_5119_2683">
<rect width="20" height="20" fill="white" transform="translate(6 6)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -24,4 +24,5 @@ export enum WorkflowType {
SpeechToTextNode = 'speech-to-text-node',
ImageGenerateNode = 'image-generate-node',
McpNode = 'mcp-node',
IntentNode = 'intent-node',
}

View File

@ -47,6 +47,8 @@ export default {
noData: 'No data',
result: 'Result',
remove: 'Remove',
classify: 'Classify',
reason: 'Reason',
removeSuccess: 'Successful',
searchBar: {
placeholder: 'Search by name',

View File

@ -277,6 +277,18 @@ export default {
label: 'Custom Tool',
text: 'Execute custom scripts to achieve data processing',
},
intentNode: {
label: 'IntentNode',
other: 'other',
placeholder: 'Please choose a classification option',
classify: {
label: 'Intent classify',
placeholder: 'Please input',
},
input: {
label: 'Input',
},
},
applicationNode: {
label: 'APP Node',
},

View File

@ -48,6 +48,8 @@ export default {
noData: '暂无数据',
result: '结果',
remove: '移除',
classify: '分类',
reason: '理由',
removeSuccess: '移除成功',
searchBar: {
placeholder: '按名称搜索',

View File

@ -282,6 +282,18 @@ export default {
label: '自定义工具',
text: '通过执行自定义脚本,实现数据处理',
},
intentNode: {
label: '意图识别',
other: '其他',
placeholder: '请选择分类项',
classify: {
label: '意图分类',
placeholder: '请输入',
},
input: {
label: '输入',
},
},
applicationNode: {
label: '应用节点',
},

View File

@ -47,6 +47,8 @@ export default {
noData: '暂无数据',
result: '結果',
remove: '移除',
classify: '分類',
reason: '理由',
removeSuccess: '移除成功',
searchBar: {
placeholder: '按名稱搜尋',

View File

@ -276,6 +276,18 @@ export default {
label: '自定義工具',
text: '通過執行自定義腳本,實現數據處理',
},
intentNode: {
label: '意圖識別',
other: '其他',
placeholder: '請選擇分類項',
classify: {
label: '意圖分類',
placeholder: '請輸入',
},
input: {
label: '輸入',
},
},
applicationNode: {
label: '應用節點',
},

View File

@ -360,11 +360,33 @@ export const toolNode = {
},
},
}
export const intentNode = {
type: WorkflowType.IntentNode,
text: t('views.applicationWorkflow.nodes.intentNode.label'),
label: t('views.applicationWorkflow.nodes.intentNode.label'),
height: 260,
properties: {
stepName: t('views.applicationWorkflow.nodes.intentNode.label'),
config: {
fields: [
{
label: t('common.classify'),
value: 'category',
},
{
label: t('common.reason'),
value: 'reason',
},
],
},
},
}
export const menuNodes = [
{
label: t('views.applicationWorkflow.nodes.classify.aiCapability'),
list: [
aiChatNode,
intentNode,
questionNode,
imageGenerateNode,
imageUnderstandNode,
@ -423,6 +445,9 @@ 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') },
@ -462,6 +487,7 @@ export const nodeDict: any = {
[WorkflowType.ImageGenerateNode]: imageGenerateNode,
[WorkflowType.VariableAssignNode]: variableAssignNode,
[WorkflowType.McpNode]: mcpNode,
[WorkflowType.IntentNode]: intentNode,
}
export function isWorkFlow(type: string | undefined) {
return type === 'WORK_FLOW'

View File

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

View File

@ -0,0 +1,75 @@
import IntentNodeVue from './index.vue'
import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
class IntentNode extends AppNode {
constructor(props: any) {
super(props, IntentNodeVue)
}
}
const get_up_index_height = (branch_lsit: Array<any>, index: number) => {
return branch_lsit
.filter((item, i) => i < index)
.map((item) => item.height + 8)
.reduce((x,y) => x+y, 0)
}
class IntentModel extends AppNodeModel {
refreshBranch() {
// 更新节点连接边的path
this.incoming.edges.forEach((edge: any) => {
// 调用自定义的更新方案
edge.updatePathByAnchor()
})
this.outgoing.edges.forEach((edge: any) => {
edge.updatePathByAnchor()
})
}
getDefaultAnchor() {
const {
id,
x,
y,
width,
height,
properties: { branch_condition_list }
} = this
if (this.height === undefined) {
this.height = 200
}
const showNode = this.properties.showNode === undefined ? true : this.properties.showNode
const anchors: any = []
anchors.push({
x: x - width / 2 + 10,
y: showNode ? y : y - 15,
id: `${id}_left`,
edgeAddable: false,
type: 'left'
})
if (branch_condition_list) {
const FORM_ITEMS_HEIGHT = 382 // 上方表单占用高度
for (let index = 0; index < branch_condition_list.length; index++) {
const element = branch_condition_list[index]
const h = get_up_index_height(branch_condition_list, index)
anchors.push({
x: x + width / 2 - 10,
y: showNode
? y - height / 2 + FORM_ITEMS_HEIGHT + h + element.height / 2
: y - 15,
id: `${id}_${element.id}_right`,
type: 'right'
})
}
}
return anchors
}
}
export default {
type: 'intent-node',
model: IntentModel,
view: IntentNode
}

View File

@ -0,0 +1,364 @@
<template>
<NodeContainer :nodeModel="nodeModel">
<h5 class="title-decoration-1 mb-8">{{ $t('views.applicationWorkflow.nodeSetting') }}</h5>
<el-card shadow="never" class="card-never" style="--el-card-padding: 12px">
<el-form
@submit.prevent
:model="form_data"
label-position="top"
require-asterisk-position="right"
label-width="auto"
ref="IntentClassifyNodeFormRef"
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">
<div>
<span
>{{ $t('views.application.form.aiModel.label')
}}<span class="color-danger">*</span></span
>
</div>
<el-button
type="primary"
link
:disabled="!form_data.model_id"
@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
prop="content_list"
:label="$t('views.applicationWorkflow.nodes.intentNode.input.label')"
:rules="{
required: true,
trigger: 'change',
}"
>
<template #label>
<div class="flex-between">
<div>
<span
>{{ $t('views.applicationWorkflow.nodes.intentNode.input.label')
}}<span class="color-danger">*</span></span
>
</div>
</div>
</template>
<NodeCascader
ref="nodeCascaderRef"
:nodeModel="nodeModel"
class="w-full"
:placeholder="$t('views.applicationWorkflow.nodes.textToSpeechNode.content.label')"
v-model="form_data.content_list"
/>
</el-form-item>
<el-form-item :label="$t('views.application.form.historyRecord.label')">
<el-input-number
v-model="form_data.dialogue_number"
:min="0"
:value-on-clear="0"
controls-position="right"
class="w-full"
:step="1"
:step-strictly="true"
/>
</el-form-item>
<el-form-item
:label="$t('views.applicationWorkflow.nodes.intentNode.classify.label')"
:rules="{
required: true,
trigger: 'change',
}"
>
<template #label>
<div class="flex-between">
<div>
<span>{{ $t('views.applicationWorkflow.nodes.intentNode.input.label') }}<span class="color-danger">*</span></span>
</div>
<el-button
@click="addClassfiyBranch"
type="primary"
size="large"
link>
<el-icon><Plus /></el-icon>
</el-button>
</div>
</template>
<div>
<div v-for="(item,index) in form_data.branch"
v-resize="(wh: any) => resizeBranch(wh, item, index)"
:key="item.id">
<el-row class="mb-8" :gutter="12" align="middle">
<el-col :span="21">
<el-input
v-model="item.content"
style="width: 210px"
:disabled="item.isOther"
:placeholder="$t('views.applicationWorkflow.nodes.intentNode.classify.placeholder')" />
</el-col>
<el-col :span="3">
<el-button
link
size="large"
class="mt-4"
v-if="!item.isOther"
:disabled="form_data.branch.filter((b:any) => !b.isOther).length <= 1"
@click="deleteClassifyBranch(item.id)">
<el-icon><Delete /></el-icon>
</el-button>
</el-col>
</el-row>
</div>
</div>
</el-form-item>
</el-form>
</el-card>
<AIModeParamSettingDialog ref="AIModeParamSettingDialogRef" @refresh="refreshParam" />
</NodeContainer>
</template>
<script setup lang="ts">
import { set, groupBy, cloneDeep } from 'lodash'
import NodeCascader from '@/workflow/common/NodeCascader.vue'
import NodeContainer from '@/workflow/common/NodeContainer.vue'
import AIModeParamSettingDialog from '@/views/application/component/AIModeParamSettingDialog.vue'
import type { FormInstance } from 'element-plus'
import { ref, computed, onMounted, inject } from 'vue'
import { isLastNode } from '@/workflow/common/data'
import { t } from '@/locales'
import { useRoute } from 'vue-router'
import { randomId } from '@/utils/common'
import { loadSharedApi } from '@/utils/dynamics-api/shared-api'
const getApplicationDetail = inject('getApplicationDetail') as any
const route = useRoute()
const {
params: { id },
} = route as any
const apiType = computed(() => {
if (route.path.includes('resource-management')) {
return 'systemManage'
} else {
return 'workspace'
}
})
const nodeCascaderRef = ref()
const AIModeParamSettingDialogRef = ref<InstanceType<typeof AIModeParamSettingDialog>>()
function addClassfiyBranch() {
const list = cloneDeep(props.nodeModel.properties.node_data.branch)
const obj = {
id: randomId(),
content: '',
isOther: false ,
}
list.splice(list.length - 1, 0 , obj)
refreshBranchAnchor(list, true)
set(props.nodeModel.properties.node_data, 'branch', list)
}
function deleteClassifyBranch(id: string) {
const list = cloneDeep(props.nodeModel.properties.node_data.branch)
const itemToDelete = list.find((item: any) => item.id === id)
if (!itemToDelete || itemToDelete.isOther) {
return
}
const commonItems = list.filter((item:any) => !item.isOther)
if (commonItems.length <= 1) {
return
}
// 线
const delete_anchor_id = `${props.nodeModel.id}_${id}_right`
const edgetToDelete = (props.nodeModel.outgoing?.edges || [])
.filter((edge: any) => edge.sourceAnchorId === delete_anchor_id)
.map((edge: any) => edge.id)
if (edgetToDelete.length > 0) {
props.nodeModel.graphModel.eventCenter.emit('delete_edge', edgetToDelete)
}
const newList = list.filter((item: any) => item.id !== id) //
set(props.nodeModel.properties.node_data, 'branch', newList) //
refreshBranchAnchor(newList, false) //
}
function refreshBranchAnchor(list: Array<any>, is_add: boolean) {
const branch_condition_list = cloneDeep(
props.nodeModel.properties.branch_condition_list
? props.nodeModel.properties.branch_condition_list
: [],
)
const new_branch_condition_list = list
.map((item, index) => {
const exist = branch_condition_list.find((b: any) => b.id === item.id)
if (exist) {
return {index: index, height: exist.height, id: item.id}
} else {
if (is_add) {
return {index: index, height: 12, id: item.id}
}
}
})
.filter((item) => item)
set(props.nodeModel.properties, 'branch_condition_list', new_branch_condition_list)
props.nodeModel.refreshBranch()
}
const resizeBranch = (wh: any, row: any, index: number) => {
const branch_condition_list = cloneDeep(
props.nodeModel.properties.branch_condition_list
? props.nodeModel.properties.branch_condition_list
: [],
)
const new_branch_condition_list = branch_condition_list.map((item: any) => {
if (item.id === row.id) {
return {
...item,
height: wh.height, //
index: index
}
}
return item
})
set(props.nodeModel.properties, 'branch_condition_list', new_branch_condition_list)
refreshBranchAnchor(props.nodeModel.properties.node_data.branch, true)
}
const wheel = (e: any) => {
if (e.ctrlKey === true) {
e.preventDefault()
return true
} else {
e.stopPropagation()
return true
}
}
const model_change = (model_id?: string) => {
if (model_id) {
AIModeParamSettingDialogRef.value?.reset_default(model_id, id)
} else {
refreshParam({})
}
}
const form = {
model_id: '',
branch: [
{
id: randomId(),
content: '',
isOther: false
},
{
id: randomId(),
content: t('views.applicationWorkflow.nodes.intentNode.other'),
isOther: true
}
],
dialogue_number: 1,
content_list: [],
}
function refreshParam(data: any) {
set(props.nodeModel.properties.node_data, 'model_params_setting', data)
}
const openAIParamSettingDialog = (modelId: string) => {
if (modelId) {
AIModeParamSettingDialogRef.value?.open(modelId, id, form_data.value.model_params_setting)
}
}
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)
refreshBranchAnchor(form.branch, true)
}
return props.nodeModel.properties.node_data
},
set: (value) => {
set(props.nodeModel.properties, 'node_data', value)
},
})
const props = defineProps<{ nodeModel: any }>()
const IntentClassifyNodeFormRef = ref<FormInstance>()
const modelOptions = ref<any>(null)
const validate = () => {
return Promise.all([
nodeCascaderRef.value ? nodeCascaderRef.value.validate() : Promise.resolve(''),
IntentClassifyNodeFormRef.value?.validate(),
]).catch((err: any) => {
return Promise.reject({ node: props.nodeModel, errMessage: err })
})
}
const application = getApplicationDetail()
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')
})
}
onMounted(() => {
getSelectModel()
if (typeof props.nodeModel.properties.node_data?.is_result === 'undefined') {
if (isLastNode(props.nodeModel)) {
set(props.nodeModel.properties.node_data, 'is_result', true)
}
}
set(props.nodeModel, 'validate', validate)
})
</script>
<style lang="scss" scoped></style>