feat: add search document node functionality and related configurations

v3.2
CaptainB 2025-10-15 17:06:14 +08:00
parent ff763c4944
commit 79fbc52741
17 changed files with 734 additions and 8 deletions

View File

@ -23,6 +23,7 @@ from .loop_start_node import *
from .mcp_node import BaseMcpNode from .mcp_node import BaseMcpNode
from .question_node import * from .question_node import *
from .reranker_node import * from .reranker_node import *
from .search_document_node import BaseSearchDocumentNode
from .search_knowledge_node import * from .search_knowledge_node import *
from .speech_to_text_step_node import BaseSpeechToTextNode from .speech_to_text_step_node import BaseSpeechToTextNode
from .start_node import * from .start_node import *
@ -34,7 +35,7 @@ from .variable_assign_node import BaseVariableAssignNode
from .variable_splitting_node import BaseVariableSplittingNode from .variable_splitting_node import BaseVariableSplittingNode
from .video_understand_step_node import BaseVideoUnderstandNode from .video_understand_step_node import BaseVideoUnderstandNode
node_list = [BaseStartStepNode, BaseChatNode, BaseSearchKnowledgeNode, BaseQuestionNode, node_list = [BaseStartStepNode, BaseChatNode, BaseSearchKnowledgeNode, BaseSearchDocumentNode, BaseQuestionNode,
BaseConditionNode, BaseReplyNode, BaseConditionNode, BaseReplyNode,
BaseToolNodeNode, BaseToolLibNodeNode, BaseRerankerNode, BaseApplicationNode, BaseToolNodeNode, BaseToolLibNodeNode, BaseRerankerNode, BaseApplicationNode,
BaseDocumentExtractNode, BaseDocumentExtractNode,

View File

@ -0,0 +1 @@
from .impl import *

View File

@ -0,0 +1,56 @@
# coding=utf-8
from typing import Type, List
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from application.flow.i_step_node import INode, NodeResult
class SearchDocumentStepNodeSerializer(serializers.Serializer):
knowledge_id_list = serializers.ListField(
required=False, child=serializers.UUIDField(required=True),
label=_("knowledge id list"), default=list
)
search_mode = serializers.ChoiceField(
required=False, choices=['auto', 'custom'], label=_("search mode"), default='auto'
)
search_scope_type = serializers.ChoiceField(
required=False, choices=['custom', 'referencing'], label=_("search scope type"),
allow_null=True, default='custom'
)
search_scope_source = serializers.ChoiceField(
required=False, choices=['document', 'knowledge'],
label=_("search scope variable type"), default='knowledge'
)
search_scope_reference = serializers.ListField(
required=False, label=_("search scope variable"), default=list
)
question_reference = serializers.ListField(
required=False, label=_("question reference address"), default=list
)
search_condition_type = serializers.ChoiceField(
required=False, choices=['AND', 'OR'], label=_("search condition type"), default='AND'
)
search_condition_list = serializers.ListField(
required=False, label=_("search condition list"), default=list
)
def is_valid(self, *, raise_exception=False):
super().is_valid(raise_exception=True)
class ISearchDocumentStepNode(INode):
type = 'search-document-node'
def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
return SearchDocumentStepNodeSerializer
def _run(self):
return self.execute(**self.node_params_serializer.data, **self.flow_params_serializer.data)
def execute(self, knowledge_id_list: List, search_mode: str, search_scope_type: str, search_scope_source: str,
search_scope_reference: List, question_reference: List, search_condition_type: str,
search_condition_list: List,
**kwargs) -> NodeResult:
pass

View File

@ -0,0 +1 @@
from .base_search_document_node import BaseSearchDocumentNode

View File

@ -0,0 +1,159 @@
# coding=utf-8
from typing import List
import jieba
from django.db.models import Q
from django.db.models import QuerySet
from application.flow.i_step_node import NodeResult
from application.flow.step_node.search_document_node.i_search_document_node import ISearchDocumentStepNode
from knowledge.models import Document, DocumentTag, Knowledge
class BaseSearchDocumentNode(ISearchDocumentStepNode):
def save_context(self, details, workflow_manage):
self.context['document_list'] = details.get('document_list')
self.context['knowledge_list'] = details.get('knowledge_list')
self.context['document_items'] = details.get('document_items')
self.context['knowledge_items'] = details.get('knowledge_items')
self.context['question'] = details.get('question')
self.context['run_time'] = details.get('run_time')
def get_reference_content(self, fields: List[str]):
return self.workflow_manage.get_reference_field(fields[0], fields[1:])
def execute(self, knowledge_id_list: List, search_mode: str, search_scope_type: str, search_scope_source: str,
search_scope_reference: List, question_reference: List, search_condition_type: str,
search_condition_list: List,
**kwargs) -> NodeResult:
if search_scope_type == 'custom': # 手动选择知识库
document_id_list = QuerySet(Document).filter(
knowledge_id__in=knowledge_id_list
).values_list('id', flat=True)
else: # 引用上一步知识库/文档
if search_scope_source == 'document': # 文档
document_id_list = self.get_reference_content(search_scope_reference)
else: # 知识库
document_id_list = QuerySet(Document).filter(
knowledge_id__in=self.get_reference_content(search_scope_reference)
).values_list('id', flat=True)
if search_mode == 'auto': # 通过问题自动检索
matched_doc_ids = self.handle_auto_tags(document_id_list, question_reference)
final_document_ids = list(matched_doc_ids)
else: # 自定义检索条件
matched_document_ids = self.handle_custom_tags(
document_id_list, search_condition_list, search_condition_type
)
final_document_ids = list(matched_document_ids)
# UUID to str
final_document_ids = [str(doc_id) for doc_id in final_document_ids]
document_items = QuerySet(Document).filter(id__in=final_document_ids).values()
final_knowledge_ids = list(set(str(doc['knowledge_id']) for doc in document_items))
knowledge_items = QuerySet(Knowledge).filter(id__in=final_knowledge_ids).values()
return NodeResult({
'document_list': final_document_ids,
'document_items': list(document_items),
'knowledge_list': final_knowledge_ids,
'knowledge_items': list(knowledge_items)
}, {})
def handle_auto_tags(self, document_id_list: list, question_reference: list):
question = self.get_reference_content(question_reference)
# 使用jieba分词
keywords = jieba.lcut(question)
if not keywords:
return set()
# 构建OR查询,一次性获取所有匹配的文档
q_objects = Q()
for keyword in keywords:
q_objects |= Q(tag__value__icontains=keyword)
# 单次数据库查询
matched_doc_ids = set(
QuerySet(DocumentTag)
.filter(document_id__in=document_id_list)
.filter(q_objects)
.values_list('document_id', flat=True)
.distinct()
)
return matched_doc_ids
def handle_custom_tags(self, document_id_list: List, search_condition_list: list, search_condition_type: str):
if not search_condition_list:
return set(document_id_list)
if search_condition_type == 'AND':
# AND逻辑:使用子查询和聚合
matched_doc_ids = set(document_id_list)
for condition in search_condition_list:
tag_key = condition['key']
field_value = self.workflow_manage.generate_prompt(condition['value'])
compare_type = condition['compare']
# 构建查询条件
if compare_type == 'contain':
q_filter = Q(tag__key=tag_key, tag__value__icontains=field_value)
elif compare_type == 'eq':
q_filter = Q(tag__key=tag_key, tag__value=field_value)
elif compare_type == 'not_contain':
q_filter = ~Q(tag__key=tag_key, tag__value__icontains=field_value)
else:
continue
# 单次查询获取符合条件的文档
tag_docs = set(QuerySet(DocumentTag).filter(
document_id__in=matched_doc_ids
).filter(q_filter).values_list('document_id', flat=True).distinct())
matched_doc_ids = matched_doc_ids.intersection(tag_docs)
return matched_doc_ids
else:
# OR逻辑:使用一次查询完成
q_objects = Q()
for condition in search_condition_list:
tag_key = condition['key']
field_value = self.workflow_manage.generate_prompt(condition['value'])
compare_type = condition['compare']
if compare_type == 'contain':
q_objects |= Q(tag__key=tag_key, tag__value__icontains=field_value)
elif compare_type == 'eq':
q_objects |= Q(tag__key=tag_key, tag__value=field_value)
elif compare_type == 'not_contain':
q_objects |= ~Q(tag__key=tag_key, tag__value__icontains=field_value)
# 一次查询获取所有匹配的文档
matched_docs = set(QuerySet(DocumentTag).filter(
document_id__in=document_id_list
).filter(q_objects).values_list('document_id', flat=True).distinct())
return matched_docs
def get_details(self, index: int, **kwargs):
return {
'name': self.node.properties.get('stepName'),
'question': self.context.get('question'),
"index": index,
'run_time': self.context.get('run_time'),
'document_list': self.context.get('document_list'),
'knowledge_list': self.context.get('knowledge_list'),
'document_items': self.context.get('document_items'),
'knowledge_items': self.context.get('knowledge_items'),
'type': self.node.type,
'status': self.status,
'err_message': self.err_message
}

View File

@ -3,6 +3,7 @@ import json
import os import os
import re import re
import traceback import traceback
from collections import defaultdict
from functools import reduce from functools import reduce
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from typing import Dict, List from typing import Dict, List
@ -13,6 +14,7 @@ from django.core import validators
from django.db import transaction, models from django.db import transaction, models
from django.db.models import QuerySet from django.db.models import QuerySet
from django.db.models.functions import Reverse, Substr from django.db.models.functions import Reverse, Substr
from django.db.models.query_utils import Q
from django.http import HttpResponse from django.http import HttpResponse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
@ -29,8 +31,9 @@ from common.utils.fork import Fork, ChildLink
from common.utils.logger import maxkb_logger from common.utils.logger import maxkb_logger
from common.utils.split_model import get_split_model from common.utils.split_model import get_split_model
from knowledge.models import Knowledge, KnowledgeScope, KnowledgeType, Document, Paragraph, Problem, \ from knowledge.models import Knowledge, KnowledgeScope, KnowledgeType, Document, Paragraph, Problem, \
ProblemParagraphMapping, TaskType, State, SearchMode, KnowledgeFolder, File ProblemParagraphMapping, TaskType, State, SearchMode, KnowledgeFolder, File, Tag
from knowledge.serializers.common import ProblemParagraphManage, drop_knowledge_index, get_embedding_model_id_by_knowledge_id, MetaSerializer, \ from knowledge.serializers.common import ProblemParagraphManage, drop_knowledge_index, \
get_embedding_model_id_by_knowledge_id, MetaSerializer, \
GenerateRelatedSerializer, get_embedding_model_by_knowledge_id, list_paragraph, write_image, zip_dir GenerateRelatedSerializer, get_embedding_model_by_knowledge_id, list_paragraph, write_image, zip_dir
from knowledge.serializers.document import DocumentSerializers from knowledge.serializers.document import DocumentSerializers
from knowledge.task.embedding import embedding_by_knowledge, delete_embedding_by_knowledge from knowledge.task.embedding import embedding_by_knowledge, delete_embedding_by_knowledge
@ -148,7 +151,8 @@ class KnowledgeSerializer(serializers.Serializer):
if "workspace_id" in self.data and self.data.get('workspace_id') is not None: if "workspace_id" in self.data and self.data.get('workspace_id') is not None:
query_set = query_set.filter(**{'temp.workspace_id': self.data.get("workspace_id")}) query_set = query_set.filter(**{'temp.workspace_id': self.data.get("workspace_id")})
folder_query_set = folder_query_set.filter(**{'workspace_id': self.data.get("workspace_id")}) folder_query_set = folder_query_set.filter(**{'workspace_id': self.data.get("workspace_id")})
if "folder_id" in self.data and self.data.get('folder_id') is not None and self.data.get('workspace_id') != self.data.get('folder_id'): if "folder_id" in self.data and self.data.get('folder_id') is not None and self.data.get(
'workspace_id') != self.data.get('folder_id'):
query_set = query_set.filter(**{'temp.folder_id': self.data.get("folder_id")}) query_set = query_set.filter(**{'temp.folder_id': self.data.get("folder_id")})
folder_query_set = folder_query_set.filter(**{'parent_id': self.data.get("folder_id")}) folder_query_set = folder_query_set.filter(**{'parent_id': self.data.get("folder_id")})
if "scope" in self.data and self.data.get('scope') is not None: if "scope" in self.data and self.data.get('scope') is not None:
@ -764,3 +768,50 @@ class KnowledgeSerializer(serializers.Serializer):
'comprehensive_score': hit_dict.get(p.get('id')).get('comprehensive_score') 'comprehensive_score': hit_dict.get(p.get('id')).get('comprehensive_score')
} for p in p_list } for p in p_list
] ]
class Tags(serializers.Serializer):
workspace_id = serializers.CharField(required=True, label=_('workspace id'))
user_id = serializers.UUIDField(required=True, label=_('user id'))
knowledge_ids = serializers.ListField(
required=True, label=_('knowledge ids'),
child=serializers.UUIDField(required=True, label=_('id'))
)
def list(self):
self.is_valid(raise_exception=True)
if self.data.get('name'):
name = self.data.get('name')
tags = QuerySet(Tag).filter(
knowledge_id__in=self.data.get('knowledge_ids')
).filter(
Q(key__icontains=name) | Q(value__icontains=name)
).values('key', 'value', 'id', 'create_time', 'update_time').order_by('create_time', 'key', 'value')
else:
# 获取所有标签,按创建时间排序保持稳定顺序
tags = QuerySet(Tag).filter(
knowledge_id__in=self.data.get('knowledge_ids')
).values('key', 'value', 'id', 'create_time', 'update_time').order_by('create_time', 'key', 'value')
# 按key分组
grouped_tags = defaultdict(list)
for tag in tags:
grouped_tags[tag['key']].append({
'id': tag['id'],
'value': tag['value'],
'create_time': tag['create_time'],
'update_time': tag['update_time']
})
# 转换为期望的格式保持key的顺序
result = []
# 按key排序以确保结果顺序一致
for key in sorted(grouped_tags.keys()):
values = grouped_tags[key]
# 按创建时间对values进行排序
values.sort(key=lambda x: x['create_time'])
result.append({
'key': key,
'values': values,
})
return result

View File

@ -12,6 +12,7 @@ urlpatterns = [
path('workspace/<str:workspace_id>/knowledge/web', views.KnowledgeWebView.as_view()), path('workspace/<str:workspace_id>/knowledge/web', views.KnowledgeWebView.as_view()),
path('workspace/<str:workspace_id>/knowledge/model', views.KnowledgeView.Model.as_view()), path('workspace/<str:workspace_id>/knowledge/model', views.KnowledgeView.Model.as_view()),
path('workspace/<str:workspace_id>/knowledge/embedding_model', views.KnowledgeView.EmbeddingModel.as_view()), path('workspace/<str:workspace_id>/knowledge/embedding_model', views.KnowledgeView.EmbeddingModel.as_view()),
path('workspace/<str:workspace_id>/knowledge/tags', views.KnowledgeView.Tags.as_view()),
path('workspace/<str:workspace_id>/knowledge/<str:knowledge_id>', views.KnowledgeView.Operate.as_view()), path('workspace/<str:workspace_id>/knowledge/<str:knowledge_id>', views.KnowledgeView.Operate.as_view()),
path('workspace/<str:workspace_id>/knowledge/<str:knowledge_id>/sync', views.KnowledgeView.SyncWeb.as_view()), path('workspace/<str:workspace_id>/knowledge/<str:knowledge_id>/sync', views.KnowledgeView.SyncWeb.as_view()),
path('workspace/<str:workspace_id>/knowledge/<str:knowledge_id>/generate_related', views.KnowledgeView.GenerateRelated.as_view()), path('workspace/<str:workspace_id>/knowledge/<str:knowledge_id>/generate_related', views.KnowledgeView.GenerateRelated.as_view()),

View File

@ -382,6 +382,29 @@ class KnowledgeView(APIView):
} }
).list(workspace_id, True)) ).list(workspace_id, True))
class Tags(APIView):
authentication_classes = [TokenAuth]
@extend_schema(
methods=['GET'],
description=_('Get all tags of knowledge base'),
summary=_('Get all tags of knowledge base'),
operation_id=_('Get all tags of knowledge base'), # type: ignore
parameters=KnowledgeReadAPI.get_parameters(),
responses=KnowledgeReadAPI.get_response(),
tags=[_('Knowledge Base')] # type: ignore
)
@has_permissions(
PermissionConstants.KNOWLEDGE_READ.get_workspace_permission(),
RoleConstants.WORKSPACE_MANAGE.get_workspace_role(), RoleConstants.USER.get_workspace_role()
)
def get(self, request: Request, workspace_id: str):
return result.success(KnowledgeSerializer.Tags(data={
'user_id': request.user.id,
'workspace_id': workspace_id,
'knowledge_ids': request.query_params.getlist('knowledge_ids[]')
}).list())
class KnowledgeBaseView(APIView): class KnowledgeBaseView(APIView):
authentication_classes = [TokenAuth] authentication_classes = [TokenAuth]

View File

@ -255,6 +255,12 @@ const putLarkKnowledge: (
return put(`${prefix.value}/lark/${knowledge_id}`, data, undefined, loading) return put(`${prefix.value}/lark/${knowledge_id}`, data, undefined, loading)
} }
const getAllTags: (params: any, loading?: Ref<boolean>) => Promise<Result<any>> = (
params,
loading,
) => {
return get(`${prefix.value}/tags`, params, loading)
}
const getTags: (knowledge_id: string, params: any, loading?: Ref<boolean>) => Promise<Result<any>> = ( const getTags: (knowledge_id: string, params: any, loading?: Ref<boolean>) => Promise<Result<any>> = (
knowledge_id, knowledge_id,
@ -315,6 +321,7 @@ export default {
postWebKnowledge, postWebKnowledge,
postLarkKnowledge, postLarkKnowledge,
putLarkKnowledge, putLarkKnowledge,
getAllTags,
getTags, getTags,
postTags, postTags,
putTag, putTag,

View File

@ -205,6 +205,13 @@ const putLarkKnowledge: (
return put(`${prefix}/lark/${knowledge_id}`, data, undefined, loading) return put(`${prefix}/lark/${knowledge_id}`, data, undefined, loading)
} }
const getAllTags: (params: any, loading?: Ref<boolean>) => Promise<Result<any>> = (
params,
loading,
) => {
return get(`${prefix}/tags`, params, loading)
}
const getTags: (knowledge_id: string, params: any, loading?: Ref<boolean>) => Promise<Result<any>> = ( const getTags: (knowledge_id: string, params: any, loading?: Ref<boolean>) => Promise<Result<any>> = (
knowledge_id, knowledge_id,
params, params,
@ -263,6 +270,7 @@ export default {
putSyncWebKnowledge, putSyncWebKnowledge,
getKnowledgeModel, getKnowledgeModel,
putLarkKnowledge, putLarkKnowledge,
getAllTags,
getTags, getTags,
postTags, postTags,
putTag, putTag,

View File

@ -249,6 +249,13 @@ const putLarkKnowledge: (
return put(`${prefix}/lark/${knowledge_id}`, data, undefined, loading) return put(`${prefix}/lark/${knowledge_id}`, data, undefined, loading)
} }
const getAllTags: (params: any, loading?: Ref<boolean>) => Promise<Result<any>> = (
params,
loading,
) => {
return get(`${prefix}/tags`, params, loading)
}
const getTags: (knowledge_id: string, params: any, loading?: Ref<boolean>) => Promise<Result<any>> = ( const getTags: (knowledge_id: string, params: any, loading?: Ref<boolean>) => Promise<Result<any>> = (
knowledge_id, knowledge_id,
params, params,
@ -308,6 +315,7 @@ export default {
postWebKnowledge, postWebKnowledge,
postLarkKnowledge, postLarkKnowledge,
putLarkKnowledge, putLarkKnowledge,
getAllTags,
getTags, getTags,
postTags, postTags,
putTag, putTag,

View File

@ -9,6 +9,7 @@ export enum WorkflowType {
Start = 'start-node', Start = 'start-node',
AiChat = 'ai-chat-node', AiChat = 'ai-chat-node',
SearchKnowledge = 'search-knowledge-node', SearchKnowledge = 'search-knowledge-node',
SearchDocument = 'search-document-node',
Question = 'question-node', Question = 'question-node',
Condition = 'condition-node', Condition = 'condition-node',
Reply = 'reply-node', Reply = 'reply-node',

View File

@ -147,6 +147,27 @@ export default {
requiredMessage: '请选择检索问题', requiredMessage: '请选择检索问题',
}, },
}, },
searchDocumentNode: {
label: '文档检索',
text: '从设定的检索范围中,根据文档标签检索出满足条件的文档',
selectKnowledge: '检索范围',
searchSetting: '检索设置',
custom: '手动',
auto: '自动',
document_list: '文档列表',
knowledge_list: '知识库列表',
result: '检索结果',
searchParam: '检索参数',
showKnowledge: {
label: '结果显示在知识来源中',
requiredMessage: '请设置参数',
},
searchQuestion: {
label: '检索问题',
placeholder: '请选择检索问题',
requiredMessage: '请选择检索问题',
},
},
questionNode: { questionNode: {
label: '问题优化', label: '问题优化',
text: '根据历史聊天记录优化完善当前问题,更利于匹配知识库分段', text: '根据历史聊天记录优化完善当前问题,更利于匹配知识库分段',

View File

@ -1,5 +1,5 @@
import {WorkflowType, WorkflowMode} from '@/enums/application' import { WorkflowType, WorkflowMode } from '@/enums/application'
import {t} from '@/locales' import { t } from '@/locales'
export const startNode = { export const startNode = {
id: WorkflowType.Start, id: WorkflowType.Start,
@ -120,6 +120,32 @@ export const searchKnowledgeNode = {
}, },
}, },
} }
/**
*
*/
export const searchDocumentNode = {
type: WorkflowType.SearchDocument,
text: t('views.applicationWorkflow.nodes.searchDocumentNode.text'),
label: t('views.applicationWorkflow.nodes.searchDocumentNode.label'),
height: 355,
properties: {
stepName: t('views.applicationWorkflow.nodes.searchDocumentNode.label'),
config: {
fields: [
{
label: t( 'views.applicationWorkflow.nodes.searchDocumentNode.knowledge_list'),
value: 'knowledge_list',
},
{
label: t('views.applicationWorkflow.nodes.searchDocumentNode.document_list'),
value: 'document_list',
},
],
},
},
}
export const questionNode = { export const questionNode = {
type: WorkflowType.Question, type: WorkflowType.Question,
text: t('views.applicationWorkflow.nodes.questionNode.text'), text: t('views.applicationWorkflow.nodes.questionNode.text'),
@ -573,7 +599,10 @@ export const menuNodes = [
questionNode, questionNode,
], ],
}, },
{label: t('views.knowledge.title'), list: [searchKnowledgeNode, rerankerNode]}, {
label: t('views.knowledge.title'),
list: [searchKnowledgeNode, searchDocumentNode, rerankerNode]
},
{ {
label: t('views.applicationWorkflow.nodes.classify.businessLogic'), label: t('views.applicationWorkflow.nodes.classify.businessLogic'),
list: [conditionNode, formNode, variableAssignNode, replyNode, loopNode], list: [conditionNode, formNode, variableAssignNode, replyNode, loopNode],
@ -603,7 +632,10 @@ export const applicationLoopMenuNodes = [
imageToVideoNode, imageToVideoNode,
], ],
}, },
{label: t('views.knowledge.title'), list: [searchKnowledgeNode, rerankerNode]}, {
label: t('views.knowledge.title'),
list: [searchKnowledgeNode, searchDocumentNode, rerankerNode]
},
{ {
label: t('views.applicationWorkflow.nodes.classify.businessLogic'), label: t('views.applicationWorkflow.nodes.classify.businessLogic'),
list: [conditionNode, formNode, variableAssignNode, replyNode, loopContinueNode, loopBreakNode], list: [conditionNode, formNode, variableAssignNode, replyNode, loopContinueNode, loopBreakNode],
@ -688,6 +720,7 @@ export const compareList = [
export const nodeDict: any = { export const nodeDict: any = {
[WorkflowType.AiChat]: aiChatNode, [WorkflowType.AiChat]: aiChatNode,
[WorkflowType.SearchKnowledge]: searchKnowledgeNode, [WorkflowType.SearchKnowledge]: searchKnowledgeNode,
[WorkflowType.SearchDocument]: searchDocumentNode,
[WorkflowType.Question]: questionNode, [WorkflowType.Question]: questionNode,
[WorkflowType.Condition]: conditionNode, [WorkflowType.Condition]: conditionNode,
[WorkflowType.Base]: baseNode, [WorkflowType.Base]: baseNode,

View File

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

View File

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

View File

@ -0,0 +1,335 @@
<template>
<NodeContainer :nodeModel="nodeModel">
<h5 class="title-decoration-1 mb-8">{{ $t('views.applicationWorkflow.nodeSetting') }}</h5>
<el-card shadow="never" class="card-never">
<el-form
@submit.prevent
:model="form_data"
label-position="top"
require-asterisk-position="right"
label-width="auto"
ref="knowledgeNodeFormRef"
>
<el-form-item :label="$t('views.chatLog.selectKnowledge')">
<template #label>
<div class="flex-between">
<span>
{{ $t('views.applicationWorkflow.nodes.searchDocumentNode.selectKnowledge') }}
</span>
<span>
<el-button type="primary" link @click="openKnowledgeDialog">
<AppIcon iconName="app-add-outlined"></AppIcon>
</el-button>
<el-select
:teleported="false"
size="small"
v-model="form_data.search_scope_type"
style="width: 85px"
>
<el-option
:label="$t('views.applicationWorkflow.nodes.searchDocumentNode.custom')"
value="custom"
/>
<el-option
:label="$t('views.applicationWorkflow.variable.Referencing')"
value="referencing"
/>
</el-select>
</span>
</div>
</template>
<div class="w-full" v-if="form_data.search_scope_type === 'custom'">
<el-text type="info" v-if="form_data.knowledge_id_list?.length === 0">
{{ $t('views.application.form.relatedKnowledge.placeholder') }}
</el-text>
<template v-for="(item, index) in form_data.knowledge_id_list" :key="index" v-else>
<div class="flex-between border border-r-6 white-bg mb-4" style="padding: 5px 8px">
<div class="flex align-center" style="line-height: 20px">
<KnowledgeIcon
:type="relatedObject(knowledgeList, item, 'id')?.type"
class="mr-8"
:size="20"
/>
<div class="ellipsis" :title="relatedObject(knowledgeList, item, 'id')?.name">
{{ relatedObject(knowledgeList, item, 'id')?.name }}
</div>
</div>
<el-button text @click="removeKnowledge(item)">
<el-icon>
<Close/>
</el-icon>
</el-button>
</div>
</template>
</div>
<div class="w-full" v-else>
<el-form-item
prop="search_scope_reference"
:rules="{
message: $t('views.applicationWorkflow.variable.placeholder'),
trigger: 'blur',
required: true,
}"
>
<template #label>
<div class="flex-between">
<span>
{{ $t('选择变量') }}
</span>
<span>
<el-button type="primary" link @click="openKnowledgeDialog">
<AppIcon iconName="app-add-outlined"></AppIcon>
</el-button>
<el-select
:teleported="false"
size="small"
v-model="form_data.search_scope_source"
style="width: 95px"
@change="form_data.search_scope_reference = []"
>
<el-option
:label="$t('知识库列表')"
value="knowledge"
/>
<el-option
:label="$t('文档列表')"
value="document"
/>
</el-select>
</span>
</div>
</template>
<NodeCascader
ref="nodeCascaderRef"
:nodeModel="nodeModel"
class="w-full"
:placeholder="$t('views.applicationWorkflow.variable.placeholder')"
v-model="form_data.search_scope_reference"
/>
</el-form-item>
</div>
</el-form-item>
<el-form-item
:label="$t('views.applicationWorkflow.nodes.searchDocumentNode.searchSetting')">
<el-radio-group v-model="form_data.search_mode">
<el-radio value="auto">
{{ $t('views.applicationWorkflow.nodes.searchDocumentNode.auto') }}
<el-tooltip content="根据检索问题自动匹配文档标签" placement="top">
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
</el-tooltip>
</el-radio>
<el-radio value="custom">
{{ $t('views.applicationWorkflow.nodes.searchDocumentNode.custom') }}
<el-tooltip content="手动设置标签过滤条件" placement="top">
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
</el-tooltip>
</el-radio>
</el-radio-group>
</el-form-item>
<div class="w-full">
<el-form-item
v-if="form_data.search_mode === 'auto'"
:label="$t('views.applicationWorkflow.nodes.searchKnowledgeNode.searchQuestion.label')"
prop="question_reference"
:rules="{
message: $t(
'views.applicationWorkflow.nodes.searchKnowledgeNode.searchQuestion.requiredMessage',
),
trigger: 'blur',
required: true,
}"
>
<NodeCascader
ref="nodeCascaderRef"
:nodeModel="nodeModel"
class="w-full"
:placeholder="
$t('views.applicationWorkflow.nodes.searchKnowledgeNode.searchQuestion.placeholder')
"
v-model="form_data.question_reference"
/>
</el-form-item>
<div v-else>
<div>
<span>
{{ $t('views.applicationWorkflow.nodes.conditionNode.conditions.info') }}
</span>
<el-select v-model="form_data.search_condition_type" size="small"
style="width: 60px; margin: 0 8px">
<el-option
:label="$t('views.applicationWorkflow.condition.AND')"
value="AND"
/>
<el-option :label="$t('views.applicationWorkflow.condition.OR')" value="OR"/>
</el-select>
<span>
{{ $t('views.applicationWorkflow.nodes.conditionNode.conditions.label') }}
</span>
</div>
<div v-for="(c, index) in form_data.search_condition_list" :key="index">
<el-select v-model="c.key" style="width: 70px;" size="small">
<el-option v-for="tag in form_data.knowledge_tags" :key="tag" :label="tag.key" :value="tag.key"/>
</el-select>
<el-select v-model="c.compare" style="width: 60px;" size="small">
<el-option v-for="item in compareList" :key="item.value" :value="item.value"
:label="item.label"/>
</el-select>
<el-input v-model="c.value" style="width: 80px" size="small"></el-input>
<el-button text @click="delCondition(index)">
<AppIcon iconName="app-delete"></AppIcon>
</el-button>
</div>
<el-button link type="primary" @click="addCondition" class="mt-4">
<AppIcon iconName="app-add-outlined" class="mr-4"></AppIcon>
{{ $t('views.applicationWorkflow.nodes.conditionNode.addCondition') }}
</el-button>
</div>
</div>
</el-form>
</el-card>
<AddKnowledgeDialog
ref="AddKnowledgeDialogRef"
@addData="addKnowledge"
:data="knowledgeList"
:loading="knowledgeLoading"
/>
</NodeContainer>
</template>
<script setup lang="ts">
import { cloneDeep, set } from 'lodash'
import NodeContainer from '@/workflow/common/NodeContainer.vue'
import NodeCascader from '@/workflow/common/NodeCascader.vue'
import AddKnowledgeDialog from '@/views/application/component/AddKnowledgeDialog.vue'
import type { FormInstance } from 'element-plus'
import { computed, onMounted, ref, watch } from 'vue'
import { relatedObject } from '@/utils/array'
import { t } from "@/locales";
import AppIcon from "@/components/app-icon/AppIcon.vue";
import { loadSharedApi } from '@/utils/dynamics-api/shared-api'
import { useRoute } from 'vue-router'
const route = useRoute()
const props = defineProps<{ nodeModel: any }>()
const nodeCascaderRef = ref()
const compareList = [
{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')},
]
const apiType = computed(() => {
if (route.path.includes('shared')) {
return 'systemShare'
} else if (route.path.includes('resource-management')) {
return 'systemManage'
} else if (route.path.includes('share/')) {
return 'workspaceShare'
} else {
return 'workspace'
}
})
const form = {
knowledge_id_list: [],
search_scope_type: 'custom',
search_scope_source: 'knowledge',
search_scope_reference: [],
search_mode: 'auto',
question_reference: [],
search_condition_type: 'AND',
search_condition_list: [],
knowledge_tags: [],
}
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 knowledgeNodeFormRef = ref<FormInstance>()
const AddKnowledgeDialogRef = ref<InstanceType<typeof AddKnowledgeDialog>>()
const knowledgeList = ref<any>([])
const knowledgeLoading = ref(false)
function removeKnowledge(id: any) {
const list = props.nodeModel.properties.node_data.knowledge_id_list.filter((v: any) => v !== id)
set(props.nodeModel.properties.node_data, 'knowledge_id_list', list)
}
function addKnowledge(val: Array<any>) {
set(props.nodeModel.properties.node_data, 'knowledge_id_list', val.map((item) => item.id))
set(props.nodeModel.properties.node_data, 'knowledge_list', val)
knowledgeList.value = val
}
function openKnowledgeDialog() {
if (AddKnowledgeDialogRef.value) {
AddKnowledgeDialogRef.value.open(form_data.value.knowledge_id_list)
}
}
function addCondition() {
const list = cloneDeep(form_data.value.search_condition_list)
list.push({
key: '',
compare: 'contain',
value: '',
})
set(form_data.value, 'search_condition_list', list)
}
function delCondition(index: number) {
const list = cloneDeep(form_data.value.search_condition_list)
list.splice(index, 1)
set(form_data.value, 'search_condition_list', list)
}
function getAllTags(knowledge_ids: any) {
if (knowledge_ids.length === 0) {
set(form_data.value, 'knowledge_tags', [])
return
}
loadSharedApi({type: 'knowledge', systemType: apiType.value})
.getAllTags({knowledge_ids: knowledge_ids}, {})
.then((res: any) => {
set(form_data.value, 'knowledge_tags', res.data)
})
}
watch(
() => form_data.value.knowledge_id_list,
(val) => {
getAllTags(val)
}
)
const validate = () => {
return Promise.all([
nodeCascaderRef.value?.validate(),
knowledgeNodeFormRef.value?.validate(),
]).catch((err) => {
return Promise.reject({node: props.nodeModel, errMessage: err})
})
}
onMounted(() => {
// console.log(props.nodeModel.properties.node_data)
knowledgeList.value = props.nodeModel.properties.node_data.knowledge_list
set(props.nodeModel, 'validate', validate)
})
</script>
<style lang="scss" scoped></style>