|
| 1 | +# coding=utf-8 |
| 2 | +""" |
| 3 | + @project: MaxKB |
| 4 | + @Author:虎虎虎 |
| 5 | + @file: search_knowledge_node.py |
| 6 | + @date:2026/7/2 10:00 |
| 7 | + @desc: |
| 8 | +""" |
| 9 | +import os |
| 10 | +import re |
| 11 | +from typing import List, Dict |
| 12 | + |
| 13 | +from django.core import validators |
| 14 | +from django.db import connection |
| 15 | +from django.db.models import QuerySet |
| 16 | +from django.utils.translation import gettext_lazy as _ |
| 17 | +from rest_framework import serializers |
| 18 | + |
| 19 | +from application.workflow.common import WorkflowType |
| 20 | +from application.workflow.i_node import INode |
| 21 | +from common.config.embedding_config import VectorStore |
| 22 | +from common.constants.permission_constants import RoleConstants |
| 23 | +from common.database_model_manage.database_model_manage import DatabaseModelManage |
| 24 | +from common.db.search import native_search |
| 25 | +from common.utils.common import flat_map, get_file_content |
| 26 | +from common.utils.shared_resource_auth import filter_authorized_ids |
| 27 | +from knowledge.models import Document, Paragraph, Knowledge, SearchMode |
| 28 | +from maxkb.conf import PROJECT_DIR |
| 29 | +from models_provider.tools import get_model_instance_by_model_workspace_id |
| 30 | + |
| 31 | + |
| 32 | +class DatasetSettingSerializer(serializers.Serializer): |
| 33 | + top_n = serializers.IntegerField(required=True, label=_("Reference segment number")) |
| 34 | + similarity = serializers.FloatField(required=True, max_value=2, min_value=0, label=_('similarity')) |
| 35 | + search_mode = serializers.CharField(required=True, validators=[ |
| 36 | + validators.RegexValidator(regex=re.compile("^embedding|keywords|blend$"), |
| 37 | + message=_("The type only supports embedding|keywords|blend"), code=500) |
| 38 | + ], label=_("Retrieval Mode")) |
| 39 | + max_paragraph_char_number = serializers.IntegerField(required=True, |
| 40 | + label=_("Maximum number of words in a quoted segment")) |
| 41 | + |
| 42 | + |
| 43 | +class SearchKnowledgeNodeSerializer(serializers.Serializer): |
| 44 | + knowledge_id_list = serializers.ListField(required=True, child=serializers.UUIDField(required=True), |
| 45 | + label=_("Dataset id list")) |
| 46 | + knowledge_setting = DatasetSettingSerializer(required=True) |
| 47 | + question_reference_address = serializers.ListField(required=True) |
| 48 | + show_knowledge = serializers.BooleanField(required=True, |
| 49 | + label=_("The results are displayed in the knowledge sources")) |
| 50 | + search_scope_type = serializers.ChoiceField( |
| 51 | + required=False, choices=['custom', 'referencing'], label=_("search scope type"), |
| 52 | + allow_null=True, default='custom' |
| 53 | + ) |
| 54 | + search_scope_source = serializers.ChoiceField( |
| 55 | + required=False, choices=['document', 'knowledge'], |
| 56 | + label=_("search scope variable type"), default='knowledge' |
| 57 | + ) |
| 58 | + search_scope_reference = serializers.ListField( |
| 59 | + required=False, label=_("search scope variable"), default=list |
| 60 | + ) |
| 61 | + |
| 62 | + |
| 63 | +def _get_paragraph_list(chat_record, node_id): |
| 64 | + return flat_map([chat_record.details[key].get('paragraph_list', []) for key in chat_record.details if |
| 65 | + (chat_record.details[ |
| 66 | + key].get('type', '') == 'search-dataset-node') and chat_record.details[key].get( |
| 67 | + 'paragraph_list', []) is not None and key == node_id]) |
| 68 | + |
| 69 | + |
| 70 | +def _get_embedding_id(dataset_id_list): |
| 71 | + dataset_list = QuerySet(Knowledge).filter(id__in=dataset_id_list) |
| 72 | + if len(set([dataset.embedding_model_id for dataset in dataset_list])) > 1: |
| 73 | + raise Exception("关联知识库的向量模型不一致,无法召回分段。") |
| 74 | + if len(dataset_list) == 0: |
| 75 | + raise Exception("知识库设置错误,请重新设置知识库") |
| 76 | + return dataset_list[0].embedding_model_id |
| 77 | + |
| 78 | + |
| 79 | +def _reset_title(title): |
| 80 | + if title is None or len(title.strip()) == 0: |
| 81 | + return "" |
| 82 | + else: |
| 83 | + return f"#### {title}\n" |
| 84 | + |
| 85 | + |
| 86 | +def _reset_meta(meta): |
| 87 | + if not meta.get('allow_download', False): |
| 88 | + return {'allow_download': False} |
| 89 | + return meta |
| 90 | + |
| 91 | + |
| 92 | +def _reset_paragraph(paragraph: Dict, embedding_list: List): |
| 93 | + filter_embedding_list = [embedding for embedding in embedding_list if |
| 94 | + str(embedding.get('paragraph_id')) == str(paragraph.get('id'))] |
| 95 | + if filter_embedding_list is not None and len(filter_embedding_list) > 0: |
| 96 | + find_embedding = filter_embedding_list[-1] |
| 97 | + return { |
| 98 | + **paragraph, |
| 99 | + 'similarity': find_embedding.get('similarity'), |
| 100 | + 'is_hit_handling_method': find_embedding.get('similarity') > paragraph.get( |
| 101 | + 'directly_return_similarity') and paragraph.get('hit_handling_method') == 'directly_return', |
| 102 | + 'update_time': paragraph.get('update_time').strftime("%Y-%m-%d %H:%M:%S"), |
| 103 | + 'create_time': paragraph.get('create_time').strftime("%Y-%m-%d %H:%M:%S"), |
| 104 | + 'id': str(paragraph.get('id')), |
| 105 | + 'knowledge_id': str(paragraph.get('knowledge_id')), |
| 106 | + 'document_id': str(paragraph.get('document_id')), |
| 107 | + 'meta': _reset_meta(paragraph.get('meta')) |
| 108 | + } |
| 109 | + |
| 110 | + |
| 111 | +def _list_paragraph(embedding_list: List, vector): |
| 112 | + paragraph_id_list = [row.get('paragraph_id') for row in embedding_list] |
| 113 | + if paragraph_id_list is None or len(paragraph_id_list) == 0: |
| 114 | + return [] |
| 115 | + paragraph_list = native_search(QuerySet(Paragraph).filter(id__in=paragraph_id_list), |
| 116 | + get_file_content( |
| 117 | + os.path.join(PROJECT_DIR, "apps", "application", 'sql', |
| 118 | + 'list_knowledge_paragraph_by_paragraph_id.sql')), |
| 119 | + with_table_name=True) |
| 120 | + if len(paragraph_list) != len(paragraph_id_list): |
| 121 | + exist_paragraph_list = [row.get('id') for row in paragraph_list] |
| 122 | + for paragraph_id in paragraph_id_list: |
| 123 | + if paragraph_id not in exist_paragraph_list: |
| 124 | + vector.delete_by_paragraph_id(paragraph_id) |
| 125 | + return paragraph_list |
| 126 | + |
| 127 | + |
| 128 | +class SearchKnowledgeNode(INode): |
| 129 | + serializer_class = SearchKnowledgeNodeSerializer |
| 130 | + supported_workflow_type_list = [WorkflowType.APPLICATION, WorkflowType.TOOL] |
| 131 | + type = 'search-knowledge-node' |
| 132 | + |
| 133 | + def execute(self): |
| 134 | + node_params = self.get_parameters() |
| 135 | + workflow_params = self.get_workflow_parameters() |
| 136 | + |
| 137 | + knowledge_id_list = node_params.get('knowledge_id_list', []) |
| 138 | + knowledge_setting = node_params.get('knowledge_setting', {}) |
| 139 | + question_reference_address = node_params.get('question_reference_address', []) |
| 140 | + show_knowledge = node_params.get('show_knowledge', False) |
| 141 | + search_scope_type = node_params.get('search_scope_type', 'custom') |
| 142 | + search_scope_source = node_params.get('search_scope_source', 'knowledge') |
| 143 | + search_scope_reference = node_params.get('search_scope_reference', []) |
| 144 | + |
| 145 | + question = str(self.workflow_manage.get_reference_field( |
| 146 | + question_reference_address[0], question_reference_address[1:])) |
| 147 | + |
| 148 | + exclude_paragraph_id_list = [] |
| 149 | + if workflow_params.get('re_chat', False): |
| 150 | + history_chat_record = workflow_params.get('history_chat_record', []) |
| 151 | + paragraph_id_list = [p.get('id') for p in flat_map( |
| 152 | + [_get_paragraph_list(chat_record, self.get_node_id()) for chat_record in history_chat_record if |
| 153 | + chat_record.problem_text == question])] |
| 154 | + exclude_paragraph_id_list = list(set(paragraph_id_list)) |
| 155 | + |
| 156 | + self.write_context('question', question) |
| 157 | + self.write_context('show_knowledge', show_knowledge) |
| 158 | + |
| 159 | + document_id_list = None |
| 160 | + if search_scope_type == 'referencing': |
| 161 | + if search_scope_source == 'knowledge': |
| 162 | + knowledge_id_list = self._get_reference_content(search_scope_reference) |
| 163 | + else: |
| 164 | + document_id_list = self._get_reference_content(search_scope_reference) |
| 165 | + knowledge_id_list = [str(k) for k in QuerySet(Document).filter( |
| 166 | + id__in=document_id_list |
| 167 | + ).values_list('knowledge_id', flat=True).distinct()] |
| 168 | + |
| 169 | + get_knowledge_list_of_authorized = DatabaseModelManage.get_model('get_knowledge_list_of_authorized') |
| 170 | + chat_user_type = workflow_params.get('chat_user_type') |
| 171 | + if get_knowledge_list_of_authorized is not None and RoleConstants.CHAT_USER.value.name == chat_user_type: |
| 172 | + knowledge_id_list = get_knowledge_list_of_authorized( |
| 173 | + workflow_params.get('chat_user_id'), knowledge_id_list) |
| 174 | + |
| 175 | + workspace_id = workflow_params.get('workspace_id') |
| 176 | + knowledge_id_list = filter_authorized_ids('knowledge', knowledge_id_list, workspace_id) |
| 177 | + |
| 178 | + if len(knowledge_id_list) == 0: |
| 179 | + self._write_empty_result(question) |
| 180 | + return |
| 181 | + |
| 182 | + model_id = _get_embedding_id(knowledge_id_list) |
| 183 | + embedding_model = get_model_instance_by_model_workspace_id(model_id, workspace_id) |
| 184 | + embedding_value = embedding_model.embed_query(question) |
| 185 | + vector = VectorStore.get_embedding_vector() |
| 186 | + |
| 187 | + exclude_document_id_list = [str(document.id) for document in |
| 188 | + QuerySet(Document).filter(knowledge_id__in=knowledge_id_list, is_active=False)] |
| 189 | + |
| 190 | + embedding_list = vector.query(question, embedding_value, knowledge_id_list, document_id_list, |
| 191 | + exclude_document_id_list, exclude_paragraph_id_list, True, |
| 192 | + knowledge_setting.get('top_n'), knowledge_setting.get('similarity'), |
| 193 | + SearchMode(knowledge_setting.get('search_mode'))) |
| 194 | + |
| 195 | + connection.close() |
| 196 | + |
| 197 | + if embedding_list is None: |
| 198 | + self._write_empty_result(question) |
| 199 | + return |
| 200 | + |
| 201 | + paragraph_list = _list_paragraph(embedding_list, vector) |
| 202 | + result = [_reset_paragraph(paragraph, embedding_list) for paragraph in paragraph_list] |
| 203 | + result = sorted(result, key=lambda p: p.get('similarity'), reverse=True) |
| 204 | + |
| 205 | + self.write_context('paragraph_list', result) |
| 206 | + self.write_context('is_hit_handling_method_list', |
| 207 | + [row for row in result if row.get('is_hit_handling_method')]) |
| 208 | + self.write_context('data', '\n'.join( |
| 209 | + [f"{_reset_title(paragraph.get('title', ''))}{paragraph.get('content')}" for paragraph in |
| 210 | + result])[0:knowledge_setting.get('max_paragraph_char_number', 5000)]) |
| 211 | + self.write_context('directly_return', '\n'.join( |
| 212 | + [paragraph.get('content') for paragraph in result if paragraph.get('is_hit_handling_method')])) |
| 213 | + |
| 214 | + def _write_empty_result(self, question): |
| 215 | + self.write_context('paragraph_list', []) |
| 216 | + self.write_context('is_hit_handling_method_list', []) |
| 217 | + self.write_context('data', '') |
| 218 | + self.write_context('directly_return', '') |
| 219 | + self.write_context('question', question) |
| 220 | + |
| 221 | + def _get_reference_content(self, fields: List[str]): |
| 222 | + if fields: |
| 223 | + return self.workflow_manage.get_reference_field(fields[0], fields[1:]) |
| 224 | + return None |
0 commit comments