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.
Files changed (73) hide show
  1. smart_bot_factory/__init__.py +3 -0
  2. smart_bot_factory/admin/__init__.py +18 -0
  3. smart_bot_factory/admin/admin_events.py +1223 -0
  4. smart_bot_factory/admin/admin_logic.py +553 -0
  5. smart_bot_factory/admin/admin_manager.py +156 -0
  6. smart_bot_factory/admin/admin_tester.py +157 -0
  7. smart_bot_factory/admin/timeout_checker.py +547 -0
  8. smart_bot_factory/aiogram_calendar/__init__.py +14 -0
  9. smart_bot_factory/aiogram_calendar/common.py +64 -0
  10. smart_bot_factory/aiogram_calendar/dialog_calendar.py +259 -0
  11. smart_bot_factory/aiogram_calendar/schemas.py +99 -0
  12. smart_bot_factory/aiogram_calendar/simple_calendar.py +224 -0
  13. smart_bot_factory/analytics/analytics_manager.py +414 -0
  14. smart_bot_factory/cli.py +806 -0
  15. smart_bot_factory/config.py +258 -0
  16. smart_bot_factory/configs/growthmed-october-24/prompts/1sales_context.txt +16 -0
  17. smart_bot_factory/configs/growthmed-october-24/prompts/2product_info.txt +582 -0
  18. smart_bot_factory/configs/growthmed-october-24/prompts/3objection_handling.txt +66 -0
  19. smart_bot_factory/configs/growthmed-october-24/prompts/final_instructions.txt +212 -0
  20. smart_bot_factory/configs/growthmed-october-24/prompts/help_message.txt +28 -0
  21. smart_bot_factory/configs/growthmed-october-24/prompts/welcome_message.txt +8 -0
  22. smart_bot_factory/configs/growthmed-october-24/reports/test_20250924_064229.txt +818 -0
  23. smart_bot_factory/configs/growthmed-october-24/reports/test_20250924_064335.txt +32 -0
  24. smart_bot_factory/configs/growthmed-october-24/reports/test_20250924_064638.txt +35 -0
  25. smart_bot_factory/configs/growthmed-october-24/tests/quick_scenarios.yaml +133 -0
  26. smart_bot_factory/configs/growthmed-october-24/tests/realistic_scenarios.yaml +108 -0
  27. smart_bot_factory/configs/growthmed-october-24/tests/scenario_examples.yaml +46 -0
  28. smart_bot_factory/configs/growthmed-october-24/welcome_file/welcome_file_msg.txt +16 -0
  29. 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
  30. smart_bot_factory/core/bot_utils.py +1108 -0
  31. smart_bot_factory/core/conversation_manager.py +653 -0
  32. smart_bot_factory/core/decorators.py +2464 -0
  33. smart_bot_factory/core/message_sender.py +729 -0
  34. smart_bot_factory/core/router.py +347 -0
  35. smart_bot_factory/core/router_manager.py +218 -0
  36. smart_bot_factory/core/states.py +27 -0
  37. smart_bot_factory/creation/__init__.py +7 -0
  38. smart_bot_factory/creation/bot_builder.py +1093 -0
  39. smart_bot_factory/creation/bot_testing.py +1122 -0
  40. smart_bot_factory/dashboard/__init__.py +3 -0
  41. smart_bot_factory/event/__init__.py +7 -0
  42. smart_bot_factory/handlers/handlers.py +2013 -0
  43. smart_bot_factory/integrations/langchain_openai.py +542 -0
  44. smart_bot_factory/integrations/openai_client.py +513 -0
  45. smart_bot_factory/integrations/supabase_client.py +1678 -0
  46. smart_bot_factory/memory/__init__.py +8 -0
  47. smart_bot_factory/memory/memory_manager.py +299 -0
  48. smart_bot_factory/memory/static_memory.py +214 -0
  49. smart_bot_factory/message/__init__.py +56 -0
  50. smart_bot_factory/rag/__init__.py +5 -0
  51. smart_bot_factory/rag/decorators.py +29 -0
  52. smart_bot_factory/rag/router.py +54 -0
  53. smart_bot_factory/rag/templates/__init__.py +3 -0
  54. smart_bot_factory/rag/templates/create_table.sql +7 -0
  55. smart_bot_factory/rag/templates/create_table_and_function_template.py +94 -0
  56. smart_bot_factory/rag/templates/match_function.sql +61 -0
  57. smart_bot_factory/rag/templates/match_services_template.py +82 -0
  58. smart_bot_factory/rag/vectorstore.py +449 -0
  59. smart_bot_factory/router/__init__.py +10 -0
  60. smart_bot_factory/setup_checker.py +512 -0
  61. smart_bot_factory/supabase/__init__.py +7 -0
  62. smart_bot_factory/supabase/client.py +631 -0
  63. smart_bot_factory/utils/__init__.py +11 -0
  64. smart_bot_factory/utils/debug_routing.py +114 -0
  65. smart_bot_factory/utils/prompt_loader.py +529 -0
  66. smart_bot_factory/utils/tool_router.py +68 -0
  67. smart_bot_factory/utils/user_prompt_loader.py +55 -0
  68. smart_bot_factory/utm_link_generator.py +123 -0
  69. smart_bot_factory-1.1.1.dist-info/METADATA +1135 -0
  70. smart_bot_factory-1.1.1.dist-info/RECORD +73 -0
  71. smart_bot_factory-1.1.1.dist-info/WHEEL +4 -0
  72. smart_bot_factory-1.1.1.dist-info/entry_points.txt +2 -0
  73. 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('<', '&lt;').replace('>', '&gt;')
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)