Spaces:
Sleeping
Sleeping
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 |
+
)
|