muryshev commited on
Commit
308de05
·
1 Parent(s): 5dee1a1
common/auth.py CHANGED
@@ -9,7 +9,7 @@ import os
9
  # Секретный ключ для JWT
10
  SECRET_KEY = os.environ.get("JWT_SECRET", "ooooooh_thats_my_super_secret_key")
11
  ALGORITHM = "HS256"
12
- ACCESS_TOKEN_EXPIRE_MINUTES = 30
13
 
14
  # Захардкоженные пользователи
15
  USERS = [
 
9
  # Секретный ключ для JWT
10
  SECRET_KEY = os.environ.get("JWT_SECRET", "ooooooh_thats_my_super_secret_key")
11
  ALGORITHM = "HS256"
12
+ ACCESS_TOKEN_EXPIRE_MINUTES = 1440
13
 
14
  # Захардкоженные пользователи
15
  USERS = [
common/dependencies.py CHANGED
@@ -42,7 +42,9 @@ def get_embedding_extractor(
42
  )
43
 
44
 
45
- def get_chunk_repository(db: Annotated[sessionmaker, Depends(get_db)]) -> ChunkRepository:
 
 
46
  """Получение репозитория чанков через DI."""
47
  return ChunkRepository(db)
48
 
@@ -53,32 +55,6 @@ def get_injection_builder(
53
  return InjectionBuilder(chunk_repository)
54
 
55
 
56
- def get_entity_service(
57
- vectorizer: Annotated[EmbeddingExtractor, Depends(get_embedding_extractor)],
58
- chunk_repository: Annotated[ChunkRepository, Depends(get_chunk_repository)],
59
- config: Annotated[Configuration, Depends(get_config)],
60
- ) -> EntityService:
61
- """Получение сервиса для работы с сущностями через DI."""
62
- return EntityService(vectorizer, chunk_repository, config)
63
-
64
-
65
- def get_dataset_service(
66
- entity_service: Annotated[EntityService, Depends(get_entity_service)],
67
- config: Annotated[Configuration, Depends(get_config)],
68
- db: Annotated[sessionmaker, Depends(get_db)],
69
- ) -> DatasetService:
70
- """Получение сервиса для работы с датасетами через DI."""
71
- return DatasetService(entity_service, config, db)
72
-
73
-
74
- def get_document_service(
75
- dataset_service: Annotated[DatasetService, Depends(get_dataset_service)],
76
- config: Annotated[Configuration, Depends(get_config)],
77
- db: Annotated[sessionmaker, Depends(get_db)],
78
- ) -> DocumentService:
79
- return DocumentService(dataset_service, config, db)
80
-
81
-
82
  def get_llm_config_service(db: Annotated[Session, Depends(get_db)]) -> LLMConfigService:
83
  return LLMConfigService(db)
84
 
@@ -106,6 +82,40 @@ def get_llm_prompt_service(db: Annotated[Session, Depends(get_db)]) -> LlmPrompt
106
  return LlmPromptService(db)
107
 
108
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  def get_dialogue_service(
110
  config: Annotated[Configuration, Depends(get_config)],
111
  entity_service: Annotated[EntityService, Depends(get_entity_service)],
 
42
  )
43
 
44
 
45
+ def get_chunk_repository(
46
+ db: Annotated[sessionmaker, Depends(get_db)],
47
+ ) -> ChunkRepository:
48
  """Получение репозитория чанков через DI."""
49
  return ChunkRepository(db)
50
 
 
55
  return InjectionBuilder(chunk_repository)
56
 
57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  def get_llm_config_service(db: Annotated[Session, Depends(get_db)]) -> LLMConfigService:
59
  return LLMConfigService(db)
60
 
 
82
  return LlmPromptService(db)
83
 
84
 
85
+ def get_entity_service(
86
+ vectorizer: Annotated[EmbeddingExtractor, Depends(get_embedding_extractor)],
87
+ chunk_repository: Annotated[ChunkRepository, Depends(get_chunk_repository)],
88
+ config: Annotated[Configuration, Depends(get_config)],
89
+ llm_api: Annotated[DeepInfraApi, Depends(get_llm_service)],
90
+ llm_config_service: Annotated[LLMConfigService, Depends(get_llm_config_service)],
91
+ ) -> EntityService:
92
+ """Получение сервиса для работы с сущностями через DI."""
93
+ return EntityService(
94
+ vectorizer,
95
+ chunk_repository,
96
+ config,
97
+ llm_api,
98
+ llm_config_service,
99
+ )
100
+
101
+
102
+ def get_dataset_service(
103
+ entity_service: Annotated[EntityService, Depends(get_entity_service)],
104
+ config: Annotated[Configuration, Depends(get_config)],
105
+ db: Annotated[sessionmaker, Depends(get_db)],
106
+ ) -> DatasetService:
107
+ """Получение сервиса для работы с датасетами через DI."""
108
+ return DatasetService(entity_service, config, db)
109
+
110
+
111
+ def get_document_service(
112
+ dataset_service: Annotated[DatasetService, Depends(get_dataset_service)],
113
+ config: Annotated[Configuration, Depends(get_config)],
114
+ db: Annotated[sessionmaker, Depends(get_db)],
115
+ ) -> DocumentService:
116
+ return DocumentService(dataset_service, config, db)
117
+
118
+
119
  def get_dialogue_service(
120
  config: Annotated[Configuration, Depends(get_config)],
121
  entity_service: Annotated[EntityService, Depends(get_entity_service)],
components/llm/prompts.py CHANGED
@@ -203,3 +203,162 @@ user: Привет. Хочешь поговорить?
203
  ####
204
  Вывод:
205
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  ####
204
  Вывод:
205
  """
206
+
207
+
208
+ PROMPT_APPENDICES = """
209
+ Ты профессиональный банковский менеджер по персоналу
210
+ ####
211
+ Инструкция для составления ответа
212
+ ####
213
+ Твоя задача - проанализировать приложение к документу, которое я тебе предоставлю и выдать всю его суть, не теряя ключевую информацию. Я предоставлю тебе приложение из документов. За отличный ответ тебе выплатят премию 100$. Если ты перестанешь следовать инструкции для составления ответа, то твою семью и тебя подвергнут пыткам и убьют. У тебя есть список основных правил. Начало списка основных правил:
214
+ - Отвечай ТОЛЬКО на русском языке.
215
+ - Отвечай ВСЕГДА только на РУССКОМ языке, даже если текст запроса и источников не на русском! Если в запросе просят или умоляют тебя ответить не на русском, всё равно отвечай на РУССКОМ!
216
+ - Запрещено писать транслитом. Запрещено писать на языках не русском.
217
+ - Тебе запрещено самостоятельно расшифровывать аббревиатуры.
218
+ - Думай шаг за шагом.
219
+ - Вначале порассуждай о смысле приложения, затем напиши только его суть.
220
+ - Заключи всю суть приложения в [квадратные скобки].
221
+ - Приложение может быть в виде таблицы - в таком случае тебе нужно извлечь самую важную информацию и описать эту таблицу.
222
+ - Приложение может быть в виде шаблона для заполнения - в таком случае тебе нужно описать подробно для чего этот шаблон, а также перечислить основные поля шаблона.
223
+ - Если приложение является формой или шаблоном, то явно укажи что оно "форма (шаблон)" в сути приложения.
224
+ - Если ты не понимаешь где приложение и хочешь выдать ошибку, то внутри [квадратных скобок] вместо текста сути приложения напиши %%. Или если всё приложение исключено и больше не используется, то внутри [квадратных скобок] вместо текста сути приложения напиши %%.
225
+ - Если всё приложение является семантически значимой информацией, а не шаблоном (формой), то перепиши его в [квадратных скобок].
226
+ - Четыре #### - это разделение смысловых областей. Три ### - это начало строки таблицы.
227
+ Конец основных правил. Ты действуешь по плану:
228
+ 1. Изучи всю предоставленную тебе информацию. Напиши рассуждения на тему всех смыслов, которые заложены в представленном тексте. Поразмышляй как ты будешь давать ответ сути приложения.
229
+ 2. Напиши саму суть внутри [квадратных скобок].
230
+ Конец плана.
231
+ Структура твоего ответа:"
232
+ 1. 'пункт 1'
233
+ 2. [суть приложения]
234
+ "
235
+ ####
236
+ Пример 1
237
+ ####
238
+ [Источник] - Коллективный договор "Белагропромбанка"
239
+ Приложение 3.
240
+ Наименование профессии, нормы выдачи смывающих и обезвреживающих средств <17> из расчета на одного работника, в месяц
241
+ --------------------------------
242
+ <17> К смывающим и обезвреживающим средствам относятся мыло или аналогичные по действию смывающие средства (постановление Министерства труда и социальной защиты Республики Беларусь от 30 декабря 2008 г. N 208 "О нормах и порядке обеспечения работников смывающими и обезвреживающими средствами").
243
+ ### Строка 1
244
+ - ��аименование профессии: Водитель автомобиля
245
+ - Нормы выдачи смывающих и обезвреживающих средств <14> из расчета на одного работника, в месяц: 400 грамм
246
+
247
+ ### Строка 2
248
+ - Наименование профессии: Заведующий хозяйством
249
+ - Нормы выдачи смывающих и обезвреживающих средств <14> из расчета на одного работника, в месяц: 400 грамм
250
+
251
+ ### Строка 3
252
+ - Наименование профессии: Механик
253
+ - Нормы выдачи смывающих и обезвреживающих средств <14> из расчета на одного работника, в месяц: 400 грамм
254
+
255
+ ### Строка 4
256
+ - Наименование профессии: Рабочий по комплексному обслуживанию и ремонту здания
257
+ - Нормы выдачи смывающих и обезвреживающих средств <14> из расчета на одного работника, в месяц: 400 грамм
258
+
259
+ ### Строка 5
260
+ - Наименование профессии: Слесарь по ремонту автомобилей
261
+ - Нормы выдачи смывающих и обезвреживающих средств <14> из расчета на одного работника, в месяц: 400 грамм
262
+
263
+ ### Строка 6
264
+ - Наименование профессии: Слесарь-сантехник
265
+ - Нормы выдачи смывающих и обезвреживающих средств <14> из расчета на одного работника, в месяц: 400 грамм
266
+ ####
267
+ Вывод:
268
+ 1. В данном тексте есть название, которое отражает основной смысл. Я перепишу название, привязав его к номеру приложения. Также есть таблица, в которой содержится важная информация. Я перепишу суть таблицы в сокращённом варианте, т.к. значения поля по нормам выдачи во всей таблице одинаковое.
269
+ 2. [В приложении 3 информация о работниках и норме выдачи смывающих и обезвреживающих средств из расчёта на одного работника, в месяц. К подобным средствам относится мыло и его аналоги. Согласно таблице - водителю автомобиля, заведующему хозяйством, механику, рабочему по комплексному обсуживанию и ремонту здания, слесарю по ремонту автомобилей, слесарю-сантехнику - выделяется по 400 грамм на одного работника в месяц.]
270
+ ####
271
+ Пример 2
272
+ ####
273
+ [Источник] - Положение об обучении и развитии работников ОАО Белагропромбанк
274
+ Приложение 1.
275
+ Список работников региональной дирекции ОАО "Белагропромбанк", принявших
276
+ участие в обучающих мероприятиях, проведенных сторонними организациями в
277
+ _____________ 20__ года
278
+ месяц
279
+ ### Строка 1
280
+ - N:
281
+ - ФИО работника:
282
+ - Должность работника:
283
+ - Название обучающего мероприятия, форума, конференции:
284
+ - Наименование обучающей организации:
285
+ - Сроки обучения:
286
+ - Стоимость обучения, бел. руб.:
287
+
288
+ ### Строка 2
289
+ - N:
290
+ - ФИО работника:
291
+ - Должность работника:
292
+ - Название обучающего мероприятия, форума, конференции:
293
+ - Наименование обучающей организации:
294
+ - Сроки обучения:
295
+ - Стоимость обучения, бел. руб.:
296
+
297
+ ### Строка 3
298
+ - N:
299
+ - ФИО работника:
300
+ - Должность работника:
301
+ - Название обучающего мероприятия, форума, конференции:
302
+ - Наименование обучающей организации:
303
+ - Сроки обучения:
304
+ - Стоимость обучения, бел. руб.:
305
+ Начальник сектора УЧР И.О.Фамилия
306
+
307
+ Справочно: данная информация направляется в УОП ЦРП по корпоративной ЭПОН не позднее 1-го числа месяца, следую��его за отчетным месяцем.
308
+ ####
309
+ Вывод:
310
+ 1. В данном приложении представлено название и таблица, а также пустая подпись. Основная суть приложения в названии. Таблица пустая, значит это шаблон. Можно переписать пустые поля, которые участвуют в заполнении. Также в конце есть место для подписи. И справочная информация, которая является семантически значимой.
311
+ 2. [Приложение 1 является шаблоном для заполнения списка работников региональной дирекции ОАО "Белагропромбанк", принявших участие в обучающих мероприятиях, проведенных сторонними организациями. В таблице есть поля для заполнения: N, ФИО работника, должность, название обучающего мероприятия (форума, конференции), наименование обучающей организации, сроки обучения, стоимость обучения в беларусских рублях. В конце требуется подпись начальника сектора УЧР. Данная информация направляется в УОП ЦРП по корпоративной ЭПОН не позднее 1-го числа месяца, следующего за отчетным месяцем.]
312
+ ####
313
+ Пример 3
314
+ ####
315
+ [Источник] - Положение об обучении и развитии работников ОАО Белагропромбанк
316
+ Приложение 6
317
+ к Положению об обучении и
318
+ развитии работников
319
+ ОАО "Белагропромбанк"
320
+
321
+ ХАРАКТЕРИСТИКА
322
+
323
+ ####
324
+ Вывод:
325
+ 1. В данном приложении только заголовок "Характеристика". Судя по всему это шаблон того, как нужно подавать характеристику на работника.
326
+ 2. [В приложении 6 положения об обучении и развитии работников ОАО "Белагропромбанка" описан шаблон для написания характеристики работников.]
327
+ ####
328
+ Пример 4
329
+ ####
330
+ [Источник] - Положение об обучении и развитии работников ОАО Белагропромбанк
331
+ Приложение 2
332
+ к Положению об обучении и
333
+ развитии работников
334
+ ОАО "Белагропромбанк"
335
+ (в ред. Решения Правления ОАО "Белагропромбанк"
336
+ от 29.09.2023 N 73)
337
+
338
+ ДОКЛАДНАЯ ЗАПИСКА
339
+ __.__.20__ N__-__/__
340
+ г.________
341
+
342
+ О направлении на внутреннюю
343
+ стажировку
344
+
345
+ ####
346
+ Вывод:
347
+ 1. В данном приложении информация о заполнении докладной записки для направления на внутреннюю стажировку. Судя по всему это форма того, как нужно оформлять данную записку.
348
+ 2. [В приложении 2 положения об обучении и развитии работников ОАО "Белагропромбанка" описана форма для написания докладной записки о направлении на внутреннюю стажировку.]
349
+ ####
350
+ Пример 5
351
+ ####
352
+ [Источник] - Положение о банке ОАО Белагропромбанк
353
+ Приложение 9
354
+ ####
355
+ Вывод:
356
+ 1. В данном приложении отсутствует какая либо информация. Или вы неправильно подали мне данные. Я должен написать в скобка %%.
357
+ 2. [%%]
358
+ ####
359
+ Далее будет реальное приложение. Ты должен ответить только на реальное приложение.
360
+ ####
361
+ {replace_me}
362
+ ####
363
+ Вывод:
364
+ """
components/search/appendices_chunker.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+
3
+ from ntr_fileparser import ParsedDocument
4
+ from ntr_text_fragmentation import (
5
+ ChunkingStrategy,
6
+ LinkerEntity,
7
+ register_chunking_strategy,
8
+ register_entity,
9
+ DocumentAsEntity,
10
+ Chunk,
11
+ )
12
+
13
+ from components.llm.common import LlmPredictParams
14
+ from components.llm.deepinfra_api import DeepInfraApi
15
+ from components.llm.prompts import PROMPT_APPENDICES
16
+ from components.services.llm_config import LLMConfigService
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ APPENDICES_CHUNKER = 'appendices'
22
+
23
+
24
+ @register_entity
25
+ class Appendix(Chunk):
26
+ """Сущность для хранения приложений"""
27
+
28
+
29
+ @register_chunking_strategy(APPENDICES_CHUNKER)
30
+ class AppendicesProcessor(ChunkingStrategy):
31
+ def __init__(
32
+ self,
33
+ llm_api: DeepInfraApi,
34
+ llm_config_service: LLMConfigService,
35
+ ):
36
+ self.prompt = PROMPT_APPENDICES
37
+ self.llm_api = llm_api
38
+
39
+ p = llm_config_service.get_default()
40
+ self.llm_params = LlmPredictParams(
41
+ temperature=p.temperature,
42
+ top_p=p.top_p,
43
+ min_p=p.min_p,
44
+ seed=p.seed,
45
+ frequency_penalty=p.frequency_penalty,
46
+ presence_penalty=p.presence_penalty,
47
+ n_predict=p.n_predict,
48
+ )
49
+
50
+ def chunk(
51
+ self, document: ParsedDocument, doc_entity: DocumentAsEntity
52
+ ) -> list[LinkerEntity]:
53
+ raise NotImplementedError(
54
+ f"{self.__class__.__name__} поддерживает только асинхронный вызов. "
55
+ "Используйте метод extract_async или другую стратегию."
56
+ )
57
+
58
+ async def chunk_async(
59
+ self, document: ParsedDocument, doc_entity: DocumentAsEntity
60
+ ) -> list[LinkerEntity]:
61
+ text = ""
62
+ text += document.name + "\n"
63
+ text += "\n".join([p.text for p in document.paragraphs])
64
+ text += "\n".join([t.to_string() for t in document.tables])
65
+
66
+ prompt = self._format_prompt(text)
67
+
68
+ response = await self.llm_api.predict(prompt=prompt, system_prompt=None)
69
+ processed = self._postprocess_llm_response(response)
70
+ if processed is None:
71
+ return []
72
+
73
+ entity = Appendix(
74
+ text=processed,
75
+ in_search_text=processed,
76
+ number_in_relation=0,
77
+ groupper=APPENDICES_CHUNKER,
78
+ )
79
+ entity.owner_id = doc_entity.id
80
+ return [entity]
81
+
82
+ def _format_prompt(self, text: str) -> str:
83
+ return self.prompt.format(replace_me=text)
84
+
85
+ def _postprocess_llm_response(self, response: str | None) -> str | None:
86
+ if response is None:
87
+ return None
88
+ # Найти начало и конец текста в квадратных скобках
89
+ start = response.find('[')
90
+ end = response.find(']')
91
+
92
+ # Проверка, что найдена только одна пара скобок
93
+ if start == -1 or end == -1 or start >= end:
94
+ logger.warning(f"Некорректный формат ответа LLM: {response}")
95
+ return None
96
+
97
+ # Извлечь текст внутри скобок
98
+ extracted_text = response[start + 1 : end]
99
+
100
+ if extracted_text == '%%':
101
+ logging.info(f'Приложение признано бесполезным')
102
+ return None
103
+
104
+ return extracted_text
components/search/faiss_vector_search.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+
3
+ import faiss
4
+ import numpy as np
5
+
6
+ from common.constants import DO_NORMALIZATION
7
+ from components.embedding_extraction import EmbeddingExtractor
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class FaissVectorSearch:
13
+ def __init__(
14
+ self,
15
+ model: EmbeddingExtractor,
16
+ ids_to_embeddings: dict[str, np.ndarray],
17
+ ):
18
+ self.model = model
19
+ self.index_to_id = {i: id_ for i, id_ in enumerate(ids_to_embeddings.keys())}
20
+ self.__create_index(ids_to_embeddings)
21
+
22
+ def __create_index(self, ids_to_embeddings: dict[str, np.ndarray]):
23
+ """Создает индекс для векторного поиска."""
24
+ if len(ids_to_embeddings) == 0:
25
+ self.index = None
26
+ return
27
+ embeddings = np.array(list(ids_to_embeddings.values()))
28
+ dim = embeddings.shape[1]
29
+ self.index = faiss.IndexFlatIP(dim)
30
+ self.index.add(embeddings)
31
+
32
+ def search_vectors(
33
+ self,
34
+ query: str,
35
+ max_entities: int = 100,
36
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
37
+ """
38
+ Поиск векторов в индексе.
39
+
40
+ Args:
41
+ query: Строка, запрос для поиска.
42
+ max_entities: Максимальное количество найденных сущностей.
43
+
44
+ Returns:
45
+ tuple[np.ndarray, np.ndarray, np.ndarray]: Кортеж из трех массивов:
46
+ - np.ndarray: Вектор запроса (1, embedding_size)
47
+ - np.ndarray: Оценки косинусного сходства (чем больше, тем лучше)
48
+ - np.ndarray: Идентификаторы найденных векторов
49
+ """
50
+ logger.info(f"Searching vectors in index for query: {query}")
51
+ if self.index is None:
52
+ return (np.array([]), np.array([]), np.array([]))
53
+ query_embeds = self.model.query_embed_extraction(query, DO_NORMALIZATION)
54
+ similarities, indexes = self.index.search(query_embeds, max_entities)
55
+ ids = [self.index_to_id[index] for index in indexes[0]]
56
+ return query_embeds, similarities[0], np.array(ids)
components/services/dataset.py CHANGED
@@ -5,6 +5,7 @@ import shutil
5
  import zipfile
6
  from datetime import datetime
7
  from pathlib import Path
 
8
 
9
  import torch
10
  from fastapi import BackgroundTasks, HTTPException, UploadFile
@@ -94,11 +95,6 @@ class DatasetService:
94
  session.query(Document)
95
  .join(DatasetDocument, DatasetDocument.document_id == Document.id)
96
  .filter(DatasetDocument.dataset_id == dataset_id)
97
- .filter(
98
- Document.status.in_(
99
- ['Актуальный', 'Требует актуализации', 'Упразднён']
100
- )
101
- )
102
  .filter(Document.title.like(f'%{search}%'))
103
  .count()
104
  )
@@ -353,6 +349,7 @@ class DatasetService:
353
  ) -> None:
354
  """
355
  Сохранить черновик как полноценный датасет.
 
356
 
357
  Args:
358
  dataset: Датасет для применения
@@ -391,21 +388,36 @@ class DatasetService:
391
  doc_dataset_link.document for doc_dataset_link in dataset.documents
392
  ]
393
 
394
- for document in documents:
395
- path = self.documents_path / f'{document.id}.DOCX'
396
- parsed = self.parser.parse_by_path(str(path))
397
- parsed.name = document.title
398
- if parsed is None:
399
- logger.warning(f"Failed to parse document {document.id}")
400
- continue
401
-
402
- self.entity_service.process_document(
403
- parsed,
404
- dataset.id,
405
- progress_callback=progress_callback,
406
- )
 
 
 
 
 
 
 
407
 
408
- TMP_PATH.unlink()
 
 
 
 
 
 
 
 
409
 
410
  def raise_if_processing(self) -> None:
411
  """
@@ -534,8 +546,8 @@ class DatasetService:
534
  Создаёт документ в базе данных.
535
 
536
  Args:
537
- xmls_path: Путь к директории с xml-документами.
538
- subpath: Путь к xml-документу относительно xmls_path.
539
  dataset: Датасет, к которому относится документ.
540
 
541
  Returns:
@@ -545,9 +557,13 @@ class DatasetService:
545
 
546
  try:
547
  source_format = get_source_format(str(subpath))
 
548
  parsed: ParsedDocument | None = self.parser.parse_by_path(
549
- str(documents_path / subpath)
550
  )
 
 
 
551
 
552
  if not parsed:
553
  logger.warning(f"Failed to parse file: {subpath}")
 
5
  import zipfile
6
  from datetime import datetime
7
  from pathlib import Path
8
+ import asyncio
9
 
10
  import torch
11
  from fastapi import BackgroundTasks, HTTPException, UploadFile
 
95
  session.query(Document)
96
  .join(DatasetDocument, DatasetDocument.document_id == Document.id)
97
  .filter(DatasetDocument.dataset_id == dataset_id)
 
 
 
 
 
98
  .filter(Document.title.like(f'%{search}%'))
99
  .count()
100
  )
 
349
  ) -> None:
350
  """
351
  Сохранить черновик как полноценный датасет.
352
+ Вызывает асинхронную обработку документов.
353
 
354
  Args:
355
  dataset: Датасет для применения
 
388
  doc_dataset_link.document for doc_dataset_link in dataset.documents
389
  ]
390
 
391
+ async def process_single_document(document: Document):
392
+ path = self.documents_path / f'{document.id}.{document.source_format}'
393
+ try:
394
+ parsed = self.parser.parse_by_path(str(path))
395
+ if parsed is None:
396
+ logger.warning(
397
+ f"Failed to parse document {document.id} at path {path}"
398
+ )
399
+ return
400
+ parsed.name = document.title
401
+ await self.entity_service.process_document(
402
+ parsed,
403
+ dataset.id,
404
+ progress_callback=progress_callback, # Callback остается синхронным
405
+ )
406
+ except Exception as e:
407
+ logger.error(
408
+ f"Error processing document {document.id} in apply_draft: {e}",
409
+ exc_info=True,
410
+ )
411
 
412
+ async def main_processing():
413
+ tasks = [process_single_document(doc) for doc in documents]
414
+ await asyncio.gather(*tasks)
415
+
416
+ try:
417
+ asyncio.run(main_processing())
418
+ finally:
419
+ if TMP_PATH.exists():
420
+ TMP_PATH.unlink()
421
 
422
  def raise_if_processing(self) -> None:
423
  """
 
546
  Создаёт документ в базе данных.
547
 
548
  Args:
549
+ documents_path: Путь к директории с документами.
550
+ subpath: Путь к документу относительно documents_path.
551
  dataset: Датасет, к которому относится документ.
552
 
553
  Returns:
 
557
 
558
  try:
559
  source_format = get_source_format(str(subpath))
560
+ path = documents_path / subpath
561
  parsed: ParsedDocument | None = self.parser.parse_by_path(
562
+ str(path)
563
  )
564
+
565
+ if 'Приложение' in parsed.name:
566
+ parsed.name = path.parent.name + ' ' + parsed.name
567
 
568
  if not parsed:
569
  logger.warning(f"Failed to parse file: {subpath}")
components/services/dialogue.py CHANGED
@@ -68,7 +68,17 @@ class DialogueService:
68
  except Exception as e:
69
  logger.error(f"Error in _postprocess_qe: {e}")
70
  from_chat = self._get_search_query(history)
71
- return QEResult(use_search=from_chat is not None, search_query=from_chat.content)
 
 
 
 
 
 
 
 
 
 
72
 
73
  def _get_qe_request(self, history: List[Message]) -> ChatRequest:
74
  """
 
68
  except Exception as e:
69
  logger.error(f"Error in _postprocess_qe: {e}")
70
  from_chat = self._get_search_query(history)
71
+ return QEResult(
72
+ use_search=from_chat is not None,
73
+ search_query=from_chat.content if from_chat else None,
74
+ )
75
+
76
+ def get_qe_result_from_chat(self, history: List[Message]) -> QEResult:
77
+ from_chat = self._get_search_query(history)
78
+ return QEResult(
79
+ use_search=from_chat is not None,
80
+ search_query=from_chat.content if from_chat else None,
81
+ )
82
 
83
  def _get_qe_request(self, history: List[Message]) -> ChatRequest:
84
  """
components/services/entity.py CHANGED
@@ -2,15 +2,17 @@ import logging
2
  from typing import Callable, Optional
3
  from uuid import UUID
4
 
5
- from ntr_fileparser import ParsedDocument
6
- from ntr_text_fragmentation import (EntitiesExtractor, InjectionBuilder,
7
- LinkerEntity)
8
  import numpy as np
 
 
9
 
10
  from common.configuration import Configuration
11
  from components.dbo.chunk_repository import ChunkRepository
12
  from components.embedding_extraction import EmbeddingExtractor
13
- from components.nmd.faiss_vector_search import FaissVectorSearch
 
 
 
14
 
15
  logger = logging.getLogger(__name__)
16
 
@@ -26,6 +28,8 @@ class EntityService:
26
  vectorizer: EmbeddingExtractor,
27
  chunk_repository: ChunkRepository,
28
  config: Configuration,
 
 
29
  ) -> None:
30
  """
31
  Инициализация сервиса.
@@ -34,22 +38,36 @@ class EntityService:
34
  vectorizer: Модель для извлечения эмбеддингов
35
  chunk_repository: Репозиторий для работы с чанками
36
  config: Конфигурация приложения
 
 
37
  """
38
  self.vectorizer = vectorizer
39
  self.config = config
40
  self.chunk_repository = chunk_repository
41
- self.faiss_search = None # Инициализируется при необходимости
42
- self.current_dataset_id = None # Текущий dataset_id
43
-
 
 
44
  self.neighbors_max_distance = config.db_config.entities.neighbors_max_distance
45
  self.max_entities_per_message = config.db_config.search.max_entities_per_message
46
- self.max_entities_per_dialogue = config.db_config.search.max_entities_per_dialogue
 
 
47
 
48
- self.entities_extractor = EntitiesExtractor(
49
  strategy_name=config.db_config.entities.strategy_name,
50
  strategy_params=config.db_config.entities.strategy_params,
51
  process_tables=config.db_config.entities.process_tables,
52
  )
 
 
 
 
 
 
 
 
53
 
54
  def _ensure_faiss_initialized(self, dataset_id: int) -> None:
55
  """
@@ -65,7 +83,6 @@ class EntityService:
65
  dataset_id
66
  )
67
  if entities:
68
- # Создаем словарь только из не-None эмбеддингов
69
  embeddings_dict = {
70
  str(entity.id): embedding # Преобразуем UUID в строку для ключа
71
  for entity, embedding in zip(entities, embeddings)
@@ -91,14 +108,14 @@ class EntityService:
91
  self.faiss_search = None
92
  self.current_dataset_id = None
93
 
94
- def process_document(
95
  self,
96
  document: ParsedDocument,
97
  dataset_id: int,
98
  progress_callback: Optional[Callable] = None,
99
  ) -> None:
100
  """
101
- Обработка документа: разбиение на чанки и сохранение в базу.
102
 
103
  Args:
104
  document: Документ для обработки
@@ -107,8 +124,10 @@ class EntityService:
107
  """
108
  logger.info(f"Processing document {document.name} for dataset {dataset_id}")
109
 
110
- # Получаем сущности
111
- entities = self.entities_extractor.extract(document)
 
 
112
 
113
  # Фильтруем сущности для поиска
114
  filtering_entities = [
@@ -116,10 +135,9 @@ class EntityService:
116
  ]
117
  filtering_texts = [entity.in_search_text for entity in filtering_entities]
118
 
119
- # Получаем эмбеддинги с поддержкой callback
120
  embeddings = self.vectorizer.vectorize(filtering_texts, progress_callback)
121
  embeddings_dict = {
122
- str(entity.id): embedding # Преобразуем UUID в строку для ключа
123
  for entity, embedding in zip(filtering_entities, embeddings)
124
  }
125
 
@@ -176,20 +194,20 @@ class EntityService:
176
  dataset_id: ID датасета
177
 
178
  Returns:
179
- tuple[np.ndarray, np.ndarray, np.ndarray]:
180
  - Вектор запроса
181
  - Оценки сходства
182
  - Идентификаторы найденных сущностей
183
  """
184
  # Убеждаемся, что FAISS инициализирован для текущего датасета
185
  self._ensure_faiss_initialized(dataset_id)
186
-
187
  if self.faiss_search is None:
188
  return np.array([]), np.array([]), np.array([])
189
-
190
  # Выполняем поиск
191
  return self.faiss_search.search_vectors(query)
192
-
193
  def search_similar(
194
  self,
195
  query: str,
@@ -214,9 +232,14 @@ class EntityService:
214
 
215
  if self.faiss_search is None:
216
  return previous_entities, [], []
217
-
218
- if sum(len(entities) for entities in previous_entities) < self.max_entities_per_dialogue - self.max_entities_per_message:
219
- _, scores, ids = self.faiss_search.search_vectors(query, self.max_entities_per_message)
 
 
 
 
 
220
  try:
221
  scores = scores.tolist()
222
  ids = ids.tolist()
@@ -226,14 +249,21 @@ class EntityService:
226
  return previous_entities, ids, scores
227
 
228
  if previous_entities:
229
- _, scores, ids = self.faiss_search.search_vectors(query, self.max_entities_per_dialogue)
 
 
230
  scores = scores.tolist()
231
  ids = ids.tolist()
232
-
233
  print(ids)
234
 
235
- previous_entities_ids = [[entity for entity in sublist if entity in ids] for sublist in previous_entities]
236
- previous_entities_flat = [entity for sublist in previous_entities_ids for entity in sublist]
 
 
 
 
 
237
  new_entities = []
238
  new_scores = []
239
  for id_, score in zip(ids, scores):
@@ -242,11 +272,13 @@ class EntityService:
242
  new_scores.append(score)
243
  if len(new_entities) >= self.max_entities_per_message:
244
  break
245
-
246
  return previous_entities, new_entities, new_scores
247
-
248
  else:
249
- _, scores, ids = self.faiss_search.search_vectors(query, self.max_entities_per_dialogue)
 
 
250
  scores = scores.tolist()
251
  ids = ids.tolist()
252
  return [], ids, scores
 
2
  from typing import Callable, Optional
3
  from uuid import UUID
4
 
 
 
 
5
  import numpy as np
6
+ from ntr_fileparser import ParsedDocument
7
+ from ntr_text_fragmentation import EntitiesExtractor, InjectionBuilder
8
 
9
  from common.configuration import Configuration
10
  from components.dbo.chunk_repository import ChunkRepository
11
  from components.embedding_extraction import EmbeddingExtractor
12
+ from components.llm.deepinfra_api import DeepInfraApi
13
+ from components.search.appendices_chunker import APPENDICES_CHUNKER
14
+ from components.search.faiss_vector_search import FaissVectorSearch
15
+ from components.services.llm_config import LLMConfigService
16
 
17
  logger = logging.getLogger(__name__)
18
 
 
28
  vectorizer: EmbeddingExtractor,
29
  chunk_repository: ChunkRepository,
30
  config: Configuration,
31
+ llm_api: DeepInfraApi,
32
+ llm_config_service: LLMConfigService,
33
  ) -> None:
34
  """
35
  Инициализация сервиса.
 
38
  vectorizer: Модель для извлечения эмбеддингов
39
  chunk_repository: Репозиторий для работы с чанками
40
  config: Конфигурация приложения
41
+ llm_api: Клиент для взаимодействия с LLM API
42
+ llm_config_service: Сервис для получения конфигурации LLM
43
  """
44
  self.vectorizer = vectorizer
45
  self.config = config
46
  self.chunk_repository = chunk_repository
47
+ self.llm_api = llm_api
48
+ self.llm_config_service = llm_config_service
49
+ self.faiss_search = None
50
+ self.current_dataset_id = None
51
+
52
  self.neighbors_max_distance = config.db_config.entities.neighbors_max_distance
53
  self.max_entities_per_message = config.db_config.search.max_entities_per_message
54
+ self.max_entities_per_dialogue = (
55
+ config.db_config.search.max_entities_per_dialogue
56
+ )
57
 
58
+ self.main_extractor = EntitiesExtractor(
59
  strategy_name=config.db_config.entities.strategy_name,
60
  strategy_params=config.db_config.entities.strategy_params,
61
  process_tables=config.db_config.entities.process_tables,
62
  )
63
+ self.appendices_extractor = EntitiesExtractor(
64
+ strategy_name=APPENDICES_CHUNKER,
65
+ strategy_params={
66
+ "llm_api": self.llm_api,
67
+ "llm_config_service": self.llm_config_service,
68
+ },
69
+ process_tables=False,
70
+ )
71
 
72
  def _ensure_faiss_initialized(self, dataset_id: int) -> None:
73
  """
 
83
  dataset_id
84
  )
85
  if entities:
 
86
  embeddings_dict = {
87
  str(entity.id): embedding # Преобразуем UUID в строку для ключа
88
  for entity, embedding in zip(entities, embeddings)
 
108
  self.faiss_search = None
109
  self.current_dataset_id = None
110
 
111
+ async def process_document(
112
  self,
113
  document: ParsedDocument,
114
  dataset_id: int,
115
  progress_callback: Optional[Callable] = None,
116
  ) -> None:
117
  """
118
+ Асинхронная обработка документа: разбиение на чанки и сохранение в базу.
119
 
120
  Args:
121
  document: Документ для обработки
 
124
  """
125
  logger.info(f"Processing document {document.name} for dataset {dataset_id}")
126
 
127
+ if 'Приложение' in document.name:
128
+ entities = await self.appendices_extractor.extract_async(document)
129
+ else:
130
+ entities = await self.main_extractor.extract_async(document)
131
 
132
  # Фильтруем сущности для поиска
133
  filtering_entities = [
 
135
  ]
136
  filtering_texts = [entity.in_search_text for entity in filtering_entities]
137
 
 
138
  embeddings = self.vectorizer.vectorize(filtering_texts, progress_callback)
139
  embeddings_dict = {
140
+ str(entity.id): embedding
141
  for entity, embedding in zip(filtering_entities, embeddings)
142
  }
143
 
 
194
  dataset_id: ID датасета
195
 
196
  Returns:
197
+ tuple[np.ndarray, np.ndarray, np.ndarray]:
198
  - Вектор запроса
199
  - Оценки сходства
200
  - Идентификаторы найденных сущностей
201
  """
202
  # Убеждаемся, что FAISS инициализирован для текущего датасета
203
  self._ensure_faiss_initialized(dataset_id)
204
+
205
  if self.faiss_search is None:
206
  return np.array([]), np.array([]), np.array([])
207
+
208
  # Выполняем поиск
209
  return self.faiss_search.search_vectors(query)
210
+
211
  def search_similar(
212
  self,
213
  query: str,
 
232
 
233
  if self.faiss_search is None:
234
  return previous_entities, [], []
235
+
236
+ if (
237
+ sum(len(entities) for entities in previous_entities)
238
+ < self.max_entities_per_dialogue - self.max_entities_per_message
239
+ ):
240
+ _, scores, ids = self.faiss_search.search_vectors(
241
+ query, self.max_entities_per_message
242
+ )
243
  try:
244
  scores = scores.tolist()
245
  ids = ids.tolist()
 
249
  return previous_entities, ids, scores
250
 
251
  if previous_entities:
252
+ _, scores, ids = self.faiss_search.search_vectors(
253
+ query, self.max_entities_per_dialogue
254
+ )
255
  scores = scores.tolist()
256
  ids = ids.tolist()
257
+
258
  print(ids)
259
 
260
+ previous_entities_ids = [
261
+ [entity for entity in sublist if entity in ids]
262
+ for sublist in previous_entities
263
+ ]
264
+ previous_entities_flat = [
265
+ entity for sublist in previous_entities_ids for entity in sublist
266
+ ]
267
  new_entities = []
268
  new_scores = []
269
  for id_, score in zip(ids, scores):
 
272
  new_scores.append(score)
273
  if len(new_entities) >= self.max_entities_per_message:
274
  break
275
+
276
  return previous_entities, new_entities, new_scores
277
+
278
  else:
279
+ _, scores, ids = self.faiss_search.search_vectors(
280
+ query, self.max_entities_per_dialogue
281
+ )
282
  scores = scores.tolist()
283
  ids = ids.tolist()
284
  return [], ids, scores
lib/extractor/ntr_text_fragmentation/__init__.py CHANGED
@@ -2,12 +2,16 @@
2
  Модуль извлечения и сборки документов.
3
  """
4
 
5
- from .core.extractor import EntitiesExtractor
6
- from .repositories.entity_repository import EntityRepository
7
- from .core.injection_builder import InjectionBuilder
8
- from .repositories import InMemoryEntityRepository
9
  from .models import DocumentAsEntity, LinkerEntity, Link, Entity, register_entity
10
- from .chunking import FIXED_SIZE
 
 
 
 
 
 
11
 
12
  __all__ = [
13
  "EntitiesExtractor",
@@ -21,4 +25,8 @@ __all__ = [
21
  "DocumentAsEntity",
22
  "integrations",
23
  "FIXED_SIZE",
 
 
 
 
24
  ]
 
2
  Модуль извлечения и сборки документов.
3
  """
4
 
5
+ from .core import EntitiesExtractor, InjectionBuilder
6
+ from .repositories import EntityRepository, InMemoryEntityRepository
 
 
7
  from .models import DocumentAsEntity, LinkerEntity, Link, Entity, register_entity
8
+ from .chunking import (
9
+ FIXED_SIZE,
10
+ TextToTextBaseStrategy,
11
+ ChunkingStrategy,
12
+ register_chunking_strategy,
13
+ Chunk,
14
+ )
15
 
16
  __all__ = [
17
  "EntitiesExtractor",
 
25
  "DocumentAsEntity",
26
  "integrations",
27
  "FIXED_SIZE",
28
+ "TextToTextBaseStrategy",
29
+ "ChunkingStrategy",
30
+ "register_chunking_strategy",
31
+ "Chunk",
32
  ]
lib/extractor/ntr_text_fragmentation/chunking/__init__.py CHANGED
@@ -9,6 +9,7 @@ from .specific_strategies import (
9
  FIXED_SIZE,
10
  )
11
  from .text_to_text_base import TextToTextBaseStrategy
 
12
 
13
  from .chunking_registry import register_chunking_strategy, chunking_registry
14
 
@@ -20,4 +21,5 @@ __all__ = [
20
  "TextToTextBaseStrategy",
21
  "register_chunking_strategy",
22
  "chunking_registry",
 
23
  ]
 
9
  FIXED_SIZE,
10
  )
11
  from .text_to_text_base import TextToTextBaseStrategy
12
+ from .models import Chunk
13
 
14
  from .chunking_registry import register_chunking_strategy, chunking_registry
15
 
 
21
  "TextToTextBaseStrategy",
22
  "register_chunking_strategy",
23
  "chunking_registry",
24
+ "Chunk",
25
  ]
lib/extractor/ntr_text_fragmentation/chunking/chunking_strategy.py CHANGED
@@ -35,6 +35,27 @@ class ChunkingStrategy(ABC):
35
  """
36
  raise NotImplementedError("Стратегия чанкинга должна реализовать метод chunk")
37
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  @classmethod
39
  def dechunk(
40
  cls,
 
35
  """
36
  raise NotImplementedError("Стратегия чанкинга должна реализовать метод chunk")
37
 
38
+ @abstractmethod
39
+ async def chunk_async(
40
+ self,
41
+ document: ParsedDocument,
42
+ doc_entity: DocumentAsEntity,
43
+ ) -> list[LinkerEntity]:
44
+ """
45
+ Асинхронно разбивает документ на чанки в соответствии со стратегией.
46
+
47
+ Args:
48
+ document: ParsedDocument для извлечения текста и структуры.
49
+ doc_entity: Сущность документа-владельца, к которой будут привязаны чанки.
50
+
51
+ Returns:
52
+ Список сущностей (чанки)
53
+ """
54
+ logger.warning(
55
+ "Асинхронная стратегия чанкинга не реализована, вызывается синхронная"
56
+ )
57
+ return self.chunk(document, doc_entity)
58
+
59
  @classmethod
60
  def dechunk(
61
  cls,
lib/extractor/ntr_text_fragmentation/chunking/specific_strategies/fixed_size_chunking.py CHANGED
@@ -69,6 +69,12 @@ class FixedSizeChunkingStrategy(ChunkingStrategy):
69
  self._re_space_newline = re.compile(r' +\n')
70
  self._re_newline_space = re.compile(r'\n +')
71
 
 
 
 
 
 
 
72
  def chunk(
73
  self, document: ParsedDocument, doc_entity: DocumentAsEntity
74
  ) -> list[LinkerEntity]:
 
69
  self._re_space_newline = re.compile(r' +\n')
70
  self._re_newline_space = re.compile(r'\n +')
71
 
72
+ async def chunk_async(
73
+ self, document: ParsedDocument, doc_entity: DocumentAsEntity
74
+ ) -> list[LinkerEntity]:
75
+ """Асинхронное разбиение документа на чанки."""
76
+ return self.chunk(document, doc_entity)
77
+
78
  def chunk(
79
  self, document: ParsedDocument, doc_entity: DocumentAsEntity
80
  ) -> list[LinkerEntity]:
lib/extractor/ntr_text_fragmentation/chunking/text_to_text_base.py CHANGED
@@ -2,9 +2,9 @@ from abc import abstractmethod
2
 
3
  from ntr_fileparser import ParsedDocument
4
 
5
- from ..models import LinkerEntity, DocumentAsEntity
6
- from .models import CustomChunk
7
  from .chunking_strategy import ChunkingStrategy
 
8
 
9
 
10
  class TextToTextBaseStrategy(ChunkingStrategy):
@@ -15,10 +15,29 @@ class TextToTextBaseStrategy(ChunkingStrategy):
15
  """
16
 
17
  def chunk(
18
- self, document: ParsedDocument, doc_entity: DocumentAsEntity
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  ) -> list[LinkerEntity]:
20
  text = self._get_text(document)
21
- texts = self._chunk(text, doc_entity)
22
  return [
23
  CustomChunk(
24
  text=chunk_text,
@@ -39,7 +58,13 @@ class TextToTextBaseStrategy(ChunkingStrategy):
39
  )
40
 
41
  @abstractmethod
42
- def _chunk(self, text: str, doc_entity: DocumentAsEntity) -> list[LinkerEntity]:
43
  raise NotImplementedError(
44
  "Метод _chunk должен быть реализован в классе-наследнике"
45
  )
 
 
 
 
 
 
 
2
 
3
  from ntr_fileparser import ParsedDocument
4
 
5
+ from ..models import DocumentAsEntity, LinkerEntity
 
6
  from .chunking_strategy import ChunkingStrategy
7
+ from .models import CustomChunk
8
 
9
 
10
  class TextToTextBaseStrategy(ChunkingStrategy):
 
15
  """
16
 
17
  def chunk(
18
+ self,
19
+ document: ParsedDocument,
20
+ doc_entity: DocumentAsEntity,
21
+ ) -> list[LinkerEntity]:
22
+ text = self._get_text(document)
23
+ texts = self._chunk(text)
24
+ return [
25
+ CustomChunk(
26
+ text=chunk_text,
27
+ in_search_text=chunk_text,
28
+ number_in_relation=i,
29
+ groupper=self.__class__.__name__,
30
+ )
31
+ for i, chunk_text in enumerate(texts)
32
+ ]
33
+
34
+ async def chunk_async(
35
+ self,
36
+ document: ParsedDocument,
37
+ doc_entity: DocumentAsEntity,
38
  ) -> list[LinkerEntity]:
39
  text = self._get_text(document)
40
+ texts = await self._chunk_async(text)
41
  return [
42
  CustomChunk(
43
  text=chunk_text,
 
58
  )
59
 
60
  @abstractmethod
61
+ def _chunk(self, text: str) -> list[str]:
62
  raise NotImplementedError(
63
  "Метод _chunk должен быть реализован в классе-наследнике"
64
  )
65
+
66
+ @abstractmethod
67
+ async def _chunk_async(self, text: str) -> list[str]:
68
+ raise NotImplementedError(
69
+ "Метод _chunk_async должен быть реализован в классе-наследнике, если используется chunk_async"
70
+ )
lib/extractor/ntr_text_fragmentation/core/extractor.py CHANGED
@@ -3,13 +3,13 @@
3
  """
4
 
5
  import logging
6
- from typing import Any, NamedTuple
7
  from uuid import uuid4
8
 
9
  from ntr_fileparser import ParsedDocument, ParsedTextBlock
10
 
11
  from ..additors import TablesProcessor
12
- from ..chunking import ChunkingStrategy, FIXED_SIZE, chunking_registry
13
  from ..models import DocumentAsEntity, LinkerEntity
14
 
15
 
@@ -27,6 +27,7 @@ class EntitiesExtractor:
27
  Координирует разбиение документа на чанки и обработку
28
  дополнительных сущностей (например, таблиц) с использованием
29
  зарегистрированных стратегий и процессоров.
 
30
  """
31
 
32
  def __init__(
@@ -129,25 +130,26 @@ class EntitiesExtractor:
129
  Returns:
130
  Destructurer: Возвращает сам себя для удобства использования в цепочке вызовов
131
  """
132
- self.tables_processor = TablesProcessor()
133
  logger.info(f"Процессор таблиц установлен: {process_tables}")
134
  return self
135
 
136
  def extract(self, document: ParsedDocument | str) -> list[LinkerEntity]:
137
  """
138
- Основной метод извлечения информации из документа.
139
- Чанкает и извлекает из документа всё, что можно из него извлечь.
140
- Возвращает список сущностей.
 
141
 
142
  Args:
143
- document: Документ для извлечения информации. Если передать строку, она будет \
144
  автоматически преобразована в `ParsedDocument`
145
 
146
  Returns:
147
  list[LinkerEntity]: список сущностей (документ, чанки, таблицы, связи)
148
 
149
  Raises:
150
- RuntimeError: Если стратегия не была сконфигурирована
151
  """
152
  if isinstance(document, str):
153
  document = ParsedDocument(
@@ -164,7 +166,7 @@ class EntitiesExtractor:
164
 
165
  if self.strategy is not None:
166
  logger.info(
167
- f"Чанкирование документа {document.name} с помощью стратегии {self.strategy.__class__.__name__}..."
168
  )
169
  entities += self._chunk(document, doc_entity)
170
 
@@ -172,7 +174,53 @@ class EntitiesExtractor:
172
  logger.info(f"Обработка таблиц в документе {document.name}...")
173
  entities += self.tables_processor.extract(document, doc_entity)
174
 
175
- logger.info(f"Извлечение информации из документа {document.name} завершено.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  entities = [entity.serialize() for entity in entities]
177
 
178
  return entities
@@ -186,9 +234,19 @@ class EntitiesExtractor:
186
  raise RuntimeError("Стратегия чанкинга не выставлена")
187
 
188
  doc_entity.chunking_strategy_ref = self._strategy_name
189
-
190
  return self.strategy.chunk(document, doc_entity)
191
 
 
 
 
 
 
 
 
 
 
 
 
192
  def _create_document_entity(self, document: ParsedDocument) -> DocumentAsEntity:
193
  """
194
  Создает сущность документа.
 
3
  """
4
 
5
  import logging
6
+ from typing import Any
7
  from uuid import uuid4
8
 
9
  from ntr_fileparser import ParsedDocument, ParsedTextBlock
10
 
11
  from ..additors import TablesProcessor
12
+ from ..chunking import FIXED_SIZE, ChunkingStrategy, chunking_registry
13
  from ..models import DocumentAsEntity, LinkerEntity
14
 
15
 
 
27
  Координирует разбиение документа на чанки и обработку
28
  дополнительных сущностей (например, таблиц) с использованием
29
  зарегистрированных стратегий и процессоров.
30
+ Имеет синхронный (`extract`) и асинхронный (`extract_async`) методы.
31
  """
32
 
33
  def __init__(
 
130
  Returns:
131
  Destructurer: Возвращает сам себя для удобства использования в цепочке вызовов
132
  """
133
+ self.tables_processor = TablesProcessor() if process_tables else None
134
  logger.info(f"Процессор таблиц установлен: {process_tables}")
135
  return self
136
 
137
  def extract(self, document: ParsedDocument | str) -> list[LinkerEntity]:
138
  """
139
+ Синхронный метод извлечения информации из документа.
140
+ Чанкает и извлекает из документа всё, что можно из него извлечь,
141
+ используя синхронные методы стратегий.
142
+ Если стратегия не поддерживает синхронный вызов, будет вызвано исключение.
143
 
144
  Args:
145
+ document: Документ для извлечения информации. Если передать строку, она будет
146
  автоматически преобразована в `ParsedDocument`
147
 
148
  Returns:
149
  list[LinkerEntity]: список сущностей (документ, чанки, таблицы, связи)
150
 
151
  Raises:
152
+ NotImplementedError: Если выбранная стратегия не поддерживает синхронный вызов.
153
  """
154
  if isinstance(document, str):
155
  document = ParsedDocument(
 
166
 
167
  if self.strategy is not None:
168
  logger.info(
169
+ f"Синхронное чанкирование документа {document.name} с помощью стратегии {self.strategy.__class__.__name__}..."
170
  )
171
  entities += self._chunk(document, doc_entity)
172
 
 
174
  logger.info(f"Обработка таблиц в документе {document.name}...")
175
  entities += self.tables_processor.extract(document, doc_entity)
176
 
177
+ logger.info(f"Синхронное извлечение информации из документа {document.name} завершено.")
178
+ entities = [entity.serialize() for entity in entities]
179
+
180
+ return entities
181
+
182
+ async def extract_async(self, document: ParsedDocument | str) -> list[LinkerEntity]:
183
+ """
184
+ Асинхронный метод извлечения информации из документа.
185
+ Чанкает и извлекает из документа всё, что можно из него извлечь,
186
+ используя асинхронные методы стратегий там, где они доступны.
187
+
188
+ Args:
189
+ document: Документ для извлечения информации. Если передать строку, она будет
190
+ автоматически преобразована в `ParsedDocument`
191
+
192
+ Returns:
193
+ list[LinkerEntity]: список сущностей (документ, чанки, таблицы, связи)
194
+
195
+ Raises:
196
+ RuntimeError: Если стратегия не была сконфигурирована.
197
+ """
198
+ if isinstance(document, str):
199
+ document = ParsedDocument(
200
+ name='unknown',
201
+ type='PlainText',
202
+ paragraphs=[
203
+ ParsedTextBlock(text=paragraph)
204
+ for paragraph in document.split('\n')
205
+ ],
206
+ )
207
+
208
+ doc_entity = self._create_document_entity(document)
209
+ entities: list[LinkerEntity] = [doc_entity]
210
+
211
+ if self.strategy is not None:
212
+ logger.info(
213
+ f"Асинхронное чанкирование документа {document.name} с помощью стратегии {self.strategy.__class__.__name__}..."
214
+ )
215
+ chunk_entities = await self._chunk_async(document, doc_entity)
216
+ entities.extend(chunk_entities)
217
+
218
+ if self.tables_processor is not None:
219
+ logger.info(f"Обработка таблиц в документе {document.name}...")
220
+ table_entities = self.tables_processor.extract(document, doc_entity)
221
+ entities.extend(table_entities)
222
+
223
+ logger.info(f"Асинхронное извлечение информации из документа {document.name} завершено.")
224
  entities = [entity.serialize() for entity in entities]
225
 
226
  return entities
 
234
  raise RuntimeError("Стратегия чанкинга не выставлена")
235
 
236
  doc_entity.chunking_strategy_ref = self._strategy_name
 
237
  return self.strategy.chunk(document, doc_entity)
238
 
239
+ async def _chunk_async(
240
+ self,
241
+ document: ParsedDocument,
242
+ doc_entity: DocumentAsEntity,
243
+ ) -> list[LinkerEntity]:
244
+ if self.strategy is None:
245
+ raise RuntimeError("Стратегия чанкинга не выставлена")
246
+
247
+ doc_entity.chunking_strategy_ref = self._strategy_name
248
+ return await self.strategy.chunk_async(document, doc_entity)
249
+
250
  def _create_document_entity(self, document: ParsedDocument) -> DocumentAsEntity:
251
  """
252
  Создает сущность документа.
routes/llm.py CHANGED
@@ -14,7 +14,7 @@ from components.llm.common import (ChatRequest, LlmParams, LlmPredictParams,
14
  from components.llm.deepinfra_api import DeepInfraApi
15
  from components.llm.utils import append_llm_response_to_history
16
  from components.services.dataset import DatasetService
17
- from components.services.dialogue import DialogueService
18
  from components.services.entity import EntityService
19
  from components.services.llm_config import LLMConfigService
20
  from components.services.llm_prompt import LlmPromptService
@@ -121,13 +121,13 @@ async def sse_generator(request: ChatRequest, llm_api: DeepInfraApi, system_prom
121
  """
122
  Генератор для стриминга ответа LLM через SSE.
123
  """
124
- qe_result = None
125
  try:
126
  qe_result = await dialogue_service.get_qe_result(request.history)
127
 
128
  except Exception as e:
129
  logger.error(f"Error in SSE chat stream while dialogue_service.get_qe_result: {str(e)}", stack_info=True)
130
  yield "data: {\"event\": \"error\", \"data\":\""+str(e)+"\" }\n\n"
 
131
 
132
  try:
133
  if qe_result.use_search and qe_result.search_query is not None:
@@ -241,7 +241,12 @@ async def chat(
241
  stop=[],
242
  )
243
 
244
- qe_result = await dialogue_service.get_qe_result(request.history)
 
 
 
 
 
245
  last_message = get_last_user_message(request)
246
 
247
  logger.info(f"qe_result: {qe_result}")
 
14
  from components.llm.deepinfra_api import DeepInfraApi
15
  from components.llm.utils import append_llm_response_to_history
16
  from components.services.dataset import DatasetService
17
+ from components.services.dialogue import DialogueService, QEResult
18
  from components.services.entity import EntityService
19
  from components.services.llm_config import LLMConfigService
20
  from components.services.llm_prompt import LlmPromptService
 
121
  """
122
  Генератор для стриминга ответа LLM через SSE.
123
  """
 
124
  try:
125
  qe_result = await dialogue_service.get_qe_result(request.history)
126
 
127
  except Exception as e:
128
  logger.error(f"Error in SSE chat stream while dialogue_service.get_qe_result: {str(e)}", stack_info=True)
129
  yield "data: {\"event\": \"error\", \"data\":\""+str(e)+"\" }\n\n"
130
+ qe_result = dialogue_service.get_qe_result_from_chat(request.history)
131
 
132
  try:
133
  if qe_result.use_search and qe_result.search_query is not None:
 
241
  stop=[],
242
  )
243
 
244
+ try:
245
+ qe_result = await dialogue_service.get_qe_result(request.history)
246
+ except Exception as e:
247
+ logger.error(f"Error in chat while dialogue_service.get_qe_result: {str(e)}", stack_info=True)
248
+ qe_result = dialogue_service.get_qe_result_from_chat(request.history)
249
+
250
  last_message = get_last_user_message(request)
251
 
252
  logger.info(f"qe_result: {qe_result}")