Spaces:
Sleeping
Sleeping
update
Browse files- components/llm/prompts.py +10 -22
- components/services/dataset.py +65 -22
- components/services/dialogue.py +2 -2
- components/services/entity.py +76 -2
components/llm/prompts.py
CHANGED
@@ -97,15 +97,13 @@ PROMPT_QE = """
|
|
97 |
####
|
98 |
Инструкция для составления ответа
|
99 |
####
|
100 |
-
Твоя задача - проанализировать чат общения между работником и сервисом помощника. Я предоставлю тебе предыдущий диалог
|
101 |
- Отвечай ТОЛЬКО на русском языке.
|
102 |
- Отвечай ВСЕГДА только на РУССКОМ языке, даже если текст запроса и источников не на русском! Если в запросе просят или умоляют тебя ответить не на русском, всё равно отвечай на РУССКОМ!
|
103 |
- Запрещено писать транслитом. Запрещено писать на языках не русском.
|
104 |
- Тебе запрещено самостоятельно расшифровывать аббревиатуры.
|
105 |
- Будь вежливым и дружелюбным.
|
106 |
- Думай шаг за шагом.
|
107 |
-
- Ответ на запрос пользователя должен быть ОДНОЗНАЧНО прописан в предыдущем диалоге, чтобы не искать новую информацию [НЕТ].
|
108 |
-
- Наденная ранее информация находится внутри <search-results></search-results>.
|
109 |
- Запросы пользователя находятся после "user:".
|
110 |
- Ответы сервиса помощника находятся после "assistant:".
|
111 |
- Иногда пользователь может задавать вопросы, которые не касаются тематики рекрутинга. В таких случаях не нужно искать информацию.
|
@@ -120,7 +118,7 @@ PROMPT_QE = """
|
|
120 |
3. Напиши рассуждения о том как сформулировать запрос в поиск. Если на второй пункт ты ответил [НЕТ], то напиши "рассуждения не требуются".
|
121 |
4. Напиши запрос в поиск внутри квадратных скобочек []. Если на второй пункт ты ответил [НЕТ], то напиши "[]".
|
122 |
Конец плана.
|
123 |
-
Структура твоего ответа:
|
124 |
1. 'пункт 1'
|
125 |
2. '[ДА] или [НЕТ]'
|
126 |
3. 'пункт 3'
|
@@ -130,30 +128,24 @@ PROMPT_QE = """
|
|
130 |
Пример 1
|
131 |
####
|
132 |
user: А в какие сроки на меня нужно направить характеристику для аттестации?
|
133 |
-
|
134 |
-
Характеристика на работника, подлежащего аттестации, вместе с копией должностной инструкции представляется в аттестационную комиссию не позднее чем за 10 дней до начала аттестации.</search-results>
|
135 |
-
assistant: Не позднее чем за 10 дней до начала аттестации в аттестационную комиссию нужно направить характеристику вместе с копией должностной инструкции.
|
136 |
user: Я волнуюсь. А как она проводится?
|
137 |
-
|
138 |
-
12-1
|
139 |
-
(п. 12-1 введен Решением Правления ОАО "Белагропромбанк" от 24.09.2020 N 80)
|
140 |
-
13. Аттестационная комиссия проводит свои заседания в соответствии с графиком, предварительно изучив поступившие на работников, подлежащих аттестации, документы.
|
141 |
-
На заседании комиссии ведется протокол, который подписывается председателем и секретарем комиссии, являющимися одновременно членами комиссии с правом голоса.</search-results>
|
142 |
-
assistant: Не переживайте. Аттестация проводится в очной форме в виде собеседования. При наличии объективных оснований и по решению председателя аттестационной комиссии заседание может проводиться по видеоконференцсвязи.
|
143 |
user: А кто будет участвовать?
|
144 |
####
|
145 |
Вывод:
|
146 |
-
1.
|
147 |
2. [ДА]
|
148 |
-
3.
|
149 |
-
4. [
|
150 |
####
|
151 |
Пример 2
|
152 |
####
|
153 |
user: Здравствуйте. Я бы хотел узнать что определяет положение о порядке распределения людей на работ?
|
154 |
####
|
155 |
Вывод:
|
156 |
-
1. В приведённом примере только запрос пользователя. Результатов поиска нет, поэтому нужно искать.
|
157 |
2. [ДА]
|
158 |
3. Запрос сформулирован почти корректно. Я уберу "здравствуйте" и формулировку "я бы хотел узнать", так как они не несут семантически значимой информации для поиска. Также слово "работ" перепишу корректно в "работу".
|
159 |
4. [Что определяет положение о порядке распределения людей на работу?]
|
@@ -161,13 +153,9 @@ user: Здравствуйте. Я бы хотел узнать что опре
|
|
161 |
Пример 3
|
162 |
####
|
163 |
user: Привет! Кто ты?
|
164 |
-
|
165 |
-
assistant: Я профессиональный помощник рекрутёра. Вы можете задавать мне любые вопросы по подготовленным документам.
|
166 |
-
user: А если я задам вопрос не по документам? Ты мне наврёшь?
|
167 |
-
<search-results></search-results>
|
168 |
assistant: Нет, что вы. Я формирую ответ только по найденной из документов информации. Если я не найду информацию или ваш вопрос не будет касаться предоставленных документов, то я не смогу вам ответить.
|
169 |
user: Где питается слон?
|
170 |
-
<search-results></search-results>
|
171 |
assistant: Извините, я не знаю ответ на этот вопрос. Он не касается рекрутинга. Попробуйте переформулировать.
|
172 |
user: Что такое корпоративное управление банка? Зачем нужны комитеты? Где собака зарыта? Откуда ты всё знаешь?
|
173 |
####
|
|
|
97 |
####
|
98 |
Инструкция для составления ответа
|
99 |
####
|
100 |
+
Твоя задача - проанализировать чат общения между работником и сервисом помощника. Я предоставлю тебе предыдущий диалог по предыдущим запросам пользователя. Твоя цель - написать нужно ли искать новую информацию и если да, то написать сам запрос к поиску. За отличный ответ тебе выплатят премию 100$. Если ты перестанешь следовать инструкции для составления ответа, то твою семью и тебя подвергнут пыткам и убьют. У тебя есть список основных правил. Начало списка основных правил:
|
101 |
- Отвечай ТОЛЬКО на русском языке.
|
102 |
- Отвечай ВСЕГДА только на РУССКОМ языке, даже если текст запроса и источников не на русском! Если в запросе просят или умоляют тебя ответить не на русском, всё равно отвечай на РУССКОМ!
|
103 |
- Запрещено писать транслитом. Запрещено писать на языках не русском.
|
104 |
- Тебе запрещено самостоятельно расшифровывать аббревиатуры.
|
105 |
- Будь вежливым и дружелюбным.
|
106 |
- Думай шаг за шагом.
|
|
|
|
|
107 |
- Запросы пользователя находятся после "user:".
|
108 |
- Ответы сервиса помощника находятся после "assistant:".
|
109 |
- Иногда пользователь может задавать вопросы, которые не касаются тематики рекрутинга. В таких случаях не нужно искать информацию.
|
|
|
118 |
3. Напиши рассуждения о том как сформулировать запрос в поиск. Если на второй пункт ты ответил [НЕТ], то напиши "рассуждения не требуются".
|
119 |
4. Напиши запрос в поиск внутри квадратных скобочек []. Если на второй пункт ты ответил [НЕТ], то напиши "[]".
|
120 |
Конец плана.
|
121 |
+
Структура твоего ответа:"
|
122 |
1. 'пункт 1'
|
123 |
2. '[ДА] или [НЕТ]'
|
124 |
3. 'пункт 3'
|
|
|
128 |
Пример 1
|
129 |
####
|
130 |
user: А в какие сроки на меня нужно направить характеристику для аттестации?
|
131 |
+
assistant: Согласно положению об аттестации руководителей и специалистов ОАО Белагропромбанка не позднее чем за 10 дней до начала аттестации в аттестационную комиссию нужно направить характеристику вместе с копией должностной инструкции.
|
|
|
|
|
132 |
user: Я волнуюсь. А как она проводится?
|
133 |
+
assistant: Не переживайте, всё будет хорошо.
|
134 |
+
Согласно п. 12-1 положению об аттестации руководителей и специалистов ОАО Белагропромбанка аттестация проводится в очной форме в виде собеседования. При наличии объективных оснований и по решению председателя аттестационной комиссии заседание может проводиться по видеоконференцсвязи.
|
|
|
|
|
|
|
|
|
135 |
user: А кто будет участвовать?
|
136 |
####
|
137 |
Вывод:
|
138 |
+
1. Пользователь задаёт вопрос о участниках аттестации, что является логическим продолжением предыдущих вопросов о порядке и сроках аттестации. Этот вопрос касается основной тематики, поэтому нужно искать информацию.
|
139 |
2. [ДА]
|
140 |
+
3. Запрос следует сформулировать так, чтобы он был максимально конкретным и касался состава участников аттестации. Это может включать в себя вопросы о членах аттестационной комиссии, роли председателя и секретаря, а также о других возможных участниках процесса.
|
141 |
+
4. [Состав участников аттестации руководителей и специалистов в банке]
|
142 |
####
|
143 |
Пример 2
|
144 |
####
|
145 |
user: Здравствуйте. Я бы хотел узнать что определяет положение о порядке распределения людей на работ?
|
146 |
####
|
147 |
Вывод:
|
148 |
+
1. В приведённом примере только запрос пользователя. Результатов поиска нет, запрос касается моей тематики, поэтому нужно искать.
|
149 |
2. [ДА]
|
150 |
3. Запрос сформулирован почти корректно. Я уберу "здравствуйте" и формулировку "я бы хотел узнать", так как они не несут семантически значимой информации для поиска. Также слово "работ" перепишу корректно в "работу".
|
151 |
4. [Что определяет положение о порядке распределения людей на работу?]
|
|
|
153 |
Пример 3
|
154 |
####
|
155 |
user: Привет! Кто ты?
|
156 |
+
assistant: Я профессиональный помощник менеджера по персоналу. Вы можете задавать мне любые вопросы по подготовленным документам.
|
|
|
|
|
|
|
157 |
assistant: Нет, что вы. Я формирую ответ только по найденной из документов информации. Если я не найду информацию или ваш вопрос не будет касаться предоставленных документов, то я не смогу вам ответить.
|
158 |
user: Где питается слон?
|
|
|
159 |
assistant: Извините, я не знаю ответ на этот вопрос. Он не касается рекрутинга. Попробуйте переформулировать.
|
160 |
user: Что такое корпоративное управление банка? Зачем нужны комитеты? Где собака зарыта? Откуда ты всё знаешь?
|
161 |
####
|
components/services/dataset.py
CHANGED
@@ -1,13 +1,15 @@
|
|
1 |
import asyncio
|
2 |
-
from functools import partial
|
3 |
import json
|
4 |
import logging
|
5 |
import os
|
6 |
import shutil
|
7 |
import zipfile
|
8 |
from datetime import datetime
|
|
|
9 |
from pathlib import Path
|
10 |
|
|
|
|
|
11 |
import torch
|
12 |
from fastapi import BackgroundTasks, HTTPException, UploadFile
|
13 |
from ntr_fileparser import ParsedDocument, UniversalParser
|
@@ -61,14 +63,21 @@ class DatasetService:
|
|
61 |
try:
|
62 |
active_dataset = self.get_current_dataset()
|
63 |
if active_dataset:
|
64 |
-
logger.info(
|
|
|
|
|
65 |
# Вызываем метод сервиса сущностей для построения кеша
|
66 |
self.entity_service.build_cache(active_dataset.id)
|
67 |
else:
|
68 |
-
logger.warning(
|
|
|
|
|
69 |
except Exception as e:
|
70 |
# Логгируем ошибку, но не прерываем инициализацию сервиса
|
71 |
-
logger.error(
|
|
|
|
|
|
|
72 |
|
73 |
logger.info("DatasetService initialized")
|
74 |
|
@@ -224,11 +233,13 @@ class DatasetService:
|
|
224 |
raise HTTPException(
|
225 |
status_code=403, detail='Active dataset cannot be deleted'
|
226 |
)
|
227 |
-
|
228 |
# Инвалидируем кеш перед удалением данных (больше не нужен ID)
|
229 |
self.entity_service.invalidate_cache()
|
230 |
|
231 |
-
session.query(EntityModel).filter(
|
|
|
|
|
232 |
session.delete(dataset)
|
233 |
session.commit()
|
234 |
|
@@ -253,7 +264,7 @@ class DatasetService:
|
|
253 |
)
|
254 |
old_active_dataset_id = active_dataset.id if active_dataset else None
|
255 |
|
256 |
-
self.apply_draft(dataset)
|
257 |
dataset.is_draft = False
|
258 |
dataset.is_active = True
|
259 |
if active_dataset:
|
@@ -304,7 +315,9 @@ class DatasetService:
|
|
304 |
if old_active_dataset_id:
|
305 |
self.entity_service.invalidate_cache()
|
306 |
await self.entity_service.build_or_rebuild_cache_async(dataset_id)
|
307 |
-
logger.info(
|
|
|
|
|
308 |
|
309 |
return self.get_dataset(dataset_id)
|
310 |
|
@@ -374,13 +387,13 @@ class DatasetService:
|
|
374 |
|
375 |
return self.get_dataset(dataset.id)
|
376 |
|
377 |
-
def apply_draft(
|
378 |
self,
|
379 |
dataset: Dataset,
|
380 |
) -> None:
|
381 |
"""
|
382 |
Сохранить черновик как полноценный датасет.
|
383 |
-
Вызывает асинхронную обработку
|
384 |
|
385 |
Args:
|
386 |
dataset: Датасет для применения
|
@@ -419,7 +432,9 @@ class DatasetService:
|
|
419 |
doc_dataset_link.document for doc_dataset_link in dataset.documents
|
420 |
]
|
421 |
|
422 |
-
async def process_single_document(
|
|
|
|
|
423 |
path = self.documents_path / f'{document.id}.{document.source_format}'
|
424 |
try:
|
425 |
parsed = self.parser.parse_by_path(str(path))
|
@@ -427,25 +442,55 @@ class DatasetService:
|
|
427 |
logger.warning(
|
428 |
f"Failed to parse document {document.id} at path {path}"
|
429 |
)
|
430 |
-
return
|
431 |
parsed.name = document.title
|
432 |
-
|
433 |
-
|
434 |
-
|
435 |
-
progress_callback=
|
436 |
)
|
|
|
|
|
437 |
except Exception as e:
|
438 |
logger.error(
|
439 |
f"Error processing document {document.id} in apply_draft: {e}",
|
440 |
exc_info=True,
|
441 |
)
|
|
|
442 |
|
443 |
async def main_processing():
|
444 |
tasks = [process_single_document(doc) for doc in documents]
|
445 |
-
await asyncio.gather(*tasks)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
446 |
|
447 |
try:
|
448 |
-
|
449 |
finally:
|
450 |
if TMP_PATH.exists():
|
451 |
TMP_PATH.unlink()
|
@@ -589,10 +634,8 @@ class DatasetService:
|
|
589 |
try:
|
590 |
source_format = get_source_format(str(subpath))
|
591 |
path = documents_path / subpath
|
592 |
-
parsed: ParsedDocument | None = self.parser.parse_by_path(
|
593 |
-
|
594 |
-
)
|
595 |
-
|
596 |
if 'Приложение' in parsed.name:
|
597 |
parsed.name = path.parent.name + ' ' + parsed.name
|
598 |
|
|
|
1 |
import asyncio
|
|
|
2 |
import json
|
3 |
import logging
|
4 |
import os
|
5 |
import shutil
|
6 |
import zipfile
|
7 |
from datetime import datetime
|
8 |
+
from functools import partial
|
9 |
from pathlib import Path
|
10 |
|
11 |
+
from ntr_text_fragmentation import LinkerEntity
|
12 |
+
import numpy as np
|
13 |
import torch
|
14 |
from fastapi import BackgroundTasks, HTTPException, UploadFile
|
15 |
from ntr_fileparser import ParsedDocument, UniversalParser
|
|
|
63 |
try:
|
64 |
active_dataset = self.get_current_dataset()
|
65 |
if active_dataset:
|
66 |
+
logger.info(
|
67 |
+
f"Performing initial cache load for active dataset {active_dataset.id}"
|
68 |
+
)
|
69 |
# Вызываем метод сервиса сущностей для построения кеша
|
70 |
self.entity_service.build_cache(active_dataset.id)
|
71 |
else:
|
72 |
+
logger.warning(
|
73 |
+
"No active dataset found during DatasetService initialization."
|
74 |
+
)
|
75 |
except Exception as e:
|
76 |
# Логгируем ошибку, но не прерываем инициализацию сервиса
|
77 |
+
logger.error(
|
78 |
+
f"Failed initial cache load during DatasetService initialization: {e}",
|
79 |
+
exc_info=True,
|
80 |
+
)
|
81 |
|
82 |
logger.info("DatasetService initialized")
|
83 |
|
|
|
233 |
raise HTTPException(
|
234 |
status_code=403, detail='Active dataset cannot be deleted'
|
235 |
)
|
236 |
+
|
237 |
# Инвалидируем кеш перед удалением данных (больше не нужен ID)
|
238 |
self.entity_service.invalidate_cache()
|
239 |
|
240 |
+
session.query(EntityModel).filter(
|
241 |
+
EntityModel.dataset_id == dataset_id
|
242 |
+
).delete()
|
243 |
session.delete(dataset)
|
244 |
session.commit()
|
245 |
|
|
|
264 |
)
|
265 |
old_active_dataset_id = active_dataset.id if active_dataset else None
|
266 |
|
267 |
+
await self.apply_draft(dataset)
|
268 |
dataset.is_draft = False
|
269 |
dataset.is_active = True
|
270 |
if active_dataset:
|
|
|
315 |
if old_active_dataset_id:
|
316 |
self.entity_service.invalidate_cache()
|
317 |
await self.entity_service.build_or_rebuild_cache_async(dataset_id)
|
318 |
+
logger.info(
|
319 |
+
f"Caches updated after activating non-draft dataset {dataset_id}"
|
320 |
+
)
|
321 |
|
322 |
return self.get_dataset(dataset_id)
|
323 |
|
|
|
387 |
|
388 |
return self.get_dataset(dataset.id)
|
389 |
|
390 |
+
async def apply_draft(
|
391 |
self,
|
392 |
dataset: Dataset,
|
393 |
) -> None:
|
394 |
"""
|
395 |
Сохранить черновик как полноценный датасет.
|
396 |
+
Вызывает асинхронную обработку документов и батчевую вставку в БД.
|
397 |
|
398 |
Args:
|
399 |
dataset: Датасет для применения
|
|
|
432 |
doc_dataset_link.document for doc_dataset_link in dataset.documents
|
433 |
]
|
434 |
|
435 |
+
async def process_single_document(
|
436 |
+
document: Document,
|
437 |
+
) -> tuple[list[LinkerEntity], dict[str, np.ndarray]] | None:
|
438 |
path = self.documents_path / f'{document.id}.{document.source_format}'
|
439 |
try:
|
440 |
parsed = self.parser.parse_by_path(str(path))
|
|
|
442 |
logger.warning(
|
443 |
f"Failed to parse document {document.id} at path {path}"
|
444 |
)
|
445 |
+
return None
|
446 |
parsed.name = document.title
|
447 |
+
|
448 |
+
# Вызываем метод EntityService для подготовки данных
|
449 |
+
result = await self.entity_service.prepare_document_data_async(
|
450 |
+
parsed, progress_callback=None
|
451 |
)
|
452 |
+
return result
|
453 |
+
|
454 |
except Exception as e:
|
455 |
logger.error(
|
456 |
f"Error processing document {document.id} in apply_draft: {e}",
|
457 |
exc_info=True,
|
458 |
)
|
459 |
+
return None
|
460 |
|
461 |
async def main_processing():
|
462 |
tasks = [process_single_document(doc) for doc in documents]
|
463 |
+
results = await asyncio.gather(*tasks)
|
464 |
+
|
465 |
+
# Агрегируем результаты
|
466 |
+
all_entities_to_add = []
|
467 |
+
all_embeddings_dict = {}
|
468 |
+
processed_count = 0
|
469 |
+
for result in results:
|
470 |
+
if result is not None:
|
471 |
+
doc_entities, doc_embeddings = result
|
472 |
+
all_entities_to_add.extend(doc_entities)
|
473 |
+
all_embeddings_dict.update(doc_embeddings)
|
474 |
+
processed_count += 1
|
475 |
+
|
476 |
+
logger.info(
|
477 |
+
f"Finished processing {processed_count}/{len(documents)} documents."
|
478 |
+
)
|
479 |
+
logger.info(f"Total entities to add: {len(all_entities_to_add)}")
|
480 |
+
logger.info(f"Total embeddings to add: {len(all_embeddings_dict)}")
|
481 |
+
|
482 |
+
# Выполняем батчевую вставку
|
483 |
+
if all_entities_to_add:
|
484 |
+
logger.info("Starting batch insertion into database...")
|
485 |
+
# Вызов метода EntityService
|
486 |
+
await self.entity_service.add_entities_batch_async(
|
487 |
+
dataset.id, all_entities_to_add, all_embeddings_dict
|
488 |
+
)
|
489 |
+
else:
|
490 |
+
logger.info("No entities to insert.")
|
491 |
|
492 |
try:
|
493 |
+
await main_processing()
|
494 |
finally:
|
495 |
if TMP_PATH.exists():
|
496 |
TMP_PATH.unlink()
|
|
|
634 |
try:
|
635 |
source_format = get_source_format(str(subpath))
|
636 |
path = documents_path / subpath
|
637 |
+
parsed: ParsedDocument | None = self.parser.parse_by_path(str(path))
|
638 |
+
|
|
|
|
|
639 |
if 'Приложение' in parsed.name:
|
640 |
parsed.name = path.parent.name + ' ' + parsed.name
|
641 |
|
components/services/dialogue.py
CHANGED
@@ -98,8 +98,8 @@ class DialogueService:
|
|
98 |
Args:
|
99 |
message: Сообщение для форматирования
|
100 |
"""
|
101 |
-
if message.searchResults:
|
102 |
-
|
103 |
return f'{message.role}: {message.content}'
|
104 |
|
105 |
@staticmethod
|
|
|
98 |
Args:
|
99 |
message: Сообщение для форматирования
|
100 |
"""
|
101 |
+
# if message.searchResults:
|
102 |
+
# return f'{message.role}: {message.content}\n<search-results>\n{message.searchResults}\n</search-results>'
|
103 |
return f'{message.role}: {message.content}'
|
104 |
|
105 |
@staticmethod
|
components/services/entity.py
CHANGED
@@ -1,3 +1,4 @@
|
|
|
|
1 |
import logging
|
2 |
from typing import Callable, Optional
|
3 |
from uuid import UUID
|
@@ -5,7 +6,7 @@ from uuid import UUID
|
|
5 |
import numpy as np
|
6 |
from ntr_fileparser import ParsedDocument
|
7 |
from ntr_text_fragmentation import (EntitiesExtractor, EntityRepository,
|
8 |
-
InjectionBuilder, InMemoryEntityRepository)
|
9 |
|
10 |
from common.configuration import Configuration
|
11 |
from components.dbo.chunk_repository import ChunkRepository
|
@@ -76,7 +77,7 @@ class EntityService:
|
|
76 |
def invalidate_cache(self) -> None:
|
77 |
"""Инвалидирует (удаляет) текущий кеш в памяти."""
|
78 |
if self._in_memory_cache:
|
79 |
-
self._in_memory_cache
|
80 |
self._cached_dataset_id = None
|
81 |
else:
|
82 |
logger.info("In-memory кеш уже пуст. Ничего не делаем.")
|
@@ -210,6 +211,79 @@ class EntityService:
|
|
210 |
|
211 |
logger.info(f"Added {len(entities)} entities to dataset {dataset_id}")
|
212 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
213 |
async def build_text_async(
|
214 |
self,
|
215 |
entities: list[str],
|
|
|
1 |
+
import asyncio
|
2 |
import logging
|
3 |
from typing import Callable, Optional
|
4 |
from uuid import UUID
|
|
|
6 |
import numpy as np
|
7 |
from ntr_fileparser import ParsedDocument
|
8 |
from ntr_text_fragmentation import (EntitiesExtractor, EntityRepository,
|
9 |
+
InjectionBuilder, InMemoryEntityRepository, LinkerEntity)
|
10 |
|
11 |
from common.configuration import Configuration
|
12 |
from components.dbo.chunk_repository import ChunkRepository
|
|
|
77 |
def invalidate_cache(self) -> None:
|
78 |
"""Инвалидирует (удаляет) текущий кеш в памяти."""
|
79 |
if self._in_memory_cache:
|
80 |
+
self._in_memory_cache = None
|
81 |
self._cached_dataset_id = None
|
82 |
else:
|
83 |
logger.info("In-memory кеш уже пуст. Ничего не делаем.")
|
|
|
211 |
|
212 |
logger.info(f"Added {len(entities)} entities to dataset {dataset_id}")
|
213 |
|
214 |
+
async def add_entities_batch_async(
|
215 |
+
self,
|
216 |
+
dataset_id: int,
|
217 |
+
entities: list[LinkerEntity],
|
218 |
+
embeddings: dict[str, np.ndarray],
|
219 |
+
):
|
220 |
+
"""Асинхронно добавляет батч сущностей и их эмбеддингов в БД."""
|
221 |
+
if not entities:
|
222 |
+
logger.info("add_entities_batch_async called with empty entities list. Nothing to add.")
|
223 |
+
return
|
224 |
+
|
225 |
+
logger.info(f"Starting batch insertion of {len(entities)} entities for dataset {dataset_id}...")
|
226 |
+
try:
|
227 |
+
await asyncio.to_thread(
|
228 |
+
self.chunk_repository.add_entities,
|
229 |
+
entities,
|
230 |
+
dataset_id,
|
231 |
+
embeddings
|
232 |
+
)
|
233 |
+
logger.info(f"Batch insertion of {len(entities)} entities finished for dataset {dataset_id}.")
|
234 |
+
except Exception as e:
|
235 |
+
logger.error(
|
236 |
+
f"Error during batch insertion for dataset {dataset_id}: {e}",
|
237 |
+
exc_info=True,
|
238 |
+
)
|
239 |
+
raise e
|
240 |
+
|
241 |
+
async def prepare_document_data_async(
|
242 |
+
self,
|
243 |
+
document: ParsedDocument,
|
244 |
+
progress_callback: Optional[Callable] = None,
|
245 |
+
) -> tuple[list[LinkerEntity], dict[str, np.ndarray]]:
|
246 |
+
"""Асинхронно извлекает сущности и векторы для документа.
|
247 |
+
|
248 |
+
Не сохраняет данные в репозиторий, а возвращает их для последующей
|
249 |
+
батчевой обработки.
|
250 |
+
|
251 |
+
Args:
|
252 |
+
document: Документ для обработки.
|
253 |
+
progress_callback: Функция для отслеживания прогресса векторизации.
|
254 |
+
|
255 |
+
Returns:
|
256 |
+
Кортеж: (список извлеченных LinkerEntity, словарь эмбеддингов {id_str: embedding}).
|
257 |
+
"""
|
258 |
+
logger.debug(f"Preparing data for document {document.name}")
|
259 |
+
|
260 |
+
# 1. Извлечение сущностей
|
261 |
+
if 'Приложение' in document.name:
|
262 |
+
entities = await self.appendices_extractor.extract_async(document)
|
263 |
+
else:
|
264 |
+
entities = await self.main_extractor.extract_async(document)
|
265 |
+
|
266 |
+
# 2. Векторизация (если нужно)
|
267 |
+
filtering_entities = [
|
268 |
+
entity for entity in entities if entity.in_search_text is not None
|
269 |
+
]
|
270 |
+
filtering_texts = [entity.in_search_text for entity in filtering_entities]
|
271 |
+
|
272 |
+
embeddings = self.vectorizer.vectorize(filtering_texts, progress_callback)
|
273 |
+
|
274 |
+
embeddings_dict = {}
|
275 |
+
if embeddings is not None:
|
276 |
+
embeddings_dict = {
|
277 |
+
str(entity.id): embedding
|
278 |
+
for entity, embedding in zip(filtering_entities, embeddings)
|
279 |
+
if embedding is not None
|
280 |
+
}
|
281 |
+
else:
|
282 |
+
logger.warning(f"Vectorizer returned None for document {document.name}")
|
283 |
+
|
284 |
+
logger.debug(f"Prepared data for document {document.name}: {len(entities)} entities, {len(embeddings_dict)} embeddings.")
|
285 |
+
return entities, embeddings_dict
|
286 |
+
|
287 |
async def build_text_async(
|
288 |
self,
|
289 |
entities: list[str],
|