Spaces:
Runtime error
Runtime error
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],
|