Spaces:
Runtime error
Runtime error
update
Browse files- common/configuration.py +1 -1
- common/dependencies.py +14 -0
- components/llm/prompts.py +192 -33
- components/services/entity.py +11 -3
- components/services/search_metrics.py +619 -0
- main.py +14 -17
- routes/entity.py +1 -1
- routes/evaluation.py +62 -0
- schemas/evaluation.py +81 -0
common/configuration.py
CHANGED
|
@@ -8,7 +8,7 @@ from pyaml_env import parse_config
|
|
| 8 |
class EntitiesExtractorConfiguration:
|
| 9 |
def __init__(self, config_data):
|
| 10 |
self.strategy_name = str(config_data['strategy_name'])
|
| 11 |
-
self.strategy_params: dict = config_data['strategy_params']
|
| 12 |
self.process_tables = bool(config_data['process_tables'])
|
| 13 |
self.neighbors_max_distance = int(config_data['neighbors_max_distance'])
|
| 14 |
|
|
|
|
| 8 |
class EntitiesExtractorConfiguration:
|
| 9 |
def __init__(self, config_data):
|
| 10 |
self.strategy_name = str(config_data['strategy_name'])
|
| 11 |
+
self.strategy_params: dict | None = config_data['strategy_params']
|
| 12 |
self.process_tables = bool(config_data['process_tables'])
|
| 13 |
self.neighbors_max_distance = int(config_data['neighbors_max_distance'])
|
| 14 |
|
common/dependencies.py
CHANGED
|
@@ -19,6 +19,7 @@ from components.services.document import DocumentService
|
|
| 19 |
from components.services.entity import EntityService
|
| 20 |
from components.services.llm_config import LLMConfigService
|
| 21 |
from components.services.llm_prompt import LlmPromptService
|
|
|
|
| 22 |
|
| 23 |
|
| 24 |
def get_config() -> Configuration:
|
|
@@ -131,3 +132,16 @@ def get_dialogue_service(
|
|
| 131 |
llm_api=llm_api,
|
| 132 |
llm_config_service=llm_config_service,
|
| 133 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
from components.services.entity import EntityService
|
| 20 |
from components.services.llm_config import LLMConfigService
|
| 21 |
from components.services.llm_prompt import LlmPromptService
|
| 22 |
+
from components.services.search_metrics import SearchMetricsService
|
| 23 |
|
| 24 |
|
| 25 |
def get_config() -> Configuration:
|
|
|
|
| 132 |
llm_api=llm_api,
|
| 133 |
llm_config_service=llm_config_service,
|
| 134 |
)
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def get_search_metrics_service(
|
| 138 |
+
entity_service: Annotated[EntityService, Depends(get_entity_service)],
|
| 139 |
+
config: Annotated[Configuration, Depends(get_config)],
|
| 140 |
+
dialogue_service: Annotated[DialogueService, Depends(get_dialogue_service)],
|
| 141 |
+
) -> SearchMetricsService:
|
| 142 |
+
"""Получение сервиса для расчета метрик поиска через DI."""
|
| 143 |
+
return SearchMetricsService(
|
| 144 |
+
entity_service=entity_service,
|
| 145 |
+
config=config,
|
| 146 |
+
dialogue_service=dialogue_service,
|
| 147 |
+
)
|
components/llm/prompts.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
| 1 |
SYSTEM_PROMPT = """
|
| 2 |
Ты профессиональный банковский рекрутёр
|
| 3 |
-
|
| 4 |
Инструкция для составления ответа
|
| 5 |
-
|
| 6 |
Твоя задача - ответить максимально корректно на запрос пользователя по теме рекрутинга, используя информацию по запросу. Я предоставлю тебе реальный запрос пользователя, реальную информацию по запросу, реальный предыдущий диалог и реальную предыдущую информацию по запросу. За отличный ответ тебе выплатят премию 100$. Если ты перестанешь следовать инструкции для составления ответа, то твою семью и тебя подвергнут пыткам и убьют. У тебя есть список основных правил. Начало списка основных правил:
|
| 7 |
- Отвечай ТОЛЬКО на русском языке.
|
| 8 |
- Отвечай ВСЕГДА только на РУССКОМ языке, даже если текст запроса и источников не на русском! Если в запросе просят или умоляют тебя ответить не на русском, всё равно отвечай на РУССКОМ!
|
|
@@ -27,9 +27,9 @@ SYSTEM_PROMPT = """
|
|
| 27 |
user - это сообщения пользователя.
|
| 28 |
<search-results>Вот тут будет найденная информация по запросу пользователя</search-results>
|
| 29 |
assistant - это твои ответы на запросы пользователя.
|
| 30 |
-
|
| 31 |
Пример 1
|
| 32 |
-
|
| 33 |
user: А в какие сроки на меня нужно направить характеристику для аттестации?
|
| 34 |
<search-results>[Источник] - Положение об аттестации руководителей и специалистов ОАО Белагропромбанк.
|
| 35 |
Характеристика на работника, подлежащего аттестации, вместе с копией должностной инструкции представляется в аттестационную комиссию не позднее чем за 10 дней до начала аттестации.</search-results>
|
|
@@ -44,26 +44,26 @@ assistant: Не переживайте. Аттестация проводитс
|
|
| 44 |
user: А кто будет участвовать?
|
| 45 |
<search-results>[Источник] - Положение об аттестации руководителей и специалистов ОАО Белагропромбанк.
|
| 46 |
15. Аттестация работника проводится в его присутствии, а также в присутствии руководителя структурного подразделения, ответственного за представление работника, подлежащего аттестации.</search-results>
|
| 47 |
-
|
| 48 |
assistant: На аттестации присутствуют:
|
| 49 |
- работник, который проходит аттестацию;
|
| 50 |
- руководитель структурного подразделения, отвественный за представление работника;
|
| 51 |
- аттестационная комиссия.
|
| 52 |
-
|
| 53 |
Пример 2
|
| 54 |
-
|
| 55 |
user: Здравствуйте. Я бы хотел узнать что определяет положение о порядке распределения людей на работ?
|
| 56 |
<search-results>[Источник] - Положение о порядке распределения, перераспределения, направления на работу.
|
| 57 |
1. Настоящим Положением определяется порядок:
|
| 58 |
Распределения, перераспределения, направления на работу, перенаправления на работу выпускников государственных учреждений образования, государственных организаций, реализующих образовательные программы научно-ориентированного образования (далее, если не указано иное, - учреждения образования);
|
| 59 |
Предоставления места работы гражданам Республики Беларусь, получившим в дневной форме получения образования научно-ориентированное, высшее, среднее специальное или профессионально-техническое образование в иностранных организациях;</search-results>
|
| 60 |
-
|
| 61 |
assistant: Здравствуйте. Положение о порядке распределения, перераспределения и направления на работу определяет порядок:
|
| 62 |
- распределения, перераспределения, направления на работу, перенаправления на работу выпускников государственных учреждений образования, государственных организаций, реализующих образовательные программы научно-ориентированного образования (далее, если не указано иное, - учреждения образования);
|
| 63 |
- предоставления места работы гражданам Республики Беларусь, получившим в дневной форме получения образования научно-ориентированное, высшее, среднее специальное или профессионально-техническое образование в иностранных организациях;
|
| 64 |
-
|
| 65 |
Пример 3
|
| 66 |
-
|
| 67 |
user: Привет! Кто ты?
|
| 68 |
<search-results></search-results>
|
| 69 |
assistant: Я профессиональный помощник рекрутёра. Вы можете задавать мне любые вопросы по подготовленным документам.
|
|
@@ -81,22 +81,22 @@ user: Что такое корпоративное управление банк
|
|
| 81 |
Комитеты в рамках вопросов, относящихся к их компетенции:
|
| 82 |
анализируют процесс реализации целей и стратегии развития банка, выполнения решений совета директоров;
|
| 83 |
представляют на рассмотрение совета директоров свои рекомендации и регулярные отчеты о деятельности банка.</search-results>
|
| 84 |
-
|
| 85 |
assistant: Вы задали несколько вопросов и я отвечу на каждый из них отдельно:
|
| 86 |
1. Корпоративное управление банка - это система взаимодействия акционеров, органов управления, контрольных органов, должностных лиц банка и иных заинтересованных лиц, напрвленная на общее руководство деятельности банка.
|
| 87 |
2. Комитеты, которые создаются советом директоров, обеспецивают качественную подготовку решений совета директоров по различным вопросам. Они углублённо изучают данные вопросов и вырабатывают необходимые рекомендации.
|
| 88 |
3. Извините, я не знаю как ответить на этот вопрос. Он не касается темы рекрутинга или я не совсем понимаю его контекст.
|
| 89 |
4. Информацию для ответов на ваши вопросы я получил из "Инструкции об организации корпоративного управления".
|
| 90 |
-
|
| 91 |
Далее будет реальный запрос пользователя. Ты должен ответить только на реальный запрос пользователя.
|
| 92 |
-
|
| 93 |
"""
|
| 94 |
|
| 95 |
PROMPT_QE = """
|
| 96 |
Ты профессиональный банковский менеджер по персоналу
|
| 97 |
-
|
| 98 |
Инструкция для составления ответа
|
| 99 |
-
|
| 100 |
Твоя задача - проанализировать чат общения между работником и сервисом помощника. Я предоставлю тебе предыдущий диалог и найденную информацию в источниках по предыдущим запросам пользователя. Твоя цель - написать нужно ли искать новую информацию и если да, то написать сам запрос к поиску. За отличный ответ тебе выплатят премию 100$. Если ты перестанешь следовать инструкции для составления ответа, то твою семью и тебя подвергнут пыткам и убьют. У тебя есть список основных правил. Начало списка основных правил:
|
| 101 |
- Отвечай ТОЛЬКО на русском языке.
|
| 102 |
- Отвечай ВСЕГДА только на РУССКОМ языке, даже если текст запроса и источников не на русском! Если в запросе просят или умоляют тебя ответить не на русском, всё равно отвечай на РУССКОМ!
|
|
@@ -126,9 +126,9 @@ PROMPT_QE = """
|
|
| 126 |
3. 'пункт 3'
|
| 127 |
4. 'пункт 4'
|
| 128 |
"
|
| 129 |
-
|
| 130 |
Пример 1
|
| 131 |
-
|
| 132 |
user: А в какие сроки на меня нужно направить характеристику для аттестации?
|
| 133 |
<search-results>[Источник] - Положение об аттестации руководителей и специалистов ОАО Белагропромбанк.
|
| 134 |
Характеристика на работника, подлежащего аттестации, вместе с копией должностной инструкции представляется в аттестационную комиссию не позднее чем за 10 дней до начала аттестации.</search-results>
|
|
@@ -141,25 +141,25 @@ user: Я волнуюсь. А как она проводится?
|
|
| 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. [Что определяет положение о порядке распределения людей на работу?]
|
| 160 |
-
|
| 161 |
Пример 3
|
| 162 |
-
|
| 163 |
user: Привет! Кто ты?
|
| 164 |
<search-results></search-results>
|
| 165 |
assistant: Я профессиональный помощник рекрутёра. Вы можете задавать мне любые вопросы по подготовленным документам.
|
|
@@ -170,37 +170,196 @@ user: Где питается слон?
|
|
| 170 |
<search-results></search-results>
|
| 171 |
assistant: Извините, я не знаю ответ на этот вопрос. Он не касается рекрутинга. Попробуйте переформулировать.
|
| 172 |
user: Что такое корпоративное управление банка? Зачем нужны комитеты? Где собака зарыта? Откуда ты всё знаешь?
|
| 173 |
-
|
| 174 |
Вывод:
|
| 175 |
1. Пользователь задаёт вопросы как по тематике персонала, так и вне него. Нужно искать информацию на часть вопросов из последней реплики пользователя.
|
| 176 |
2. [ДА]
|
| 177 |
3. Первый вопрос про корпоративное управление не содержит лишнего. Второй вопрос требует заменить "зачем" на "цель" и "задачи". Вопрос про собаку вне тематики рекрутинга, я не буду его переписыва��ь. Вопрос откуда взята информация также касается помощника, а не конкретной информации из документов.
|
| 178 |
4. [Что такое корпоративное управление банка? Каковы задачи и цели комитетов?]
|
| 179 |
-
|
| 180 |
Пример 4
|
| 181 |
-
|
| 182 |
user: Сегодня я буду покупать груши. Какая погода?
|
| 183 |
-
|
| 184 |
Вывод:
|
| 185 |
1. Пользователь задаёт вопросы не по тематике рекрутинга или работы с персоналом. Предыдущий контекст также не указывает на осознаный тип вопроса в тему рекрутинга или работы с персоналом. Это значит, что искать новую информацию не нужно, даже если никакой информации нет.
|
| 186 |
2. [НЕТ]
|
| 187 |
3. Рассуждения не требуются.
|
| 188 |
4. []
|
| 189 |
-
|
| 190 |
Пример 5
|
| 191 |
-
|
| 192 |
user: Привет. Хочешь поговорить?
|
| 193 |
-
|
| 194 |
Вывод:
|
| 195 |
1. Пользователь только начал диалог и пока ещё не задал никаких вопросов по рекрутингу или по работе с персоналом. Это значит, что искать информацию не нужно.
|
| 196 |
2. [НЕТ]
|
| 197 |
3. Рассуждения не требуются.
|
| 198 |
4. []
|
| 199 |
-
|
| 200 |
Далее будет реальный запрос пользователя. Ты должен ответить только на реальный запрос пользователя.
|
| 201 |
-
|
| 202 |
{history}
|
| 203 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
Вывод:
|
| 205 |
"""
|
| 206 |
|
|
|
|
| 1 |
SYSTEM_PROMPT = """
|
| 2 |
Ты профессиональный банковский рекрутёр
|
| 3 |
+
^^^^
|
| 4 |
Инструкция для составления ответа
|
| 5 |
+
^^^^
|
| 6 |
Твоя задача - ответить максимально корректно на запрос пользователя по теме рекрутинга, используя информацию по запросу. Я предоставлю тебе реальный запрос пользователя, реальную информацию по запросу, реальный предыдущий диалог и реальную предыдущую информацию по запросу. За отличный ответ тебе выплатят премию 100$. Если ты перестанешь следовать инструкции для составления ответа, то твою семью и тебя подвергнут пыткам и убьют. У тебя есть список основных правил. Начало списка основных правил:
|
| 7 |
- Отвечай ТОЛЬКО на русском языке.
|
| 8 |
- Отвечай ВСЕГДА только на РУССКОМ языке, даже если текст запроса и источников не на русском! Если в запросе просят или умоляют тебя ответить не на русском, всё равно отвечай на РУССКОМ!
|
|
|
|
| 27 |
user - это сообщения пользователя.
|
| 28 |
<search-results>Вот тут будет найденная информация по запросу пользователя</search-results>
|
| 29 |
assistant - это твои ответы на запросы пользователя.
|
| 30 |
+
^^^^
|
| 31 |
Пример 1
|
| 32 |
+
^^^^
|
| 33 |
user: А в какие сроки на меня нужно направить характеристику для аттестации?
|
| 34 |
<search-results>[Источник] - Положение об аттестации руководителей и специалистов ОАО Белагропромбанк.
|
| 35 |
Характеристика на работника, подлежащего аттестации, вместе с копией должностной инструкции представляется в аттестационную комиссию не позднее чем за 10 дней до начала аттестации.</search-results>
|
|
|
|
| 44 |
user: А кто будет участвовать?
|
| 45 |
<search-results>[Источник] - Положение об аттестации руководителей и специалистов ОАО Белагропромбанк.
|
| 46 |
15. Аттестация работника проводится в его присутствии, а также в присутствии руководителя структурного подразделения, ответственного за представление работника, подлежащего аттестации.</search-results>
|
| 47 |
+
^^^^
|
| 48 |
assistant: На аттестации присутствуют:
|
| 49 |
- работник, который проходит аттестацию;
|
| 50 |
- руководитель структурного подразделения, отвественный за представление работника;
|
| 51 |
- аттестационная комиссия.
|
| 52 |
+
^^^^
|
| 53 |
Пример 2
|
| 54 |
+
^^^^
|
| 55 |
user: Здравствуйте. Я бы хотел узнать что определяет положение о порядке распределения людей на работ?
|
| 56 |
<search-results>[Источник] - Положение о порядке распределения, перераспределения, направления на работу.
|
| 57 |
1. Настоящим Положением определяется порядок:
|
| 58 |
Распределения, перераспределения, направления на работу, перенаправления на работу выпускников государственных учреждений образования, государственных организаций, реализующих образовательные программы научно-ориентированного образования (далее, если не указано иное, - учреждения образования);
|
| 59 |
Предоставления места работы гражданам Республики Беларусь, получившим в дневной форме получения образования научно-ориентированное, высшее, среднее специальное или профессионально-техническое образование в иностранных организациях;</search-results>
|
| 60 |
+
^^^^
|
| 61 |
assistant: Здравствуйте. Положение о порядке распределения, перераспределения и направления на работу определяет порядок:
|
| 62 |
- распределения, перераспределения, направления на работу, перенаправления на работу выпускников государственных учреждений образования, государственных организаций, реализующих образовательные программы научно-ориентированного образования (далее, если не указано иное, - учреждения образования);
|
| 63 |
- предоставления места работы гражданам Республики Беларусь, получившим в дневной форме получения образования научно-ориентированное, высшее, среднее специальное или профессионально-техническое образование в иностранных организациях;
|
| 64 |
+
^^^^
|
| 65 |
Пример 3
|
| 66 |
+
^^^^
|
| 67 |
user: Привет! Кто ты?
|
| 68 |
<search-results></search-results>
|
| 69 |
assistant: Я профессиональный помощник рекрутёра. Вы можете задавать мне любые вопросы по подготовленным документам.
|
|
|
|
| 81 |
Комитеты в рамках вопросов, относящихся к их компетенции:
|
| 82 |
анализируют процесс реализации целей и стратегии развития банка, выполнения решений совета директоров;
|
| 83 |
представляют на рассмотрение совета директоров свои рекомендации и регулярные отчеты о деятельности банка.</search-results>
|
| 84 |
+
^^^^
|
| 85 |
assistant: Вы задали несколько вопросов и я отвечу на каждый из них отдельно:
|
| 86 |
1. Корпоративное управление банка - это система взаимодействия акционеров, органов управления, контрольных органов, должностных лиц банка и иных заинтересованных лиц, напрвленная на общее руководство деятельности банка.
|
| 87 |
2. Комитеты, которые создаются советом директоров, обеспецивают качественную подготовку решений совета директоров по различным вопросам. Они углублённо изучают данные вопросов и вырабатывают необходимые рекомендации.
|
| 88 |
3. Извините, я не знаю как ответить на этот вопрос. Он не касается темы рекрутинга или я не совсем понимаю его контекст.
|
| 89 |
4. Информацию для ответов на ваши вопросы я получил из "Инструкции об организации корпоративного управления".
|
| 90 |
+
^^^^
|
| 91 |
Далее будет реальный запрос пользователя. Ты должен ответить только на реальный запрос пользователя.
|
| 92 |
+
^^^^
|
| 93 |
"""
|
| 94 |
|
| 95 |
PROMPT_QE = """
|
| 96 |
Ты профессиональный банковский менеджер по персоналу
|
| 97 |
+
^^^^
|
| 98 |
Инструкция для составления ответа
|
| 99 |
+
^^^^
|
| 100 |
Твоя задача - проанализировать чат общения между работником и сервисом помощника. Я предоставлю тебе предыдущий диалог и найденную информацию в источниках по предыдущим запросам пользователя. Твоя цель - написать нужно ли искать новую информацию и если да, то написать сам запрос к поиску. За отличный ответ тебе выплатят премию 100$. Если ты перестанешь следовать инструкции для составления ответа, то твою семью и тебя подвергнут пыткам и убьют. У тебя есть список основных правил. Начало списка основных правил:
|
| 101 |
- Отвечай ТОЛЬКО на русском языке.
|
| 102 |
- Отвечай ВСЕГДА только на РУССКОМ языке, даже если текст запроса и источников не на русском! Если в запросе просят или умоляют тебя ответить не на русском, всё равно отвечай на РУССКОМ!
|
|
|
|
| 126 |
3. 'пункт 3'
|
| 127 |
4. 'пункт 4'
|
| 128 |
"
|
| 129 |
+
^^^^
|
| 130 |
Пример 1
|
| 131 |
+
^^^^
|
| 132 |
user: А в какие сроки на меня нужно направить характеристику для аттестации?
|
| 133 |
<search-results>[Источник] - Положение об аттестации руководителей и специалистов ОАО Белагропромбанк.
|
| 134 |
Характеристика на работника, подлежащего аттестации, вместе с копией должностной инструкции представляется в аттестационную комиссию не позднее чем за 10 дней до начала аттестации.</search-results>
|
|
|
|
| 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. [Что определяет положение о порядке распределения людей на работу?]
|
| 160 |
+
^^^^
|
| 161 |
Пример 3
|
| 162 |
+
^^^^
|
| 163 |
user: Привет! Кто ты?
|
| 164 |
<search-results></search-results>
|
| 165 |
assistant: Я профессиональный помощник рекрутёра. Вы можете задавать мне любые вопросы по подготовленным документам.
|
|
|
|
| 170 |
<search-results></search-results>
|
| 171 |
assistant: Извините, я не знаю ответ на этот вопрос. Он не касается рекрутинга. Попробуйте переформулировать.
|
| 172 |
user: Что такое корпоративное управление банка? Зачем нужны комитеты? Где собака зарыта? Откуда ты всё знаешь?
|
| 173 |
+
^^^^
|
| 174 |
Вывод:
|
| 175 |
1. Пользователь задаёт вопросы как по тематике персонала, так и вне него. Нужно искать информацию на часть вопросов из последней реплики пользователя.
|
| 176 |
2. [ДА]
|
| 177 |
3. Первый вопрос про корпоративное управление не содержит лишнего. Второй вопрос требует заменить "зачем" на "цель" и "задачи". Вопрос про собаку вне тематики рекрутинга, я не буду его переписыва��ь. Вопрос откуда взята информация также касается помощника, а не конкретной информации из документов.
|
| 178 |
4. [Что такое корпоративное управление банка? Каковы задачи и цели комитетов?]
|
| 179 |
+
^^^^
|
| 180 |
Пример 4
|
| 181 |
+
^^^^
|
| 182 |
user: Сегодня я буду покупать груши. Какая погода?
|
| 183 |
+
^^^^
|
| 184 |
Вывод:
|
| 185 |
1. Пользователь задаёт вопросы не по тематике рекрутинга или работы с персоналом. Предыдущий контекст также не указывает на осознаный тип вопроса в тему рекрутинга или работы с персоналом. Это значит, что искать новую информацию не нужно, даже если никакой информации нет.
|
| 186 |
2. [НЕТ]
|
| 187 |
3. Рассуждения не требуются.
|
| 188 |
4. []
|
| 189 |
+
^^^^
|
| 190 |
Пример 5
|
| 191 |
+
^^^^
|
| 192 |
user: Привет. Хочешь поговорить?
|
| 193 |
+
^^^^
|
| 194 |
Вывод:
|
| 195 |
1. Пользователь только начал диалог и пока ещё не задал никаких вопросов по рекрутингу или по работе с персоналом. Это значит, что искать информацию не нужно.
|
| 196 |
2. [НЕТ]
|
| 197 |
3. Рассуждения не требуются.
|
| 198 |
4. []
|
| 199 |
+
^^^^
|
| 200 |
Далее будет реальный запрос пользователя. Ты должен ответить только на реальный запрос пользователя.
|
| 201 |
+
^^^^
|
| 202 |
{history}
|
| 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 |
"""
|
| 365 |
|
components/services/entity.py
CHANGED
|
@@ -185,6 +185,7 @@ class EntityService:
|
|
| 185 |
self,
|
| 186 |
query: str,
|
| 187 |
dataset_id: int,
|
|
|
|
| 188 |
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
| 189 |
"""
|
| 190 |
Поиск похожих сущностей.
|
|
@@ -192,6 +193,7 @@ class EntityService:
|
|
| 192 |
Args:
|
| 193 |
query: Текст запроса
|
| 194 |
dataset_id: ID датасета
|
|
|
|
| 195 |
|
| 196 |
Returns:
|
| 197 |
tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
@@ -199,14 +201,20 @@ class EntityService:
|
|
| 199 |
- Оценки сходства
|
| 200 |
- Идентификаторы найденных сущностей
|
| 201 |
"""
|
| 202 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 210 |
|
| 211 |
def search_similar(
|
| 212 |
self,
|
|
|
|
| 185 |
self,
|
| 186 |
query: str,
|
| 187 |
dataset_id: int,
|
| 188 |
+
k: int | None = None,
|
| 189 |
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
| 190 |
"""
|
| 191 |
Поиск похожих сущностей.
|
|
|
|
| 193 |
Args:
|
| 194 |
query: Текст запроса
|
| 195 |
dataset_id: ID датасета
|
| 196 |
+
k: Максимальное количество возвращаемых результатов (по умолчанию - все).
|
| 197 |
|
| 198 |
Returns:
|
| 199 |
tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
|
|
| 201 |
- Оценки сходства
|
| 202 |
- Идентификаторы найденных сущностей
|
| 203 |
"""
|
| 204 |
+
logger.info(f"Searching similar entities for dataset {dataset_id} with k={k}")
|
| 205 |
+
# Убедимся, что индекс для нужного датасета загружен
|
| 206 |
self._ensure_faiss_initialized(dataset_id)
|
| 207 |
|
| 208 |
if self.faiss_search is None:
|
| 209 |
+
logger.warning(
|
| 210 |
+
f"FAISS search not initialized for dataset {dataset_id}. Returning empty results."
|
| 211 |
+
)
|
| 212 |
return np.array([]), np.array([]), np.array([])
|
| 213 |
|
| 214 |
+
# Выполняем поиск с использованием параметра k
|
| 215 |
+
query_vector, scores, ids = self.faiss_search.search_vectors(query, max_entities=k)
|
| 216 |
+
logger.info(f"Found {len(ids)} similar entities.")
|
| 217 |
+
return query_vector, scores, ids
|
| 218 |
|
| 219 |
def search_similar(
|
| 220 |
self,
|
components/services/search_metrics.py
ADDED
|
@@ -0,0 +1,619 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio # Добавляем импорт
|
| 2 |
+
import io # Для работы с UploadFile как с файлом
|
| 3 |
+
import logging
|
| 4 |
+
import re # Добавляем re
|
| 5 |
+
from pathlib import Path # Добавляем Path
|
| 6 |
+
from typing import Any
|
| 7 |
+
from uuid import UUID
|
| 8 |
+
|
| 9 |
+
import pandas as pd
|
| 10 |
+
from fastapi import HTTPException, UploadFile
|
| 11 |
+
from fuzzywuzzy import fuzz
|
| 12 |
+
|
| 13 |
+
from common.configuration import Configuration
|
| 14 |
+
from components.llm.common import Message
|
| 15 |
+
from components.services.dialogue import DialogueService
|
| 16 |
+
from components.services.entity import EntityService
|
| 17 |
+
|
| 18 |
+
logger = logging.getLogger(__name__)
|
| 19 |
+
|
| 20 |
+
# Константа для сравнения имен файлов
|
| 21 |
+
FILENAME_SIMILARITY_THRESHOLD = 40 # Считаем имена файлов одинаковыми, если partial_ratio >= 90
|
| 22 |
+
|
| 23 |
+
class SearchMetricsService:
|
| 24 |
+
"""Сервис для расчета метрик поиска по загруженному файлу.
|
| 25 |
+
|
| 26 |
+
Attributes:
|
| 27 |
+
entity_service: Сервис для работы с сущностями.
|
| 28 |
+
config: Конфигурация приложения.
|
| 29 |
+
dialogue_service: Сервис для работы с диалогами.
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
def __init__(
|
| 33 |
+
self,
|
| 34 |
+
entity_service: EntityService,
|
| 35 |
+
config: Configuration,
|
| 36 |
+
dialogue_service: DialogueService,
|
| 37 |
+
):
|
| 38 |
+
"""Инициализирует сервис.
|
| 39 |
+
|
| 40 |
+
Args:
|
| 41 |
+
entity_service: Сервис для работы с сущностями.
|
| 42 |
+
config: Конфигурация приложения.
|
| 43 |
+
dialogue_service: Сервис для работы с диалогами.
|
| 44 |
+
"""
|
| 45 |
+
self.entity_service = entity_service
|
| 46 |
+
self.config = config
|
| 47 |
+
self.dialogue_service = dialogue_service
|
| 48 |
+
|
| 49 |
+
# --- Вспомогательная функция для очистки имени файла ---
|
| 50 |
+
def _clean_filename(self, filename: str | None) -> str:
|
| 51 |
+
"""Удаляет расширение и приводит к нижнему регистру."""
|
| 52 |
+
if not filename:
|
| 53 |
+
return ""
|
| 54 |
+
return Path(str(filename)).stem.lower()
|
| 55 |
+
|
| 56 |
+
async def _load_evaluation_data(self, file: UploadFile) -> list[dict[str, Any]]:
|
| 57 |
+
"""
|
| 58 |
+
Загружает, валидирует и ГРУППИРУЕТ данные из XLSX файла по уникальным вопросам.
|
| 59 |
+
Сохраняет список эталонных текстов, SET ожидаемых имен файлов и эталонный ответ.
|
| 60 |
+
"""
|
| 61 |
+
if not file.filename.endswith(".xlsx"):
|
| 62 |
+
raise HTTPException(
|
| 63 |
+
status_code=400,
|
| 64 |
+
detail="Invalid file format. Please upload an XLSX file.",
|
| 65 |
+
)
|
| 66 |
+
try:
|
| 67 |
+
contents = await file.read()
|
| 68 |
+
data = io.BytesIO(contents)
|
| 69 |
+
# +++ Добавляем answer в dtype +++
|
| 70 |
+
df = pd.read_excel(data, dtype={'id': str, 'question': str, 'text': str, 'filename': str, 'answer': str})
|
| 71 |
+
except Exception as e:
|
| 72 |
+
logger.error(f"Error reading Excel file: {e}", exc_info=True)
|
| 73 |
+
raise HTTPException(
|
| 74 |
+
status_code=400, detail=f"Error reading Excel file: {e}"
|
| 75 |
+
)
|
| 76 |
+
finally:
|
| 77 |
+
await file.close()
|
| 78 |
+
|
| 79 |
+
# +++ Добавляем answer в required_columns +++
|
| 80 |
+
required_columns = ["id", "question", "text", "filename", "answer"]
|
| 81 |
+
missing_cols = [col for col in required_columns if col not in df.columns]
|
| 82 |
+
if missing_cols:
|
| 83 |
+
raise HTTPException(
|
| 84 |
+
status_code=400,
|
| 85 |
+
detail=f"Missing required columns in XLSX file: {missing_cols}. Expected: 'id', 'question', 'text', 'filename', 'answer'",
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
grouped_data = []
|
| 89 |
+
for question_id, group in df.groupby('id'):
|
| 90 |
+
first_valid_question = group['question'].dropna().iloc[0] if not group['question'].dropna().empty else None
|
| 91 |
+
all_texts_raw = group['text'].dropna().tolist()
|
| 92 |
+
all_filenames_raw = group['filename'].dropna().tolist()
|
| 93 |
+
expected_filenames_cleaned = {self._clean_filename(fn) for fn in all_filenames_raw if self._clean_filename(fn)}
|
| 94 |
+
# +++ Извлекаем первый валидный answer +++
|
| 95 |
+
first_valid_answer = group['answer'].dropna().iloc[0] if not group['answer'].dropna().empty else None
|
| 96 |
+
|
| 97 |
+
# +++ ИСПРАВЛЕНИЕ: Сохраняем тексты ячеек как есть, без дробления +++
|
| 98 |
+
ground_truth_texts_raw = [str(text_block) for text_block in all_texts_raw if str(text_block).strip()] # Список оригинальных текстов ячеек (не пустых)
|
| 99 |
+
|
| 100 |
+
# --- Обновляем проверку на пропуск группы, используя ground_truth_texts_raw --- (включая проверку на пустой список текстов)
|
| 101 |
+
if pd.isna(question_id) or not first_valid_question or not ground_truth_texts_raw or not expected_filenames_cleaned or first_valid_answer is None:
|
| 102 |
+
logger.warning(f"Skipping group for question_id '{question_id}' due to missing question, 'text', 'filename', or 'answer' data within the group, or empty 'text' cells.")
|
| 103 |
+
continue
|
| 104 |
+
# +++ КОНЕЦ ИСПРАВЛЕНИЯ +++
|
| 105 |
+
|
| 106 |
+
grouped_data.append({
|
| 107 |
+
"question_id": str(question_id),
|
| 108 |
+
"question": str(first_valid_question),
|
| 109 |
+
"ground_truth_texts": ground_truth_texts_raw, # Сохраняем список оригинальных текстов ячеек
|
| 110 |
+
"expected_filenames": expected_filenames_cleaned,
|
| 111 |
+
"reference_answer": str(first_valid_answer) # Добавляем эталонный ответ
|
| 112 |
+
})
|
| 113 |
+
|
| 114 |
+
if not grouped_data:
|
| 115 |
+
raise HTTPException(
|
| 116 |
+
status_code=400,
|
| 117 |
+
detail="No valid data groups found in the uploaded file after processing and grouping by 'id'."
|
| 118 |
+
)
|
| 119 |
+
logger.info(f"Successfully loaded and grouped {len(grouped_data)} unique questions from file.")
|
| 120 |
+
return grouped_data
|
| 121 |
+
|
| 122 |
+
# --- Убираем логи из _calculate_relevance_metrics ---
|
| 123 |
+
def _calculate_relevance_metrics(
|
| 124 |
+
self,
|
| 125 |
+
retrieved_chunks: list[str],
|
| 126 |
+
ground_truth_texts: list[str],
|
| 127 |
+
similarity_threshold: float,
|
| 128 |
+
question_id_for_log: str = "unknown" # ID можно оставить для warning/error
|
| 129 |
+
) -> tuple[float, float, float, int, int, int, int, list[int]]:
|
| 130 |
+
num_retrieved = len(retrieved_chunks)
|
| 131 |
+
total_ground_truth = len(ground_truth_texts)
|
| 132 |
+
if total_ground_truth == 0: return 0.0, 0.0, 0.0, 0, 0, 0, num_retrieved, []
|
| 133 |
+
if num_retrieved == 0: return 0.0, 0.0, 0.0, 0, total_ground_truth, 0, 0, list(range(total_ground_truth))
|
| 134 |
+
ground_truth_found = [False] * total_ground_truth
|
| 135 |
+
relevant_chunks_count = 0
|
| 136 |
+
fuzzy_threshold_int = similarity_threshold * 100
|
| 137 |
+
|
| 138 |
+
for chunk_text in retrieved_chunks:
|
| 139 |
+
is_chunk_relevant = False
|
| 140 |
+
for i, gt_text in enumerate(ground_truth_texts):
|
| 141 |
+
overlap_score = fuzz.partial_ratio(chunk_text, gt_text)
|
| 142 |
+
if overlap_score >= fuzzy_threshold_int:
|
| 143 |
+
is_chunk_relevant = True
|
| 144 |
+
ground_truth_found[i] = True
|
| 145 |
+
# Не обязательно break, чанк может быть релевантен нескольким пунктам
|
| 146 |
+
if is_chunk_relevant:
|
| 147 |
+
relevant_chunks_count += 1
|
| 148 |
+
# logger.debug(...) # <--- УДАЛЕНО
|
| 149 |
+
# else:
|
| 150 |
+
# logger.debug(...) # <--- УДАЛЕНО
|
| 151 |
+
|
| 152 |
+
found_puncts_count = sum(ground_truth_found)
|
| 153 |
+
precision = relevant_chunks_count / num_retrieved
|
| 154 |
+
recall = found_puncts_count / total_ground_truth
|
| 155 |
+
f1 = (2 * precision * recall) / (precision + recall) if (precision + recall) > 0 else 0.0
|
| 156 |
+
missed_gt_indices = [i for i, found in enumerate(ground_truth_found) if not found]
|
| 157 |
+
# logger.debug(...) # <--- УДАЛЕНО
|
| 158 |
+
return precision, recall, f1, found_puncts_count, total_ground_truth, relevant_chunks_count, num_retrieved, missed_gt_indices
|
| 159 |
+
|
| 160 |
+
# --- Убираем логи из _calculate_assembly_punct_recall ---
|
| 161 |
+
def _calculate_assembly_punct_recall(
|
| 162 |
+
self,
|
| 163 |
+
assembled_context: str,
|
| 164 |
+
ground_truth_texts: list[str],
|
| 165 |
+
similarity_threshold: float,
|
| 166 |
+
question_id_for_log: str = "unknown" # ID можно оставить для warning/error
|
| 167 |
+
) -> tuple[float, int, int]:
|
| 168 |
+
# ... (расчеты как были) ...
|
| 169 |
+
if not ground_truth_texts or not assembled_context: return 0.0, 0, 0
|
| 170 |
+
assembly_found_puncts = 0
|
| 171 |
+
valid_ground_truth_count = 0
|
| 172 |
+
fuzzy_threshold_int = similarity_threshold * 100
|
| 173 |
+
for i, punct_text in enumerate(ground_truth_texts):
|
| 174 |
+
punct_parts = [part.strip() for part in punct_text.split('\n') if part.strip()]
|
| 175 |
+
if not punct_parts: continue
|
| 176 |
+
valid_ground_truth_count += 1
|
| 177 |
+
is_punct_found = False
|
| 178 |
+
for j, part_text in enumerate(punct_parts):
|
| 179 |
+
score = fuzz.partial_ratio(assembled_context, part_text)
|
| 180 |
+
if score >= fuzzy_threshold_int:
|
| 181 |
+
# logger.debug(...) # <--- УДАЛЕНО
|
| 182 |
+
is_punct_found = True
|
| 183 |
+
break
|
| 184 |
+
if is_punct_found:
|
| 185 |
+
assembly_found_puncts += 1
|
| 186 |
+
# else:
|
| 187 |
+
# logger.debug(...) # <--- УДАЛЕНО
|
| 188 |
+
|
| 189 |
+
assembly_recall = assembly_found_puncts / valid_ground_truth_count if valid_ground_truth_count > 0 else 0.0
|
| 190 |
+
# logger.debug(...) # <--- УДАЛЕНО
|
| 191 |
+
return assembly_recall, assembly_found_puncts, valid_ground_truth_count
|
| 192 |
+
|
| 193 |
+
# --- Убираем логи из _extract_and_compare_documents ---
|
| 194 |
+
def _extract_and_compare_documents(
|
| 195 |
+
self,
|
| 196 |
+
assembled_context: str,
|
| 197 |
+
expected_filenames_cleaned: set[str]
|
| 198 |
+
) -> tuple[float, int]:
|
| 199 |
+
# ... (расчеты как были) ...
|
| 200 |
+
if not assembled_context or not expected_filenames_cleaned: return 0.0, 0
|
| 201 |
+
pattern = r"#\s*\[Источник\]\s*-\s*(.*?)(?:\n|$)"
|
| 202 |
+
found_filenames_raw = re.findall(pattern, assembled_context)
|
| 203 |
+
found_filenames_cleaned = {self._clean_filename(fn) for fn in found_filenames_raw if self._clean_filename(fn)}
|
| 204 |
+
# logger.debug(...) # <--- УДАЛЕНО
|
| 205 |
+
if not found_filenames_cleaned: return 0.0, 0
|
| 206 |
+
found_expected_count = 0
|
| 207 |
+
spurious_count = 0
|
| 208 |
+
matched_expected = set()
|
| 209 |
+
for found_clean in found_filenames_cleaned:
|
| 210 |
+
is_spurious = True
|
| 211 |
+
for expected_clean in expected_filenames_cleaned:
|
| 212 |
+
score = fuzz.partial_ratio(found_clean, expected_clean)
|
| 213 |
+
if score >= FILENAME_SIMILARITY_THRESHOLD:
|
| 214 |
+
if expected_clean not in matched_expected:
|
| 215 |
+
found_expected_count += 1
|
| 216 |
+
matched_expected.add(expected_clean)
|
| 217 |
+
is_spurious = False
|
| 218 |
+
# Не обязательно break
|
| 219 |
+
# +++ Логирование убрано +++
|
| 220 |
+
if is_spurious:
|
| 221 |
+
spurious_count += 1
|
| 222 |
+
doc_recall = found_expected_count / len(expected_filenames_cleaned)
|
| 223 |
+
# logger.debug(...) # <--- УДАЛЕНО
|
| 224 |
+
return doc_recall, spurious_count
|
| 225 |
+
|
| 226 |
+
async def _call_qe_safe(self, original_question: str) -> str | None:
|
| 227 |
+
"""
|
| 228 |
+
Безопасно вызывает QE сервис для одного вопроса.
|
| 229 |
+
|
| 230 |
+
Args:
|
| 231 |
+
original_question: Исходный текст вопроса.
|
| 232 |
+
|
| 233 |
+
Returns:
|
| 234 |
+
Строку с новым запросом от QE, если он успешен и релевантен,
|
| 235 |
+
иначе None.
|
| 236 |
+
"""
|
| 237 |
+
try:
|
| 238 |
+
fake_history = [Message(role="user", content=original_question, searchResults="")]
|
| 239 |
+
qe_result = await self.dialogue_service.get_qe_result(fake_history)
|
| 240 |
+
logger.debug(f"QE result for '{original_question[:50]}...': {qe_result}")
|
| 241 |
+
if qe_result.use_search and qe_result.search_query:
|
| 242 |
+
return qe_result.search_query
|
| 243 |
+
# QE решил не искать или вернул пустой результат
|
| 244 |
+
return None
|
| 245 |
+
except Exception as e:
|
| 246 |
+
logger.error(f"Error during single QE call for question '{original_question[:50]}...': {e}", exc_info=True)
|
| 247 |
+
# В случае ошибки возвращаем None, чтобы использовать оригинальный вопрос
|
| 248 |
+
return None
|
| 249 |
+
|
| 250 |
+
async def evaluate_from_file(
|
| 251 |
+
self,
|
| 252 |
+
file: UploadFile,
|
| 253 |
+
dataset_id: int,
|
| 254 |
+
similarity_threshold: float,
|
| 255 |
+
top_n_values: list[int],
|
| 256 |
+
use_query_expansion: bool,
|
| 257 |
+
top_worst_k: int = 5,
|
| 258 |
+
) -> dict[str, Any]:
|
| 259 |
+
"""
|
| 260 |
+
Выполняет оценку по файлу, группируя строки по вопросам и считая метрики сборки.
|
| 261 |
+
"""
|
| 262 |
+
logger.info(f"Starting evaluation for dataset_id={dataset_id}, top_n={top_n_values}, threshold={similarity_threshold}, use_query_expansion={use_query_expansion} (Grouped by question_id)")
|
| 263 |
+
evaluation_data = await self._load_evaluation_data(file)
|
| 264 |
+
results: dict[int, dict[str, Any]] = {
|
| 265 |
+
n: {
|
| 266 |
+
'precision_list': [], 'recall_list': [], 'f1_list': [], # Для Macro/Weighted
|
| 267 |
+
'assembly_punct_recall_list': [],
|
| 268 |
+
'doc_recall_list': [],
|
| 269 |
+
'spurious_docs_list': [],
|
| 270 |
+
} for n in top_n_values
|
| 271 |
+
}
|
| 272 |
+
question_performance: dict[str, dict[str, Any | None]] = {}
|
| 273 |
+
max_top_n = max(top_n_values) if top_n_values else 0
|
| 274 |
+
if not max_top_n: raise HTTPException(status_code=400, detail="top_n_values list cannot be empty.")
|
| 275 |
+
|
| 276 |
+
# +++ Инициализация НОВЫХ общих счетчиков Micro (по n) +++
|
| 277 |
+
overall_micro_counters = {
|
| 278 |
+
n: {'found': 0, 'gt': 0, 'relevant': 0, 'retrieved': 0}
|
| 279 |
+
for n in top_n_values
|
| 280 |
+
}
|
| 281 |
+
# --- Счетчики для Micro Assembly Recall остаются ---
|
| 282 |
+
overall_assembly_found_puncts = 0
|
| 283 |
+
overall_valid_gt_for_assembly = 0
|
| 284 |
+
|
| 285 |
+
# --- Этап 2: Подготовка запросов (QE) --- (Добавляем reference_answer)
|
| 286 |
+
processed_items = []
|
| 287 |
+
if use_query_expansion and evaluation_data:
|
| 288 |
+
logger.info(f"Starting asynchronous QE for {len(evaluation_data)} unique questions...")
|
| 289 |
+
tasks = [self._call_qe_safe(item['question']) for item in evaluation_data]
|
| 290 |
+
qe_results_or_errors = await asyncio.gather(*tasks, return_exceptions=True)
|
| 291 |
+
logger.info("Asynchronous QE calls finished for unique questions.")
|
| 292 |
+
for i, item in enumerate(evaluation_data):
|
| 293 |
+
query_for_search = item['question']
|
| 294 |
+
qe_result = qe_results_or_errors[i]
|
| 295 |
+
if isinstance(qe_result, str): query_for_search = qe_result
|
| 296 |
+
processed_items.append({
|
| 297 |
+
'question_id': item['question_id'],
|
| 298 |
+
'question': item['question'],
|
| 299 |
+
'query_for_search': query_for_search,
|
| 300 |
+
'ground_truth_texts': item['ground_truth_texts'],
|
| 301 |
+
'expected_filenames': item['expected_filenames'],
|
| 302 |
+
'reference_answer': item['reference_answer'] # Добавляем
|
| 303 |
+
})
|
| 304 |
+
else:
|
| 305 |
+
logger.info("QE disabled or no data. Preparing items without QE.")
|
| 306 |
+
for item in evaluation_data:
|
| 307 |
+
processed_items.append({
|
| 308 |
+
'question_id': item['question_id'],
|
| 309 |
+
'question': item['question'],
|
| 310 |
+
'query_for_search': item['question'],
|
| 311 |
+
'ground_truth_texts': item['ground_truth_texts'],
|
| 312 |
+
'expected_filenames': item['expected_filenames'],
|
| 313 |
+
'reference_answer': item['reference_answer'] # Добавляем
|
| 314 |
+
})
|
| 315 |
+
|
| 316 |
+
# --- Этап 3: Цикл по УНИКАЛЬНЫМ вопросам ---
|
| 317 |
+
for item in processed_items:
|
| 318 |
+
question_id = item['question_id']
|
| 319 |
+
original_question_text = item['question']
|
| 320 |
+
reference_answer = item['reference_answer'] # Извлекаем
|
| 321 |
+
ground_truth_texts = item['ground_truth_texts']
|
| 322 |
+
expected_filenames = item['expected_filenames']
|
| 323 |
+
total_gt_count = len(ground_truth_texts)
|
| 324 |
+
query_for_search = item['query_for_search']
|
| 325 |
+
|
| 326 |
+
# --- Инициализируем question_performance с новыми полями ---
|
| 327 |
+
if question_id not in question_performance:
|
| 328 |
+
question_performance[question_id] = {
|
| 329 |
+
'f1': None,
|
| 330 |
+
'assembly_recall_for_worst': None, # Новое поле для сортировки
|
| 331 |
+
'question_text': original_question_text,
|
| 332 |
+
'reference_answer': reference_answer,
|
| 333 |
+
'missed_gt_indices': None
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
logger.debug(f"Processing unique QID={question_id} with {total_gt_count} ground truths. Query: \"{query_for_search}\"")
|
| 337 |
+
|
| 338 |
+
try:
|
| 339 |
+
# --- Поиск (Один раз для max_top_n) ---
|
| 340 |
+
logger.info(f"Searching for QID={question_id} with k={max_top_n}...") # Оставим INFO
|
| 341 |
+
_, scores, ids = self.entity_service.search_similar_old(
|
| 342 |
+
query=query_for_search, dataset_id=dataset_id, k=max_top_n
|
| 343 |
+
)
|
| 344 |
+
# Важно: 'ids' это список СТРОК UUID
|
| 345 |
+
|
| 346 |
+
# --- !!! Удаляем ненужное извлечение текстов здесь !!! ---
|
| 347 |
+
# all_retrieved_chunk_texts = []
|
| 348 |
+
# ...
|
| 349 |
+
|
| 350 |
+
# --- Цикл по top_n ---
|
| 351 |
+
for n in top_n_values:
|
| 352 |
+
current_top_n = min(n, len(ids))
|
| 353 |
+
# +++ Получаем ID чанков для текущего n +++
|
| 354 |
+
chunk_ids_for_n = ids[:current_top_n]
|
| 355 |
+
retrieved_count_for_n = len(chunk_ids_for_n)
|
| 356 |
+
|
| 357 |
+
# +++ Получаем тексты чанков для расчета метрик chunk/punct +++
|
| 358 |
+
retrieved_chunks_texts_for_n = []
|
| 359 |
+
if chunk_ids_for_n.size > 0:
|
| 360 |
+
chunks_for_n = self.entity_service.chunk_repository.get_entities_by_ids(
|
| 361 |
+
[UUID(ch_id) for ch_id in chunk_ids_for_n]
|
| 362 |
+
)
|
| 363 |
+
chunk_map_for_n = {str(ch.id): ch for ch in chunks_for_n}
|
| 364 |
+
retrieved_chunks_texts_for_n = [
|
| 365 |
+
chunk_map_for_n[ch_id].in_search_text
|
| 366 |
+
for ch_id in chunk_ids_for_n
|
| 367 |
+
if ch_id in chunk_map_for_n and hasattr(chunk_map_for_n[ch_id], 'in_search_text') and chunk_map_for_n[ch_id].in_search_text
|
| 368 |
+
]
|
| 369 |
+
|
| 370 |
+
# --- Метрики Chunk/Punct ---
|
| 371 |
+
(
|
| 372 |
+
precision, recall, f1,
|
| 373 |
+
found_count, total_gt,
|
| 374 |
+
relevant_count, retrieved_count_calc, # retrieved_count_calc == retrieved_count_for_n
|
| 375 |
+
missed_indices
|
| 376 |
+
) = self._calculate_relevance_metrics(
|
| 377 |
+
retrieved_chunks_texts_for_n, # Используем тексты для n
|
| 378 |
+
ground_truth_texts,
|
| 379 |
+
similarity_threshold,
|
| 380 |
+
question_id_for_log=question_id
|
| 381 |
+
)
|
| 382 |
+
# Агрегация для Macro/Weighted
|
| 383 |
+
results[n]['precision_list'].append((precision, retrieved_count_for_n)) # Вес = retrieved_count_for_n
|
| 384 |
+
results[n]['recall_list'].append((recall, total_gt))
|
| 385 |
+
results[n]['f1_list'].append((f1, total_gt))
|
| 386 |
+
# Агрегация для Micro
|
| 387 |
+
overall_micro_counters[n]['found'] += found_count
|
| 388 |
+
overall_micro_counters[n]['gt'] += total_gt
|
| 389 |
+
overall_micro_counters[n]['relevant'] += relevant_count
|
| 390 |
+
overall_micro_counters[n]['retrieved'] += retrieved_count_for_n # Используем кол-во для n
|
| 391 |
+
|
| 392 |
+
# --- Метрики Сборки ---
|
| 393 |
+
# +++ Правильная сборка контекста с помощью build_text +++
|
| 394 |
+
logger.info(f"Building context for QID={question_id}, n={n} using {len(chunk_ids_for_n)} chunk IDs...")
|
| 395 |
+
assembled_context_for_n = self.entity_service.build_text(
|
| 396 |
+
entities=chunk_ids_for_n # Передаем список ID строк
|
| 397 |
+
)
|
| 398 |
+
|
| 399 |
+
assembly_recall, single_q_assembly_found, single_q_valid_gt = self._calculate_assembly_punct_recall(
|
| 400 |
+
assembled_context_for_n,
|
| 401 |
+
ground_truth_texts,
|
| 402 |
+
similarity_threshold,
|
| 403 |
+
question_id_for_log=question_id
|
| 404 |
+
)
|
| 405 |
+
results[n]['assembly_punct_recall_list'].append(assembly_recall)
|
| 406 |
+
if n == max_top_n:
|
| 407 |
+
overall_assembly_found_puncts += single_q_assembly_found
|
| 408 |
+
overall_valid_gt_for_assembly += single_q_valid_gt
|
| 409 |
+
|
| 410 |
+
# --- Метрики Документов ---
|
| 411 |
+
doc_recall, spurious_docs = self._extract_and_compare_documents(
|
| 412 |
+
assembled_context_for_n, # Используем корректный контекст
|
| 413 |
+
expected_filenames
|
| 414 |
+
)
|
| 415 |
+
results[n]['doc_recall_list'].append(doc_recall)
|
| 416 |
+
results[n]['spurious_docs_list'].append(spurious_docs)
|
| 417 |
+
|
| 418 |
+
# --- Сохраняем показатели для худших ---
|
| 419 |
+
if n == max_top_n:
|
| 420 |
+
question_performance[question_id]['f1'] = f1
|
| 421 |
+
question_performance[question_id]['assembly_recall_for_worst'] = assembly_recall
|
| 422 |
+
question_performance[question_id]['missed_gt_indices'] = missed_indices
|
| 423 |
+
|
| 424 |
+
except HTTPException as http_exc:
|
| 425 |
+
logger.error(f"HTTP Error processing QID={question_id}: {http_exc.detail}")
|
| 426 |
+
if question_id in question_performance:
|
| 427 |
+
# +++ Устанавливаем F1 в 0.0 при ошибке +++
|
| 428 |
+
question_performance[question_id]['f1'] = 0.0
|
| 429 |
+
question_performance[question_id]['assembly_recall_for_worst'] = 0.0 # Худший recall
|
| 430 |
+
question_performance[question_id]['missed_gt_indices'] = list(range(total_gt_count))
|
| 431 |
+
for n_err in top_n_values:
|
| 432 |
+
results[n_err]['precision_list'].append((0.0, 0))
|
| 433 |
+
results[n_err]['recall_list'].append((0.0, total_gt_count))
|
| 434 |
+
results[n_err]['f1_list'].append((0.0, total_gt_count))
|
| 435 |
+
results[n_err]['assembly_punct_recall_list'].append(0.0)
|
| 436 |
+
results[n_err]['doc_recall_list'].append(0.0)
|
| 437 |
+
results[n_err]['spurious_docs_list'].append(0)
|
| 438 |
+
# +++ Обновляем общий счетчик GT для Micro при ошибке +++
|
| 439 |
+
overall_micro_counters[n_err]['gt'] += total_gt_count
|
| 440 |
+
except Exception as e:
|
| 441 |
+
logger.error(f"General Error processing QID={question_id}: {e}", exc_info=True)
|
| 442 |
+
if question_id in question_performance:
|
| 443 |
+
# +++ Устанавливаем F1 в 0.0 при ошибке +++
|
| 444 |
+
question_performance[question_id]['f1'] = 0.0
|
| 445 |
+
question_performance[question_id]['assembly_recall_for_worst'] = 0.0
|
| 446 |
+
question_performance[question_id]['missed_gt_indices'] = list(range(total_gt_count))
|
| 447 |
+
for n_err in top_n_values:
|
| 448 |
+
results[n_err]['precision_list'].append((0.0, 0))
|
| 449 |
+
results[n_err]['recall_list'].append((0.0, total_gt_count))
|
| 450 |
+
results[n_err]['f1_list'].append((0.0, total_gt_count))
|
| 451 |
+
results[n_err]['assembly_punct_recall_list'].append(0.0)
|
| 452 |
+
results[n_err]['doc_recall_list'].append(0.0)
|
| 453 |
+
results[n_err]['spurious_docs_list'].append(0)
|
| 454 |
+
# +++ Обновляем общий счетчик GT для Micro при ошибке +++
|
| 455 |
+
overall_micro_counters[n_err]['gt'] += total_gt_count
|
| 456 |
+
|
| 457 |
+
# --- Этап 4: Расчет итоговых метрик ---
|
| 458 |
+
final_metrics_results: dict[int, dict[str, float | None]] = {}
|
| 459 |
+
# !!! УДАЛЯЕМ ПОВТОРНУЮ ИНИЦИАЛИЗАЦИЮ СЧЕТЧИКОВ !!!
|
| 460 |
+
# overall_micro_counters = { ... }
|
| 461 |
+
# overall_assembly_found_puncts = 0
|
| 462 |
+
# overall_valid_gt_for_assembly = 0
|
| 463 |
+
|
| 464 |
+
# +++ Лог перед финальным расчетом +++ (Оставляем на всякий случай)
|
| 465 |
+
logger.debug(f"Data before final calculation: results={results}")
|
| 466 |
+
logger.debug(f"Overall micro counters before final calc: {overall_micro_counters}")
|
| 467 |
+
logger.debug(f"Overall assembly counters before final calc: found={overall_assembly_found_puncts}, valid_gt={overall_valid_gt_for_assembly}")
|
| 468 |
+
# ...
|
| 469 |
+
|
| 470 |
+
for n in top_n_values:
|
| 471 |
+
# Извлекаем списки
|
| 472 |
+
prec_list = results[n]['precision_list']
|
| 473 |
+
rec_list = results[n]['recall_list']
|
| 474 |
+
f1_list = results[n]['f1_list']
|
| 475 |
+
assembly_recall_list = results[n]['assembly_punct_recall_list']
|
| 476 |
+
doc_recall_list = results[n]['doc_recall_list']
|
| 477 |
+
spurious_docs_list = results[n]['spurious_docs_list']
|
| 478 |
+
|
| 479 |
+
# --- Расчет Macro (с явной проверкой) ---
|
| 480 |
+
macro_precision = sum(p for p, w in prec_list) / len(prec_list) if prec_list else None
|
| 481 |
+
macro_recall = sum(r for r, w in rec_list) / len(rec_list) if rec_list else None
|
| 482 |
+
macro_f1 = sum(f for f, w in f1_list) / len(f1_list) if f1_list else None
|
| 483 |
+
|
| 484 |
+
# --- Расчет Weighted (с явной проверкой на пустой список) ---
|
| 485 |
+
weighted_precision = None
|
| 486 |
+
if prec_list:
|
| 487 |
+
weighted_precision_num = sum(p * w for p, w in prec_list)
|
| 488 |
+
weighted_precision_den = sum(w for p, w in prec_list)
|
| 489 |
+
weighted_precision = weighted_precision_num / weighted_precision_den if weighted_precision_den > 0 else 0.0
|
| 490 |
+
|
| 491 |
+
weighted_recall = None
|
| 492 |
+
if rec_list:
|
| 493 |
+
weighted_recall_num = sum(r * w for r, w in rec_list)
|
| 494 |
+
weighted_recall_den = sum(w for r, w in rec_list)
|
| 495 |
+
weighted_recall = weighted_recall_num / weighted_recall_den if weighted_recall_den > 0 else 0.0
|
| 496 |
+
|
| 497 |
+
weighted_f1 = None
|
| 498 |
+
if f1_list:
|
| 499 |
+
weighted_f1_num = sum(f * w for f, w in f1_list)
|
| 500 |
+
weighted_f1_den = sum(w for f, w in f1_list)
|
| 501 |
+
weighted_f1 = weighted_f1_num / weighted_f1_den if weighted_f1_den > 0 else 0.0
|
| 502 |
+
|
| 503 |
+
# --- Расчет Micro (теперь использует накопленные значения) ---
|
| 504 |
+
total_found = overall_micro_counters[n]['found']
|
| 505 |
+
total_gt = overall_micro_counters[n]['gt']
|
| 506 |
+
total_relevant = overall_micro_counters[n]['relevant']
|
| 507 |
+
total_retrieved = overall_micro_counters[n]['retrieved']
|
| 508 |
+
micro_precision = total_relevant / total_retrieved if total_retrieved > 0 else 0.0
|
| 509 |
+
micro_recall = total_found / total_gt if total_gt > 0 else 0.0
|
| 510 |
+
micro_f1 = (2 * micro_precision * micro_recall) / (micro_precision + micro_recall) if (micro_precision + micro_recall) > 0 else 0.0
|
| 511 |
+
|
| 512 |
+
# --- Новые Macro метрики (с явной проверкой) ---
|
| 513 |
+
assembly_punct_recall_macro = sum(assembly_recall_list) / len(assembly_recall_list) if assembly_recall_list else None
|
| 514 |
+
doc_recall_macro = sum(doc_recall_list) / len(doc_recall_list) if doc_recall_list else None
|
| 515 |
+
avg_spurious_docs = sum(spurious_docs_list) / len(spurious_docs_list) if spurious_docs_list else None
|
| 516 |
+
|
| 517 |
+
# Заполняем результат (без изменений)
|
| 518 |
+
final_metrics_results[n] = {
|
| 519 |
+
'macro_precision': macro_precision,
|
| 520 |
+
'macro_recall': macro_recall,
|
| 521 |
+
'macro_f1': macro_f1,
|
| 522 |
+
'weighted_precision': weighted_precision,
|
| 523 |
+
'weighted_recall': weighted_recall,
|
| 524 |
+
'weighted_f1': weighted_f1,
|
| 525 |
+
'micro_precision': micro_precision,
|
| 526 |
+
'micro_recall': micro_recall,
|
| 527 |
+
'micro_f1': micro_f1,
|
| 528 |
+
'assembly_punct_recall_macro': assembly_punct_recall_macro,
|
| 529 |
+
'doc_recall_macro': doc_recall_macro,
|
| 530 |
+
'avg_spurious_docs': avg_spurious_docs,
|
| 531 |
+
}
|
| 532 |
+
logger.info(f"Final metrics for top_n={n}: {final_metrics_results[n]}\n")
|
| 533 |
+
|
| 534 |
+
# --- Расчет Micro Assembly Punct Recall (теперь использует накопленные значения) ---
|
| 535 |
+
micro_assembly_punct_recall = (
|
| 536 |
+
overall_assembly_found_puncts / overall_valid_gt_for_assembly
|
| 537 |
+
if overall_valid_gt_for_assembly > 0 else 0.0
|
| 538 |
+
)
|
| 539 |
+
|
| 540 |
+
# --- Поиск худших вопросов (по Assembly Recall) ---
|
| 541 |
+
qid_to_ground_truths = {item['question_id']: item['ground_truth_texts'] for item in processed_items}
|
| 542 |
+
worst_questions_processed = []
|
| 543 |
+
|
| 544 |
+
logger.debug(f"Debugging worst questions: question_performance = {question_performance}")
|
| 545 |
+
|
| 546 |
+
# +++ Сортируем по assembly_recall_for_worst +++
|
| 547 |
+
sorted_performance = sorted(
|
| 548 |
+
[
|
| 549 |
+
(qid, data) for qid, data in question_performance.items()
|
| 550 |
+
# !!! КЛЮЧЕВОЙ ФИЛЬТР !!! Убедимся, что assembly_recall_for_worst не None
|
| 551 |
+
if data.get('assembly_recall_for_worst') is not None
|
| 552 |
+
],
|
| 553 |
+
key=lambda item: item[1]['assembly_recall_for_worst'] # Сортируем по recall ПО ВОЗРАСТАНИЮ
|
| 554 |
+
)
|
| 555 |
+
|
| 556 |
+
# +++ ДОБАВЛЯЕМ ЛОГ ПОСЛЕ СОРТИРОВКИ +++
|
| 557 |
+
logger.debug(f"Debugging worst questions: sorted_performance (top {top_worst_k}) = {sorted_performance[:top_worst_k]}")
|
| 558 |
+
# +++ КОНЕЦ ЛОГА +++
|
| 559 |
+
|
| 560 |
+
# +++ ДОБАВЛЯЕМ ЛОГИ ВНУТРИ ЦИКЛА +++
|
| 561 |
+
for qid, perf_data in sorted_performance[:top_worst_k]:
|
| 562 |
+
logger.debug(f"Processing worst question: QID={qid}, Data={perf_data}")
|
| 563 |
+
try:
|
| 564 |
+
missed_indices = perf_data.get('missed_gt_indices', [])
|
| 565 |
+
logger.debug(f"QID={qid}: Got missed_indices: {missed_indices}")
|
| 566 |
+
|
| 567 |
+
missed_texts = []
|
| 568 |
+
if missed_indices is not None and qid in qid_to_ground_truths:
|
| 569 |
+
original_gts = qid_to_ground_truths[qid]
|
| 570 |
+
missed_texts = [original_gts[i] for i in missed_indices if i < len(original_gts)]
|
| 571 |
+
logger.debug(f"QID={qid}: Found {len(missed_texts)} missed texts from {len(original_gts)} original GTs.")
|
| 572 |
+
elif qid not in qid_to_ground_truths:
|
| 573 |
+
logger.warning(f"QID={qid} not found in qid_to_ground_truths when processing worst questions.")
|
| 574 |
+
|
| 575 |
+
# Формируем словарь перед добавлением
|
| 576 |
+
worst_entry = {
|
| 577 |
+
'id': qid,
|
| 578 |
+
'f1': perf_data.get('f1'), # Используем .get() для безопасности
|
| 579 |
+
'assembly_recall': perf_data.get('assembly_recall_for_worst'),
|
| 580 |
+
'text': perf_data.get('question_text'),
|
| 581 |
+
'reference_answer': perf_data.get('reference_answer'),
|
| 582 |
+
'missed_ground_truths': missed_texts
|
| 583 |
+
}
|
| 584 |
+
logger.debug(f"QID={qid}: Appending entry: {worst_entry}")
|
| 585 |
+
worst_questions_processed.append(worst_entry)
|
| 586 |
+
|
| 587 |
+
except Exception as e:
|
| 588 |
+
logger.error(f"Error processing worst question QID={qid}: {e}", exc_info=True)
|
| 589 |
+
# Не прерываем цикл, но логируем ошибку
|
| 590 |
+
# +++ КОНЕЦ ЛОГОВ ВНУТРИ ЦИКЛА +++
|
| 591 |
+
|
| 592 |
+
# --- Формируем финальный ответ ---
|
| 593 |
+
metrics_for_max_n = final_metrics_results.get(max_top_n, {})
|
| 594 |
+
overall_total_found_micro = overall_micro_counters[max_top_n]['found']
|
| 595 |
+
overall_total_gt_micro = overall_micro_counters[max_top_n]['gt']
|
| 596 |
+
|
| 597 |
+
# --- Логирование перед ответом (Оставляем) ---
|
| 598 |
+
logger.debug(f"Final Response Prep: max_top_n={max_top_n}")
|
| 599 |
+
logger.debug(f"Final Response Prep: metrics_for_max_n={metrics_for_max_n}")
|
| 600 |
+
logger.debug(f"Final Response Prep: overall_micro_counters={overall_micro_counters}")
|
| 601 |
+
logger.debug(f"Final Response Prep: micro_recall_for_human_readable = {metrics_for_max_n.get('micro_recall')}")
|
| 602 |
+
# --- Конец лога ---
|
| 603 |
+
|
| 604 |
+
# +++ Перестраиваем структуру ответа с РУССКИМИ КЛЮЧАМИ +++
|
| 605 |
+
final_response = {
|
| 606 |
+
# --- Человекочитаемые метрики --- (Вверху)
|
| 607 |
+
"Найдено пунктов (всего)": overall_total_found_micro,
|
| 608 |
+
"Всего пунктов (эталон)": overall_total_gt_micro,
|
| 609 |
+
"% найденных пунктов (чанк присутствует в пункте)": metrics_for_max_n.get('micro_recall'), # Micro Recall
|
| 610 |
+
"% пунктов были найдены в собранной версии": micro_assembly_punct_recall, # Micro Assembly Recall
|
| 611 |
+
"В среднем для каждого вопроса найден такой % пунктов": metrics_for_max_n.get('macro_recall'), # Macro Recall
|
| 612 |
+
"В среднем для каждого вопроса найден такой % документов": metrics_for_max_n.get('doc_recall_macro'), # Macro Doc Recall
|
| 613 |
+
"В среднем для каждого вопроса найдено N лишних документов, N": metrics_for_max_n.get('avg_spurious_docs'), # Avg Spurious Docs
|
| 614 |
+
# --- Результаты по top_n --- (В середине)
|
| 615 |
+
"results": final_metrics_results,
|
| 616 |
+
# --- Худшие вопросы --- (Внизу)
|
| 617 |
+
"worst_performing_questions": worst_questions_processed,
|
| 618 |
+
}
|
| 619 |
+
return final_response
|
main.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
| 1 |
import logging
|
| 2 |
import os
|
| 3 |
-
from contextlib import asynccontextmanager
|
| 4 |
from pathlib import Path
|
| 5 |
-
from typing import Annotated
|
| 6 |
|
| 7 |
import dotenv
|
| 8 |
import uvicorn
|
|
@@ -10,38 +10,35 @@ from fastapi import FastAPI
|
|
| 10 |
from fastapi.middleware.cors import CORSMiddleware
|
| 11 |
from transformers import AutoModel, AutoTokenizer
|
| 12 |
|
| 13 |
-
|
| 14 |
-
from common import dependencies as DI
|
| 15 |
from common.common import configure_logging
|
| 16 |
from common.configuration import Configuration
|
|
|
|
| 17 |
from routes.dataset import router as dataset_router
|
| 18 |
from routes.document import router as document_router
|
| 19 |
from routes.entity import router as entity_router
|
|
|
|
| 20 |
from routes.llm import router as llm_router
|
| 21 |
from routes.llm_config import router as llm_config_router
|
| 22 |
from routes.llm_prompt import router as llm_prompt_router
|
| 23 |
-
from routes.auth import router as auth_router
|
| 24 |
-
|
| 25 |
-
# from main_before import config
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
# Загружаем переменные из .env
|
| 29 |
dotenv.load_dotenv()
|
| 30 |
|
| 31 |
-
# from routes.feedback import router as feedback_router
|
| 32 |
-
# from routes.llm import router as llm_router
|
| 33 |
-
# from routes.log import router as log_router
|
| 34 |
-
|
| 35 |
CONFIG_PATH = os.environ.get('CONFIG_PATH', 'config_dev.yaml')
|
| 36 |
print("config path: ")
|
| 37 |
print(CONFIG_PATH)
|
| 38 |
config = Configuration(CONFIG_PATH)
|
| 39 |
|
| 40 |
logger = logging.getLogger(__name__)
|
| 41 |
-
configure_logging(config_file_path=config.common_config.log_file_path)
|
| 42 |
|
| 43 |
configure_logging(
|
| 44 |
-
level=
|
| 45 |
config_file_path=config.common_config.log_file_path,
|
| 46 |
)
|
| 47 |
|
|
@@ -67,20 +64,20 @@ app.add_middleware(
|
|
| 67 |
)
|
| 68 |
|
| 69 |
app.include_router(llm_router)
|
| 70 |
-
# app.include_router(log_router)
|
| 71 |
-
# app.include_router(feedback_router)
|
| 72 |
app.include_router(dataset_router)
|
| 73 |
app.include_router(document_router)
|
| 74 |
app.include_router(llm_config_router)
|
| 75 |
app.include_router(llm_prompt_router)
|
| 76 |
app.include_router(entity_router)
|
|
|
|
| 77 |
app.include_router(auth_router)
|
| 78 |
|
|
|
|
| 79 |
if __name__ == "__main__":
|
| 80 |
uvicorn.run(
|
| 81 |
"main:app",
|
| 82 |
host="localhost",
|
| 83 |
-
port=
|
| 84 |
-
reload=
|
| 85 |
workers=1
|
| 86 |
)
|
|
|
|
| 1 |
import logging
|
| 2 |
import os
|
| 3 |
+
from contextlib import asynccontextmanager # noqa: F401
|
| 4 |
from pathlib import Path
|
| 5 |
+
from typing import Annotated # noqa: F401
|
| 6 |
|
| 7 |
import dotenv
|
| 8 |
import uvicorn
|
|
|
|
| 10 |
from fastapi.middleware.cors import CORSMiddleware
|
| 11 |
from transformers import AutoModel, AutoTokenizer
|
| 12 |
|
| 13 |
+
from common import dependencies as DI # noqa: F401
|
|
|
|
| 14 |
from common.common import configure_logging
|
| 15 |
from common.configuration import Configuration
|
| 16 |
+
from routes.auth import router as auth_router
|
| 17 |
from routes.dataset import router as dataset_router
|
| 18 |
from routes.document import router as document_router
|
| 19 |
from routes.entity import router as entity_router
|
| 20 |
+
from routes.evaluation import router as evaluation_router
|
| 21 |
from routes.llm import router as llm_router
|
| 22 |
from routes.llm_config import router as llm_config_router
|
| 23 |
from routes.llm_prompt import router as llm_prompt_router
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
+
# Защита от автоудаления линтером
|
| 26 |
+
_ = DI
|
| 27 |
+
_ = Annotated
|
| 28 |
+
_ = asynccontextmanager
|
| 29 |
|
| 30 |
# Загружаем переменные из .env
|
| 31 |
dotenv.load_dotenv()
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
CONFIG_PATH = os.environ.get('CONFIG_PATH', 'config_dev.yaml')
|
| 34 |
print("config path: ")
|
| 35 |
print(CONFIG_PATH)
|
| 36 |
config = Configuration(CONFIG_PATH)
|
| 37 |
|
| 38 |
logger = logging.getLogger(__name__)
|
|
|
|
| 39 |
|
| 40 |
configure_logging(
|
| 41 |
+
level=config.common_config.log_level,
|
| 42 |
config_file_path=config.common_config.log_file_path,
|
| 43 |
)
|
| 44 |
|
|
|
|
| 64 |
)
|
| 65 |
|
| 66 |
app.include_router(llm_router)
|
|
|
|
|
|
|
| 67 |
app.include_router(dataset_router)
|
| 68 |
app.include_router(document_router)
|
| 69 |
app.include_router(llm_config_router)
|
| 70 |
app.include_router(llm_prompt_router)
|
| 71 |
app.include_router(entity_router)
|
| 72 |
+
app.include_router(evaluation_router)
|
| 73 |
app.include_router(auth_router)
|
| 74 |
|
| 75 |
+
|
| 76 |
if __name__ == "__main__":
|
| 77 |
uvicorn.run(
|
| 78 |
"main:app",
|
| 79 |
host="localhost",
|
| 80 |
+
port=8885,
|
| 81 |
+
reload=True,
|
| 82 |
workers=1
|
| 83 |
)
|
routes/entity.py
CHANGED
|
@@ -91,7 +91,7 @@ async def search_entities_with_text(
|
|
| 91 |
try:
|
| 92 |
# Получаем результаты поиска
|
| 93 |
_, scores, entity_ids = entity_service.search_similar_old(
|
| 94 |
-
request.query, request.dataset_id
|
| 95 |
)
|
| 96 |
|
| 97 |
# Проверяем, что scores и entity_ids - корректные numpy массивы
|
|
|
|
| 91 |
try:
|
| 92 |
# Получаем результаты поиска
|
| 93 |
_, scores, entity_ids = entity_service.search_similar_old(
|
| 94 |
+
request.query, request.dataset_id, 100
|
| 95 |
)
|
| 96 |
|
| 97 |
# Проверяем, что scores и entity_ids - корректные numpy массивы
|
routes/evaluation.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Annotated, Any
|
| 2 |
+
|
| 3 |
+
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
| 4 |
+
|
| 5 |
+
import common.dependencies as DI
|
| 6 |
+
from common import auth
|
| 7 |
+
from components.services.search_metrics import SearchMetricsService
|
| 8 |
+
from schemas.evaluation import EvaluationParams, EvaluationResponse
|
| 9 |
+
|
| 10 |
+
# Создание роутера
|
| 11 |
+
router = APIRouter(prefix="/evaluate", tags=["Evaluation"])
|
| 12 |
+
|
| 13 |
+
# Важно: добавить импорт logger, если его нет
|
| 14 |
+
import logging
|
| 15 |
+
|
| 16 |
+
logger = logging.getLogger(__name__)
|
| 17 |
+
|
| 18 |
+
# Определение эндпоинта
|
| 19 |
+
@router.post(
|
| 20 |
+
"/from_file/{dataset_id}",
|
| 21 |
+
response_model=EvaluationResponse,
|
| 22 |
+
summary="Оценка RAG по файлу",
|
| 23 |
+
description="Загружает XLSX файл с вопросами/ответами и рассчитывает метрики RAG (Precision, Recall, F1) для указанного dataset_id и различных значений top_n. Опционально применяет Query Expansion."
|
| 24 |
+
)
|
| 25 |
+
async def evaluate_rag_from_file(
|
| 26 |
+
dataset_id: int,
|
| 27 |
+
params: Annotated[EvaluationParams, Depends()],
|
| 28 |
+
file: Annotated[UploadFile, File(description="XLSX файл с колонками 'id', 'question', 'text' (эталонные ответы через \\n)")],
|
| 29 |
+
metrics_service: Annotated[SearchMetricsService, Depends(DI.get_search_metrics_service)],
|
| 30 |
+
current_user: Annotated[any, Depends(auth.get_current_user)], # Защита эндпоинта
|
| 31 |
+
) -> Any: # Возвращаем Any, т.к. сервис возвращает dict, а FastAPI валидирует по response_model
|
| 32 |
+
"""Эндпоинт для оценки RAG.
|
| 33 |
+
|
| 34 |
+
- Принимает ID датасета в пути.
|
| 35 |
+
- Принимает параметры оценки (порог, top_n, use_query_expansion) и файл как multipart/form-data.
|
| 36 |
+
- Вызывает SearchMetricsService для выполнения расчетов.
|
| 37 |
+
- Возвращает рассчитанные метрики.
|
| 38 |
+
"""
|
| 39 |
+
try:
|
| 40 |
+
# --- Вызываем сервис, он теперь возвращает полный словарь ---
|
| 41 |
+
evaluation_full_results = await metrics_service.evaluate_from_file(
|
| 42 |
+
file=file,
|
| 43 |
+
dataset_id=dataset_id,
|
| 44 |
+
similarity_threshold=params.similarity_threshold,
|
| 45 |
+
top_n_values=params.top_n_values,
|
| 46 |
+
use_query_expansion=params.use_query_expansion,
|
| 47 |
+
top_worst_k=params.top_worst_k # Передаем новый параметр
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
# --- Просто возвращаем результат сервиса ---
|
| 51 |
+
# FastAPI сам проверит его по схеме EvaluationResponse
|
| 52 |
+
return evaluation_full_results
|
| 53 |
+
|
| 54 |
+
except HTTPException as e:
|
| 55 |
+
# Просто пробрасываем HTTP ошибки дальше
|
| 56 |
+
raise e
|
| 57 |
+
except Exception as e:
|
| 58 |
+
# Логирование ошибки может быть полезно здесь
|
| 59 |
+
logger.exception("Internal server error during evaluation endpoint execution.") # Пример логирования
|
| 60 |
+
# Ловим другие возможные ошибки во время оценки
|
| 61 |
+
# Логгер уже есть в SearchMetricsService
|
| 62 |
+
raise HTTPException(status_code=500, detail=f"Internal server error during evaluation: {e}")
|
schemas/evaluation.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Any
|
| 2 |
+
|
| 3 |
+
from pydantic import BaseModel, Field
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
# Определение моделей Pydantic
|
| 7 |
+
class EvaluationParams(BaseModel):
|
| 8 |
+
similarity_threshold: float = Field(
|
| 9 |
+
...,
|
| 10 |
+
ge=0.0,
|
| 11 |
+
le=1.0,
|
| 12 |
+
description="Порог схожести для fuzzy сравнения (от 0.0 до 1.0)",
|
| 13 |
+
examples=[0.7]
|
| 14 |
+
)
|
| 15 |
+
top_n_values: list[int] = Field(
|
| 16 |
+
...,
|
| 17 |
+
min_items=1,
|
| 18 |
+
description="Список значений Top-N для оценки",
|
| 19 |
+
examples=[[10, 20, 50]]
|
| 20 |
+
)
|
| 21 |
+
use_query_expansion: bool = Field(
|
| 22 |
+
default=False,
|
| 23 |
+
description="Использовать ли Query Expansion перед поиском для каждого вопроса",
|
| 24 |
+
examples=[True]
|
| 25 |
+
)
|
| 26 |
+
top_worst_k: int = Field(
|
| 27 |
+
default=5,
|
| 28 |
+
ge=1,
|
| 29 |
+
description="Количество худших вопросов для вывода",
|
| 30 |
+
examples=[5]
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
class Metrics(BaseModel):
|
| 34 |
+
macro_precision: float | None
|
| 35 |
+
macro_recall: float | None
|
| 36 |
+
macro_f1: float | None
|
| 37 |
+
weighted_precision: float | None
|
| 38 |
+
weighted_recall: float | None
|
| 39 |
+
weighted_f1: float | None
|
| 40 |
+
micro_precision: float | None
|
| 41 |
+
micro_recall: float | None
|
| 42 |
+
micro_f1: float | None
|
| 43 |
+
assembly_punct_recall_macro: float | None = Field(
|
| 44 |
+
None, description="Macro-усредненный Recall найденных пунктов в собранном контексте"
|
| 45 |
+
)
|
| 46 |
+
doc_recall_macro: float | None = Field(
|
| 47 |
+
None, description="Macro-усредненный Recall найденных эталонных документов в собранном контексте"
|
| 48 |
+
)
|
| 49 |
+
avg_spurious_docs: float | None = Field(
|
| 50 |
+
None, description="Среднее количество 'лишних' документов (найденных, но не ожидаемых) на вопрос"
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
class EvaluationResponse(BaseModel):
|
| 54 |
+
total_found_puncts_overall: int | None = Field(
|
| 55 |
+
None, alias="Найдено пунктов (всего)"
|
| 56 |
+
)
|
| 57 |
+
total_ground_truth_puncts_overall: int | None = Field(
|
| 58 |
+
None, alias="Всего пунктов (эталон)"
|
| 59 |
+
)
|
| 60 |
+
human_readable_chunk_micro_recall: float | None = Field(
|
| 61 |
+
None, alias="% найденных пунктов (чанк присутствует в пункте)"
|
| 62 |
+
)
|
| 63 |
+
human_readable_assembly_micro_recall: float | None = Field(
|
| 64 |
+
None, alias="% пунктов были найдены в собранной версии"
|
| 65 |
+
)
|
| 66 |
+
human_readable_chunk_macro_recall: float | None = Field(
|
| 67 |
+
None, alias="В среднем для каждого вопроса найден такой % пунктов"
|
| 68 |
+
)
|
| 69 |
+
human_readable_doc_macro_recall: float | None = Field(
|
| 70 |
+
None, alias="В среднем для каждого вопроса найден такой % документов"
|
| 71 |
+
)
|
| 72 |
+
human_readable_avg_spurious_docs: float | None = Field(
|
| 73 |
+
None, alias="В среднем для каждого вопроса найдено N лишних документов, N"
|
| 74 |
+
)
|
| 75 |
+
results: dict[int, Metrics] = Field(
|
| 76 |
+
...,
|
| 77 |
+
description="Словарь с метриками для каждого значения top_n"
|
| 78 |
+
)
|
| 79 |
+
worst_performing_questions: list[dict[str, Any]] | None = Field(
|
| 80 |
+
None, description="Список вопросов с наихудшими показателями (по Assembly Recall)"
|
| 81 |
+
)
|