Spaces:
Runtime error
Runtime error
update
Browse files- common/auth.py +1 -1
- common/dependencies.py +37 -27
- components/llm/prompts.py +159 -0
- components/search/appendices_chunker.py +104 -0
- components/search/faiss_vector_search.py +56 -0
- components/services/dataset.py +38 -22
- components/services/dialogue.py +11 -1
- components/services/entity.py +62 -30
- lib/extractor/ntr_text_fragmentation/__init__.py +13 -5
- lib/extractor/ntr_text_fragmentation/chunking/__init__.py +2 -0
- lib/extractor/ntr_text_fragmentation/chunking/chunking_strategy.py +21 -0
- lib/extractor/ntr_text_fragmentation/chunking/specific_strategies/fixed_size_chunking.py +6 -0
- lib/extractor/ntr_text_fragmentation/chunking/text_to_text_base.py +30 -5
- lib/extractor/ntr_text_fragmentation/core/extractor.py +69 -11
- routes/llm.py +8 -3
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 =
|
| 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(
|
|
|
|
|
|
|
| 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 |
-
|
| 395 |
-
path = self.documents_path / f'{document.id}.
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
parsed
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 407 |
|
| 408 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 409 |
|
| 410 |
def raise_if_processing(self) -> None:
|
| 411 |
"""
|
|
@@ -534,8 +546,8 @@ class DatasetService:
|
|
| 534 |
Создаёт документ в базе данных.
|
| 535 |
|
| 536 |
Args:
|
| 537 |
-
|
| 538 |
-
subpath: Путь к
|
| 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(
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 42 |
-
self.
|
| 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 =
|
|
|
|
|
|
|
| 47 |
|
| 48 |
-
self.
|
| 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 |
-
|
|
|
|
|
|
|
| 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
|
| 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
|
| 219 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
| 230 |
scores = scores.tolist()
|
| 231 |
ids = ids.tolist()
|
| 232 |
-
|
| 233 |
print(ids)
|
| 234 |
|
| 235 |
-
previous_entities_ids = [
|
| 236 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
| 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
|
| 6 |
-
from .repositories
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
) -> list[LinkerEntity]:
|
| 20 |
text = self._get_text(document)
|
| 21 |
-
texts = self.
|
| 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
|
| 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
|
| 7 |
from uuid import uuid4
|
| 8 |
|
| 9 |
from ntr_fileparser import ParsedDocument, ParsedTextBlock
|
| 10 |
|
| 11 |
from ..additors import TablesProcessor
|
| 12 |
-
from ..chunking import
|
| 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 |
-
|
| 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"
|
| 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"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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}")
|