smart-bot-factory 1.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- smart_bot_factory/__init__.py +3 -0
- smart_bot_factory/admin/__init__.py +18 -0
- smart_bot_factory/admin/admin_events.py +1223 -0
- smart_bot_factory/admin/admin_logic.py +553 -0
- smart_bot_factory/admin/admin_manager.py +156 -0
- smart_bot_factory/admin/admin_tester.py +157 -0
- smart_bot_factory/admin/timeout_checker.py +547 -0
- smart_bot_factory/aiogram_calendar/__init__.py +14 -0
- smart_bot_factory/aiogram_calendar/common.py +64 -0
- smart_bot_factory/aiogram_calendar/dialog_calendar.py +259 -0
- smart_bot_factory/aiogram_calendar/schemas.py +99 -0
- smart_bot_factory/aiogram_calendar/simple_calendar.py +224 -0
- smart_bot_factory/analytics/analytics_manager.py +414 -0
- smart_bot_factory/cli.py +806 -0
- smart_bot_factory/config.py +258 -0
- smart_bot_factory/configs/growthmed-october-24/prompts/1sales_context.txt +16 -0
- smart_bot_factory/configs/growthmed-october-24/prompts/2product_info.txt +582 -0
- smart_bot_factory/configs/growthmed-october-24/prompts/3objection_handling.txt +66 -0
- smart_bot_factory/configs/growthmed-october-24/prompts/final_instructions.txt +212 -0
- smart_bot_factory/configs/growthmed-october-24/prompts/help_message.txt +28 -0
- smart_bot_factory/configs/growthmed-october-24/prompts/welcome_message.txt +8 -0
- smart_bot_factory/configs/growthmed-october-24/reports/test_20250924_064229.txt +818 -0
- smart_bot_factory/configs/growthmed-october-24/reports/test_20250924_064335.txt +32 -0
- smart_bot_factory/configs/growthmed-october-24/reports/test_20250924_064638.txt +35 -0
- smart_bot_factory/configs/growthmed-october-24/tests/quick_scenarios.yaml +133 -0
- smart_bot_factory/configs/growthmed-october-24/tests/realistic_scenarios.yaml +108 -0
- smart_bot_factory/configs/growthmed-october-24/tests/scenario_examples.yaml +46 -0
- smart_bot_factory/configs/growthmed-october-24/welcome_file/welcome_file_msg.txt +16 -0
- smart_bot_factory/configs/growthmed-october-24/welcome_file//342/225/250/320/267/342/225/250/342/225/241/342/225/250/342/225/221 /342/225/250/342/225/227/342/225/250/342/225/225/342/225/244/320/221/342/225/244/320/222 /342/225/250/342/224/220/342/225/250/342/225/233 152/342/225/250/320/264/342/225/250/320/247 /342/225/250/342/225/225 323/342/225/250/320/264/342/225/250/320/247 /342/225/250/342/224/244/342/225/250/342/225/227/342/225/244/320/237 /342/225/250/342/225/235/342/225/250/342/225/241/342/225/250/342/224/244/342/225/250/342/225/225/342/225/244/320/226/342/225/250/342/225/225/342/225/250/342/225/234/342/225/244/320/233.pdf +0 -0
- smart_bot_factory/core/bot_utils.py +1108 -0
- smart_bot_factory/core/conversation_manager.py +653 -0
- smart_bot_factory/core/decorators.py +2464 -0
- smart_bot_factory/core/message_sender.py +729 -0
- smart_bot_factory/core/router.py +347 -0
- smart_bot_factory/core/router_manager.py +218 -0
- smart_bot_factory/core/states.py +27 -0
- smart_bot_factory/creation/__init__.py +7 -0
- smart_bot_factory/creation/bot_builder.py +1093 -0
- smart_bot_factory/creation/bot_testing.py +1122 -0
- smart_bot_factory/dashboard/__init__.py +3 -0
- smart_bot_factory/event/__init__.py +7 -0
- smart_bot_factory/handlers/handlers.py +2013 -0
- smart_bot_factory/integrations/langchain_openai.py +542 -0
- smart_bot_factory/integrations/openai_client.py +513 -0
- smart_bot_factory/integrations/supabase_client.py +1678 -0
- smart_bot_factory/memory/__init__.py +8 -0
- smart_bot_factory/memory/memory_manager.py +299 -0
- smart_bot_factory/memory/static_memory.py +214 -0
- smart_bot_factory/message/__init__.py +56 -0
- smart_bot_factory/rag/__init__.py +5 -0
- smart_bot_factory/rag/decorators.py +29 -0
- smart_bot_factory/rag/router.py +54 -0
- smart_bot_factory/rag/templates/__init__.py +3 -0
- smart_bot_factory/rag/templates/create_table.sql +7 -0
- smart_bot_factory/rag/templates/create_table_and_function_template.py +94 -0
- smart_bot_factory/rag/templates/match_function.sql +61 -0
- smart_bot_factory/rag/templates/match_services_template.py +82 -0
- smart_bot_factory/rag/vectorstore.py +449 -0
- smart_bot_factory/router/__init__.py +10 -0
- smart_bot_factory/setup_checker.py +512 -0
- smart_bot_factory/supabase/__init__.py +7 -0
- smart_bot_factory/supabase/client.py +631 -0
- smart_bot_factory/utils/__init__.py +11 -0
- smart_bot_factory/utils/debug_routing.py +114 -0
- smart_bot_factory/utils/prompt_loader.py +529 -0
- smart_bot_factory/utils/tool_router.py +68 -0
- smart_bot_factory/utils/user_prompt_loader.py +55 -0
- smart_bot_factory/utm_link_generator.py +123 -0
- smart_bot_factory-1.1.1.dist-info/METADATA +1135 -0
- smart_bot_factory-1.1.1.dist-info/RECORD +73 -0
- smart_bot_factory-1.1.1.dist-info/WHEEL +4 -0
- smart_bot_factory-1.1.1.dist-info/entry_points.txt +2 -0
- smart_bot_factory-1.1.1.dist-info/licenses/LICENSE +24 -0
|
@@ -0,0 +1,2013 @@
|
|
|
1
|
+
# Исправленный handlers.py с отладкой маршрутизации
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
from aiogram import F, Router
|
|
7
|
+
from aiogram.filters import Command, StateFilter
|
|
8
|
+
from aiogram.fsm.context import FSMContext
|
|
9
|
+
from aiogram.types import CallbackQuery, Message
|
|
10
|
+
|
|
11
|
+
from ..core.bot_utils import (parse_ai_response, process_events, send_message,
|
|
12
|
+
send_welcome_file)
|
|
13
|
+
from ..core.states import UserStates
|
|
14
|
+
|
|
15
|
+
from langchain.messages import HumanMessage, SystemMessage, AIMessage
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ============ ФУНКЦИИ КОНВЕРТАЦИИ МЕЖДУ LANGCHAIN И OPENAI ============
|
|
21
|
+
|
|
22
|
+
def langchain_to_openai(message):
|
|
23
|
+
"""
|
|
24
|
+
Конвертирует одно LangChain сообщение в формат OpenAI (словарь).
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
message: SystemMessage, AIMessage или HumanMessage из LangChain
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
dict: Словарь в формате OpenAI {"role": "...", "content": "..."}
|
|
31
|
+
"""
|
|
32
|
+
# Сначала проверяем тип через isinstance
|
|
33
|
+
if isinstance(message, SystemMessage):
|
|
34
|
+
role = "system"
|
|
35
|
+
elif isinstance(message, AIMessage):
|
|
36
|
+
role = "assistant"
|
|
37
|
+
elif isinstance(message, HumanMessage):
|
|
38
|
+
role = "user"
|
|
39
|
+
else:
|
|
40
|
+
# Если не распознали по типу, проверяем атрибут type (может быть "human", "ai", "system")
|
|
41
|
+
message_type = getattr(message, 'type', None)
|
|
42
|
+
if message_type == "system":
|
|
43
|
+
role = "system"
|
|
44
|
+
elif message_type in ("ai", "assistant"):
|
|
45
|
+
role = "assistant"
|
|
46
|
+
elif message_type in ("human", "user"):
|
|
47
|
+
role = "user"
|
|
48
|
+
else:
|
|
49
|
+
raise ValueError(f"Неподдерживаемый тип сообщения: {type(message)}, type={message_type}")
|
|
50
|
+
|
|
51
|
+
content = message.content if hasattr(message, 'content') else str(message)
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
"role": role,
|
|
55
|
+
"content": content
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _is_langchain_message(message):
|
|
60
|
+
"""Проверяет, является ли сообщение LangChain сообщением"""
|
|
61
|
+
return isinstance(message, (SystemMessage, AIMessage, HumanMessage))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _is_openai_dict(message):
|
|
65
|
+
"""Проверяет, является ли сообщение словарем OpenAI"""
|
|
66
|
+
return isinstance(message, dict) and "role" in message and "content" in message
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def langchain_messages_to_openai(messages):
|
|
70
|
+
"""
|
|
71
|
+
Конвертирует массив сообщений в массив словарей OpenAI.
|
|
72
|
+
Поддерживает смешанные типы: LangChain сообщения и словари OpenAI.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
messages: Список сообщений (LangChain сообщения или словари OpenAI)
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
list: Список словарей в формате OpenAI
|
|
79
|
+
"""
|
|
80
|
+
result = []
|
|
81
|
+
for msg in messages:
|
|
82
|
+
if _is_openai_dict(msg):
|
|
83
|
+
# Уже в формате OpenAI, оставляем как есть
|
|
84
|
+
result.append(msg)
|
|
85
|
+
elif _is_langchain_message(msg):
|
|
86
|
+
# LangChain сообщение, конвертируем
|
|
87
|
+
result.append(langchain_to_openai(msg))
|
|
88
|
+
else:
|
|
89
|
+
raise ValueError(f"Неподдерживаемый тип сообщения в массиве: {type(msg)}")
|
|
90
|
+
return result
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def openai_to_langchain(message_dict):
|
|
94
|
+
"""
|
|
95
|
+
Конвертирует один словарь OpenAI в LangChain сообщение.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
message_dict: Словарь в формате OpenAI {"role": "...", "content": "..."}
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
SystemMessage, AIMessage или HumanMessage в зависимости от role
|
|
102
|
+
"""
|
|
103
|
+
role = message_dict.get("role", "").lower() if message_dict.get("role") else ""
|
|
104
|
+
content = message_dict.get("content", "")
|
|
105
|
+
|
|
106
|
+
# Обрабатываем синонимы: human -> user, ai -> assistant
|
|
107
|
+
if role == "system":
|
|
108
|
+
return SystemMessage(content=content)
|
|
109
|
+
elif role in ("assistant", "ai"):
|
|
110
|
+
return AIMessage(content=content)
|
|
111
|
+
elif role in ("user", "human"):
|
|
112
|
+
return HumanMessage(content=content)
|
|
113
|
+
else:
|
|
114
|
+
raise ValueError(f"Неподдерживаемый role: {role}")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def openai_messages_to_langchain(messages_list):
|
|
118
|
+
"""
|
|
119
|
+
Конвертирует массив сообщений в массив LangChain сообщений.
|
|
120
|
+
Поддерживает смешанные типы: словари OpenAI и LangChain сообщения.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
messages_list: Список сообщений (словари OpenAI или LangChain сообщения)
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
list: Список LangChain сообщений (SystemMessage, AIMessage, HumanMessage)
|
|
127
|
+
"""
|
|
128
|
+
result = []
|
|
129
|
+
for msg in messages_list:
|
|
130
|
+
if _is_langchain_message(msg):
|
|
131
|
+
# Уже LangChain сообщение, оставляем как есть
|
|
132
|
+
result.append(msg)
|
|
133
|
+
elif _is_openai_dict(msg):
|
|
134
|
+
# Словарь OpenAI, конвертируем
|
|
135
|
+
result.append(openai_to_langchain(msg))
|
|
136
|
+
else:
|
|
137
|
+
raise ValueError(f"Неподдерживаемый тип сообщения в массиве: {type(msg)}")
|
|
138
|
+
return result
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def fix_html_markup(text: str) -> str:
|
|
142
|
+
"""
|
|
143
|
+
Исправляет HTML разметку в тексте, экранируя неправильные теги.
|
|
144
|
+
Экранирует < и >, которые не являются частью валидных HTML тегов Telegram.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
text: Текст с возможными проблемными HTML тегами
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
str: Текст с исправленной HTML разметкой
|
|
151
|
+
"""
|
|
152
|
+
import re
|
|
153
|
+
|
|
154
|
+
if not text:
|
|
155
|
+
return text
|
|
156
|
+
|
|
157
|
+
# Валидные HTML теги Telegram
|
|
158
|
+
valid_tags = ['b', 'i', 'u', 's', 'code', 'pre', 'a']
|
|
159
|
+
|
|
160
|
+
# Создаем паттерн для всех валидных тегов (открывающих и закрывающих)
|
|
161
|
+
valid_tag_patterns = []
|
|
162
|
+
for tag in valid_tags:
|
|
163
|
+
# Открывающие теги: <b>, <i>, <code>, <pre>, <a href="...">
|
|
164
|
+
if tag == 'a':
|
|
165
|
+
# Для тега <a> учитываем атрибут href
|
|
166
|
+
valid_tag_patterns.append(r'<a\s+href="[^"]*">')
|
|
167
|
+
else:
|
|
168
|
+
valid_tag_patterns.append(f'<{tag}>')
|
|
169
|
+
# Закрывающие теги: </b>, </i>, </code>, </pre>, </a>
|
|
170
|
+
valid_tag_patterns.append(f'</{tag}>')
|
|
171
|
+
|
|
172
|
+
# Объединяем все паттерны
|
|
173
|
+
combined_pattern = '|'.join(valid_tag_patterns)
|
|
174
|
+
|
|
175
|
+
# Находим все валидные теги и заменяем их на плейсхолдеры
|
|
176
|
+
placeholders = {}
|
|
177
|
+
placeholder_counter = 0
|
|
178
|
+
|
|
179
|
+
def replace_valid_tag(match):
|
|
180
|
+
nonlocal placeholder_counter
|
|
181
|
+
placeholder = f"__VALID_TAG_{placeholder_counter}__"
|
|
182
|
+
placeholders[placeholder] = match.group()
|
|
183
|
+
placeholder_counter += 1
|
|
184
|
+
return placeholder
|
|
185
|
+
|
|
186
|
+
# Заменяем валидные теги на плейсхолдеры
|
|
187
|
+
text_with_placeholders = re.sub(combined_pattern, replace_valid_tag, text, flags=re.IGNORECASE)
|
|
188
|
+
|
|
189
|
+
# Экранируем все оставшиеся < и >
|
|
190
|
+
text_escaped = text_with_placeholders.replace('<', '<').replace('>', '>')
|
|
191
|
+
|
|
192
|
+
# Восстанавливаем валидные теги
|
|
193
|
+
for placeholder, tag in placeholders.items():
|
|
194
|
+
text_escaped = text_escaped.replace(placeholder, tag)
|
|
195
|
+
|
|
196
|
+
return text_escaped
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
async def send_message_in_parts(
|
|
200
|
+
message: Message,
|
|
201
|
+
text: str,
|
|
202
|
+
files_list: list = [],
|
|
203
|
+
directories_list: list = [],
|
|
204
|
+
max_length: int = 4090,
|
|
205
|
+
):
|
|
206
|
+
"""
|
|
207
|
+
Отправляет сообщение, разбивая его на части, если оно превышает максимальную длину.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
message: Message объект от aiogram
|
|
211
|
+
text: Текст сообщения для отправки
|
|
212
|
+
files_list: Список файлов для отправки (отправляются только с первой частью)
|
|
213
|
+
directories_list: Список каталогов для отправки (отправляются только с первой частью)
|
|
214
|
+
max_length: Максимальная длина одного сообщения (по умолчанию 4090)
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
int: Количество отправленных частей
|
|
218
|
+
"""
|
|
219
|
+
from ..core.bot_utils import send_message
|
|
220
|
+
|
|
221
|
+
# Проверяем, что текст не пустой
|
|
222
|
+
if not text or not text.strip():
|
|
223
|
+
logger.warning("⚠️ Попытка отправить пустое сообщение")
|
|
224
|
+
return 0
|
|
225
|
+
|
|
226
|
+
if len(text) <= max_length:
|
|
227
|
+
# Сообщение нормального размера, отправляем как обычно
|
|
228
|
+
await send_message(
|
|
229
|
+
message,
|
|
230
|
+
text,
|
|
231
|
+
files_list=files_list,
|
|
232
|
+
directories_list=directories_list,
|
|
233
|
+
)
|
|
234
|
+
logger.info(f"✅ Сообщение отправлено ({len(text)} символов)")
|
|
235
|
+
return 1
|
|
236
|
+
|
|
237
|
+
logger.info(f"📏 Сообщение слишком длинное ({len(text)} символов), разбиваем на части")
|
|
238
|
+
|
|
239
|
+
# Разбиваем текст на части
|
|
240
|
+
parts = []
|
|
241
|
+
current_part = ""
|
|
242
|
+
|
|
243
|
+
# Разбиваем по строкам, чтобы не разрывать слова
|
|
244
|
+
lines = text.split('\n')
|
|
245
|
+
|
|
246
|
+
for line in lines:
|
|
247
|
+
# Если добавление текущей строки не превысит лимит
|
|
248
|
+
if len(current_part) + len(line) + 1 <= max_length:
|
|
249
|
+
if current_part:
|
|
250
|
+
current_part += '\n' + line
|
|
251
|
+
else:
|
|
252
|
+
current_part = line
|
|
253
|
+
else:
|
|
254
|
+
# Сохраняем текущую часть
|
|
255
|
+
if current_part:
|
|
256
|
+
parts.append(current_part)
|
|
257
|
+
|
|
258
|
+
# Если сама строка длиннее лимита, разбиваем её по словам
|
|
259
|
+
if len(line) > max_length:
|
|
260
|
+
words = line.split(' ')
|
|
261
|
+
current_part = ""
|
|
262
|
+
for word in words:
|
|
263
|
+
if len(current_part) + len(word) + 1 <= max_length:
|
|
264
|
+
if current_part:
|
|
265
|
+
current_part += ' ' + word
|
|
266
|
+
else:
|
|
267
|
+
current_part = word
|
|
268
|
+
else:
|
|
269
|
+
if current_part:
|
|
270
|
+
parts.append(current_part)
|
|
271
|
+
current_part = word
|
|
272
|
+
else:
|
|
273
|
+
current_part = line
|
|
274
|
+
|
|
275
|
+
# Добавляем последнюю часть
|
|
276
|
+
if current_part:
|
|
277
|
+
parts.append(current_part)
|
|
278
|
+
|
|
279
|
+
logger.info(f"📦 Сообщение разбито на {len(parts)} частей")
|
|
280
|
+
|
|
281
|
+
# Отправляем каждую часть отдельным сообщением
|
|
282
|
+
try:
|
|
283
|
+
for idx, part in enumerate(parts, 1):
|
|
284
|
+
# Файлы отправляем только с первой частью
|
|
285
|
+
if idx == 1:
|
|
286
|
+
await send_message(
|
|
287
|
+
message,
|
|
288
|
+
part,
|
|
289
|
+
files_list=files_list,
|
|
290
|
+
directories_list=directories_list,
|
|
291
|
+
)
|
|
292
|
+
else:
|
|
293
|
+
await send_message(message, part)
|
|
294
|
+
logger.info(f"✅ Часть {idx}/{len(parts)} отправлена ({len(part)} символов)")
|
|
295
|
+
|
|
296
|
+
logger.info(
|
|
297
|
+
f"✅ Все {len(parts)} частей успешно отправлены пользователю {message.from_user.id}"
|
|
298
|
+
)
|
|
299
|
+
return len(parts)
|
|
300
|
+
except Exception as e:
|
|
301
|
+
logger.error(f"❌ ОШИБКА ОТПРАВКИ СООБЩЕНИЯ: {e}")
|
|
302
|
+
# Пытаемся отправить простое сообщение об ошибке
|
|
303
|
+
try:
|
|
304
|
+
await message.answer(
|
|
305
|
+
"Произошла ошибка при отправке ответа. Попробуйте еще раз."
|
|
306
|
+
)
|
|
307
|
+
except Exception as e2:
|
|
308
|
+
logger.error(f"❌ Не удалось отправить даже сообщение об ошибке: {e2}")
|
|
309
|
+
raise
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
# Создаем роутер для обработчиков
|
|
313
|
+
router = Router()
|
|
314
|
+
|
|
315
|
+
def setup_handlers(dp):
|
|
316
|
+
"""Настройка основных обработчиков"""
|
|
317
|
+
# Подключаем middleware
|
|
318
|
+
router.message.middleware()(admin_middleware)
|
|
319
|
+
|
|
320
|
+
# Регистрируем роутер
|
|
321
|
+
dp.include_router(router)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# Функция для получения глобальных переменных
|
|
325
|
+
def get_global_var(var_name):
|
|
326
|
+
"""Получает глобальную переменную из модуля handlers"""
|
|
327
|
+
import sys
|
|
328
|
+
|
|
329
|
+
current_module = sys.modules[__name__]
|
|
330
|
+
return getattr(current_module, var_name, None)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
# Middleware для проверки админов
|
|
334
|
+
async def admin_middleware(handler, event: Message, data: dict):
|
|
335
|
+
"""Middleware для обновления информации об админах"""
|
|
336
|
+
admin_manager = get_global_var("admin_manager")
|
|
337
|
+
|
|
338
|
+
if admin_manager and admin_manager.is_admin(event.from_user.id):
|
|
339
|
+
await admin_manager.update_admin_info(event.from_user)
|
|
340
|
+
|
|
341
|
+
return await handler(event, data)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
@router.message(Command(commands=["start", "старт", "ст"]))
|
|
345
|
+
async def start_handler(message: Message, state: FSMContext):
|
|
346
|
+
"""Обработчик команды /start - сброс сессии и начало заново"""
|
|
347
|
+
admin_manager = get_global_var("admin_manager")
|
|
348
|
+
from ..admin.admin_logic import admin_start_handler
|
|
349
|
+
from ..utils.debug_routing import debug_user_state
|
|
350
|
+
|
|
351
|
+
try:
|
|
352
|
+
await debug_user_state(message, state, "START_COMMAND")
|
|
353
|
+
|
|
354
|
+
# Проверяем, админ ли это и в каком режиме
|
|
355
|
+
if admin_manager.is_admin(message.from_user.id):
|
|
356
|
+
if admin_manager.is_in_admin_mode(message.from_user.id):
|
|
357
|
+
# Админ в режиме администратора - работаем как админ
|
|
358
|
+
await admin_start_handler(message, state)
|
|
359
|
+
return
|
|
360
|
+
# Админ в режиме пользователя - работаем как обычный пользователь
|
|
361
|
+
|
|
362
|
+
await user_start_handler(message, state)
|
|
363
|
+
|
|
364
|
+
except Exception as e:
|
|
365
|
+
logger.error(f"Ошибка при обработке /start: {e}")
|
|
366
|
+
await send_message(
|
|
367
|
+
message, "Произошла ошибка при инициализации. Попробуйте позже."
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
@router.message(Command(commands=["timeup", "вперед"]))
|
|
372
|
+
async def timeup_handler(message: Message, state: FSMContext):
|
|
373
|
+
"""Обработчик команды /timeup (или /вперед) - выполнение ближайшего запланированного события"""
|
|
374
|
+
from datetime import datetime, timezone
|
|
375
|
+
|
|
376
|
+
from ..core.decorators import process_scheduled_event, update_event_result, process_admin_event
|
|
377
|
+
|
|
378
|
+
supabase_client = get_global_var("supabase_client")
|
|
379
|
+
|
|
380
|
+
try:
|
|
381
|
+
await message.answer("🔄 Ищу ближайшее запланированное событие...")
|
|
382
|
+
|
|
383
|
+
# Получаем события для этого пользователя И глобальные события (user_id = null)
|
|
384
|
+
# 1. События пользователя
|
|
385
|
+
user_events_query = (
|
|
386
|
+
supabase_client.client.table("scheduled_events")
|
|
387
|
+
.select("*")
|
|
388
|
+
.eq("user_id", message.from_user.id)
|
|
389
|
+
.in_("status", ["pending", "immediate"])
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
user_events_query = user_events_query.eq("bot_id", supabase_client.bot_id)
|
|
393
|
+
|
|
394
|
+
user_events = user_events_query.execute()
|
|
395
|
+
|
|
396
|
+
# 2. Глобальные события (без user_id)
|
|
397
|
+
global_events_query = (
|
|
398
|
+
supabase_client.client.table("scheduled_events")
|
|
399
|
+
.select("*")
|
|
400
|
+
.is_("user_id", "null")
|
|
401
|
+
.in_("status", ["pending", "immediate"])
|
|
402
|
+
.eq("bot_id", supabase_client.bot_id)
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
global_events = global_events_query.execute()
|
|
406
|
+
|
|
407
|
+
# Объединяем события
|
|
408
|
+
all_events = (user_events.data or []) + (global_events.data or [])
|
|
409
|
+
|
|
410
|
+
if not all_events:
|
|
411
|
+
await message.answer("📭 Нет запланированных событий для выполнения")
|
|
412
|
+
return
|
|
413
|
+
|
|
414
|
+
# Находим ближайшее событие по времени
|
|
415
|
+
now = datetime.now(timezone.utc)
|
|
416
|
+
nearest_event = None
|
|
417
|
+
nearest_time = None
|
|
418
|
+
|
|
419
|
+
for event in all_events:
|
|
420
|
+
scheduled_at_str = event.get("scheduled_at")
|
|
421
|
+
|
|
422
|
+
# События immediate (scheduled_at = null) считаются ближайшими
|
|
423
|
+
if scheduled_at_str is None:
|
|
424
|
+
nearest_event = event
|
|
425
|
+
nearest_time = None # Немедленное выполнение
|
|
426
|
+
break
|
|
427
|
+
|
|
428
|
+
# Парсим время события
|
|
429
|
+
try:
|
|
430
|
+
scheduled_at = datetime.fromisoformat(scheduled_at_str.replace("Z", "+00:00"))
|
|
431
|
+
if nearest_time is None or scheduled_at < nearest_time:
|
|
432
|
+
nearest_time = scheduled_at
|
|
433
|
+
nearest_event = event
|
|
434
|
+
except Exception as e:
|
|
435
|
+
logger.warning(f"⚠️ Не удалось распарсить scheduled_at для события {event.get('id')}: {e}")
|
|
436
|
+
continue
|
|
437
|
+
|
|
438
|
+
if not nearest_event:
|
|
439
|
+
await message.answer("📭 Не удалось определить ближайшее событие")
|
|
440
|
+
return
|
|
441
|
+
|
|
442
|
+
event_id = nearest_event["id"]
|
|
443
|
+
event_type = nearest_event["event_type"]
|
|
444
|
+
event_category = nearest_event["event_category"]
|
|
445
|
+
is_global = nearest_event.get("user_id") is None
|
|
446
|
+
|
|
447
|
+
# Заменяем _ на пробелы для красивого отображения
|
|
448
|
+
event_type_display = event_type.replace("_", " ")
|
|
449
|
+
event_category_display = event_category.replace("_", " ")
|
|
450
|
+
|
|
451
|
+
event_label = f"🌍 {event_type_display}" if is_global else f"👤 {event_type_display}"
|
|
452
|
+
|
|
453
|
+
# Форматируем время запланированного запуска
|
|
454
|
+
scheduled_time_str = "немедленно"
|
|
455
|
+
if nearest_time:
|
|
456
|
+
try:
|
|
457
|
+
# Конвертируем в московское время для отображения
|
|
458
|
+
from zoneinfo import ZoneInfo
|
|
459
|
+
moscow_tz = ZoneInfo("Europe/Moscow")
|
|
460
|
+
moscow_time = nearest_time.astimezone(moscow_tz)
|
|
461
|
+
scheduled_time_str = moscow_time.strftime("%d.%m.%Y %H:%M:%S (МСК)")
|
|
462
|
+
except Exception:
|
|
463
|
+
scheduled_time_str = nearest_time.strftime("%d.%m.%Y %H:%M:%S UTC")
|
|
464
|
+
|
|
465
|
+
logger.info(
|
|
466
|
+
f"⏭️ Обрабатываем ближайшее событие {event_id}: {event_category}/{event_type} "
|
|
467
|
+
f"({'глобальное' if is_global else f'пользователя {message.from_user.id}'}), "
|
|
468
|
+
f"запланировано на: {scheduled_time_str}"
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
try:
|
|
472
|
+
logger.info(f"🚀 Начинаю выполнение события {event_id}...")
|
|
473
|
+
|
|
474
|
+
# Выполняем событие
|
|
475
|
+
if event_category == "admin_event":
|
|
476
|
+
# Для админских событий используем тестовую отправку только текущему пользователю
|
|
477
|
+
logger.info(f"📧 Обработка админского события {event_id} для пользователя {message.from_user.id}")
|
|
478
|
+
result = await process_admin_event(nearest_event, single_user_id=message.from_user.id)
|
|
479
|
+
logger.info(f"📧 Результат админского события: {result}")
|
|
480
|
+
# Не отмечаем админское событие как выполненное при тестовой отправке
|
|
481
|
+
logger.info(f"✅ Событие {event_id} протестировано для пользователя {message.from_user.id}")
|
|
482
|
+
else:
|
|
483
|
+
logger.info(f"⚙️ Обработка события {event_id} категории {event_category}")
|
|
484
|
+
result = await process_scheduled_event(nearest_event)
|
|
485
|
+
logger.info(f"⚙️ Результат обработки события: {result}")
|
|
486
|
+
# Помечаем как выполненное только не-админские события
|
|
487
|
+
if event_category != "global_handler":
|
|
488
|
+
await update_event_result(
|
|
489
|
+
event_id,
|
|
490
|
+
"completed",
|
|
491
|
+
{
|
|
492
|
+
"executed": True,
|
|
493
|
+
"test_mode": True,
|
|
494
|
+
"tested_by_user": message.from_user.id,
|
|
495
|
+
"tested_at": datetime.now().isoformat(),
|
|
496
|
+
},
|
|
497
|
+
)
|
|
498
|
+
logger.info(f"✅ Событие {event_id} успешно выполнено")
|
|
499
|
+
|
|
500
|
+
# Отправляем сообщение о выполненном событии
|
|
501
|
+
result_text = [
|
|
502
|
+
"✅ *Событие успешно обработано*",
|
|
503
|
+
"",
|
|
504
|
+
"━━━━━━━━━━━━━━━━━━━━",
|
|
505
|
+
f"📋 **Тип события:**",
|
|
506
|
+
f" {event_label}",
|
|
507
|
+
"",
|
|
508
|
+
f"🏷️ **Категория:**",
|
|
509
|
+
f" {event_category_display}",
|
|
510
|
+
"",
|
|
511
|
+
f"⏰ **Запланировано на:**",
|
|
512
|
+
f" {scheduled_time_str}",
|
|
513
|
+
"━━━━━━━━━━━━━━━━━━━━",
|
|
514
|
+
]
|
|
515
|
+
|
|
516
|
+
await message.answer("\n".join(result_text), parse_mode="Markdown")
|
|
517
|
+
|
|
518
|
+
except Exception as e:
|
|
519
|
+
import traceback
|
|
520
|
+
error_msg = str(e)
|
|
521
|
+
error_traceback = traceback.format_exc()
|
|
522
|
+
logger.error(f"❌ Ошибка выполнения события {event_id}: {error_msg}")
|
|
523
|
+
logger.error(f"❌ Трассировка ошибки: {error_traceback}")
|
|
524
|
+
|
|
525
|
+
# Помечаем как failed
|
|
526
|
+
try:
|
|
527
|
+
await update_event_result(event_id, "failed", None, error_msg)
|
|
528
|
+
except Exception as update_error:
|
|
529
|
+
logger.error(f"❌ Ошибка обновления статуса события: {update_error}")
|
|
530
|
+
|
|
531
|
+
result_text = [
|
|
532
|
+
"❌ *Ошибка обработки события*",
|
|
533
|
+
"",
|
|
534
|
+
"━━━━━━━━━━━━━━━━━━━━",
|
|
535
|
+
f"📋 **Тип события:**",
|
|
536
|
+
f" {event_label}",
|
|
537
|
+
"",
|
|
538
|
+
f"🏷️ **Категория:**",
|
|
539
|
+
f" {event_category_display}",
|
|
540
|
+
"",
|
|
541
|
+
f"⏰ **Запланировано на:**",
|
|
542
|
+
f" {scheduled_time_str}",
|
|
543
|
+
"",
|
|
544
|
+
f"⚠️ **Ошибка:**",
|
|
545
|
+
f" `{error_msg[:100]}`",
|
|
546
|
+
"━━━━━━━━━━━━━━━━━━━━",
|
|
547
|
+
]
|
|
548
|
+
|
|
549
|
+
await message.answer("\n".join(result_text), parse_mode="Markdown")
|
|
550
|
+
|
|
551
|
+
except Exception as e:
|
|
552
|
+
import traceback
|
|
553
|
+
logger.error(f"❌ Критическая ошибка в timeup_handler: {e}")
|
|
554
|
+
logger.error(f"❌ Трассировка критической ошибки: {traceback.format_exc()}")
|
|
555
|
+
await message.answer(f"❌ Ошибка выполнения: `{str(e)}`", parse_mode="Markdown")
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
@router.message(F.voice | F.audio)
|
|
559
|
+
async def voice_handler(message: Message, state: FSMContext):
|
|
560
|
+
"""Обработчик голосовых и аудио сообщений"""
|
|
561
|
+
openai_client = get_global_var("openai_client")
|
|
562
|
+
bot = get_global_var("bot")
|
|
563
|
+
admin_manager = get_global_var("admin_manager")
|
|
564
|
+
|
|
565
|
+
import os
|
|
566
|
+
from datetime import datetime
|
|
567
|
+
from pathlib import Path
|
|
568
|
+
|
|
569
|
+
processing_msg = None
|
|
570
|
+
|
|
571
|
+
try:
|
|
572
|
+
# Проверяем что это не админ в режиме администратора
|
|
573
|
+
if admin_manager.is_admin(message.from_user.id):
|
|
574
|
+
if admin_manager.is_in_admin_mode(message.from_user.id):
|
|
575
|
+
return # Админы работают с текстом
|
|
576
|
+
|
|
577
|
+
logger.info(f"🎤 Получено голосовое сообщение от {message.from_user.id}")
|
|
578
|
+
|
|
579
|
+
# Получаем файл
|
|
580
|
+
if message.voice:
|
|
581
|
+
file_id = message.voice.file_id
|
|
582
|
+
duration = message.voice.duration
|
|
583
|
+
else:
|
|
584
|
+
file_id = message.audio.file_id
|
|
585
|
+
duration = message.audio.duration
|
|
586
|
+
|
|
587
|
+
# Показываем что обрабатываем
|
|
588
|
+
processing_msg = await message.answer("🎤 Распознаю голос...")
|
|
589
|
+
|
|
590
|
+
try:
|
|
591
|
+
# Скачиваем файл
|
|
592
|
+
file = await bot.get_file(file_id)
|
|
593
|
+
|
|
594
|
+
# Путь для сохранения
|
|
595
|
+
temp_dir = Path("temp_audio")
|
|
596
|
+
temp_dir.mkdir(exist_ok=True)
|
|
597
|
+
|
|
598
|
+
file_path = (
|
|
599
|
+
temp_dir
|
|
600
|
+
/ f"{message.from_user.id}_{int(datetime.now().timestamp())}.ogg"
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
# Скачиваем
|
|
604
|
+
await bot.download_file(file.file_path, file_path)
|
|
605
|
+
logger.info(f"📥 Файл скачан: {file_path} ({duration} сек)")
|
|
606
|
+
|
|
607
|
+
# Распознаем через Whisper
|
|
608
|
+
recognized_text = await openai_client.transcribe_audio(str(file_path))
|
|
609
|
+
|
|
610
|
+
# Удаляем временный файл и папку
|
|
611
|
+
try:
|
|
612
|
+
os.remove(file_path)
|
|
613
|
+
logger.info(f"🗑️ Временный файл удален: {file_path}")
|
|
614
|
+
|
|
615
|
+
# Проверяем, пуста ли папка
|
|
616
|
+
if not any(temp_dir.iterdir()):
|
|
617
|
+
temp_dir.rmdir()
|
|
618
|
+
logger.info(f"🗑️ Временная папка удалена: {temp_dir}")
|
|
619
|
+
|
|
620
|
+
except Exception as e:
|
|
621
|
+
logger.warning(f"⚠️ Не удалось удалить временные файлы: {e}")
|
|
622
|
+
|
|
623
|
+
if not recognized_text:
|
|
624
|
+
await processing_msg.edit_text(
|
|
625
|
+
"❌ Не удалось распознать голос. Попробуйте еще раз."
|
|
626
|
+
)
|
|
627
|
+
return
|
|
628
|
+
|
|
629
|
+
logger.info(f"✅ Текст распознан успешно: '{recognized_text[:100]}...'")
|
|
630
|
+
|
|
631
|
+
# Получаем данные сессии
|
|
632
|
+
current_state = await state.get_state()
|
|
633
|
+
data = await state.get_data()
|
|
634
|
+
|
|
635
|
+
logger.info(f"🔍 Текущее состояние: {current_state}")
|
|
636
|
+
logger.info(f"🔍 Данные в state: {data}")
|
|
637
|
+
|
|
638
|
+
session_id = data.get("session_id")
|
|
639
|
+
system_prompt = data.get("system_prompt")
|
|
640
|
+
|
|
641
|
+
logger.info(f"📝 session_id из state: {session_id}")
|
|
642
|
+
|
|
643
|
+
# Если session_id нет в state, пытаемся получить из БД
|
|
644
|
+
if not session_id:
|
|
645
|
+
logger.warning(
|
|
646
|
+
"⚠️ session_id не найден в state, ищем активную сессию в БД..."
|
|
647
|
+
)
|
|
648
|
+
supabase_client = get_global_var("supabase_client")
|
|
649
|
+
|
|
650
|
+
session_info = await supabase_client.get_active_session(
|
|
651
|
+
message.from_user.id
|
|
652
|
+
)
|
|
653
|
+
if session_info:
|
|
654
|
+
session_id = session_info["id"]
|
|
655
|
+
system_prompt = session_info["system_prompt"]
|
|
656
|
+
|
|
657
|
+
# Сохраняем в state для следующих сообщений
|
|
658
|
+
await state.update_data(
|
|
659
|
+
session_id=session_id, system_prompt=system_prompt
|
|
660
|
+
)
|
|
661
|
+
await state.set_state(UserStates.waiting_for_message)
|
|
662
|
+
|
|
663
|
+
logger.info(f"✅ Сессия восстановлена из БД: {session_id}")
|
|
664
|
+
else:
|
|
665
|
+
logger.error("❌ Активная сессия не найдена в БД")
|
|
666
|
+
|
|
667
|
+
if session_id:
|
|
668
|
+
# Сохраняем распознанный текст в state
|
|
669
|
+
await state.update_data(voice_recognized_text=recognized_text)
|
|
670
|
+
await state.set_state(UserStates.voice_confirmation)
|
|
671
|
+
|
|
672
|
+
# Показываем распознанный текст с кнопками выбора
|
|
673
|
+
from aiogram.types import (InlineKeyboardButton,
|
|
674
|
+
InlineKeyboardMarkup)
|
|
675
|
+
|
|
676
|
+
keyboard = InlineKeyboardMarkup(
|
|
677
|
+
inline_keyboard=[
|
|
678
|
+
[
|
|
679
|
+
InlineKeyboardButton(
|
|
680
|
+
text="✅ Отправить", callback_data="voice_send"
|
|
681
|
+
)
|
|
682
|
+
],
|
|
683
|
+
[
|
|
684
|
+
InlineKeyboardButton(
|
|
685
|
+
text="✏️ Изменить текст", callback_data="voice_edit"
|
|
686
|
+
)
|
|
687
|
+
],
|
|
688
|
+
[
|
|
689
|
+
InlineKeyboardButton(
|
|
690
|
+
text="🎤 Надиктовать заново",
|
|
691
|
+
callback_data="voice_retry",
|
|
692
|
+
)
|
|
693
|
+
],
|
|
694
|
+
]
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
# Удаляем сообщение "Обрабатываю"
|
|
698
|
+
try:
|
|
699
|
+
await processing_msg.delete()
|
|
700
|
+
except Exception:
|
|
701
|
+
pass
|
|
702
|
+
|
|
703
|
+
# Показываем результат с кнопками
|
|
704
|
+
await message.answer(
|
|
705
|
+
f"✅ Распознано:\n\n<i>{recognized_text}</i>\n\n"
|
|
706
|
+
f"Выберите действие:",
|
|
707
|
+
reply_markup=keyboard,
|
|
708
|
+
parse_mode="HTML",
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
logger.info("✅ Показаны кнопки подтверждения голосового сообщения")
|
|
712
|
+
else:
|
|
713
|
+
logger.warning("❌ Нет session_id в состоянии")
|
|
714
|
+
await processing_msg.edit_text(
|
|
715
|
+
f"✅ Распознано:\n\n{recognized_text}\n\n"
|
|
716
|
+
f"Сессия не найдена. Напишите /start"
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
except Exception as e:
|
|
720
|
+
logger.error(f"❌ Ошибка в процессе обработки голоса: {e}")
|
|
721
|
+
logger.exception("Полный стек ошибки:")
|
|
722
|
+
if processing_msg:
|
|
723
|
+
await processing_msg.edit_text(
|
|
724
|
+
"❌ Ошибка обработки. Попробуйте написать текстом."
|
|
725
|
+
)
|
|
726
|
+
else:
|
|
727
|
+
await message.answer(
|
|
728
|
+
"❌ Ошибка обработки. Попробуйте написать текстом."
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
except Exception as e:
|
|
732
|
+
logger.error(f"❌ КРИТИЧЕСКАЯ ошибка обработки голоса: {e}")
|
|
733
|
+
logger.exception("Полный стек критической ошибки:")
|
|
734
|
+
try:
|
|
735
|
+
if processing_msg:
|
|
736
|
+
await processing_msg.edit_text(
|
|
737
|
+
"❌ Ошибка распознавания. Попробуйте написать текстом."
|
|
738
|
+
)
|
|
739
|
+
else:
|
|
740
|
+
await message.answer(
|
|
741
|
+
"❌ Ошибка распознавания. Попробуйте написать текстом."
|
|
742
|
+
)
|
|
743
|
+
except Exception:
|
|
744
|
+
pass
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
async def user_start_handler(message: Message, state: FSMContext):
|
|
748
|
+
"""Обработчик /start для обычных пользователей"""
|
|
749
|
+
supabase_client = get_global_var("supabase_client")
|
|
750
|
+
prompt_loader = get_global_var("prompt_loader")
|
|
751
|
+
from ..core.bot_utils import parse_utm_from_start_param
|
|
752
|
+
|
|
753
|
+
try:
|
|
754
|
+
# 0. ПОЛУЧАЕМ UTM ДАННЫЕ
|
|
755
|
+
start_param = (
|
|
756
|
+
message.text.split(" ", 1)[1] if len(message.text.split()) > 1 else None
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
# Логируем входящий start параметр
|
|
760
|
+
# Пример поддерживаемого формата: @https://t.me/bot?start=utmSource-vk_utmCampaign-summer2025 не более 64 символов после strat=
|
|
761
|
+
|
|
762
|
+
logger.info(f"📥 Получен start параметр: '{start_param}'")
|
|
763
|
+
|
|
764
|
+
utm_data = {}
|
|
765
|
+
if start_param:
|
|
766
|
+
# Парсим UTM данные
|
|
767
|
+
utm_data = parse_utm_from_start_param(start_param)
|
|
768
|
+
|
|
769
|
+
# Подробное логирование UTM
|
|
770
|
+
logger.info(f"📊 UTM данные для пользователя {message.from_user.id}:")
|
|
771
|
+
if utm_data:
|
|
772
|
+
for key, value in utm_data.items():
|
|
773
|
+
logger.info(f" • {key}: {value}")
|
|
774
|
+
logger.info("✅ UTM данные успешно распознаны")
|
|
775
|
+
else:
|
|
776
|
+
logger.warning(f"⚠️ UTM данные не найдены в параметре: '{start_param}'")
|
|
777
|
+
else:
|
|
778
|
+
logger.info("ℹ️ Start параметр отсутствует (обычный /start)")
|
|
779
|
+
|
|
780
|
+
# 1. ЯВНО ОЧИЩАЕМ СОСТОЯНИЕ FSM
|
|
781
|
+
await state.clear()
|
|
782
|
+
logger.info(f"🔄 Состояние FSM очищено для пользователя {message.from_user.id}")
|
|
783
|
+
|
|
784
|
+
# 2. ЗАГРУЖАЕМ ПРОМПТЫ
|
|
785
|
+
logger.info(f"Загрузка промптов для пользователя {message.from_user.id}")
|
|
786
|
+
system_prompt = await prompt_loader.load_system_prompt()
|
|
787
|
+
|
|
788
|
+
# Загружаем приветственное сообщение
|
|
789
|
+
welcome_message = await prompt_loader.load_welcome_message()
|
|
790
|
+
|
|
791
|
+
# 3. ПОЛУЧАЕМ ДАННЫЕ ПОЛЬЗОВАТЕЛЯ
|
|
792
|
+
user_data = {
|
|
793
|
+
"telegram_id": message.from_user.id,
|
|
794
|
+
"username": message.from_user.username,
|
|
795
|
+
"first_name": message.from_user.first_name,
|
|
796
|
+
"last_name": message.from_user.last_name,
|
|
797
|
+
"language_code": message.from_user.language_code,
|
|
798
|
+
"source": utm_data.get("utm_source"),
|
|
799
|
+
"medium": utm_data.get("utm_medium"),
|
|
800
|
+
"campaign": utm_data.get("utm_campaign"),
|
|
801
|
+
"content": utm_data.get("utm_content"),
|
|
802
|
+
"term": utm_data.get("utm_term"),
|
|
803
|
+
"segment": utm_data.get("segment"),
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
# 4. СОЗДАЕМ НОВУЮ СЕССИЮ (автоматически закроет активные)
|
|
807
|
+
# Добавляем UTM данные в метаданные пользователя
|
|
808
|
+
if utm_data:
|
|
809
|
+
user_data["metadata"] = {"utm_data": utm_data}
|
|
810
|
+
logger.info("📈 UTM данные добавлены в метаданные пользователя")
|
|
811
|
+
|
|
812
|
+
session_id = await supabase_client.create_chat_session(user_data, system_prompt)
|
|
813
|
+
logger.info(
|
|
814
|
+
f"✅ Создана новая сессия {session_id} для пользователя {message.from_user.id}"
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
# 5. УСТАНАВЛИВАЕМ НОВОЕ СОСТОЯНИЕ
|
|
818
|
+
await state.update_data(session_id=session_id, system_prompt=system_prompt)
|
|
819
|
+
await state.set_state(UserStates.waiting_for_message)
|
|
820
|
+
|
|
821
|
+
# 6. ОТПРАВЛЯЕМ ПРИВЕТСТВЕННОЕ СООБЩЕНИЕ
|
|
822
|
+
try:
|
|
823
|
+
await send_message(message, welcome_message)
|
|
824
|
+
logger.info(
|
|
825
|
+
f"Приветственное сообщение отправлено пользователю {message.from_user.id}"
|
|
826
|
+
)
|
|
827
|
+
except Exception as e:
|
|
828
|
+
if "Forbidden: bot was blocked by the user" in str(e):
|
|
829
|
+
logger.warning(
|
|
830
|
+
f"🚫 Бот заблокирован пользователем {message.from_user.id}"
|
|
831
|
+
)
|
|
832
|
+
return
|
|
833
|
+
else:
|
|
834
|
+
logger.error(f"❌ Ошибка отправки приветственного сообщения: {e}")
|
|
835
|
+
raise
|
|
836
|
+
|
|
837
|
+
# 7. ЕСЛИ ЕСТЬ ФАЙЛ ОТПРАВЛЯЕМ ВМЕСТЕ С ПОДПИСЬЮ
|
|
838
|
+
logging.info(
|
|
839
|
+
f"📎 Попытка отправки приветственного файла для сессии {session_id}"
|
|
840
|
+
)
|
|
841
|
+
caption = await send_welcome_file(message)
|
|
842
|
+
|
|
843
|
+
# 8. СОХРАНЯЕМ ПРИВЕТСТВЕННОЕ СООБЩЕНИЕ В БД
|
|
844
|
+
if caption:
|
|
845
|
+
logging.info(
|
|
846
|
+
f"📄 Добавление подписи к файлу в приветственное сообщение для сессии {session_id}"
|
|
847
|
+
)
|
|
848
|
+
welcome_message = f"{welcome_message}\n\nПодпись к файлу:\n\n{caption}"
|
|
849
|
+
else:
|
|
850
|
+
logging.info(
|
|
851
|
+
f"📄 Приветственный файл отправлен без подписи для сессии {session_id}"
|
|
852
|
+
)
|
|
853
|
+
|
|
854
|
+
logging.info(
|
|
855
|
+
f"💾 Сохранение приветственного сообщения в БД для сессии {session_id}"
|
|
856
|
+
)
|
|
857
|
+
|
|
858
|
+
await supabase_client.add_message(
|
|
859
|
+
session_id=session_id,
|
|
860
|
+
role="assistant",
|
|
861
|
+
content=welcome_message,
|
|
862
|
+
message_type="text",
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
logging.info(
|
|
866
|
+
f"✅ Приветственное сообщение успешно сохранено в БД для сессии {session_id}"
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
# ВЫЗЫВАЕМ ПОЛЬЗОВАТЕЛЬСКИЕ ОБРАБОТЧИКИ on_start
|
|
870
|
+
start_handlers = get_global_var("start_handlers")
|
|
871
|
+
if start_handlers:
|
|
872
|
+
logger.info(
|
|
873
|
+
f"🔔 Вызов {len(start_handlers)} пользовательских обработчиков on_start"
|
|
874
|
+
)
|
|
875
|
+
for handler in start_handlers:
|
|
876
|
+
try:
|
|
877
|
+
await handler(
|
|
878
|
+
user_id=message.from_user.id,
|
|
879
|
+
session_id=session_id,
|
|
880
|
+
message=message,
|
|
881
|
+
state=state,
|
|
882
|
+
)
|
|
883
|
+
logger.info(
|
|
884
|
+
f"✅ Обработчик on_start '{handler.__name__}' выполнен успешно"
|
|
885
|
+
)
|
|
886
|
+
except Exception as handler_error:
|
|
887
|
+
logger.error(
|
|
888
|
+
f"❌ Ошибка в обработчике on_start '{handler.__name__}': {handler_error}"
|
|
889
|
+
)
|
|
890
|
+
# Продолжаем выполнение остальных обработчиков
|
|
891
|
+
|
|
892
|
+
except Exception as e:
|
|
893
|
+
logger.error(f"Ошибка при обработке user /start: {e}")
|
|
894
|
+
await send_message(
|
|
895
|
+
message, "Произошла ошибка при инициализации. Попробуйте позже."
|
|
896
|
+
)
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
@router.message(StateFilter(None))
|
|
900
|
+
async def message_without_state_handler(message: Message, state: FSMContext):
|
|
901
|
+
"""Обработчик сообщений без состояния (после перезапуска бота)"""
|
|
902
|
+
admin_manager = get_global_var("admin_manager")
|
|
903
|
+
supabase_client = get_global_var("supabase_client")
|
|
904
|
+
conversation_manager = get_global_var("conversation_manager")
|
|
905
|
+
from ..admin.admin_logic import AdminStates as AdminLogicStates
|
|
906
|
+
from ..utils.debug_routing import debug_user_state
|
|
907
|
+
|
|
908
|
+
try:
|
|
909
|
+
await debug_user_state(message, state, "NO_STATE")
|
|
910
|
+
|
|
911
|
+
# СНАЧАЛА проверяем диалог с админом
|
|
912
|
+
conversation = await conversation_manager.is_user_in_admin_chat(
|
|
913
|
+
message.from_user.id
|
|
914
|
+
)
|
|
915
|
+
|
|
916
|
+
if conversation:
|
|
917
|
+
logger.info(
|
|
918
|
+
f"✅ Найден диалог с админом {conversation['admin_id']}, устанавливаем состояние admin_chat"
|
|
919
|
+
)
|
|
920
|
+
|
|
921
|
+
# Устанавливаем состояние admin_chat
|
|
922
|
+
await state.set_state(UserStates.admin_chat)
|
|
923
|
+
await state.update_data(admin_conversation=conversation)
|
|
924
|
+
|
|
925
|
+
# Сразу пересылаем сообщение админу
|
|
926
|
+
await conversation_manager.forward_message_to_admin(message, conversation)
|
|
927
|
+
|
|
928
|
+
# Сохраняем сообщение в БД
|
|
929
|
+
session_info = await supabase_client.get_active_session(
|
|
930
|
+
message.from_user.id
|
|
931
|
+
)
|
|
932
|
+
if session_info:
|
|
933
|
+
await supabase_client.add_message(
|
|
934
|
+
session_id=session_info["id"],
|
|
935
|
+
role="user",
|
|
936
|
+
content=message.text,
|
|
937
|
+
message_type="text",
|
|
938
|
+
metadata={
|
|
939
|
+
"in_admin_chat": True,
|
|
940
|
+
"admin_id": conversation["admin_id"],
|
|
941
|
+
},
|
|
942
|
+
)
|
|
943
|
+
|
|
944
|
+
return
|
|
945
|
+
|
|
946
|
+
# Проверяем, админ ли это
|
|
947
|
+
if admin_manager.is_admin(message.from_user.id):
|
|
948
|
+
logger.info("👑 Админ в режиме администратора без состояния")
|
|
949
|
+
await state.set_state(AdminLogicStates.admin_mode)
|
|
950
|
+
await message.answer(
|
|
951
|
+
"👑 Режим администратора\nИспользуйте /start для панели управления"
|
|
952
|
+
)
|
|
953
|
+
return
|
|
954
|
+
|
|
955
|
+
logger.info("👤 Обычный пользователь без состояния, ищем активную сессию")
|
|
956
|
+
|
|
957
|
+
# Ищем активную сессию в БД
|
|
958
|
+
session_info = await supabase_client.get_active_session(message.from_user.id)
|
|
959
|
+
|
|
960
|
+
if session_info:
|
|
961
|
+
logger.info(f"📝 Восстанавливаем сессию {session_info['id']}")
|
|
962
|
+
# Восстанавливаем сессию из БД
|
|
963
|
+
session_id = session_info["id"]
|
|
964
|
+
system_prompt = session_info["system_prompt"]
|
|
965
|
+
|
|
966
|
+
# Сохраняем в состояние
|
|
967
|
+
await state.update_data(session_id=session_id, system_prompt=system_prompt)
|
|
968
|
+
await state.set_state(UserStates.waiting_for_message)
|
|
969
|
+
|
|
970
|
+
logger.info("✅ Сессия восстановлена, обрабатываем сообщение")
|
|
971
|
+
|
|
972
|
+
# Теперь обрабатываем сообщение как обычно
|
|
973
|
+
await process_user_message(message, state, session_id, system_prompt)
|
|
974
|
+
else:
|
|
975
|
+
logger.info("❌ Нет активной сессии, просим написать /start")
|
|
976
|
+
await send_message(message, "Привет! Напишите /start для начала диалога.")
|
|
977
|
+
|
|
978
|
+
except Exception as e:
|
|
979
|
+
logger.error(f"❌ Ошибка при обработке сообщения без состояния: {e}")
|
|
980
|
+
await send_message(
|
|
981
|
+
message, "Произошла ошибка. Попробуйте написать /start для начала диалога."
|
|
982
|
+
)
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
# ✅ ИСПРАВЛЕНИЕ: Обработчик admin_chat должен быть ПЕРВЫМ и более приоритетным
|
|
986
|
+
@router.message(StateFilter(UserStates.admin_chat))
|
|
987
|
+
async def user_in_admin_chat_handler(message: Message, state: FSMContext):
|
|
988
|
+
"""ПРИОРИТЕТНЫЙ обработчик сообщений пользователей в диалоге с админом"""
|
|
989
|
+
conversation_manager = get_global_var("conversation_manager")
|
|
990
|
+
supabase_client = get_global_var("supabase_client")
|
|
991
|
+
from ..utils.debug_routing import debug_user_state
|
|
992
|
+
|
|
993
|
+
await debug_user_state(message, state, "ADMIN_CHAT_HANDLER")
|
|
994
|
+
|
|
995
|
+
user_id = message.from_user.id
|
|
996
|
+
logger.info(f"🎯 ADMIN_CHAT HANDLER: сообщение от {user_id}: '{message.text}'")
|
|
997
|
+
|
|
998
|
+
# Проверяем, есть ли еще активный диалог
|
|
999
|
+
conversation = await conversation_manager.is_user_in_admin_chat(user_id)
|
|
1000
|
+
|
|
1001
|
+
if conversation:
|
|
1002
|
+
logger.info(f"✅ Диалог активен, пересылаем админу {conversation['admin_id']}")
|
|
1003
|
+
|
|
1004
|
+
try:
|
|
1005
|
+
# Сохраняем сообщение в БД
|
|
1006
|
+
session_info = await supabase_client.get_active_session(user_id)
|
|
1007
|
+
if session_info:
|
|
1008
|
+
await supabase_client.add_message(
|
|
1009
|
+
session_id=session_info["id"],
|
|
1010
|
+
role="user",
|
|
1011
|
+
content=message.text,
|
|
1012
|
+
message_type="text",
|
|
1013
|
+
metadata={
|
|
1014
|
+
"in_admin_chat": True,
|
|
1015
|
+
"admin_id": conversation["admin_id"],
|
|
1016
|
+
},
|
|
1017
|
+
)
|
|
1018
|
+
logger.info("💾 Сообщение сохранено в БД")
|
|
1019
|
+
|
|
1020
|
+
# Пересылаем админу
|
|
1021
|
+
await conversation_manager.forward_message_to_admin(message, conversation)
|
|
1022
|
+
logger.info("📤 Сообщение переслано админу")
|
|
1023
|
+
|
|
1024
|
+
except Exception as e:
|
|
1025
|
+
logger.error(f"❌ Ошибка обработки admin_chat: {e}")
|
|
1026
|
+
await message.answer("Произошла ошибка. Попробуйте позже.")
|
|
1027
|
+
else:
|
|
1028
|
+
logger.info("💬 Диалог завершен, возвращаем к обычному режиму")
|
|
1029
|
+
# Диалог завершен, возвращаем к обычному режиму
|
|
1030
|
+
await state.set_state(UserStates.waiting_for_message)
|
|
1031
|
+
|
|
1032
|
+
# Обрабатываем как обычное сообщение
|
|
1033
|
+
data = await state.get_data()
|
|
1034
|
+
session_id = data.get("session_id")
|
|
1035
|
+
system_prompt = data.get("system_prompt")
|
|
1036
|
+
|
|
1037
|
+
if session_id:
|
|
1038
|
+
await process_user_message(message, state, session_id, system_prompt)
|
|
1039
|
+
else:
|
|
1040
|
+
await send_message(
|
|
1041
|
+
message, "Сессия не найдена. Пожалуйста, напишите /start"
|
|
1042
|
+
)
|
|
1043
|
+
|
|
1044
|
+
|
|
1045
|
+
# Обработчик для обычных сообщений (НЕ в admin_chat)
|
|
1046
|
+
@router.message(StateFilter(UserStates.waiting_for_message), ~F.text.startswith("/"))
|
|
1047
|
+
async def user_message_handler(message: Message, state: FSMContext):
|
|
1048
|
+
"""Обработчик сообщений пользователей (исключая admin_chat)"""
|
|
1049
|
+
conversation_manager = get_global_var("conversation_manager")
|
|
1050
|
+
from ..utils.debug_routing import debug_user_state
|
|
1051
|
+
|
|
1052
|
+
try:
|
|
1053
|
+
await debug_user_state(message, state, "USER_MESSAGE_HANDLER")
|
|
1054
|
+
|
|
1055
|
+
# ✅ ВАЖНО: Сначала проверяем диалог с админом
|
|
1056
|
+
conversation = await conversation_manager.is_user_in_admin_chat(
|
|
1057
|
+
message.from_user.id
|
|
1058
|
+
)
|
|
1059
|
+
|
|
1060
|
+
if conversation:
|
|
1061
|
+
logger.info(
|
|
1062
|
+
"⚠️ НЕОЖИДАННО: пользователь в waiting_for_message, но есть диалог с админом!"
|
|
1063
|
+
)
|
|
1064
|
+
logger.info("🔄 Принудительно переключаем в admin_chat состояние")
|
|
1065
|
+
|
|
1066
|
+
# Принудительно переключаем состояние
|
|
1067
|
+
await state.set_state(UserStates.admin_chat)
|
|
1068
|
+
await state.update_data(admin_conversation=conversation)
|
|
1069
|
+
|
|
1070
|
+
# Обрабатываем сообщение как admin_chat
|
|
1071
|
+
await user_in_admin_chat_handler(message, state)
|
|
1072
|
+
return
|
|
1073
|
+
|
|
1074
|
+
logger.info("🤖 Обычный диалог с ботом")
|
|
1075
|
+
data = await state.get_data()
|
|
1076
|
+
session_id = data.get("session_id")
|
|
1077
|
+
system_prompt = data.get("system_prompt")
|
|
1078
|
+
|
|
1079
|
+
if not session_id:
|
|
1080
|
+
logger.warning("❌ Нет session_id в состоянии")
|
|
1081
|
+
await send_message(
|
|
1082
|
+
message, "Сессия не найдена. Пожалуйста, напишите /start"
|
|
1083
|
+
)
|
|
1084
|
+
return
|
|
1085
|
+
|
|
1086
|
+
logger.info(f"📝 Обрабатываем сообщение с session_id: {session_id}")
|
|
1087
|
+
await process_user_message(message, state, session_id, system_prompt)
|
|
1088
|
+
|
|
1089
|
+
except Exception as e:
|
|
1090
|
+
logger.error(f"❌ Ошибка при обработке сообщения пользователя: {e}")
|
|
1091
|
+
await send_message(
|
|
1092
|
+
message,
|
|
1093
|
+
"Произошла ошибка. Попробуйте еще раз или напишите /start для перезапуска.",
|
|
1094
|
+
)
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
@router.callback_query(F.data == "voice_send")
|
|
1098
|
+
async def voice_send_handler(callback: CallbackQuery, state: FSMContext):
|
|
1099
|
+
"""Обработчик кнопки 'Отправить' для голосового сообщения"""
|
|
1100
|
+
try:
|
|
1101
|
+
data = await state.get_data()
|
|
1102
|
+
recognized_text = data.get("voice_recognized_text")
|
|
1103
|
+
session_id = data.get("session_id")
|
|
1104
|
+
system_prompt = data.get("system_prompt")
|
|
1105
|
+
|
|
1106
|
+
if not recognized_text or not session_id:
|
|
1107
|
+
await callback.answer("❌ Ошибка: текст не найден", show_alert=True)
|
|
1108
|
+
return
|
|
1109
|
+
|
|
1110
|
+
# Удаляем сообщение с кнопками
|
|
1111
|
+
await callback.message.delete()
|
|
1112
|
+
|
|
1113
|
+
# Обрабатываем текст сразу без промежуточного сообщения
|
|
1114
|
+
await process_voice_message(
|
|
1115
|
+
callback.message, state, session_id, system_prompt, recognized_text
|
|
1116
|
+
)
|
|
1117
|
+
|
|
1118
|
+
# Возвращаем в обычное состояние
|
|
1119
|
+
await state.set_state(UserStates.waiting_for_message)
|
|
1120
|
+
await state.update_data(voice_recognized_text=None)
|
|
1121
|
+
|
|
1122
|
+
await callback.answer()
|
|
1123
|
+
|
|
1124
|
+
except Exception as e:
|
|
1125
|
+
logger.error(f"❌ Ошибка отправки голосового: {e}")
|
|
1126
|
+
await callback.answer("❌ Ошибка обработки", show_alert=True)
|
|
1127
|
+
|
|
1128
|
+
|
|
1129
|
+
@router.callback_query(F.data == "voice_edit")
|
|
1130
|
+
async def voice_edit_handler(callback: CallbackQuery, state: FSMContext):
|
|
1131
|
+
"""Обработчик кнопки 'Изменить текст' для голосового сообщения"""
|
|
1132
|
+
try:
|
|
1133
|
+
data = await state.get_data()
|
|
1134
|
+
recognized_text = data.get("voice_recognized_text")
|
|
1135
|
+
|
|
1136
|
+
if not recognized_text:
|
|
1137
|
+
await callback.answer("❌ Ошибка: текст не найден", show_alert=True)
|
|
1138
|
+
return
|
|
1139
|
+
|
|
1140
|
+
# Переводим в режим редактирования
|
|
1141
|
+
await state.set_state(UserStates.voice_editing)
|
|
1142
|
+
|
|
1143
|
+
# Показываем текст для редактирования (обычный формат)
|
|
1144
|
+
await callback.message.edit_text(
|
|
1145
|
+
f"✏️ Отредактируйте распознанный текст:\n\n"
|
|
1146
|
+
f"{recognized_text}\n\n"
|
|
1147
|
+
f"Напишите исправленный текст:"
|
|
1148
|
+
)
|
|
1149
|
+
|
|
1150
|
+
await callback.answer()
|
|
1151
|
+
|
|
1152
|
+
except Exception as e:
|
|
1153
|
+
logger.error(f"❌ Ошибка редактирования: {e}")
|
|
1154
|
+
await callback.answer("❌ Ошибка", show_alert=True)
|
|
1155
|
+
|
|
1156
|
+
|
|
1157
|
+
@router.callback_query(F.data == "voice_retry")
|
|
1158
|
+
async def voice_retry_handler(callback: CallbackQuery, state: FSMContext):
|
|
1159
|
+
"""Обработчик кнопки 'Надиктовать заново' для голосового сообщения"""
|
|
1160
|
+
try:
|
|
1161
|
+
# Удаляем сообщение с кнопками
|
|
1162
|
+
await callback.message.delete()
|
|
1163
|
+
|
|
1164
|
+
# Возвращаем в обычное состояние
|
|
1165
|
+
await state.set_state(UserStates.waiting_for_message)
|
|
1166
|
+
await state.update_data(voice_recognized_text=None)
|
|
1167
|
+
|
|
1168
|
+
# Просим отправить заново
|
|
1169
|
+
await callback.message.answer("🎤 Отправьте голосовое сообщение заново")
|
|
1170
|
+
|
|
1171
|
+
await callback.answer()
|
|
1172
|
+
|
|
1173
|
+
except Exception as e:
|
|
1174
|
+
logger.error(f"❌ Ошибка повтора: {e}")
|
|
1175
|
+
await callback.answer("❌ Ошибка", show_alert=True)
|
|
1176
|
+
|
|
1177
|
+
|
|
1178
|
+
@router.message(StateFilter(UserStates.voice_editing))
|
|
1179
|
+
async def voice_edit_text_handler(message: Message, state: FSMContext):
|
|
1180
|
+
"""Обработчик получения отредактированного текста"""
|
|
1181
|
+
try:
|
|
1182
|
+
edited_text = message.text.strip()
|
|
1183
|
+
|
|
1184
|
+
if not edited_text:
|
|
1185
|
+
await message.answer("⚠️ Текст не может быть пустым. Напишите текст:")
|
|
1186
|
+
return
|
|
1187
|
+
|
|
1188
|
+
# Получаем данные сессии
|
|
1189
|
+
data = await state.get_data()
|
|
1190
|
+
session_id = data.get("session_id")
|
|
1191
|
+
system_prompt = data.get("system_prompt")
|
|
1192
|
+
|
|
1193
|
+
if not session_id:
|
|
1194
|
+
await message.answer("❌ Сессия не найдена. Напишите /start")
|
|
1195
|
+
return
|
|
1196
|
+
|
|
1197
|
+
# Обрабатываем отредактированный текст сразу
|
|
1198
|
+
await process_voice_message(
|
|
1199
|
+
message, state, session_id, system_prompt, edited_text
|
|
1200
|
+
)
|
|
1201
|
+
|
|
1202
|
+
# Возвращаем в обычное состояние
|
|
1203
|
+
await state.set_state(UserStates.waiting_for_message)
|
|
1204
|
+
await state.update_data(voice_recognized_text=None)
|
|
1205
|
+
|
|
1206
|
+
except Exception as e:
|
|
1207
|
+
logger.error(f"❌ Ошибка обработки отредактированного текста: {e}")
|
|
1208
|
+
await message.answer(
|
|
1209
|
+
"❌ Ошибка обработки. Попробуйте еще раз или напишите /start"
|
|
1210
|
+
)
|
|
1211
|
+
|
|
1212
|
+
|
|
1213
|
+
@router.message()
|
|
1214
|
+
async def catch_all_handler(message: Message, state: FSMContext):
|
|
1215
|
+
"""Перехватчик всех необработанных сообщений"""
|
|
1216
|
+
admin_manager = get_global_var("admin_manager")
|
|
1217
|
+
from ..utils.debug_routing import debug_user_state
|
|
1218
|
+
|
|
1219
|
+
await debug_user_state(message, state, "CATCH_ALL")
|
|
1220
|
+
|
|
1221
|
+
current_state = await state.get_state()
|
|
1222
|
+
logger.warning(
|
|
1223
|
+
f"⚠️ НЕОБРАБОТАННОЕ СООБЩЕНИЕ от {message.from_user.id}: '{message.text}', состояние: {current_state}"
|
|
1224
|
+
)
|
|
1225
|
+
|
|
1226
|
+
# Проверяем, админ ли это
|
|
1227
|
+
if admin_manager.is_admin(message.from_user.id):
|
|
1228
|
+
logger.info("👑 Необработанное сообщение админа")
|
|
1229
|
+
await message.answer("Команда не распознана. Используйте /help для справки.")
|
|
1230
|
+
else:
|
|
1231
|
+
logger.info("👤 Необработанное сообщение пользователя")
|
|
1232
|
+
await message.answer("Не понимаю. Напишите /start для начала диалога.")
|
|
1233
|
+
|
|
1234
|
+
|
|
1235
|
+
async def process_user_message(
|
|
1236
|
+
message: Message, state: FSMContext, session_id: str, system_prompt: str
|
|
1237
|
+
):
|
|
1238
|
+
"""Общая функция для обработки сообщений пользователя"""
|
|
1239
|
+
supabase_client = get_global_var("supabase_client")
|
|
1240
|
+
openai_client = get_global_var("openai_client")
|
|
1241
|
+
config = get_global_var("config")
|
|
1242
|
+
bot = get_global_var("bot")
|
|
1243
|
+
prompt_loader = get_global_var("prompt_loader")
|
|
1244
|
+
message_hooks = get_global_var("message_hooks") or {}
|
|
1245
|
+
from datetime import datetime
|
|
1246
|
+
|
|
1247
|
+
import pytz # Добавляем импорт для работы с временными зонами
|
|
1248
|
+
|
|
1249
|
+
try:
|
|
1250
|
+
# ============ ХУК 1: ВАЛИДАЦИЯ СООБЩЕНИЯ ============
|
|
1251
|
+
validators = message_hooks.get("validators", [])
|
|
1252
|
+
for validator in validators:
|
|
1253
|
+
try:
|
|
1254
|
+
user_message = message.text
|
|
1255
|
+
message_obj = message
|
|
1256
|
+
|
|
1257
|
+
should_continue = await validator(user_message, message_obj)
|
|
1258
|
+
if not should_continue:
|
|
1259
|
+
logger.info(
|
|
1260
|
+
f"⛔ Валидатор '{validator.__name__}' прервал обработку"
|
|
1261
|
+
)
|
|
1262
|
+
return # Прерываем обработку
|
|
1263
|
+
except Exception as e:
|
|
1264
|
+
logger.error(f"❌ Ошибка в валидаторе '{validator.__name__}': {e}")
|
|
1265
|
+
|
|
1266
|
+
# Сохраняем сообщение пользователя
|
|
1267
|
+
await supabase_client.add_message(
|
|
1268
|
+
session_id=session_id,
|
|
1269
|
+
role="user",
|
|
1270
|
+
content=message.text,
|
|
1271
|
+
message_type="text",
|
|
1272
|
+
)
|
|
1273
|
+
logger.info("✅ Сообщение пользователя сохранено в БД")
|
|
1274
|
+
|
|
1275
|
+
# ДОБАВЛЯЕМ ПОЛУЧЕНИЕ ТЕКУЩЕГО ВРЕМЕНИ
|
|
1276
|
+
moscow_tz = pytz.timezone("Europe/Moscow")
|
|
1277
|
+
current_time = datetime.now(moscow_tz)
|
|
1278
|
+
time_info = current_time.strftime("%H:%M, %d.%m.%Y, %A")
|
|
1279
|
+
|
|
1280
|
+
# Базовый системный промпт с временем
|
|
1281
|
+
system_prompt_with_time = f"""
|
|
1282
|
+
{system_prompt}
|
|
1283
|
+
|
|
1284
|
+
ТЕКУЩЕЕ ВРЕМЯ: {time_info} (московское время)
|
|
1285
|
+
"""
|
|
1286
|
+
|
|
1287
|
+
# ============ ХУК 2: ОБОГАЩЕНИЕ ПРОМПТА ============
|
|
1288
|
+
prompt_enrichers = message_hooks.get("prompt_enrichers", [])
|
|
1289
|
+
for enricher in prompt_enrichers:
|
|
1290
|
+
try:
|
|
1291
|
+
system_prompt_with_time = await enricher(
|
|
1292
|
+
system_prompt_with_time, message.from_user.id
|
|
1293
|
+
)
|
|
1294
|
+
logger.info(f"✅ Промпт обогащен '{enricher.__name__}'")
|
|
1295
|
+
except Exception as e:
|
|
1296
|
+
logger.error(
|
|
1297
|
+
f"❌ Ошибка в обогатителе промпта '{enricher.__name__}': {e}"
|
|
1298
|
+
)
|
|
1299
|
+
|
|
1300
|
+
# Формируем контекст для OpenAI с обновленным системным промптом
|
|
1301
|
+
messages = [{"role": "system", "content": system_prompt_with_time}]
|
|
1302
|
+
|
|
1303
|
+
logger.info("🧾 Проверяем историю сообщений перед формированием контекста:")
|
|
1304
|
+
|
|
1305
|
+
# Получаем историю сообщений через MemoryManager
|
|
1306
|
+
memory_manager = get_global_var("memory_manager")
|
|
1307
|
+
if not memory_manager:
|
|
1308
|
+
logger.warning("⚠️ MemoryManager не найден в глобальных переменных — создаю новый экземпляр")
|
|
1309
|
+
from ..memory.memory_manager import MemoryManager
|
|
1310
|
+
|
|
1311
|
+
memory_manager = MemoryManager()
|
|
1312
|
+
|
|
1313
|
+
import sys
|
|
1314
|
+
|
|
1315
|
+
setattr(sys.modules[__name__], "memory_manager", memory_manager)
|
|
1316
|
+
logger.info("✅ MemoryManager инициализирован динамически")
|
|
1317
|
+
|
|
1318
|
+
logger.info("🧠 Загружаем историю из MemoryManager")
|
|
1319
|
+
memory_messages = await memory_manager.get_memory_messages(session_id)
|
|
1320
|
+
logger.info(
|
|
1321
|
+
f"📚 История MemoryManager: {len(memory_messages)} сообщений"
|
|
1322
|
+
)
|
|
1323
|
+
|
|
1324
|
+
for idx, msg in enumerate(memory_messages):
|
|
1325
|
+
role = msg.get("role", "unknown")
|
|
1326
|
+
content_preview = (msg.get("content", "") or "")[:120].replace("\n", " ")
|
|
1327
|
+
logger.info(f" #{idx + 1} [{role}] {content_preview}")
|
|
1328
|
+
|
|
1329
|
+
messages.extend(memory_messages)
|
|
1330
|
+
|
|
1331
|
+
# Добавляем финальные инструкции в конец контекста
|
|
1332
|
+
final_instructions = await prompt_loader.load_final_instructions()
|
|
1333
|
+
if final_instructions:
|
|
1334
|
+
messages.append({"role": "system", "content": final_instructions})
|
|
1335
|
+
logger.info("✅ Добавлены финальные инструкции")
|
|
1336
|
+
else:
|
|
1337
|
+
logger.warning("⚠️ Нет финальных инструкций")
|
|
1338
|
+
|
|
1339
|
+
# ============ ХУК 3: ОБОГАЩЕНИЕ КОНТЕКСТА ============
|
|
1340
|
+
context_enrichers = message_hooks.get("context_enrichers", [])
|
|
1341
|
+
for enricher in context_enrichers:
|
|
1342
|
+
try:
|
|
1343
|
+
# Вызываем хук с сообщениями в формате OpenAI
|
|
1344
|
+
messages = await enricher(messages)
|
|
1345
|
+
logger.info(f"✅ Контекст обогащен '{enricher.__name__}'")
|
|
1346
|
+
except Exception as e:
|
|
1347
|
+
logger.error(
|
|
1348
|
+
f"❌ Ошибка в обогатителе контекста '{enricher.__name__}': {e}"
|
|
1349
|
+
)
|
|
1350
|
+
|
|
1351
|
+
logger.info(
|
|
1352
|
+
f"📝 Контекст сформирован: {len(messages)} сообщений (включая время: {time_info})"
|
|
1353
|
+
)
|
|
1354
|
+
|
|
1355
|
+
await bot.send_chat_action(message.chat.id, "typing")
|
|
1356
|
+
|
|
1357
|
+
start_time = time.time()
|
|
1358
|
+
|
|
1359
|
+
messages = openai_messages_to_langchain(messages)
|
|
1360
|
+
ai_response = await openai_client.get_completion(messages)
|
|
1361
|
+
|
|
1362
|
+
processing_time = int((time.time() - start_time) * 1000)
|
|
1363
|
+
|
|
1364
|
+
logger.info(
|
|
1365
|
+
f"🤖 OpenAI ответил за {processing_time}мс, длина ответа: {len(ai_response) if ai_response else 0}"
|
|
1366
|
+
)
|
|
1367
|
+
|
|
1368
|
+
# ИСПРАВЛЕННАЯ ЛОГИКА: инициализируем все переменные заранее
|
|
1369
|
+
tokens_used = 0
|
|
1370
|
+
ai_metadata = {}
|
|
1371
|
+
response_text = ""
|
|
1372
|
+
|
|
1373
|
+
ai_response = ai_response['messages'][-1].content
|
|
1374
|
+
|
|
1375
|
+
logger.info(f"🤖 OpenAI ответил: {ai_response}")
|
|
1376
|
+
|
|
1377
|
+
# Проверяем ответ
|
|
1378
|
+
if not ai_response or not ai_response.strip():
|
|
1379
|
+
logger.warning("❌ OpenAI вернул пустой/пробельный ответ!")
|
|
1380
|
+
|
|
1381
|
+
# Проверяем, были ли использованы токены при пустом ответе
|
|
1382
|
+
if hasattr(openai_client, "last_completion_tokens"):
|
|
1383
|
+
logger.warning(
|
|
1384
|
+
f"⚠️ Токены использованы ({openai_client.last_completion_tokens}), но ответ пустой"
|
|
1385
|
+
)
|
|
1386
|
+
|
|
1387
|
+
# Устанавливаем fallback ответ
|
|
1388
|
+
fallback_message = "Извините, произошла техническая ошибка. Попробуйте переформулировать вопрос или напишите /start для перезапуска."
|
|
1389
|
+
ai_response = fallback_message
|
|
1390
|
+
response_text = fallback_message
|
|
1391
|
+
tokens_used = 0
|
|
1392
|
+
ai_metadata = {}
|
|
1393
|
+
|
|
1394
|
+
else:
|
|
1395
|
+
logger.info("📤 Сырой ответ OpenAI получен, обрабатываем...")
|
|
1396
|
+
|
|
1397
|
+
tokens_used = openai_client.estimate_tokens(ai_response)
|
|
1398
|
+
|
|
1399
|
+
# Парсим JSON метаданные
|
|
1400
|
+
response_text, ai_metadata = parse_ai_response(ai_response)
|
|
1401
|
+
|
|
1402
|
+
logger.info("🔍 После парсинга JSON:")
|
|
1403
|
+
logger.info(
|
|
1404
|
+
f" 📝 Текст ответа: {len(response_text)} символов: '{response_text[:100]}...'"
|
|
1405
|
+
)
|
|
1406
|
+
logger.info(f" 📊 Метаданные: {ai_metadata}")
|
|
1407
|
+
|
|
1408
|
+
# Более надежная проверка
|
|
1409
|
+
if not ai_metadata:
|
|
1410
|
+
logger.info("ℹ️ JSON не найден, используем исходный ответ")
|
|
1411
|
+
response_text = ai_response
|
|
1412
|
+
ai_metadata = {}
|
|
1413
|
+
elif not response_text.strip():
|
|
1414
|
+
logger.warning(
|
|
1415
|
+
"⚠️ JSON найден, но текст ответа пустой! Используем исходный ответ."
|
|
1416
|
+
)
|
|
1417
|
+
response_text = ai_response
|
|
1418
|
+
|
|
1419
|
+
logger.info(
|
|
1420
|
+
f"✅ Финальный текст для отправки: {len(response_text)} символов"
|
|
1421
|
+
)
|
|
1422
|
+
|
|
1423
|
+
# ============ ХУК 4: ОБРАБОТКА ОТВЕТА ============
|
|
1424
|
+
response_processors = message_hooks.get("response_processors", [])
|
|
1425
|
+
for processor in response_processors:
|
|
1426
|
+
try:
|
|
1427
|
+
response_text, ai_metadata = await processor(
|
|
1428
|
+
response_text, ai_metadata, message.from_user.id
|
|
1429
|
+
)
|
|
1430
|
+
logger.info(f"✅ Ответ обработан '{processor.__name__}'")
|
|
1431
|
+
except Exception as e:
|
|
1432
|
+
logger.error(
|
|
1433
|
+
f"❌ Ошибка в обработчике ответа '{processor.__name__}': {e}"
|
|
1434
|
+
)
|
|
1435
|
+
|
|
1436
|
+
# Проверяем response_text после обработки процессорами
|
|
1437
|
+
if not response_text or not response_text.strip():
|
|
1438
|
+
logger.warning("⚠️ response_text стал пустым после обработки процессорами, устанавливаем fallback")
|
|
1439
|
+
response_text = "Извините, произошла техническая ошибка. Попробуйте переформулировать вопрос или напишите /start для перезапуска."
|
|
1440
|
+
|
|
1441
|
+
# Обновляем этап сессии и качество лида
|
|
1442
|
+
if ai_metadata:
|
|
1443
|
+
logger.info("🔍 Анализ метаданных от ИИ:")
|
|
1444
|
+
|
|
1445
|
+
# Вывод информации об этапе
|
|
1446
|
+
stage = ai_metadata.get("этап")
|
|
1447
|
+
if stage:
|
|
1448
|
+
logger.info(f" 📈 Этап диалога: {stage}")
|
|
1449
|
+
|
|
1450
|
+
# Вывод информации о качестве лида
|
|
1451
|
+
quality = ai_metadata.get("качество")
|
|
1452
|
+
if quality is not None:
|
|
1453
|
+
quality_emoji = "⭐" * min(quality, 5) # Максимум 5 звезд
|
|
1454
|
+
logger.info(f" {quality_emoji} Качество лида: {quality}/10")
|
|
1455
|
+
|
|
1456
|
+
# Обновляем в базе данных
|
|
1457
|
+
if stage or quality is not None:
|
|
1458
|
+
await supabase_client.update_session_stage(session_id, stage, quality)
|
|
1459
|
+
logger.info(" ✅ Этап и качество обновлены в БД")
|
|
1460
|
+
|
|
1461
|
+
# Обрабатываем события
|
|
1462
|
+
events = ai_metadata.get("события", [])
|
|
1463
|
+
if events:
|
|
1464
|
+
logger.info(f"\n🔔 События в диалоге ({len(events)}):")
|
|
1465
|
+
for idx, event in enumerate(events, 1):
|
|
1466
|
+
event_type = event.get("тип", "неизвестно")
|
|
1467
|
+
event_info = event.get("инфо", "нет информации")
|
|
1468
|
+
|
|
1469
|
+
# Подбираем эмодзи для разных типов событий
|
|
1470
|
+
event_emoji = {
|
|
1471
|
+
"телефон": "📱",
|
|
1472
|
+
"email": "📧",
|
|
1473
|
+
"встреча": "📅",
|
|
1474
|
+
"заказ": "🛍️",
|
|
1475
|
+
"вопрос": "❓",
|
|
1476
|
+
"консультация": "💬",
|
|
1477
|
+
"жалоба": "⚠️",
|
|
1478
|
+
"отзыв": "💭",
|
|
1479
|
+
}.get(event_type.lower(), "📌")
|
|
1480
|
+
|
|
1481
|
+
logger.info(f" {idx}. {event_emoji} {event_type}: {event_info}")
|
|
1482
|
+
|
|
1483
|
+
# Обрабатываем события в системе
|
|
1484
|
+
should_send_response = await process_events(
|
|
1485
|
+
session_id, events, message.from_user.id
|
|
1486
|
+
)
|
|
1487
|
+
logger.warning(
|
|
1488
|
+
f" ✅ События обработаны, should_send_response = {should_send_response}"
|
|
1489
|
+
)
|
|
1490
|
+
|
|
1491
|
+
# Обрабатываем файлы и каталоги
|
|
1492
|
+
files_list = ai_metadata.get("файлы", [])
|
|
1493
|
+
directories_list = ai_metadata.get("каталоги", [])
|
|
1494
|
+
|
|
1495
|
+
# Форматируем информацию о файлах
|
|
1496
|
+
if files_list:
|
|
1497
|
+
logger.info("📎 Найденные файлы:")
|
|
1498
|
+
for idx, file in enumerate(files_list, 1):
|
|
1499
|
+
logger.info(f" {idx}. 📄 {file}")
|
|
1500
|
+
|
|
1501
|
+
# Форматируем информацию о каталогах
|
|
1502
|
+
if directories_list:
|
|
1503
|
+
logger.info("📂 Найденные каталоги:")
|
|
1504
|
+
for idx, directory in enumerate(directories_list, 1):
|
|
1505
|
+
logger.info(f" {idx}. 📁 {directory}")
|
|
1506
|
+
|
|
1507
|
+
# Добавляем информацию в текст ответа
|
|
1508
|
+
if files_list or directories_list:
|
|
1509
|
+
files_info = []
|
|
1510
|
+
if files_list:
|
|
1511
|
+
files_str = "\n".join(f"• {file}" for file in files_list)
|
|
1512
|
+
files_info.append(f"\n\n📎 Доступные файлы:\n{files_str}")
|
|
1513
|
+
|
|
1514
|
+
if directories_list:
|
|
1515
|
+
dirs_str = "\n".join(f"• {directory}" for directory in directories_list)
|
|
1516
|
+
files_info.append(f"\n\n📂 Доступные каталоги:\n{dirs_str}")
|
|
1517
|
+
|
|
1518
|
+
else:
|
|
1519
|
+
logger.info("📎 Файлы и каталоги не указаны")
|
|
1520
|
+
|
|
1521
|
+
# Сохраняем ответ ассистента с метаданными
|
|
1522
|
+
try:
|
|
1523
|
+
await supabase_client.add_message(
|
|
1524
|
+
session_id=session_id,
|
|
1525
|
+
role="assistant",
|
|
1526
|
+
content=response_text,
|
|
1527
|
+
message_type="text",
|
|
1528
|
+
tokens_used=tokens_used,
|
|
1529
|
+
processing_time_ms=processing_time,
|
|
1530
|
+
ai_metadata=ai_metadata,
|
|
1531
|
+
)
|
|
1532
|
+
logger.info("✅ Ответ ассистента сохранен в БД")
|
|
1533
|
+
except Exception as e:
|
|
1534
|
+
logger.error(f"❌ Ошибка сохранения ответа в БД: {e}")
|
|
1535
|
+
|
|
1536
|
+
# Определяем финальный ответ для пользователя
|
|
1537
|
+
if config.DEBUG_MODE:
|
|
1538
|
+
# В режиме отладки показываем полный ответ с JSON
|
|
1539
|
+
final_response = ai_response
|
|
1540
|
+
logger.info("🐛 Режим отладки: отправляем полный ответ с JSON")
|
|
1541
|
+
else:
|
|
1542
|
+
# В обычном режиме показываем только текст без JSON
|
|
1543
|
+
final_response = response_text
|
|
1544
|
+
logger.info("👤 Обычный режим: отправляем очищенный текст")
|
|
1545
|
+
|
|
1546
|
+
# Проверяем, что есть что отправлять
|
|
1547
|
+
if not final_response or not final_response.strip():
|
|
1548
|
+
logger.error("❌ КРИТИЧЕСКАЯ ОШИБКА: Финальный ответ пуст!")
|
|
1549
|
+
final_response = "Извините, произошла ошибка при формировании ответа. Попробуйте еще раз."
|
|
1550
|
+
|
|
1551
|
+
logger.info(f"📱 Отправляем пользователю: {len(final_response)} символов")
|
|
1552
|
+
|
|
1553
|
+
# ============ ПРОВЕРКА: НУЖНО ЛИ ОТПРАВЛЯТЬ СООБЩЕНИЕ ОТ ИИ ============
|
|
1554
|
+
# Проверяем флаг из событий (если события запретили отправку)
|
|
1555
|
+
logger.warning(
|
|
1556
|
+
f"🔍 Проверка should_send_response: exists={('should_send_response' in locals())}, value={locals().get('should_send_response', 'NOT_SET')}"
|
|
1557
|
+
)
|
|
1558
|
+
|
|
1559
|
+
if "should_send_response" in locals() and not should_send_response:
|
|
1560
|
+
logger.warning(
|
|
1561
|
+
"🔇🔇🔇 СОБЫТИЯ ЗАПРЕТИЛИ ОТПРАВКУ СООБЩЕНИЯ ОТ ИИ, ПРОПУСКАЕМ ОТПРАВКУ 🔇🔇🔇"
|
|
1562
|
+
)
|
|
1563
|
+
return
|
|
1564
|
+
|
|
1565
|
+
# ============ ХУК 5: ФИЛЬТРЫ ОТПРАВКИ ============
|
|
1566
|
+
send_filters = message_hooks.get("send_filters", [])
|
|
1567
|
+
for filter_func in send_filters:
|
|
1568
|
+
try:
|
|
1569
|
+
should_send = await filter_func(message.from_user.id)
|
|
1570
|
+
if should_send:
|
|
1571
|
+
# True = блокируем (для совместимости с should_block_ai_response)
|
|
1572
|
+
logger.info(
|
|
1573
|
+
f"⛔ Фильтр '{filter_func.__name__}' заблокировал отправку (вернул True)"
|
|
1574
|
+
)
|
|
1575
|
+
return # Не отправляем
|
|
1576
|
+
except Exception as e:
|
|
1577
|
+
logger.error(
|
|
1578
|
+
f"❌ Ошибка в фильтре отправки '{filter_func.__name__}': {e}"
|
|
1579
|
+
)
|
|
1580
|
+
|
|
1581
|
+
# ============ ИСПРАВЛЕНИЕ HTML РАЗМЕТКИ (только для HTML parse_mode) ============
|
|
1582
|
+
parse_mode = config.MESSAGE_PARSE_MODE if config.MESSAGE_PARSE_MODE != "None" else None
|
|
1583
|
+
if parse_mode and parse_mode.upper() == "HTML":
|
|
1584
|
+
logger.info("🔧 Проверяем и исправляем HTML разметку перед отправкой")
|
|
1585
|
+
final_response = fix_html_markup(final_response)
|
|
1586
|
+
logger.info("✅ HTML разметка исправлена")
|
|
1587
|
+
|
|
1588
|
+
# ============ ОТПРАВКА СООБЩЕНИЯ (с автоматическим разбиением на части) ============
|
|
1589
|
+
try:
|
|
1590
|
+
logger.info(f"📤 Вызываем send_message_in_parts с текстом длиной {len(final_response)} символов")
|
|
1591
|
+
parts_sent = await send_message_in_parts(
|
|
1592
|
+
message,
|
|
1593
|
+
final_response,
|
|
1594
|
+
files_list=files_list,
|
|
1595
|
+
directories_list=directories_list,
|
|
1596
|
+
)
|
|
1597
|
+
if parts_sent == 0:
|
|
1598
|
+
logger.warning("⚠️ send_message_in_parts вернула 0, сообщение не было отправлено")
|
|
1599
|
+
except Exception as e:
|
|
1600
|
+
logger.error(f"❌ ОШИБКА ОТПРАВКИ СООБЩЕНИЯ: {e}", exc_info=True)
|
|
1601
|
+
# Пытаемся отправить простое сообщение об ошибке
|
|
1602
|
+
try:
|
|
1603
|
+
await message.answer(
|
|
1604
|
+
"Произошла ошибка при отправке ответа. Попробуйте еще раз."
|
|
1605
|
+
)
|
|
1606
|
+
except Exception as e2:
|
|
1607
|
+
logger.error(f"❌ Не удалось отправить даже сообщение об ошибке: {e2}")
|
|
1608
|
+
|
|
1609
|
+
except Exception as e:
|
|
1610
|
+
logger.error(f"❌ КРИТИЧЕСКАЯ ОШИБКА в process_user_message: {e}")
|
|
1611
|
+
logger.exception("Полный стек ошибки:")
|
|
1612
|
+
try:
|
|
1613
|
+
await message.answer(
|
|
1614
|
+
"Произошла критическая ошибка. Попробуйте написать /start для перезапуска."
|
|
1615
|
+
)
|
|
1616
|
+
except Exception:
|
|
1617
|
+
logger.error("❌ Не удалось отправить сообщение об критической ошибке", exc_info=True)
|
|
1618
|
+
|
|
1619
|
+
|
|
1620
|
+
async def process_voice_message(
|
|
1621
|
+
message: Message,
|
|
1622
|
+
state: FSMContext,
|
|
1623
|
+
session_id: str,
|
|
1624
|
+
system_prompt: str,
|
|
1625
|
+
recognized_text: str,
|
|
1626
|
+
):
|
|
1627
|
+
"""Обработка распознанного голосового сообщения"""
|
|
1628
|
+
supabase_client = get_global_var("supabase_client")
|
|
1629
|
+
openai_client = get_global_var("openai_client")
|
|
1630
|
+
config = get_global_var("config")
|
|
1631
|
+
bot = get_global_var("bot")
|
|
1632
|
+
prompt_loader = get_global_var("prompt_loader")
|
|
1633
|
+
message_hooks = get_global_var("message_hooks") or {}
|
|
1634
|
+
from datetime import datetime
|
|
1635
|
+
|
|
1636
|
+
import pytz
|
|
1637
|
+
import time
|
|
1638
|
+
|
|
1639
|
+
try:
|
|
1640
|
+
# Сохраняем распознанное сообщение как текст пользователя
|
|
1641
|
+
await supabase_client.add_message(
|
|
1642
|
+
session_id=session_id,
|
|
1643
|
+
role="user",
|
|
1644
|
+
content=recognized_text,
|
|
1645
|
+
message_type="text",
|
|
1646
|
+
metadata={
|
|
1647
|
+
"original_type": "voice",
|
|
1648
|
+
"duration": message.voice.duration if message.voice else 0,
|
|
1649
|
+
},
|
|
1650
|
+
)
|
|
1651
|
+
logger.info("✅ Распознанное сообщение сохранено в БД")
|
|
1652
|
+
|
|
1653
|
+
# ============ ХУК 1: ВАЛИДАЦИЯ СООБЩЕНИЯ ============
|
|
1654
|
+
validators = message_hooks.get("validators", [])
|
|
1655
|
+
for validator in validators:
|
|
1656
|
+
try:
|
|
1657
|
+
should_continue = await validator(recognized_text, message)
|
|
1658
|
+
if not should_continue:
|
|
1659
|
+
logger.info(
|
|
1660
|
+
f"⛔ Валидатор '{validator.__name__}' прервал обработку"
|
|
1661
|
+
)
|
|
1662
|
+
return
|
|
1663
|
+
except Exception as e:
|
|
1664
|
+
logger.error(f"❌ Ошибка в валидаторе '{validator.__name__}': {e}")
|
|
1665
|
+
|
|
1666
|
+
# ДОБАВЛЯЕМ ПОЛУЧЕНИЕ ТЕКУЩЕГО ВРЕМЕНИ
|
|
1667
|
+
moscow_tz = pytz.timezone("Europe/Moscow")
|
|
1668
|
+
current_time = datetime.now(moscow_tz)
|
|
1669
|
+
time_info = current_time.strftime("%H:%M, %d.%m.%Y, %A")
|
|
1670
|
+
|
|
1671
|
+
# Базовый системный промпт с временем
|
|
1672
|
+
system_prompt_with_time = f"""
|
|
1673
|
+
{system_prompt}
|
|
1674
|
+
|
|
1675
|
+
ТЕКУЩЕЕ ВРЕМЯ: {time_info} (московское время)
|
|
1676
|
+
"""
|
|
1677
|
+
|
|
1678
|
+
# ============ ХУК 2: ОБОГАЩЕНИЕ ПРОМПТА ============
|
|
1679
|
+
prompt_enrichers = message_hooks.get("prompt_enrichers", [])
|
|
1680
|
+
for enricher in prompt_enrichers:
|
|
1681
|
+
try:
|
|
1682
|
+
system_prompt_with_time = await enricher(
|
|
1683
|
+
system_prompt_with_time, message.from_user.id
|
|
1684
|
+
)
|
|
1685
|
+
logger.info(f"✅ Промпт обогащен '{enricher.__name__}'")
|
|
1686
|
+
except Exception as e:
|
|
1687
|
+
logger.error(
|
|
1688
|
+
f"❌ Ошибка в обогатителе промпта '{enricher.__name__}': {e}"
|
|
1689
|
+
)
|
|
1690
|
+
|
|
1691
|
+
# Формируем контекст для OpenAI с обновленным системным промптом
|
|
1692
|
+
messages = [{"role": "system", "content": system_prompt_with_time}]
|
|
1693
|
+
|
|
1694
|
+
logger.info("🧾 Проверяем историю сообщений перед формированием контекста:")
|
|
1695
|
+
|
|
1696
|
+
# Получаем историю сообщений через MemoryManager
|
|
1697
|
+
memory_manager = get_global_var("memory_manager")
|
|
1698
|
+
if not memory_manager:
|
|
1699
|
+
logger.warning("⚠️ MemoryManager не найден в глобальных переменных — создаю новый экземпляр")
|
|
1700
|
+
from ..memory.memory_manager import MemoryManager
|
|
1701
|
+
|
|
1702
|
+
memory_manager = MemoryManager()
|
|
1703
|
+
|
|
1704
|
+
import sys
|
|
1705
|
+
|
|
1706
|
+
setattr(sys.modules[__name__], "memory_manager", memory_manager)
|
|
1707
|
+
logger.info("✅ MemoryManager инициализирован динамически")
|
|
1708
|
+
|
|
1709
|
+
logger.info("🧠 Загружаем историю из MemoryManager")
|
|
1710
|
+
memory_messages = await memory_manager.get_memory_messages(session_id)
|
|
1711
|
+
logger.info(
|
|
1712
|
+
f"📚 История MemoryManager: {len(memory_messages)} сообщений"
|
|
1713
|
+
)
|
|
1714
|
+
|
|
1715
|
+
for idx, msg in enumerate(memory_messages):
|
|
1716
|
+
role = msg.get("role", "unknown")
|
|
1717
|
+
content_preview = (msg.get("content", "") or "")[:120].replace("\n", " ")
|
|
1718
|
+
logger.info(f" #{idx + 1} [{role}] {content_preview}")
|
|
1719
|
+
|
|
1720
|
+
messages.extend(memory_messages)
|
|
1721
|
+
|
|
1722
|
+
# Добавляем финальные инструкции в конец контекста
|
|
1723
|
+
final_instructions = await prompt_loader.load_final_instructions()
|
|
1724
|
+
if final_instructions:
|
|
1725
|
+
messages.append({"role": "system", "content": final_instructions})
|
|
1726
|
+
logger.info("✅ Добавлены финальные инструкции")
|
|
1727
|
+
else:
|
|
1728
|
+
logger.warning("⚠️ Нет финальных инструкций")
|
|
1729
|
+
|
|
1730
|
+
# ============ ХУК 3: ОБОГАЩЕНИЕ КОНТЕКСТА ============
|
|
1731
|
+
context_enrichers = message_hooks.get("context_enrichers", [])
|
|
1732
|
+
for enricher in context_enrichers:
|
|
1733
|
+
try:
|
|
1734
|
+
# Вызываем хук с сообщениями в формате OpenAI
|
|
1735
|
+
messages = await enricher(messages)
|
|
1736
|
+
logger.info(f"✅ Контекст обогащен '{enricher.__name__}'")
|
|
1737
|
+
except Exception as e:
|
|
1738
|
+
logger.error(
|
|
1739
|
+
f"❌ Ошибка в обогатителе контекста '{enricher.__name__}': {e}"
|
|
1740
|
+
)
|
|
1741
|
+
|
|
1742
|
+
logger.info(
|
|
1743
|
+
f"📝 Контекст сформирован: {len(messages)} сообщений (включая время: {time_info})"
|
|
1744
|
+
)
|
|
1745
|
+
|
|
1746
|
+
await bot.send_chat_action(message.chat.id, "typing")
|
|
1747
|
+
|
|
1748
|
+
start_time = time.time()
|
|
1749
|
+
|
|
1750
|
+
messages = openai_messages_to_langchain(messages)
|
|
1751
|
+
ai_response = await openai_client.get_completion(messages)
|
|
1752
|
+
|
|
1753
|
+
processing_time = int((time.time() - start_time) * 1000)
|
|
1754
|
+
|
|
1755
|
+
logger.info(
|
|
1756
|
+
f"🤖 OpenAI ответил за {processing_time}мс, длина ответа: {len(ai_response) if ai_response else 0}"
|
|
1757
|
+
)
|
|
1758
|
+
|
|
1759
|
+
# ИСПРАВЛЕННАЯ ЛОГИКА: инициализируем все переменные заранее
|
|
1760
|
+
tokens_used = 0
|
|
1761
|
+
ai_metadata = {}
|
|
1762
|
+
response_text = ""
|
|
1763
|
+
|
|
1764
|
+
ai_response = ai_response['messages'][-1].content
|
|
1765
|
+
|
|
1766
|
+
logger.info(f"🤖 OpenAI ответил: {ai_response}")
|
|
1767
|
+
|
|
1768
|
+
# Проверяем ответ
|
|
1769
|
+
if not ai_response or not ai_response.strip():
|
|
1770
|
+
logger.warning("❌ OpenAI вернул пустой/пробельный ответ!")
|
|
1771
|
+
|
|
1772
|
+
# Проверяем, были ли использованы токены при пустом ответе
|
|
1773
|
+
if hasattr(openai_client, "last_completion_tokens"):
|
|
1774
|
+
logger.warning(
|
|
1775
|
+
f"⚠️ Токены использованы ({openai_client.last_completion_tokens}), но ответ пустой"
|
|
1776
|
+
)
|
|
1777
|
+
|
|
1778
|
+
# Устанавливаем fallback ответ
|
|
1779
|
+
fallback_message = "Извините, произошла техническая ошибка. Попробуйте переформулировать вопрос или напишите /start для перезапуска."
|
|
1780
|
+
ai_response = fallback_message
|
|
1781
|
+
response_text = fallback_message
|
|
1782
|
+
tokens_used = 0
|
|
1783
|
+
ai_metadata = {}
|
|
1784
|
+
|
|
1785
|
+
else:
|
|
1786
|
+
logger.info("📤 Сырой ответ OpenAI получен, обрабатываем...")
|
|
1787
|
+
|
|
1788
|
+
tokens_used = openai_client.estimate_tokens(ai_response)
|
|
1789
|
+
|
|
1790
|
+
# Парсим JSON метаданные
|
|
1791
|
+
response_text, ai_metadata = parse_ai_response(ai_response)
|
|
1792
|
+
|
|
1793
|
+
logger.info("🔍 После парсинга JSON:")
|
|
1794
|
+
logger.info(
|
|
1795
|
+
f" 📝 Текст ответа: {len(response_text)} символов: '{response_text[:100]}...'"
|
|
1796
|
+
)
|
|
1797
|
+
logger.info(f" 📊 Метаданные: {ai_metadata}")
|
|
1798
|
+
|
|
1799
|
+
# Более надежная проверка
|
|
1800
|
+
if not ai_metadata:
|
|
1801
|
+
logger.info("ℹ️ JSON не найден, используем исходный ответ")
|
|
1802
|
+
response_text = ai_response
|
|
1803
|
+
ai_metadata = {}
|
|
1804
|
+
elif not response_text.strip():
|
|
1805
|
+
logger.warning(
|
|
1806
|
+
"⚠️ JSON найден, но текст ответа пустой! Используем исходный ответ."
|
|
1807
|
+
)
|
|
1808
|
+
response_text = ai_response
|
|
1809
|
+
|
|
1810
|
+
logger.info(
|
|
1811
|
+
f"✅ Финальный текст для отправки: {len(response_text)} символов"
|
|
1812
|
+
)
|
|
1813
|
+
|
|
1814
|
+
# ============ ХУК 4: ОБРАБОТКА ОТВЕТА ============
|
|
1815
|
+
response_processors = message_hooks.get("response_processors", [])
|
|
1816
|
+
for processor in response_processors:
|
|
1817
|
+
try:
|
|
1818
|
+
response_text, ai_metadata = await processor(
|
|
1819
|
+
response_text, ai_metadata, message.from_user.id
|
|
1820
|
+
)
|
|
1821
|
+
logger.info(f"✅ Ответ обработан '{processor.__name__}'")
|
|
1822
|
+
except Exception as e:
|
|
1823
|
+
logger.error(
|
|
1824
|
+
f"❌ Ошибка в обработчике ответа '{processor.__name__}': {e}"
|
|
1825
|
+
)
|
|
1826
|
+
|
|
1827
|
+
# Проверяем response_text после обработки процессорами
|
|
1828
|
+
if not response_text or not response_text.strip():
|
|
1829
|
+
logger.warning("⚠️ response_text стал пустым после обработки процессорами, устанавливаем fallback")
|
|
1830
|
+
response_text = "Извините, произошла техническая ошибка. Попробуйте переформулировать вопрос или напишите /start для перезапуска."
|
|
1831
|
+
|
|
1832
|
+
# Обновляем этап сессии и качество лида
|
|
1833
|
+
if ai_metadata:
|
|
1834
|
+
logger.info("🔍 Анализ метаданных от ИИ:")
|
|
1835
|
+
|
|
1836
|
+
# Вывод информации об этапе
|
|
1837
|
+
stage = ai_metadata.get("этап")
|
|
1838
|
+
if stage:
|
|
1839
|
+
logger.info(f" 📈 Этап диалога: {stage}")
|
|
1840
|
+
|
|
1841
|
+
# Вывод информации о качестве лида
|
|
1842
|
+
quality = ai_metadata.get("качество")
|
|
1843
|
+
if quality is not None:
|
|
1844
|
+
quality_emoji = "⭐" * min(quality, 5) # Максимум 5 звезд
|
|
1845
|
+
logger.info(f" {quality_emoji} Качество лида: {quality}/10")
|
|
1846
|
+
|
|
1847
|
+
# Обновляем в базе данных
|
|
1848
|
+
if stage or quality is not None:
|
|
1849
|
+
await supabase_client.update_session_stage(session_id, stage, quality)
|
|
1850
|
+
logger.info(" ✅ Этап и качество обновлены в БД")
|
|
1851
|
+
|
|
1852
|
+
# Обрабатываем события
|
|
1853
|
+
events = ai_metadata.get("события", [])
|
|
1854
|
+
if events:
|
|
1855
|
+
logger.info(f"\n🔔 События в диалоге ({len(events)}):")
|
|
1856
|
+
for idx, event in enumerate(events, 1):
|
|
1857
|
+
event_type = event.get("тип", "неизвестно")
|
|
1858
|
+
event_info = event.get("инфо", "нет информации")
|
|
1859
|
+
|
|
1860
|
+
# Подбираем эмодзи для разных типов событий
|
|
1861
|
+
event_emoji = {
|
|
1862
|
+
"телефон": "📱",
|
|
1863
|
+
"email": "📧",
|
|
1864
|
+
"встреча": "📅",
|
|
1865
|
+
"заказ": "🛍️",
|
|
1866
|
+
"вопрос": "❓",
|
|
1867
|
+
"консультация": "💬",
|
|
1868
|
+
"жалоба": "⚠️",
|
|
1869
|
+
"отзыв": "💭",
|
|
1870
|
+
}.get(event_type.lower(), "📌")
|
|
1871
|
+
|
|
1872
|
+
logger.info(f" {idx}. {event_emoji} {event_type}: {event_info}")
|
|
1873
|
+
|
|
1874
|
+
# Обрабатываем события в системе
|
|
1875
|
+
should_send_response = await process_events(
|
|
1876
|
+
session_id, events, message.from_user.id
|
|
1877
|
+
)
|
|
1878
|
+
logger.warning(
|
|
1879
|
+
f" ✅ События обработаны, should_send_response = {should_send_response}"
|
|
1880
|
+
)
|
|
1881
|
+
|
|
1882
|
+
# Обрабатываем файлы и каталоги
|
|
1883
|
+
files_list = ai_metadata.get("файлы", [])
|
|
1884
|
+
directories_list = ai_metadata.get("каталоги", [])
|
|
1885
|
+
|
|
1886
|
+
# Форматируем информацию о файлах
|
|
1887
|
+
if files_list:
|
|
1888
|
+
logger.info("📎 Найденные файлы:")
|
|
1889
|
+
for idx, file in enumerate(files_list, 1):
|
|
1890
|
+
logger.info(f" {idx}. 📄 {file}")
|
|
1891
|
+
|
|
1892
|
+
# Форматируем информацию о каталогах
|
|
1893
|
+
if directories_list:
|
|
1894
|
+
logger.info("📂 Найденные каталоги:")
|
|
1895
|
+
for idx, directory in enumerate(directories_list, 1):
|
|
1896
|
+
logger.info(f" {idx}. 📁 {directory}")
|
|
1897
|
+
|
|
1898
|
+
# Добавляем информацию в текст ответа
|
|
1899
|
+
if files_list or directories_list:
|
|
1900
|
+
files_info = []
|
|
1901
|
+
if files_list:
|
|
1902
|
+
files_str = "\n".join(f"• {file}" for file in files_list)
|
|
1903
|
+
files_info.append(f"\n\n📎 Доступные файлы:\n{files_str}")
|
|
1904
|
+
|
|
1905
|
+
if directories_list:
|
|
1906
|
+
dirs_str = "\n".join(f"• {directory}" for directory in directories_list)
|
|
1907
|
+
files_info.append(f"\n\n📂 Доступные каталоги:\n{dirs_str}")
|
|
1908
|
+
|
|
1909
|
+
else:
|
|
1910
|
+
logger.info("📎 Файлы и каталоги не указаны")
|
|
1911
|
+
|
|
1912
|
+
# Сохраняем ответ ассистента с метаданными
|
|
1913
|
+
try:
|
|
1914
|
+
await supabase_client.add_message(
|
|
1915
|
+
session_id=session_id,
|
|
1916
|
+
role="assistant",
|
|
1917
|
+
content=response_text,
|
|
1918
|
+
message_type="text",
|
|
1919
|
+
tokens_used=tokens_used,
|
|
1920
|
+
processing_time_ms=processing_time,
|
|
1921
|
+
ai_metadata=ai_metadata,
|
|
1922
|
+
)
|
|
1923
|
+
logger.info("✅ Ответ ассистента сохранен в БД")
|
|
1924
|
+
except Exception as e:
|
|
1925
|
+
logger.error(f"❌ Ошибка сохранения ответа в БД: {e}")
|
|
1926
|
+
|
|
1927
|
+
# Определяем финальный ответ для пользователя
|
|
1928
|
+
if config.DEBUG_MODE:
|
|
1929
|
+
# В режиме отладки показываем полный ответ с JSON
|
|
1930
|
+
final_response = ai_response
|
|
1931
|
+
logger.info("🐛 Режим отладки: отправляем полный ответ с JSON")
|
|
1932
|
+
else:
|
|
1933
|
+
# В обычном режиме показываем только текст без JSON
|
|
1934
|
+
final_response = response_text
|
|
1935
|
+
logger.info("👤 Обычный режим: отправляем очищенный текст")
|
|
1936
|
+
|
|
1937
|
+
# Проверяем, что есть что отправлять
|
|
1938
|
+
if not final_response or not final_response.strip():
|
|
1939
|
+
logger.error("❌ КРИТИЧЕСКАЯ ОШИБКА: Финальный ответ пуст!")
|
|
1940
|
+
final_response = "Извините, произошла ошибка при формировании ответа. Попробуйте еще раз."
|
|
1941
|
+
|
|
1942
|
+
logger.info(f"📱 Отправляем пользователю: {len(final_response)} символов")
|
|
1943
|
+
|
|
1944
|
+
# ============ ПРОВЕРКА: НУЖНО ЛИ ОТПРАВЛЯТЬ СООБЩЕНИЕ ОТ ИИ ============
|
|
1945
|
+
# Проверяем флаг из событий (если события запретили отправку)
|
|
1946
|
+
logger.warning(
|
|
1947
|
+
f"🔍 Проверка should_send_response: exists={('should_send_response' in locals())}, value={locals().get('should_send_response', 'NOT_SET')}"
|
|
1948
|
+
)
|
|
1949
|
+
|
|
1950
|
+
if "should_send_response" in locals() and not should_send_response:
|
|
1951
|
+
logger.warning(
|
|
1952
|
+
"🔇🔇🔇 СОБЫТИЯ ЗАПРЕТИЛИ ОТПРАВКУ СООБЩЕНИЯ ОТ ИИ, ПРОПУСКАЕМ ОТПРАВКУ 🔇🔇🔇"
|
|
1953
|
+
)
|
|
1954
|
+
return
|
|
1955
|
+
|
|
1956
|
+
# ============ ХУК 5: ФИЛЬТРЫ ОТПРАВКИ ============
|
|
1957
|
+
send_filters = message_hooks.get("send_filters", [])
|
|
1958
|
+
for filter_func in send_filters:
|
|
1959
|
+
try:
|
|
1960
|
+
should_send = await filter_func(message.from_user.id)
|
|
1961
|
+
if should_send:
|
|
1962
|
+
# True = блокируем (для совместимости с should_block_ai_response)
|
|
1963
|
+
logger.info(
|
|
1964
|
+
f"⛔ Фильтр '{filter_func.__name__}' заблокировал отправку (вернул True)"
|
|
1965
|
+
)
|
|
1966
|
+
return # Не отправляем
|
|
1967
|
+
except Exception as e:
|
|
1968
|
+
logger.error(
|
|
1969
|
+
f"❌ Ошибка в фильтре отправки '{filter_func.__name__}': {e}"
|
|
1970
|
+
)
|
|
1971
|
+
|
|
1972
|
+
# ============ ИСПРАВЛЕНИЕ HTML РАЗМЕТКИ (только для HTML parse_mode) ============
|
|
1973
|
+
parse_mode = config.MESSAGE_PARSE_MODE if config.MESSAGE_PARSE_MODE != "None" else None
|
|
1974
|
+
if parse_mode and parse_mode.upper() == "HTML":
|
|
1975
|
+
logger.info("🔧 Проверяем и исправляем HTML разметку перед отправкой")
|
|
1976
|
+
final_response = fix_html_markup(final_response)
|
|
1977
|
+
logger.info("✅ HTML разметка исправлена")
|
|
1978
|
+
|
|
1979
|
+
# ============ ОТПРАВКА СООБЩЕНИЯ (с автоматическим разбиением на части) ============
|
|
1980
|
+
try:
|
|
1981
|
+
logger.info(f"📤 Вызываем send_message_in_parts с текстом длиной {len(final_response)} символов")
|
|
1982
|
+
logger.info(f"📤 Первые 200 символов текста: '{final_response[:200]}'")
|
|
1983
|
+
parts_sent = await send_message_in_parts(
|
|
1984
|
+
message,
|
|
1985
|
+
final_response,
|
|
1986
|
+
files_list=files_list,
|
|
1987
|
+
directories_list=directories_list,
|
|
1988
|
+
)
|
|
1989
|
+
logger.info(f"✅ send_message_in_parts вернула {parts_sent} частей")
|
|
1990
|
+
if parts_sent == 0:
|
|
1991
|
+
logger.warning("⚠️ send_message_in_parts вернула 0, сообщение не было отправлено - отправляем fallback")
|
|
1992
|
+
await message.answer(
|
|
1993
|
+
"Извините, произошла ошибка при формировании ответа. Попробуйте еще раз."
|
|
1994
|
+
)
|
|
1995
|
+
except Exception as e:
|
|
1996
|
+
logger.error(f"❌ ОШИБКА ОТПРАВКИ СООБЩЕНИЯ: {e}", exc_info=True)
|
|
1997
|
+
# Пытаемся отправить простое сообщение об ошибке
|
|
1998
|
+
try:
|
|
1999
|
+
await message.answer(
|
|
2000
|
+
"Произошла ошибка при отправке ответа. Попробуйте еще раз."
|
|
2001
|
+
)
|
|
2002
|
+
except Exception as e2:
|
|
2003
|
+
logger.error(f"❌ Не удалось отправить даже сообщение об ошибке: {e2}")
|
|
2004
|
+
|
|
2005
|
+
except Exception as e:
|
|
2006
|
+
logger.error(f"❌ КРИТИЧЕСКАЯ ОШИБКА в process_voice_message: {e}")
|
|
2007
|
+
logger.exception("Полный стек ошибки:")
|
|
2008
|
+
try:
|
|
2009
|
+
await message.answer(
|
|
2010
|
+
"Произошла критическая ошибка. Попробуйте написать /start для перезапуска."
|
|
2011
|
+
)
|
|
2012
|
+
except Exception:
|
|
2013
|
+
logger.error("❌ Не удалось отправить сообщение об критической ошибке", exc_info=True)
|