smart-bot-factory 0.3.7__py3-none-any.whl → 0.3.8__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.
Potentially problematic release.
This version of smart-bot-factory might be problematic. Click here for more details.
- smart_bot_factory/admin/__init__.py +7 -7
- smart_bot_factory/admin/admin_events.py +483 -383
- smart_bot_factory/admin/admin_logic.py +234 -158
- smart_bot_factory/admin/admin_manager.py +68 -53
- smart_bot_factory/admin/admin_tester.py +46 -40
- smart_bot_factory/admin/timeout_checker.py +201 -153
- smart_bot_factory/aiogram_calendar/__init__.py +11 -3
- smart_bot_factory/aiogram_calendar/common.py +12 -18
- smart_bot_factory/aiogram_calendar/dialog_calendar.py +126 -64
- smart_bot_factory/aiogram_calendar/schemas.py +49 -28
- smart_bot_factory/aiogram_calendar/simple_calendar.py +94 -50
- smart_bot_factory/analytics/analytics_manager.py +414 -392
- smart_bot_factory/cli.py +204 -148
- smart_bot_factory/config.py +123 -102
- smart_bot_factory/core/bot_utils.py +474 -332
- smart_bot_factory/core/conversation_manager.py +287 -200
- smart_bot_factory/core/decorators.py +1129 -749
- smart_bot_factory/core/message_sender.py +287 -266
- smart_bot_factory/core/router.py +170 -100
- smart_bot_factory/core/router_manager.py +121 -83
- smart_bot_factory/core/states.py +4 -3
- smart_bot_factory/creation/__init__.py +1 -1
- smart_bot_factory/creation/bot_builder.py +320 -242
- smart_bot_factory/creation/bot_testing.py +440 -365
- smart_bot_factory/dashboard/__init__.py +1 -3
- smart_bot_factory/event/__init__.py +2 -7
- smart_bot_factory/handlers/handlers.py +676 -472
- smart_bot_factory/integrations/openai_client.py +218 -168
- smart_bot_factory/integrations/supabase_client.py +928 -637
- smart_bot_factory/message/__init__.py +18 -22
- smart_bot_factory/router/__init__.py +2 -2
- smart_bot_factory/setup_checker.py +162 -126
- smart_bot_factory/supabase/__init__.py +1 -1
- smart_bot_factory/supabase/client.py +631 -515
- smart_bot_factory/utils/__init__.py +2 -3
- smart_bot_factory/utils/debug_routing.py +38 -27
- smart_bot_factory/utils/prompt_loader.py +153 -120
- smart_bot_factory/utils/user_prompt_loader.py +55 -56
- smart_bot_factory/utm_link_generator.py +123 -116
- {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.8.dist-info}/METADATA +3 -1
- smart_bot_factory-0.3.8.dist-info/RECORD +59 -0
- smart_bot_factory-0.3.7.dist-info/RECORD +0 -59
- {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.8.dist-info}/WHEEL +0 -0
- {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.8.dist-info}/entry_points.txt +0 -0
- {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.8.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,54 +2,61 @@
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
import time
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
from aiogram import F, Router
|
|
6
7
|
from aiogram.filters import Command, StateFilter
|
|
7
8
|
from aiogram.fsm.context import FSMContext
|
|
8
|
-
from aiogram.types import
|
|
9
|
+
from aiogram.types import CallbackQuery, Message
|
|
9
10
|
|
|
10
|
-
from ..core.bot_utils import
|
|
11
|
-
|
|
11
|
+
from ..core.bot_utils import (parse_ai_response, process_events, send_message,
|
|
12
|
+
send_welcome_file)
|
|
13
|
+
from ..core.states import UserStates
|
|
12
14
|
|
|
13
15
|
logger = logging.getLogger(__name__)
|
|
14
16
|
|
|
15
17
|
# Создаем роутер для обработчиков
|
|
16
18
|
router = Router()
|
|
17
19
|
|
|
20
|
+
|
|
18
21
|
def setup_handlers(dp):
|
|
19
22
|
"""Настройка основных обработчиков"""
|
|
20
23
|
# Подключаем middleware
|
|
21
24
|
router.message.middleware()(admin_middleware)
|
|
22
|
-
|
|
25
|
+
|
|
23
26
|
# Регистрируем роутер
|
|
24
27
|
dp.include_router(router)
|
|
25
28
|
|
|
29
|
+
|
|
26
30
|
# Функция для получения глобальных переменных
|
|
27
31
|
def get_global_var(var_name):
|
|
28
32
|
"""Получает глобальную переменную из модуля handlers"""
|
|
29
33
|
import sys
|
|
34
|
+
|
|
30
35
|
current_module = sys.modules[__name__]
|
|
31
36
|
return getattr(current_module, var_name, None)
|
|
32
37
|
|
|
38
|
+
|
|
33
39
|
# Middleware для проверки админов
|
|
34
40
|
async def admin_middleware(handler, event: Message, data: dict):
|
|
35
41
|
"""Middleware для обновления информации об админах"""
|
|
36
|
-
admin_manager = get_global_var(
|
|
37
|
-
|
|
42
|
+
admin_manager = get_global_var("admin_manager")
|
|
43
|
+
|
|
38
44
|
if admin_manager and admin_manager.is_admin(event.from_user.id):
|
|
39
45
|
await admin_manager.update_admin_info(event.from_user)
|
|
40
|
-
|
|
46
|
+
|
|
41
47
|
return await handler(event, data)
|
|
42
48
|
|
|
49
|
+
|
|
43
50
|
@router.message(Command(commands=["start", "старт", "ст"]))
|
|
44
51
|
async def start_handler(message: Message, state: FSMContext):
|
|
45
52
|
"""Обработчик команды /start - сброс сессии и начало заново"""
|
|
46
|
-
admin_manager = get_global_var(
|
|
53
|
+
admin_manager = get_global_var("admin_manager")
|
|
47
54
|
from ..admin.admin_logic import admin_start_handler
|
|
48
55
|
from ..utils.debug_routing import debug_user_state
|
|
49
|
-
|
|
56
|
+
|
|
50
57
|
try:
|
|
51
58
|
await debug_user_state(message, state, "START_COMMAND")
|
|
52
|
-
|
|
59
|
+
|
|
53
60
|
# Проверяем, админ ли это и в каком режиме
|
|
54
61
|
if admin_manager.is_admin(message.from_user.id):
|
|
55
62
|
if admin_manager.is_in_admin_mode(message.from_user.id):
|
|
@@ -57,107 +64,125 @@ async def start_handler(message: Message, state: FSMContext):
|
|
|
57
64
|
await admin_start_handler(message, state)
|
|
58
65
|
return
|
|
59
66
|
# Админ в режиме пользователя - работаем как обычный пользователь
|
|
60
|
-
|
|
67
|
+
|
|
61
68
|
await user_start_handler(message, state)
|
|
62
|
-
|
|
69
|
+
|
|
63
70
|
except Exception as e:
|
|
64
71
|
logger.error(f"Ошибка при обработке /start: {e}")
|
|
65
|
-
await send_message(
|
|
72
|
+
await send_message(
|
|
73
|
+
message, "Произошла ошибка при инициализации. Попробуйте позже."
|
|
74
|
+
)
|
|
75
|
+
|
|
66
76
|
|
|
67
77
|
@router.message(Command(commands=["timeup", "вперед"]))
|
|
68
78
|
async def timeup_handler(message: Message, state: FSMContext):
|
|
69
79
|
"""Обработчик команды /timeup (или /вперед) - тестирование запланированных событий"""
|
|
70
|
-
from ..core.decorators import process_scheduled_event, update_event_result
|
|
71
80
|
from datetime import datetime
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
81
|
+
|
|
82
|
+
from ..core.decorators import process_scheduled_event, update_event_result
|
|
83
|
+
|
|
84
|
+
supabase_client = get_global_var("supabase_client")
|
|
85
|
+
|
|
75
86
|
try:
|
|
76
87
|
await message.answer("🔄 Запускаю тестирование запланированных событий...")
|
|
77
|
-
|
|
88
|
+
|
|
78
89
|
# Получаем события для этого пользователя И глобальные события (user_id = null)
|
|
79
90
|
# 1. События пользователя
|
|
80
|
-
user_events_query =
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
91
|
+
user_events_query = (
|
|
92
|
+
supabase_client.client.table("scheduled_events")
|
|
93
|
+
.select("*")
|
|
94
|
+
.eq("user_id", message.from_user.id)
|
|
95
|
+
.in_("status", ["pending", "immediate"])
|
|
96
|
+
)
|
|
97
|
+
|
|
84
98
|
# 🆕 Фильтруем по bot_id если указан
|
|
85
99
|
if supabase_client.bot_id:
|
|
86
|
-
user_events_query = user_events_query.eq(
|
|
87
|
-
|
|
100
|
+
user_events_query = user_events_query.eq("bot_id", supabase_client.bot_id)
|
|
101
|
+
|
|
88
102
|
user_events = user_events_query.execute()
|
|
89
|
-
|
|
103
|
+
|
|
90
104
|
# 2. Глобальные события (без user_id)
|
|
91
|
-
global_events_query =
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
105
|
+
global_events_query = (
|
|
106
|
+
supabase_client.client.table("scheduled_events")
|
|
107
|
+
.select("*")
|
|
108
|
+
.is_("user_id", "null")
|
|
109
|
+
.in_("status", ["pending", "immediate"])
|
|
110
|
+
)
|
|
111
|
+
|
|
95
112
|
# 🆕 Фильтруем по bot_id если указан
|
|
96
113
|
if supabase_client.bot_id:
|
|
97
|
-
global_events_query = global_events_query.eq(
|
|
98
|
-
|
|
114
|
+
global_events_query = global_events_query.eq(
|
|
115
|
+
"bot_id", supabase_client.bot_id
|
|
116
|
+
)
|
|
117
|
+
|
|
99
118
|
global_events = global_events_query.execute()
|
|
100
|
-
|
|
119
|
+
|
|
101
120
|
# Объединяем события
|
|
102
121
|
all_events = (user_events.data or []) + (global_events.data or [])
|
|
103
|
-
|
|
122
|
+
|
|
104
123
|
if not all_events:
|
|
105
124
|
await message.answer("📭 Нет запланированных событий для тестирования")
|
|
106
125
|
return
|
|
107
|
-
|
|
126
|
+
|
|
108
127
|
total_events = len(all_events)
|
|
109
128
|
user_count = len(user_events.data or [])
|
|
110
129
|
global_count = len(global_events.data or [])
|
|
111
|
-
|
|
130
|
+
|
|
112
131
|
status_msg = f"📋 Найдено {total_events} событий:"
|
|
113
132
|
if user_count > 0:
|
|
114
133
|
status_msg += f"\n 👤 Ваших: {user_count}"
|
|
115
134
|
if global_count > 0:
|
|
116
135
|
status_msg += f"\n 🌍 Глобальных: {global_count}"
|
|
117
136
|
status_msg += "\n\nВыполняю их немедленно..."
|
|
118
|
-
|
|
137
|
+
|
|
119
138
|
await message.answer(status_msg)
|
|
120
|
-
|
|
139
|
+
|
|
121
140
|
# Выполняем каждое событие
|
|
122
141
|
success_count = 0
|
|
123
142
|
failed_count = 0
|
|
124
143
|
results = []
|
|
125
|
-
|
|
144
|
+
|
|
126
145
|
for event in all_events:
|
|
127
|
-
event_id = event[
|
|
128
|
-
event_type = event[
|
|
129
|
-
event_category = event[
|
|
130
|
-
is_global = event.get(
|
|
131
|
-
|
|
146
|
+
event_id = event["id"]
|
|
147
|
+
event_type = event["event_type"]
|
|
148
|
+
event_category = event["event_category"]
|
|
149
|
+
is_global = event.get("user_id") is None
|
|
150
|
+
|
|
132
151
|
try:
|
|
133
152
|
event_label = f"🌍 {event_type}" if is_global else f"👤 {event_type}"
|
|
134
|
-
logger.info(
|
|
135
|
-
|
|
153
|
+
logger.info(
|
|
154
|
+
f"🧪 Тестируем событие {event_id}: {event_category}/{event_type} ({'глобальное' if is_global else f'пользователя {message.from_user.id}'})"
|
|
155
|
+
)
|
|
156
|
+
|
|
136
157
|
# Выполняем событие
|
|
137
158
|
await process_scheduled_event(event)
|
|
138
|
-
|
|
159
|
+
|
|
139
160
|
# Помечаем как выполненное
|
|
140
|
-
await update_event_result(
|
|
141
|
-
|
|
142
|
-
"
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
161
|
+
await update_event_result(
|
|
162
|
+
event_id,
|
|
163
|
+
"completed",
|
|
164
|
+
{
|
|
165
|
+
"executed": True,
|
|
166
|
+
"test_mode": True,
|
|
167
|
+
"tested_by_user": message.from_user.id,
|
|
168
|
+
"tested_at": datetime.now().isoformat(),
|
|
169
|
+
},
|
|
170
|
+
)
|
|
171
|
+
|
|
147
172
|
success_count += 1
|
|
148
173
|
results.append(f"✅ {event_label}")
|
|
149
174
|
logger.info(f"✅ Событие {event_id} успешно выполнено")
|
|
150
|
-
|
|
175
|
+
|
|
151
176
|
except Exception as e:
|
|
152
177
|
failed_count += 1
|
|
153
178
|
error_msg = str(e)
|
|
154
179
|
event_label = f"🌍 {event_type}" if is_global else f"👤 {event_type}"
|
|
155
180
|
results.append(f"❌ {event_label}: {error_msg[:50]}")
|
|
156
181
|
logger.error(f"❌ Ошибка выполнения события {event_id}: {error_msg}")
|
|
157
|
-
|
|
182
|
+
|
|
158
183
|
# Помечаем как failed
|
|
159
|
-
await update_event_result(event_id,
|
|
160
|
-
|
|
184
|
+
await update_event_result(event_id, "failed", None, error_msg)
|
|
185
|
+
|
|
161
186
|
# Отправляем итоговую статистику
|
|
162
187
|
result_text = [
|
|
163
188
|
"📊 **Результаты тестирования:**",
|
|
@@ -167,45 +192,46 @@ async def timeup_handler(message: Message, state: FSMContext):
|
|
|
167
192
|
f"📋 Всего: {total_events}",
|
|
168
193
|
"",
|
|
169
194
|
"**События:**",
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
""
|
|
195
|
+
"👤 - ваши события",
|
|
196
|
+
"🌍 - глобальные события",
|
|
197
|
+
"",
|
|
173
198
|
]
|
|
174
|
-
|
|
199
|
+
|
|
175
200
|
# Добавляем результаты (максимум 10 событий)
|
|
176
201
|
for result in results[:10]:
|
|
177
202
|
result_text.append(result)
|
|
178
|
-
|
|
203
|
+
|
|
179
204
|
if len(results) > 10:
|
|
180
205
|
result_text.append(f"... и еще {len(results) - 10} событий")
|
|
181
|
-
|
|
206
|
+
|
|
182
207
|
await message.answer("\n".join(result_text))
|
|
183
|
-
|
|
208
|
+
|
|
184
209
|
except Exception as e:
|
|
185
210
|
logger.error(f"❌ Критическая ошибка в timeup_handler: {e}")
|
|
186
211
|
await message.answer(f"❌ Ошибка тестирования: {str(e)}")
|
|
187
|
-
|
|
212
|
+
|
|
213
|
+
|
|
188
214
|
@router.message(F.voice | F.audio)
|
|
189
215
|
async def voice_handler(message: Message, state: FSMContext):
|
|
190
216
|
"""Обработчик голосовых и аудио сообщений"""
|
|
191
|
-
openai_client = get_global_var(
|
|
192
|
-
bot = get_global_var(
|
|
193
|
-
admin_manager = get_global_var(
|
|
194
|
-
|
|
217
|
+
openai_client = get_global_var("openai_client")
|
|
218
|
+
bot = get_global_var("bot")
|
|
219
|
+
admin_manager = get_global_var("admin_manager")
|
|
220
|
+
|
|
195
221
|
import os
|
|
196
|
-
from pathlib import Path
|
|
197
222
|
from datetime import datetime
|
|
198
|
-
|
|
223
|
+
from pathlib import Path
|
|
224
|
+
|
|
199
225
|
processing_msg = None
|
|
200
|
-
|
|
226
|
+
|
|
201
227
|
try:
|
|
202
228
|
# Проверяем что это не админ в режиме администратора
|
|
203
229
|
if admin_manager.is_admin(message.from_user.id):
|
|
204
230
|
if admin_manager.is_in_admin_mode(message.from_user.id):
|
|
205
231
|
return # Админы работают с текстом
|
|
206
|
-
|
|
232
|
+
|
|
207
233
|
logger.info(f"🎤 Получено голосовое сообщение от {message.from_user.id}")
|
|
208
|
-
|
|
234
|
+
|
|
209
235
|
# Получаем файл
|
|
210
236
|
if message.voice:
|
|
211
237
|
file_id = message.voice.file_id
|
|
@@ -213,648 +239,767 @@ async def voice_handler(message: Message, state: FSMContext):
|
|
|
213
239
|
else:
|
|
214
240
|
file_id = message.audio.file_id
|
|
215
241
|
duration = message.audio.duration
|
|
216
|
-
|
|
242
|
+
|
|
217
243
|
# Показываем что обрабатываем
|
|
218
244
|
processing_msg = await message.answer("🎤 Распознаю голос...")
|
|
219
|
-
|
|
245
|
+
|
|
220
246
|
try:
|
|
221
247
|
# Скачиваем файл
|
|
222
248
|
file = await bot.get_file(file_id)
|
|
223
|
-
|
|
249
|
+
|
|
224
250
|
# Путь для сохранения
|
|
225
251
|
temp_dir = Path("temp_audio")
|
|
226
252
|
temp_dir.mkdir(exist_ok=True)
|
|
227
|
-
|
|
228
|
-
file_path =
|
|
229
|
-
|
|
253
|
+
|
|
254
|
+
file_path = (
|
|
255
|
+
temp_dir
|
|
256
|
+
/ f"{message.from_user.id}_{int(datetime.now().timestamp())}.ogg"
|
|
257
|
+
)
|
|
258
|
+
|
|
230
259
|
# Скачиваем
|
|
231
260
|
await bot.download_file(file.file_path, file_path)
|
|
232
261
|
logger.info(f"📥 Файл скачан: {file_path} ({duration} сек)")
|
|
233
|
-
|
|
262
|
+
|
|
234
263
|
# Распознаем через Whisper
|
|
235
264
|
recognized_text = await openai_client.transcribe_audio(str(file_path))
|
|
236
|
-
|
|
265
|
+
|
|
237
266
|
# Удаляем временный файл и папку
|
|
238
267
|
try:
|
|
239
268
|
os.remove(file_path)
|
|
240
269
|
logger.info(f"🗑️ Временный файл удален: {file_path}")
|
|
241
|
-
|
|
270
|
+
|
|
242
271
|
# Проверяем, пуста ли папка
|
|
243
272
|
if not any(temp_dir.iterdir()):
|
|
244
273
|
temp_dir.rmdir()
|
|
245
274
|
logger.info(f"🗑️ Временная папка удалена: {temp_dir}")
|
|
246
|
-
|
|
275
|
+
|
|
247
276
|
except Exception as e:
|
|
248
277
|
logger.warning(f"⚠️ Не удалось удалить временные файлы: {e}")
|
|
249
|
-
|
|
278
|
+
|
|
250
279
|
if not recognized_text:
|
|
251
|
-
await processing_msg.edit_text(
|
|
280
|
+
await processing_msg.edit_text(
|
|
281
|
+
"❌ Не удалось распознать голос. Попробуйте еще раз."
|
|
282
|
+
)
|
|
252
283
|
return
|
|
253
|
-
|
|
284
|
+
|
|
254
285
|
logger.info(f"✅ Текст распознан успешно: '{recognized_text[:100]}...'")
|
|
255
|
-
|
|
286
|
+
|
|
256
287
|
# Получаем данные сессии
|
|
257
288
|
current_state = await state.get_state()
|
|
258
289
|
data = await state.get_data()
|
|
259
|
-
|
|
290
|
+
|
|
260
291
|
logger.info(f"🔍 Текущее состояние: {current_state}")
|
|
261
292
|
logger.info(f"🔍 Данные в state: {data}")
|
|
262
|
-
|
|
263
|
-
session_id = data.get(
|
|
264
|
-
system_prompt = data.get(
|
|
265
|
-
|
|
293
|
+
|
|
294
|
+
session_id = data.get("session_id")
|
|
295
|
+
system_prompt = data.get("system_prompt")
|
|
296
|
+
|
|
266
297
|
logger.info(f"📝 session_id из state: {session_id}")
|
|
267
|
-
|
|
298
|
+
|
|
268
299
|
# Если session_id нет в state, пытаемся получить из БД
|
|
269
300
|
if not session_id:
|
|
270
|
-
logger.warning(
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
301
|
+
logger.warning(
|
|
302
|
+
"⚠️ session_id не найден в state, ищем активную сессию в БД..."
|
|
303
|
+
)
|
|
304
|
+
supabase_client = get_global_var("supabase_client")
|
|
305
|
+
|
|
306
|
+
session_info = await supabase_client.get_active_session(
|
|
307
|
+
message.from_user.id
|
|
308
|
+
)
|
|
274
309
|
if session_info:
|
|
275
|
-
session_id = session_info[
|
|
276
|
-
system_prompt = session_info[
|
|
277
|
-
|
|
310
|
+
session_id = session_info["id"]
|
|
311
|
+
system_prompt = session_info["system_prompt"]
|
|
312
|
+
|
|
278
313
|
# Сохраняем в state для следующих сообщений
|
|
279
|
-
await state.update_data(
|
|
314
|
+
await state.update_data(
|
|
315
|
+
session_id=session_id, system_prompt=system_prompt
|
|
316
|
+
)
|
|
280
317
|
await state.set_state(UserStates.waiting_for_message)
|
|
281
|
-
|
|
318
|
+
|
|
282
319
|
logger.info(f"✅ Сессия восстановлена из БД: {session_id}")
|
|
283
320
|
else:
|
|
284
|
-
logger.error(
|
|
285
|
-
|
|
321
|
+
logger.error("❌ Активная сессия не найдена в БД")
|
|
322
|
+
|
|
286
323
|
if session_id:
|
|
287
324
|
# Сохраняем распознанный текст в state
|
|
288
325
|
await state.update_data(voice_recognized_text=recognized_text)
|
|
289
326
|
await state.set_state(UserStates.voice_confirmation)
|
|
290
|
-
|
|
327
|
+
|
|
291
328
|
# Показываем распознанный текст с кнопками выбора
|
|
292
|
-
from aiogram.types import
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
[
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
329
|
+
from aiogram.types import (InlineKeyboardButton,
|
|
330
|
+
InlineKeyboardMarkup)
|
|
331
|
+
|
|
332
|
+
keyboard = InlineKeyboardMarkup(
|
|
333
|
+
inline_keyboard=[
|
|
334
|
+
[
|
|
335
|
+
InlineKeyboardButton(
|
|
336
|
+
text="✅ Отправить", callback_data="voice_send"
|
|
337
|
+
)
|
|
338
|
+
],
|
|
339
|
+
[
|
|
340
|
+
InlineKeyboardButton(
|
|
341
|
+
text="✏️ Изменить текст", callback_data="voice_edit"
|
|
342
|
+
)
|
|
343
|
+
],
|
|
344
|
+
[
|
|
345
|
+
InlineKeyboardButton(
|
|
346
|
+
text="🎤 Надиктовать заново",
|
|
347
|
+
callback_data="voice_retry",
|
|
348
|
+
)
|
|
349
|
+
],
|
|
350
|
+
]
|
|
351
|
+
)
|
|
352
|
+
|
|
300
353
|
# Удаляем сообщение "Обрабатываю"
|
|
301
354
|
try:
|
|
302
355
|
await processing_msg.delete()
|
|
303
|
-
except:
|
|
356
|
+
except Exception:
|
|
304
357
|
pass
|
|
305
|
-
|
|
358
|
+
|
|
306
359
|
# Показываем результат с кнопками
|
|
307
360
|
await message.answer(
|
|
308
361
|
f"✅ Распознано:\n\n<i>{recognized_text}</i>\n\n"
|
|
309
362
|
f"Выберите действие:",
|
|
310
363
|
reply_markup=keyboard,
|
|
311
|
-
parse_mode=
|
|
364
|
+
parse_mode="HTML",
|
|
312
365
|
)
|
|
313
|
-
|
|
314
|
-
logger.info(
|
|
366
|
+
|
|
367
|
+
logger.info("✅ Показаны кнопки подтверждения голосового сообщения")
|
|
315
368
|
else:
|
|
316
|
-
logger.warning(
|
|
369
|
+
logger.warning("❌ Нет session_id в состоянии")
|
|
317
370
|
await processing_msg.edit_text(
|
|
318
371
|
f"✅ Распознано:\n\n{recognized_text}\n\n"
|
|
319
372
|
f"Сессия не найдена. Напишите /start"
|
|
320
373
|
)
|
|
321
|
-
|
|
374
|
+
|
|
322
375
|
except Exception as e:
|
|
323
376
|
logger.error(f"❌ Ошибка в процессе обработки голоса: {e}")
|
|
324
377
|
logger.exception("Полный стек ошибки:")
|
|
325
378
|
if processing_msg:
|
|
326
|
-
await processing_msg.edit_text(
|
|
379
|
+
await processing_msg.edit_text(
|
|
380
|
+
"❌ Ошибка обработки. Попробуйте написать текстом."
|
|
381
|
+
)
|
|
327
382
|
else:
|
|
328
|
-
await message.answer(
|
|
329
|
-
|
|
383
|
+
await message.answer(
|
|
384
|
+
"❌ Ошибка обработки. Попробуйте написать текстом."
|
|
385
|
+
)
|
|
386
|
+
|
|
330
387
|
except Exception as e:
|
|
331
388
|
logger.error(f"❌ КРИТИЧЕСКАЯ ошибка обработки голоса: {e}")
|
|
332
389
|
logger.exception("Полный стек критической ошибки:")
|
|
333
390
|
try:
|
|
334
391
|
if processing_msg:
|
|
335
|
-
await processing_msg.edit_text(
|
|
392
|
+
await processing_msg.edit_text(
|
|
393
|
+
"❌ Ошибка распознавания. Попробуйте написать текстом."
|
|
394
|
+
)
|
|
336
395
|
else:
|
|
337
|
-
await message.answer(
|
|
338
|
-
|
|
396
|
+
await message.answer(
|
|
397
|
+
"❌ Ошибка распознавания. Попробуйте написать текстом."
|
|
398
|
+
)
|
|
399
|
+
except Exception:
|
|
339
400
|
pass
|
|
340
401
|
|
|
402
|
+
|
|
341
403
|
async def user_start_handler(message: Message, state: FSMContext):
|
|
342
404
|
"""Обработчик /start для обычных пользователей"""
|
|
343
|
-
supabase_client = get_global_var(
|
|
344
|
-
prompt_loader = get_global_var(
|
|
405
|
+
supabase_client = get_global_var("supabase_client")
|
|
406
|
+
prompt_loader = get_global_var("prompt_loader")
|
|
345
407
|
from ..core.bot_utils import parse_utm_from_start_param
|
|
346
|
-
|
|
408
|
+
|
|
347
409
|
try:
|
|
348
410
|
# 0. ПОЛУЧАЕМ UTM ДАННЫЕ
|
|
349
|
-
start_param =
|
|
350
|
-
|
|
411
|
+
start_param = (
|
|
412
|
+
message.text.split(" ", 1)[1] if len(message.text.split()) > 1 else None
|
|
413
|
+
)
|
|
414
|
+
|
|
351
415
|
# Логируем входящий start параметр
|
|
352
416
|
# Пример поддерживаемого формата: @https://t.me/bot?start=utmSource-vk_utmCampaign-summer2025 не более 64 символов после strat=
|
|
353
|
-
|
|
417
|
+
|
|
354
418
|
logger.info(f"📥 Получен start параметр: '{start_param}'")
|
|
355
|
-
|
|
419
|
+
|
|
356
420
|
utm_data = {}
|
|
357
421
|
if start_param:
|
|
358
422
|
# Парсим UTM данные
|
|
359
423
|
utm_data = parse_utm_from_start_param(start_param)
|
|
360
|
-
|
|
424
|
+
|
|
361
425
|
# Подробное логирование UTM
|
|
362
426
|
logger.info(f"📊 UTM данные для пользователя {message.from_user.id}:")
|
|
363
427
|
if utm_data:
|
|
364
428
|
for key, value in utm_data.items():
|
|
365
429
|
logger.info(f" • {key}: {value}")
|
|
366
|
-
logger.info(
|
|
430
|
+
logger.info("✅ UTM данные успешно распознаны")
|
|
367
431
|
else:
|
|
368
432
|
logger.warning(f"⚠️ UTM данные не найдены в параметре: '{start_param}'")
|
|
369
433
|
else:
|
|
370
434
|
logger.info("ℹ️ Start параметр отсутствует (обычный /start)")
|
|
371
|
-
|
|
435
|
+
|
|
372
436
|
# 1. ЯВНО ОЧИЩАЕМ СОСТОЯНИЕ FSM
|
|
373
437
|
await state.clear()
|
|
374
438
|
logger.info(f"🔄 Состояние FSM очищено для пользователя {message.from_user.id}")
|
|
375
|
-
|
|
439
|
+
|
|
376
440
|
# 2. ЗАГРУЖАЕМ ПРОМПТЫ
|
|
377
441
|
logger.info(f"Загрузка промптов для пользователя {message.from_user.id}")
|
|
378
442
|
system_prompt = await prompt_loader.load_system_prompt()
|
|
379
|
-
|
|
443
|
+
|
|
380
444
|
# Загружаем приветственное сообщение
|
|
381
445
|
welcome_message = await prompt_loader.load_welcome_message()
|
|
382
|
-
|
|
446
|
+
|
|
383
447
|
# 3. ПОЛУЧАЕМ ДАННЫЕ ПОЛЬЗОВАТЕЛЯ
|
|
384
448
|
user_data = {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
449
|
+
"telegram_id": message.from_user.id,
|
|
450
|
+
"username": message.from_user.username,
|
|
451
|
+
"first_name": message.from_user.first_name,
|
|
452
|
+
"last_name": message.from_user.last_name,
|
|
453
|
+
"language_code": message.from_user.language_code,
|
|
454
|
+
"source": utm_data.get("utm_source"),
|
|
455
|
+
"medium": utm_data.get("utm_medium"),
|
|
456
|
+
"campaign": utm_data.get("utm_campaign"),
|
|
457
|
+
"content": utm_data.get("utm_content"),
|
|
458
|
+
"term": utm_data.get("utm_term"),
|
|
459
|
+
"segment": utm_data.get("segment"),
|
|
396
460
|
}
|
|
397
|
-
|
|
461
|
+
|
|
398
462
|
# 4. СОЗДАЕМ НОВУЮ СЕССИЮ (автоматически закроет активные)
|
|
399
463
|
# Добавляем UTM данные в метаданные пользователя
|
|
400
464
|
if utm_data:
|
|
401
|
-
user_data[
|
|
402
|
-
logger.info(
|
|
403
|
-
|
|
465
|
+
user_data["metadata"] = {"utm_data": utm_data}
|
|
466
|
+
logger.info("📈 UTM данные добавлены в метаданные пользователя")
|
|
467
|
+
|
|
404
468
|
session_id = await supabase_client.create_chat_session(user_data, system_prompt)
|
|
405
|
-
logger.info(
|
|
406
|
-
|
|
469
|
+
logger.info(
|
|
470
|
+
f"✅ Создана новая сессия {session_id} для пользователя {message.from_user.id}"
|
|
471
|
+
)
|
|
472
|
+
|
|
407
473
|
# 5. УСТАНАВЛИВАЕМ НОВОЕ СОСТОЯНИЕ
|
|
408
474
|
await state.update_data(session_id=session_id, system_prompt=system_prompt)
|
|
409
475
|
await state.set_state(UserStates.waiting_for_message)
|
|
410
|
-
|
|
476
|
+
|
|
411
477
|
# 6. ОТПРАВЛЯЕМ ПРИВЕТСТВЕННОЕ СООБЩЕНИЕ
|
|
412
478
|
try:
|
|
413
479
|
await send_message(message, welcome_message)
|
|
414
|
-
logger.info(
|
|
480
|
+
logger.info(
|
|
481
|
+
f"Приветственное сообщение отправлено пользователю {message.from_user.id}"
|
|
482
|
+
)
|
|
415
483
|
except Exception as e:
|
|
416
484
|
if "Forbidden: bot was blocked by the user" in str(e):
|
|
417
|
-
logger.warning(
|
|
485
|
+
logger.warning(
|
|
486
|
+
f"🚫 Бот заблокирован пользователем {message.from_user.id}"
|
|
487
|
+
)
|
|
418
488
|
return
|
|
419
489
|
else:
|
|
420
490
|
logger.error(f"❌ Ошибка отправки приветственного сообщения: {e}")
|
|
421
491
|
raise
|
|
422
|
-
|
|
492
|
+
|
|
423
493
|
# 7. ЕСЛИ ЕСТЬ ФАЙЛ ОТПРАВЛЯЕМ ВМЕСТЕ С ПОДПИСЬЮ
|
|
424
|
-
logging.info(
|
|
494
|
+
logging.info(
|
|
495
|
+
f"📎 Попытка отправки приветственного файла для сессии {session_id}"
|
|
496
|
+
)
|
|
425
497
|
caption = await send_welcome_file(message)
|
|
426
|
-
|
|
498
|
+
|
|
427
499
|
# 8. СОХРАНЯЕМ ПРИВЕТСТВЕННОЕ СООБЩЕНИЕ В БД
|
|
428
500
|
if caption:
|
|
429
|
-
logging.info(
|
|
501
|
+
logging.info(
|
|
502
|
+
f"📄 Добавление подписи к файлу в приветственное сообщение для сессии {session_id}"
|
|
503
|
+
)
|
|
430
504
|
welcome_message = f"{welcome_message}\n\nПодпись к файлу:\n\n{caption}"
|
|
431
505
|
else:
|
|
432
|
-
logging.info(
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
506
|
+
logging.info(
|
|
507
|
+
f"📄 Приветственный файл отправлен без подписи для сессии {session_id}"
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
logging.info(
|
|
511
|
+
f"💾 Сохранение приветственного сообщения в БД для сессии {session_id}"
|
|
512
|
+
)
|
|
513
|
+
|
|
436
514
|
await supabase_client.add_message(
|
|
437
515
|
session_id=session_id,
|
|
438
|
-
role=
|
|
516
|
+
role="assistant",
|
|
439
517
|
content=welcome_message,
|
|
440
|
-
message_type=
|
|
518
|
+
message_type="text",
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
logging.info(
|
|
522
|
+
f"✅ Приветственное сообщение успешно сохранено в БД для сессии {session_id}"
|
|
441
523
|
)
|
|
442
|
-
|
|
443
|
-
logging.info(f"✅ Приветственное сообщение успешно сохранено в БД для сессии {session_id}")
|
|
444
|
-
|
|
524
|
+
|
|
445
525
|
# ВЫЗЫВАЕМ ПОЛЬЗОВАТЕЛЬСКИЕ ОБРАБОТЧИКИ on_start
|
|
446
|
-
start_handlers = get_global_var(
|
|
526
|
+
start_handlers = get_global_var("start_handlers")
|
|
447
527
|
if start_handlers:
|
|
448
|
-
logger.info(
|
|
528
|
+
logger.info(
|
|
529
|
+
f"🔔 Вызов {len(start_handlers)} пользовательских обработчиков on_start"
|
|
530
|
+
)
|
|
449
531
|
for handler in start_handlers:
|
|
450
532
|
try:
|
|
451
533
|
await handler(
|
|
452
534
|
user_id=message.from_user.id,
|
|
453
535
|
session_id=session_id,
|
|
454
536
|
message=message,
|
|
455
|
-
state=state
|
|
537
|
+
state=state,
|
|
538
|
+
)
|
|
539
|
+
logger.info(
|
|
540
|
+
f"✅ Обработчик on_start '{handler.__name__}' выполнен успешно"
|
|
456
541
|
)
|
|
457
|
-
logger.info(f"✅ Обработчик on_start '{handler.__name__}' выполнен успешно")
|
|
458
542
|
except Exception as handler_error:
|
|
459
|
-
logger.error(
|
|
543
|
+
logger.error(
|
|
544
|
+
f"❌ Ошибка в обработчике on_start '{handler.__name__}': {handler_error}"
|
|
545
|
+
)
|
|
460
546
|
# Продолжаем выполнение остальных обработчиков
|
|
461
|
-
|
|
547
|
+
|
|
462
548
|
except Exception as e:
|
|
463
549
|
logger.error(f"Ошибка при обработке user /start: {e}")
|
|
464
|
-
await send_message(
|
|
550
|
+
await send_message(
|
|
551
|
+
message, "Произошла ошибка при инициализации. Попробуйте позже."
|
|
552
|
+
)
|
|
553
|
+
|
|
465
554
|
|
|
466
555
|
@router.message(StateFilter(None))
|
|
467
556
|
async def message_without_state_handler(message: Message, state: FSMContext):
|
|
468
557
|
"""Обработчик сообщений без состояния (после перезапуска бота)"""
|
|
469
|
-
admin_manager = get_global_var(
|
|
470
|
-
supabase_client = get_global_var(
|
|
471
|
-
conversation_manager = get_global_var(
|
|
472
|
-
from ..admin.admin_logic import AdminStates
|
|
558
|
+
admin_manager = get_global_var("admin_manager")
|
|
559
|
+
supabase_client = get_global_var("supabase_client")
|
|
560
|
+
conversation_manager = get_global_var("conversation_manager")
|
|
561
|
+
from ..admin.admin_logic import AdminStates as AdminLogicStates
|
|
473
562
|
from ..utils.debug_routing import debug_user_state
|
|
474
|
-
|
|
563
|
+
|
|
475
564
|
try:
|
|
476
565
|
await debug_user_state(message, state, "NO_STATE")
|
|
477
|
-
|
|
566
|
+
|
|
478
567
|
# СНАЧАЛА проверяем диалог с админом
|
|
479
|
-
conversation = await conversation_manager.is_user_in_admin_chat(
|
|
480
|
-
|
|
568
|
+
conversation = await conversation_manager.is_user_in_admin_chat(
|
|
569
|
+
message.from_user.id
|
|
570
|
+
)
|
|
571
|
+
|
|
481
572
|
if conversation:
|
|
482
|
-
logger.info(
|
|
483
|
-
|
|
573
|
+
logger.info(
|
|
574
|
+
f"✅ Найден диалог с админом {conversation['admin_id']}, устанавливаем состояние admin_chat"
|
|
575
|
+
)
|
|
576
|
+
|
|
484
577
|
# Устанавливаем состояние admin_chat
|
|
485
578
|
await state.set_state(UserStates.admin_chat)
|
|
486
579
|
await state.update_data(admin_conversation=conversation)
|
|
487
|
-
|
|
580
|
+
|
|
488
581
|
# Сразу пересылаем сообщение админу
|
|
489
582
|
await conversation_manager.forward_message_to_admin(message, conversation)
|
|
490
|
-
|
|
583
|
+
|
|
491
584
|
# Сохраняем сообщение в БД
|
|
492
|
-
session_info = await supabase_client.get_active_session(
|
|
585
|
+
session_info = await supabase_client.get_active_session(
|
|
586
|
+
message.from_user.id
|
|
587
|
+
)
|
|
493
588
|
if session_info:
|
|
494
589
|
await supabase_client.add_message(
|
|
495
|
-
session_id=session_info[
|
|
496
|
-
role=
|
|
590
|
+
session_id=session_info["id"],
|
|
591
|
+
role="user",
|
|
497
592
|
content=message.text,
|
|
498
|
-
message_type=
|
|
499
|
-
metadata={
|
|
593
|
+
message_type="text",
|
|
594
|
+
metadata={
|
|
595
|
+
"in_admin_chat": True,
|
|
596
|
+
"admin_id": conversation["admin_id"],
|
|
597
|
+
},
|
|
500
598
|
)
|
|
501
|
-
|
|
599
|
+
|
|
502
600
|
return
|
|
503
|
-
|
|
601
|
+
|
|
504
602
|
# Проверяем, админ ли это
|
|
505
603
|
if admin_manager.is_admin(message.from_user.id):
|
|
506
|
-
logger.info(
|
|
507
|
-
await state.set_state(
|
|
508
|
-
await message.answer(
|
|
604
|
+
logger.info("👑 Админ в режиме администратора без состояния")
|
|
605
|
+
await state.set_state(AdminLogicStates.admin_mode)
|
|
606
|
+
await message.answer(
|
|
607
|
+
"👑 Режим администратора\nИспользуйте /start для панели управления"
|
|
608
|
+
)
|
|
509
609
|
return
|
|
510
|
-
|
|
511
|
-
logger.info(
|
|
512
|
-
|
|
610
|
+
|
|
611
|
+
logger.info("👤 Обычный пользователь без состояния, ищем активную сессию")
|
|
612
|
+
|
|
513
613
|
# Ищем активную сессию в БД
|
|
514
614
|
session_info = await supabase_client.get_active_session(message.from_user.id)
|
|
515
|
-
|
|
615
|
+
|
|
516
616
|
if session_info:
|
|
517
617
|
logger.info(f"📝 Восстанавливаем сессию {session_info['id']}")
|
|
518
618
|
# Восстанавливаем сессию из БД
|
|
519
|
-
session_id = session_info[
|
|
520
|
-
system_prompt = session_info[
|
|
521
|
-
|
|
619
|
+
session_id = session_info["id"]
|
|
620
|
+
system_prompt = session_info["system_prompt"]
|
|
621
|
+
|
|
522
622
|
# Сохраняем в состояние
|
|
523
623
|
await state.update_data(session_id=session_id, system_prompt=system_prompt)
|
|
524
624
|
await state.set_state(UserStates.waiting_for_message)
|
|
525
|
-
|
|
526
|
-
logger.info(
|
|
527
|
-
|
|
625
|
+
|
|
626
|
+
logger.info("✅ Сессия восстановлена, обрабатываем сообщение")
|
|
627
|
+
|
|
528
628
|
# Теперь обрабатываем сообщение как обычно
|
|
529
629
|
await process_user_message(message, state, session_id, system_prompt)
|
|
530
630
|
else:
|
|
531
|
-
logger.info(
|
|
631
|
+
logger.info("❌ Нет активной сессии, просим написать /start")
|
|
532
632
|
await send_message(message, "Привет! Напишите /start для начала диалога.")
|
|
533
|
-
|
|
633
|
+
|
|
534
634
|
except Exception as e:
|
|
535
635
|
logger.error(f"❌ Ошибка при обработке сообщения без состояния: {e}")
|
|
536
|
-
await send_message(
|
|
636
|
+
await send_message(
|
|
637
|
+
message, "Произошла ошибка. Попробуйте написать /start для начала диалога."
|
|
638
|
+
)
|
|
639
|
+
|
|
537
640
|
|
|
538
641
|
# ✅ ИСПРАВЛЕНИЕ: Обработчик admin_chat должен быть ПЕРВЫМ и более приоритетным
|
|
539
642
|
@router.message(StateFilter(UserStates.admin_chat))
|
|
540
643
|
async def user_in_admin_chat_handler(message: Message, state: FSMContext):
|
|
541
644
|
"""ПРИОРИТЕТНЫЙ обработчик сообщений пользователей в диалоге с админом"""
|
|
542
|
-
conversation_manager = get_global_var(
|
|
543
|
-
supabase_client = get_global_var(
|
|
645
|
+
conversation_manager = get_global_var("conversation_manager")
|
|
646
|
+
supabase_client = get_global_var("supabase_client")
|
|
544
647
|
from ..utils.debug_routing import debug_user_state
|
|
545
|
-
|
|
648
|
+
|
|
546
649
|
await debug_user_state(message, state, "ADMIN_CHAT_HANDLER")
|
|
547
|
-
|
|
650
|
+
|
|
548
651
|
user_id = message.from_user.id
|
|
549
652
|
logger.info(f"🎯 ADMIN_CHAT HANDLER: сообщение от {user_id}: '{message.text}'")
|
|
550
|
-
|
|
653
|
+
|
|
551
654
|
# Проверяем, есть ли еще активный диалог
|
|
552
655
|
conversation = await conversation_manager.is_user_in_admin_chat(user_id)
|
|
553
|
-
|
|
656
|
+
|
|
554
657
|
if conversation:
|
|
555
658
|
logger.info(f"✅ Диалог активен, пересылаем админу {conversation['admin_id']}")
|
|
556
|
-
|
|
659
|
+
|
|
557
660
|
try:
|
|
558
661
|
# Сохраняем сообщение в БД
|
|
559
662
|
session_info = await supabase_client.get_active_session(user_id)
|
|
560
663
|
if session_info:
|
|
561
664
|
await supabase_client.add_message(
|
|
562
|
-
session_id=session_info[
|
|
563
|
-
role=
|
|
665
|
+
session_id=session_info["id"],
|
|
666
|
+
role="user",
|
|
564
667
|
content=message.text,
|
|
565
|
-
message_type=
|
|
566
|
-
metadata={
|
|
668
|
+
message_type="text",
|
|
669
|
+
metadata={
|
|
670
|
+
"in_admin_chat": True,
|
|
671
|
+
"admin_id": conversation["admin_id"],
|
|
672
|
+
},
|
|
567
673
|
)
|
|
568
|
-
logger.info(
|
|
569
|
-
|
|
674
|
+
logger.info("💾 Сообщение сохранено в БД")
|
|
675
|
+
|
|
570
676
|
# Пересылаем админу
|
|
571
677
|
await conversation_manager.forward_message_to_admin(message, conversation)
|
|
572
|
-
logger.info(
|
|
573
|
-
|
|
678
|
+
logger.info("📤 Сообщение переслано админу")
|
|
679
|
+
|
|
574
680
|
except Exception as e:
|
|
575
681
|
logger.error(f"❌ Ошибка обработки admin_chat: {e}")
|
|
576
682
|
await message.answer("Произошла ошибка. Попробуйте позже.")
|
|
577
683
|
else:
|
|
578
|
-
logger.info(
|
|
684
|
+
logger.info("💬 Диалог завершен, возвращаем к обычному режиму")
|
|
579
685
|
# Диалог завершен, возвращаем к обычному режиму
|
|
580
686
|
await state.set_state(UserStates.waiting_for_message)
|
|
581
|
-
|
|
687
|
+
|
|
582
688
|
# Обрабатываем как обычное сообщение
|
|
583
689
|
data = await state.get_data()
|
|
584
|
-
session_id = data.get(
|
|
585
|
-
system_prompt = data.get(
|
|
586
|
-
|
|
690
|
+
session_id = data.get("session_id")
|
|
691
|
+
system_prompt = data.get("system_prompt")
|
|
692
|
+
|
|
587
693
|
if session_id:
|
|
588
694
|
await process_user_message(message, state, session_id, system_prompt)
|
|
589
695
|
else:
|
|
590
|
-
await send_message(
|
|
696
|
+
await send_message(
|
|
697
|
+
message, "Сессия не найдена. Пожалуйста, напишите /start"
|
|
698
|
+
)
|
|
699
|
+
|
|
591
700
|
|
|
592
701
|
# Обработчик для обычных сообщений (НЕ в admin_chat)
|
|
593
|
-
@router.message(StateFilter(UserStates.waiting_for_message), ~F.text.startswith(
|
|
702
|
+
@router.message(StateFilter(UserStates.waiting_for_message), ~F.text.startswith("/"))
|
|
594
703
|
async def user_message_handler(message: Message, state: FSMContext):
|
|
595
704
|
"""Обработчик сообщений пользователей (исключая admin_chat)"""
|
|
596
|
-
conversation_manager = get_global_var(
|
|
705
|
+
conversation_manager = get_global_var("conversation_manager")
|
|
597
706
|
from ..utils.debug_routing import debug_user_state
|
|
598
|
-
|
|
707
|
+
|
|
599
708
|
try:
|
|
600
709
|
await debug_user_state(message, state, "USER_MESSAGE_HANDLER")
|
|
601
|
-
|
|
710
|
+
|
|
602
711
|
# ✅ ВАЖНО: Сначала проверяем диалог с админом
|
|
603
|
-
conversation = await conversation_manager.is_user_in_admin_chat(
|
|
604
|
-
|
|
712
|
+
conversation = await conversation_manager.is_user_in_admin_chat(
|
|
713
|
+
message.from_user.id
|
|
714
|
+
)
|
|
715
|
+
|
|
605
716
|
if conversation:
|
|
606
|
-
logger.info(
|
|
607
|
-
|
|
608
|
-
|
|
717
|
+
logger.info(
|
|
718
|
+
"⚠️ НЕОЖИДАННО: пользователь в waiting_for_message, но есть диалог с админом!"
|
|
719
|
+
)
|
|
720
|
+
logger.info("🔄 Принудительно переключаем в admin_chat состояние")
|
|
721
|
+
|
|
609
722
|
# Принудительно переключаем состояние
|
|
610
723
|
await state.set_state(UserStates.admin_chat)
|
|
611
724
|
await state.update_data(admin_conversation=conversation)
|
|
612
|
-
|
|
725
|
+
|
|
613
726
|
# Обрабатываем сообщение как admin_chat
|
|
614
727
|
await user_in_admin_chat_handler(message, state)
|
|
615
728
|
return
|
|
616
|
-
|
|
617
|
-
logger.info(
|
|
729
|
+
|
|
730
|
+
logger.info("🤖 Обычный диалог с ботом")
|
|
618
731
|
data = await state.get_data()
|
|
619
|
-
session_id = data.get(
|
|
620
|
-
system_prompt = data.get(
|
|
621
|
-
|
|
732
|
+
session_id = data.get("session_id")
|
|
733
|
+
system_prompt = data.get("system_prompt")
|
|
734
|
+
|
|
622
735
|
if not session_id:
|
|
623
|
-
logger.warning(
|
|
624
|
-
await send_message(
|
|
736
|
+
logger.warning("❌ Нет session_id в состоянии")
|
|
737
|
+
await send_message(
|
|
738
|
+
message, "Сессия не найдена. Пожалуйста, напишите /start"
|
|
739
|
+
)
|
|
625
740
|
return
|
|
626
|
-
|
|
741
|
+
|
|
627
742
|
logger.info(f"📝 Обрабатываем сообщение с session_id: {session_id}")
|
|
628
743
|
await process_user_message(message, state, session_id, system_prompt)
|
|
629
|
-
|
|
744
|
+
|
|
630
745
|
except Exception as e:
|
|
631
746
|
logger.error(f"❌ Ошибка при обработке сообщения пользователя: {e}")
|
|
632
|
-
await send_message(
|
|
633
|
-
|
|
747
|
+
await send_message(
|
|
748
|
+
message,
|
|
749
|
+
"Произошла ошибка. Попробуйте еще раз или напишите /start для перезапуска.",
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
|
|
634
753
|
@router.callback_query(F.data == "voice_send")
|
|
635
754
|
async def voice_send_handler(callback: CallbackQuery, state: FSMContext):
|
|
636
755
|
"""Обработчик кнопки 'Отправить' для голосового сообщения"""
|
|
637
756
|
try:
|
|
638
757
|
data = await state.get_data()
|
|
639
|
-
recognized_text = data.get(
|
|
640
|
-
session_id = data.get(
|
|
641
|
-
system_prompt = data.get(
|
|
642
|
-
|
|
758
|
+
recognized_text = data.get("voice_recognized_text")
|
|
759
|
+
session_id = data.get("session_id")
|
|
760
|
+
system_prompt = data.get("system_prompt")
|
|
761
|
+
|
|
643
762
|
if not recognized_text or not session_id:
|
|
644
763
|
await callback.answer("❌ Ошибка: текст не найден", show_alert=True)
|
|
645
764
|
return
|
|
646
|
-
|
|
765
|
+
|
|
647
766
|
# Удаляем сообщение с кнопками
|
|
648
767
|
await callback.message.delete()
|
|
649
|
-
|
|
768
|
+
|
|
650
769
|
# Обрабатываем текст сразу без промежуточного сообщения
|
|
651
|
-
await process_voice_message(
|
|
652
|
-
|
|
770
|
+
await process_voice_message(
|
|
771
|
+
callback.message, state, session_id, system_prompt, recognized_text
|
|
772
|
+
)
|
|
773
|
+
|
|
653
774
|
# Возвращаем в обычное состояние
|
|
654
775
|
await state.set_state(UserStates.waiting_for_message)
|
|
655
776
|
await state.update_data(voice_recognized_text=None)
|
|
656
|
-
|
|
777
|
+
|
|
657
778
|
await callback.answer()
|
|
658
|
-
|
|
779
|
+
|
|
659
780
|
except Exception as e:
|
|
660
781
|
logger.error(f"❌ Ошибка отправки голосового: {e}")
|
|
661
782
|
await callback.answer("❌ Ошибка обработки", show_alert=True)
|
|
662
783
|
|
|
784
|
+
|
|
663
785
|
@router.callback_query(F.data == "voice_edit")
|
|
664
786
|
async def voice_edit_handler(callback: CallbackQuery, state: FSMContext):
|
|
665
787
|
"""Обработчик кнопки 'Изменить текст' для голосового сообщения"""
|
|
666
788
|
try:
|
|
667
789
|
data = await state.get_data()
|
|
668
|
-
recognized_text = data.get(
|
|
669
|
-
|
|
790
|
+
recognized_text = data.get("voice_recognized_text")
|
|
791
|
+
|
|
670
792
|
if not recognized_text:
|
|
671
793
|
await callback.answer("❌ Ошибка: текст не найден", show_alert=True)
|
|
672
794
|
return
|
|
673
|
-
|
|
795
|
+
|
|
674
796
|
# Переводим в режим редактирования
|
|
675
797
|
await state.set_state(UserStates.voice_editing)
|
|
676
|
-
|
|
798
|
+
|
|
677
799
|
# Показываем текст для редактирования (обычный формат)
|
|
678
800
|
await callback.message.edit_text(
|
|
679
801
|
f"✏️ Отредактируйте распознанный текст:\n\n"
|
|
680
802
|
f"{recognized_text}\n\n"
|
|
681
803
|
f"Напишите исправленный текст:"
|
|
682
804
|
)
|
|
683
|
-
|
|
805
|
+
|
|
684
806
|
await callback.answer()
|
|
685
|
-
|
|
807
|
+
|
|
686
808
|
except Exception as e:
|
|
687
809
|
logger.error(f"❌ Ошибка редактирования: {e}")
|
|
688
810
|
await callback.answer("❌ Ошибка", show_alert=True)
|
|
689
811
|
|
|
812
|
+
|
|
690
813
|
@router.callback_query(F.data == "voice_retry")
|
|
691
814
|
async def voice_retry_handler(callback: CallbackQuery, state: FSMContext):
|
|
692
815
|
"""Обработчик кнопки 'Надиктовать заново' для голосового сообщения"""
|
|
693
816
|
try:
|
|
694
817
|
# Удаляем сообщение с кнопками
|
|
695
818
|
await callback.message.delete()
|
|
696
|
-
|
|
819
|
+
|
|
697
820
|
# Возвращаем в обычное состояние
|
|
698
821
|
await state.set_state(UserStates.waiting_for_message)
|
|
699
822
|
await state.update_data(voice_recognized_text=None)
|
|
700
|
-
|
|
823
|
+
|
|
701
824
|
# Просим отправить заново
|
|
702
825
|
await callback.message.answer("🎤 Отправьте голосовое сообщение заново")
|
|
703
|
-
|
|
826
|
+
|
|
704
827
|
await callback.answer()
|
|
705
|
-
|
|
828
|
+
|
|
706
829
|
except Exception as e:
|
|
707
830
|
logger.error(f"❌ Ошибка повтора: {e}")
|
|
708
831
|
await callback.answer("❌ Ошибка", show_alert=True)
|
|
709
832
|
|
|
833
|
+
|
|
710
834
|
@router.message(StateFilter(UserStates.voice_editing))
|
|
711
835
|
async def voice_edit_text_handler(message: Message, state: FSMContext):
|
|
712
836
|
"""Обработчик получения отредактированного текста"""
|
|
713
837
|
try:
|
|
714
838
|
edited_text = message.text.strip()
|
|
715
|
-
|
|
839
|
+
|
|
716
840
|
if not edited_text:
|
|
717
841
|
await message.answer("⚠️ Текст не может быть пустым. Напишите текст:")
|
|
718
842
|
return
|
|
719
|
-
|
|
843
|
+
|
|
720
844
|
# Получаем данные сессии
|
|
721
845
|
data = await state.get_data()
|
|
722
|
-
session_id = data.get(
|
|
723
|
-
system_prompt = data.get(
|
|
724
|
-
|
|
846
|
+
session_id = data.get("session_id")
|
|
847
|
+
system_prompt = data.get("system_prompt")
|
|
848
|
+
|
|
725
849
|
if not session_id:
|
|
726
850
|
await message.answer("❌ Сессия не найдена. Напишите /start")
|
|
727
851
|
return
|
|
728
|
-
|
|
852
|
+
|
|
729
853
|
# Обрабатываем отредактированный текст сразу
|
|
730
|
-
await process_voice_message(
|
|
731
|
-
|
|
854
|
+
await process_voice_message(
|
|
855
|
+
message, state, session_id, system_prompt, edited_text
|
|
856
|
+
)
|
|
857
|
+
|
|
732
858
|
# Возвращаем в обычное состояние
|
|
733
859
|
await state.set_state(UserStates.waiting_for_message)
|
|
734
860
|
await state.update_data(voice_recognized_text=None)
|
|
735
|
-
|
|
861
|
+
|
|
736
862
|
except Exception as e:
|
|
737
863
|
logger.error(f"❌ Ошибка обработки отредактированного текста: {e}")
|
|
738
|
-
await message.answer(
|
|
864
|
+
await message.answer(
|
|
865
|
+
"❌ Ошибка обработки. Попробуйте еще раз или напишите /start"
|
|
866
|
+
)
|
|
867
|
+
|
|
739
868
|
|
|
740
869
|
@router.message()
|
|
741
870
|
async def catch_all_handler(message: Message, state: FSMContext):
|
|
742
871
|
"""Перехватчик всех необработанных сообщений"""
|
|
743
|
-
admin_manager = get_global_var(
|
|
872
|
+
admin_manager = get_global_var("admin_manager")
|
|
744
873
|
from ..utils.debug_routing import debug_user_state
|
|
745
|
-
|
|
874
|
+
|
|
746
875
|
await debug_user_state(message, state, "CATCH_ALL")
|
|
747
|
-
|
|
876
|
+
|
|
748
877
|
current_state = await state.get_state()
|
|
749
|
-
logger.warning(
|
|
750
|
-
|
|
878
|
+
logger.warning(
|
|
879
|
+
f"⚠️ НЕОБРАБОТАННОЕ СООБЩЕНИЕ от {message.from_user.id}: '{message.text}', состояние: {current_state}"
|
|
880
|
+
)
|
|
881
|
+
|
|
751
882
|
# Проверяем, админ ли это
|
|
752
883
|
if admin_manager.is_admin(message.from_user.id):
|
|
753
|
-
logger.info(
|
|
884
|
+
logger.info("👑 Необработанное сообщение админа")
|
|
754
885
|
await message.answer("Команда не распознана. Используйте /help для справки.")
|
|
755
886
|
else:
|
|
756
|
-
logger.info(
|
|
887
|
+
logger.info("👤 Необработанное сообщение пользователя")
|
|
757
888
|
await message.answer("Не понимаю. Напишите /start для начала диалога.")
|
|
758
889
|
|
|
759
|
-
|
|
890
|
+
|
|
891
|
+
async def process_user_message(
|
|
892
|
+
message: Message, state: FSMContext, session_id: str, system_prompt: str
|
|
893
|
+
):
|
|
760
894
|
"""Общая функция для обработки сообщений пользователя"""
|
|
761
|
-
supabase_client = get_global_var(
|
|
762
|
-
openai_client = get_global_var(
|
|
763
|
-
config = get_global_var(
|
|
764
|
-
bot = get_global_var(
|
|
765
|
-
prompt_loader = get_global_var(
|
|
766
|
-
message_hooks = get_global_var(
|
|
895
|
+
supabase_client = get_global_var("supabase_client")
|
|
896
|
+
openai_client = get_global_var("openai_client")
|
|
897
|
+
config = get_global_var("config")
|
|
898
|
+
bot = get_global_var("bot")
|
|
899
|
+
prompt_loader = get_global_var("prompt_loader")
|
|
900
|
+
message_hooks = get_global_var("message_hooks") or {}
|
|
767
901
|
from datetime import datetime
|
|
902
|
+
|
|
768
903
|
import pytz # Добавляем импорт для работы с временными зонами
|
|
769
|
-
|
|
904
|
+
|
|
770
905
|
try:
|
|
771
906
|
# ============ ХУК 1: ВАЛИДАЦИЯ СООБЩЕНИЯ ============
|
|
772
|
-
validators = message_hooks.get(
|
|
907
|
+
validators = message_hooks.get("validators", [])
|
|
773
908
|
for validator in validators:
|
|
774
909
|
try:
|
|
775
910
|
user_message = message.text
|
|
776
911
|
message_obj = message
|
|
777
|
-
|
|
912
|
+
|
|
778
913
|
should_continue = await validator(user_message, message_obj)
|
|
779
914
|
if not should_continue:
|
|
780
|
-
logger.info(
|
|
915
|
+
logger.info(
|
|
916
|
+
f"⛔ Валидатор '{validator.__name__}' прервал обработку"
|
|
917
|
+
)
|
|
781
918
|
return # Прерываем обработку
|
|
782
919
|
except Exception as e:
|
|
783
920
|
logger.error(f"❌ Ошибка в валидаторе '{validator.__name__}': {e}")
|
|
784
|
-
|
|
921
|
+
|
|
785
922
|
# Сохраняем сообщение пользователя
|
|
786
923
|
await supabase_client.add_message(
|
|
787
924
|
session_id=session_id,
|
|
788
|
-
role=
|
|
925
|
+
role="user",
|
|
789
926
|
content=message.text,
|
|
790
|
-
message_type=
|
|
927
|
+
message_type="text",
|
|
791
928
|
)
|
|
792
|
-
logger.info(
|
|
793
|
-
|
|
929
|
+
logger.info("✅ Сообщение пользователя сохранено в БД")
|
|
930
|
+
|
|
794
931
|
# Получаем историю сообщений
|
|
795
|
-
chat_history = await supabase_client.get_chat_history(
|
|
932
|
+
chat_history = await supabase_client.get_chat_history(
|
|
933
|
+
session_id, limit=config.MAX_CONTEXT_MESSAGES
|
|
934
|
+
)
|
|
796
935
|
logger.info(f"📚 Загружена история: {len(chat_history)} сообщений")
|
|
797
|
-
|
|
936
|
+
|
|
798
937
|
# ДОБАВЛЯЕМ ПОЛУЧЕНИЕ ТЕКУЩЕГО ВРЕМЕНИ
|
|
799
|
-
moscow_tz = pytz.timezone(
|
|
938
|
+
moscow_tz = pytz.timezone("Europe/Moscow")
|
|
800
939
|
current_time = datetime.now(moscow_tz)
|
|
801
|
-
time_info = current_time.strftime(
|
|
802
|
-
|
|
940
|
+
time_info = current_time.strftime("%H:%M, %d.%m.%Y, %A")
|
|
941
|
+
|
|
803
942
|
# Базовый системный промпт с временем
|
|
804
943
|
system_prompt_with_time = f"""
|
|
805
944
|
{system_prompt}
|
|
806
945
|
|
|
807
946
|
ТЕКУЩЕЕ ВРЕМЯ: {time_info} (московское время)
|
|
808
947
|
"""
|
|
809
|
-
|
|
948
|
+
|
|
810
949
|
# ============ ХУК 2: ОБОГАЩЕНИЕ ПРОМПТА ============
|
|
811
|
-
prompt_enrichers = message_hooks.get(
|
|
950
|
+
prompt_enrichers = message_hooks.get("prompt_enrichers", [])
|
|
812
951
|
for enricher in prompt_enrichers:
|
|
813
952
|
try:
|
|
814
953
|
system_prompt_with_time = await enricher(
|
|
815
|
-
system_prompt_with_time,
|
|
816
|
-
message.from_user.id
|
|
954
|
+
system_prompt_with_time, message.from_user.id
|
|
817
955
|
)
|
|
818
956
|
logger.info(f"✅ Промпт обогащен '{enricher.__name__}'")
|
|
819
957
|
except Exception as e:
|
|
820
|
-
logger.error(
|
|
821
|
-
|
|
958
|
+
logger.error(
|
|
959
|
+
f"❌ Ошибка в обогатителе промпта '{enricher.__name__}': {e}"
|
|
960
|
+
)
|
|
961
|
+
|
|
822
962
|
# Формируем контекст для OpenAI с обновленным системным промптом
|
|
823
963
|
messages = [{"role": "system", "content": system_prompt_with_time}]
|
|
824
|
-
|
|
825
|
-
for msg in chat_history[
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
964
|
+
|
|
965
|
+
for msg in chat_history[
|
|
966
|
+
-config.MAX_CONTEXT_MESSAGES :
|
|
967
|
+
]: # Ограничиваем контекст
|
|
968
|
+
messages.append({"role": msg["role"], "content": msg["content"]})
|
|
969
|
+
|
|
831
970
|
# Добавляем финальные инструкции в конец контекста
|
|
832
971
|
final_instructions = await prompt_loader.load_final_instructions()
|
|
833
972
|
if final_instructions:
|
|
834
973
|
messages.append({"role": "system", "content": final_instructions})
|
|
835
|
-
logger.info(
|
|
836
|
-
|
|
974
|
+
logger.info(
|
|
975
|
+
f"🎯 Добавлены финальные инструкции ({len(final_instructions)} символов)"
|
|
976
|
+
)
|
|
977
|
+
|
|
837
978
|
# ============ ХУК 3: ОБОГАЩЕНИЕ КОНТЕКСТА ============
|
|
838
|
-
context_enrichers = message_hooks.get(
|
|
979
|
+
context_enrichers = message_hooks.get("context_enrichers", [])
|
|
839
980
|
for enricher in context_enrichers:
|
|
840
981
|
try:
|
|
841
|
-
messages = await enricher(
|
|
842
|
-
messages
|
|
843
|
-
)
|
|
982
|
+
messages = await enricher(messages)
|
|
844
983
|
logger.info(f"✅ Контекст обогащен '{enricher.__name__}'")
|
|
845
984
|
except Exception as e:
|
|
846
|
-
logger.error(
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
985
|
+
logger.error(
|
|
986
|
+
f"❌ Ошибка в обогатителе контекста '{enricher.__name__}': {e}"
|
|
987
|
+
)
|
|
988
|
+
|
|
989
|
+
logger.info(
|
|
990
|
+
f"📝 Контекст сформирован: {len(messages)} сообщений (включая время: {time_info})"
|
|
991
|
+
)
|
|
992
|
+
|
|
850
993
|
await bot.send_chat_action(message.chat.id, "typing")
|
|
851
|
-
|
|
994
|
+
|
|
852
995
|
start_time = time.time()
|
|
853
996
|
ai_response = await openai_client.get_completion(messages)
|
|
854
997
|
processing_time = int((time.time() - start_time) * 1000)
|
|
855
998
|
|
|
856
|
-
logger.info(
|
|
857
|
-
|
|
999
|
+
logger.info(
|
|
1000
|
+
f"🤖 OpenAI ответил за {processing_time}мс, длина ответа: {len(ai_response) if ai_response else 0}"
|
|
1001
|
+
)
|
|
1002
|
+
|
|
858
1003
|
# ИСПРАВЛЕННАЯ ЛОГИКА: инициализируем все переменные заранее
|
|
859
1004
|
tokens_used = 0
|
|
860
1005
|
ai_metadata = {}
|
|
@@ -862,29 +1007,33 @@ async def process_user_message(message: Message, state: FSMContext, session_id:
|
|
|
862
1007
|
|
|
863
1008
|
# Проверяем ответ
|
|
864
1009
|
if not ai_response or not ai_response.strip():
|
|
865
|
-
logger.warning(
|
|
866
|
-
|
|
1010
|
+
logger.warning("❌ OpenAI вернул пустой/пробельный ответ!")
|
|
1011
|
+
|
|
867
1012
|
# Проверяем, были ли использованы токены при пустом ответе
|
|
868
|
-
if hasattr(openai_client,
|
|
869
|
-
logger.warning(
|
|
870
|
-
|
|
1013
|
+
if hasattr(openai_client, "last_completion_tokens"):
|
|
1014
|
+
logger.warning(
|
|
1015
|
+
f"⚠️ Токены использованы ({openai_client.last_completion_tokens}), но ответ пустой"
|
|
1016
|
+
)
|
|
1017
|
+
|
|
871
1018
|
# Устанавливаем fallback ответ
|
|
872
1019
|
fallback_message = "Извините, произошла техническая ошибка. Попробуйте переформулировать вопрос или напишите /start для перезапуска."
|
|
873
1020
|
ai_response = fallback_message
|
|
874
1021
|
response_text = fallback_message
|
|
875
1022
|
tokens_used = 0
|
|
876
1023
|
ai_metadata = {}
|
|
877
|
-
|
|
1024
|
+
|
|
878
1025
|
else:
|
|
879
|
-
logger.info(
|
|
880
|
-
|
|
1026
|
+
logger.info("📤 Сырой ответ OpenAI получен, обрабатываем...")
|
|
1027
|
+
|
|
881
1028
|
tokens_used = openai_client.estimate_tokens(ai_response)
|
|
882
|
-
|
|
1029
|
+
|
|
883
1030
|
# Парсим JSON метаданные
|
|
884
1031
|
response_text, ai_metadata = parse_ai_response(ai_response)
|
|
885
1032
|
|
|
886
|
-
logger.info(
|
|
887
|
-
logger.info(
|
|
1033
|
+
logger.info("🔍 После парсинга JSON:")
|
|
1034
|
+
logger.info(
|
|
1035
|
+
f" 📝 Текст ответа: {len(response_text)} символов: '{response_text[:100]}...'"
|
|
1036
|
+
)
|
|
888
1037
|
logger.info(f" 📊 Метаданные: {ai_metadata}")
|
|
889
1038
|
|
|
890
1039
|
# Более надежная проверка
|
|
@@ -893,97 +1042,105 @@ async def process_user_message(message: Message, state: FSMContext, session_id:
|
|
|
893
1042
|
response_text = ai_response
|
|
894
1043
|
ai_metadata = {}
|
|
895
1044
|
elif not response_text.strip():
|
|
896
|
-
logger.warning(
|
|
1045
|
+
logger.warning(
|
|
1046
|
+
"⚠️ JSON найден, но текст ответа пустой! Используем исходный ответ."
|
|
1047
|
+
)
|
|
897
1048
|
response_text = ai_response
|
|
898
1049
|
|
|
899
|
-
logger.info(
|
|
900
|
-
|
|
1050
|
+
logger.info(
|
|
1051
|
+
f"✅ Финальный текст для отправки: {len(response_text)} символов"
|
|
1052
|
+
)
|
|
1053
|
+
|
|
901
1054
|
# ============ ХУК 4: ОБРАБОТКА ОТВЕТА ============
|
|
902
|
-
response_processors = message_hooks.get(
|
|
1055
|
+
response_processors = message_hooks.get("response_processors", [])
|
|
903
1056
|
for processor in response_processors:
|
|
904
1057
|
try:
|
|
905
1058
|
response_text, ai_metadata = await processor(
|
|
906
|
-
response_text,
|
|
907
|
-
ai_metadata,
|
|
908
|
-
message.from_user.id
|
|
1059
|
+
response_text, ai_metadata, message.from_user.id
|
|
909
1060
|
)
|
|
910
1061
|
logger.info(f"✅ Ответ обработан '{processor.__name__}'")
|
|
911
1062
|
except Exception as e:
|
|
912
|
-
logger.error(
|
|
913
|
-
|
|
1063
|
+
logger.error(
|
|
1064
|
+
f"❌ Ошибка в обработчике ответа '{processor.__name__}': {e}"
|
|
1065
|
+
)
|
|
1066
|
+
|
|
914
1067
|
# Обновляем этап сессии и качество лида
|
|
915
1068
|
if ai_metadata:
|
|
916
1069
|
logger.info("🔍 Анализ метаданных от ИИ:")
|
|
917
|
-
|
|
1070
|
+
|
|
918
1071
|
# Вывод информации об этапе
|
|
919
|
-
stage = ai_metadata.get(
|
|
1072
|
+
stage = ai_metadata.get("этап")
|
|
920
1073
|
if stage:
|
|
921
1074
|
logger.info(f" 📈 Этап диалога: {stage}")
|
|
922
|
-
|
|
1075
|
+
|
|
923
1076
|
# Вывод информации о качестве лида
|
|
924
|
-
quality = ai_metadata.get(
|
|
1077
|
+
quality = ai_metadata.get("качество")
|
|
925
1078
|
if quality is not None:
|
|
926
1079
|
quality_emoji = "⭐" * min(quality, 5) # Максимум 5 звезд
|
|
927
1080
|
logger.info(f" {quality_emoji} Качество лида: {quality}/10")
|
|
928
|
-
|
|
1081
|
+
|
|
929
1082
|
# Обновляем в базе данных
|
|
930
1083
|
if stage or quality is not None:
|
|
931
1084
|
await supabase_client.update_session_stage(session_id, stage, quality)
|
|
932
|
-
logger.info(
|
|
933
|
-
|
|
1085
|
+
logger.info(" ✅ Этап и качество обновлены в БД")
|
|
1086
|
+
|
|
934
1087
|
# Обрабатываем события
|
|
935
|
-
events = ai_metadata.get(
|
|
1088
|
+
events = ai_metadata.get("события", [])
|
|
936
1089
|
if events:
|
|
937
1090
|
logger.info(f"\n🔔 События в диалоге ({len(events)}):")
|
|
938
1091
|
for idx, event in enumerate(events, 1):
|
|
939
|
-
event_type = event.get(
|
|
940
|
-
event_info = event.get(
|
|
1092
|
+
event_type = event.get("тип", "неизвестно")
|
|
1093
|
+
event_info = event.get("инфо", "нет информации")
|
|
941
1094
|
|
|
942
1095
|
# Подбираем эмодзи для разных типов событий
|
|
943
1096
|
event_emoji = {
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
}.get(event_type.lower(),
|
|
1097
|
+
"телефон": "📱",
|
|
1098
|
+
"email": "📧",
|
|
1099
|
+
"встреча": "📅",
|
|
1100
|
+
"заказ": "🛍️",
|
|
1101
|
+
"вопрос": "❓",
|
|
1102
|
+
"консультация": "💬",
|
|
1103
|
+
"жалоба": "⚠️",
|
|
1104
|
+
"отзыв": "💭",
|
|
1105
|
+
}.get(event_type.lower(), "📌")
|
|
953
1106
|
|
|
954
1107
|
logger.info(f" {idx}. {event_emoji} {event_type}: {event_info}")
|
|
955
1108
|
|
|
956
1109
|
# Обрабатываем события в системе
|
|
957
|
-
should_send_response = await process_events(
|
|
958
|
-
|
|
959
|
-
|
|
1110
|
+
should_send_response = await process_events(
|
|
1111
|
+
session_id, events, message.from_user.id
|
|
1112
|
+
)
|
|
1113
|
+
logger.warning(
|
|
1114
|
+
f" ✅ События обработаны, should_send_response = {should_send_response}"
|
|
1115
|
+
)
|
|
1116
|
+
|
|
960
1117
|
# Обрабатываем файлы и каталоги
|
|
961
|
-
files_list = ai_metadata.get(
|
|
962
|
-
directories_list = ai_metadata.get(
|
|
963
|
-
|
|
1118
|
+
files_list = ai_metadata.get("файлы", [])
|
|
1119
|
+
directories_list = ai_metadata.get("каталоги", [])
|
|
1120
|
+
|
|
964
1121
|
# Форматируем информацию о файлах
|
|
965
1122
|
if files_list:
|
|
966
1123
|
logger.info("📎 Найденные файлы:")
|
|
967
1124
|
for idx, file in enumerate(files_list, 1):
|
|
968
1125
|
logger.info(f" {idx}. 📄 {file}")
|
|
969
|
-
|
|
1126
|
+
|
|
970
1127
|
# Форматируем информацию о каталогах
|
|
971
1128
|
if directories_list:
|
|
972
1129
|
logger.info("📂 Найденные каталоги:")
|
|
973
1130
|
for idx, directory in enumerate(directories_list, 1):
|
|
974
1131
|
logger.info(f" {idx}. 📁 {directory}")
|
|
975
|
-
|
|
1132
|
+
|
|
976
1133
|
# Добавляем информацию в текст ответа
|
|
977
1134
|
if files_list or directories_list:
|
|
978
1135
|
files_info = []
|
|
979
1136
|
if files_list:
|
|
980
1137
|
files_str = "\n".join(f"• {file}" for file in files_list)
|
|
981
1138
|
files_info.append(f"\n\n📎 Доступные файлы:\n{files_str}")
|
|
982
|
-
|
|
1139
|
+
|
|
983
1140
|
if directories_list:
|
|
984
1141
|
dirs_str = "\n".join(f"• {directory}" for directory in directories_list)
|
|
985
1142
|
files_info.append(f"\n\n📂 Доступные каталоги:\n{dirs_str}")
|
|
986
|
-
|
|
1143
|
+
|
|
987
1144
|
else:
|
|
988
1145
|
logger.info("📎 Файлы и каталоги не указаны")
|
|
989
1146
|
|
|
@@ -991,14 +1148,14 @@ async def process_user_message(message: Message, state: FSMContext, session_id:
|
|
|
991
1148
|
try:
|
|
992
1149
|
await supabase_client.add_message(
|
|
993
1150
|
session_id=session_id,
|
|
994
|
-
role=
|
|
1151
|
+
role="assistant",
|
|
995
1152
|
content=response_text,
|
|
996
|
-
message_type=
|
|
1153
|
+
message_type="text",
|
|
997
1154
|
tokens_used=tokens_used,
|
|
998
1155
|
processing_time_ms=processing_time,
|
|
999
|
-
ai_metadata=ai_metadata
|
|
1156
|
+
ai_metadata=ai_metadata,
|
|
1000
1157
|
)
|
|
1001
|
-
logger.info(
|
|
1158
|
+
logger.info("✅ Ответ ассистента сохранен в БД")
|
|
1002
1159
|
except Exception as e:
|
|
1003
1160
|
logger.error(f"❌ Ошибка сохранения ответа в БД: {e}")
|
|
1004
1161
|
|
|
@@ -1006,129 +1163,161 @@ async def process_user_message(message: Message, state: FSMContext, session_id:
|
|
|
1006
1163
|
if config.DEBUG_MODE:
|
|
1007
1164
|
# В режиме отладки показываем полный ответ с JSON
|
|
1008
1165
|
final_response = ai_response
|
|
1009
|
-
logger.info(
|
|
1166
|
+
logger.info("🐛 Режим отладки: отправляем полный ответ с JSON")
|
|
1010
1167
|
else:
|
|
1011
1168
|
# В обычном режиме показываем только текст без JSON
|
|
1012
1169
|
final_response = response_text
|
|
1013
|
-
logger.info(
|
|
1170
|
+
logger.info("👤 Обычный режим: отправляем очищенный текст")
|
|
1014
1171
|
|
|
1015
1172
|
# Проверяем, что есть что отправлять
|
|
1016
1173
|
if not final_response or not final_response.strip():
|
|
1017
|
-
logger.error(
|
|
1174
|
+
logger.error("❌ КРИТИЧЕСКАЯ ОШИБКА: Финальный ответ пуст!")
|
|
1018
1175
|
final_response = "Извините, произошла ошибка при формировании ответа. Попробуйте еще раз."
|
|
1019
1176
|
|
|
1020
1177
|
logger.info(f"📱 Отправляем пользователю: {len(final_response)} символов")
|
|
1021
|
-
|
|
1178
|
+
|
|
1022
1179
|
# ============ ПРОВЕРКА: НУЖНО ЛИ ОТПРАВЛЯТЬ СООБЩЕНИЕ ОТ ИИ ============
|
|
1023
1180
|
# Проверяем флаг из событий (если события запретили отправку)
|
|
1024
|
-
logger.warning(
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1181
|
+
logger.warning(
|
|
1182
|
+
f"🔍 Проверка should_send_response: exists={('should_send_response' in locals())}, value={locals().get('should_send_response', 'NOT_SET')}"
|
|
1183
|
+
)
|
|
1184
|
+
|
|
1185
|
+
if "should_send_response" in locals() and not should_send_response:
|
|
1186
|
+
logger.warning(
|
|
1187
|
+
"🔇🔇🔇 СОБЫТИЯ ЗАПРЕТИЛИ ОТПРАВКУ СООБЩЕНИЯ ОТ ИИ, ПРОПУСКАЕМ ОТПРАВКУ 🔇🔇🔇"
|
|
1188
|
+
)
|
|
1028
1189
|
return
|
|
1029
|
-
|
|
1190
|
+
|
|
1030
1191
|
# ============ ХУК 5: ФИЛЬТРЫ ОТПРАВКИ ============
|
|
1031
|
-
send_filters = message_hooks.get(
|
|
1192
|
+
send_filters = message_hooks.get("send_filters", [])
|
|
1032
1193
|
for filter_func in send_filters:
|
|
1033
1194
|
try:
|
|
1034
1195
|
should_send = await filter_func(message.from_user.id)
|
|
1035
1196
|
if should_send:
|
|
1036
1197
|
# True = блокируем (для совместимости с should_block_ai_response)
|
|
1037
|
-
logger.info(
|
|
1198
|
+
logger.info(
|
|
1199
|
+
f"⛔ Фильтр '{filter_func.__name__}' заблокировал отправку (вернул True)"
|
|
1200
|
+
)
|
|
1038
1201
|
return # Не отправляем
|
|
1039
1202
|
except Exception as e:
|
|
1040
|
-
logger.error(
|
|
1041
|
-
|
|
1203
|
+
logger.error(
|
|
1204
|
+
f"❌ Ошибка в фильтре отправки '{filter_func.__name__}': {e}"
|
|
1205
|
+
)
|
|
1206
|
+
|
|
1042
1207
|
# Отправляем ответ пользователю
|
|
1043
1208
|
try:
|
|
1044
|
-
await send_message(
|
|
1045
|
-
|
|
1209
|
+
await send_message(
|
|
1210
|
+
message,
|
|
1211
|
+
final_response,
|
|
1212
|
+
files_list=files_list,
|
|
1213
|
+
directories_list=directories_list,
|
|
1214
|
+
)
|
|
1215
|
+
logger.info(
|
|
1216
|
+
f"✅ Ответ успешно отправлен пользователю {message.from_user.id}"
|
|
1217
|
+
)
|
|
1046
1218
|
except Exception as e:
|
|
1047
1219
|
logger.error(f"❌ ОШИБКА ОТПРАВКИ СООБЩЕНИЯ: {e}")
|
|
1048
1220
|
# Пытаемся отправить простое сообщение об ошибке
|
|
1049
1221
|
try:
|
|
1050
|
-
await message.answer(
|
|
1222
|
+
await message.answer(
|
|
1223
|
+
"Произошла ошибка при отправке ответа. Попробуйте еще раз."
|
|
1224
|
+
)
|
|
1051
1225
|
except Exception as e2:
|
|
1052
1226
|
logger.error(f"❌ Не удалось отправить даже сообщение об ошибке: {e2}")
|
|
1053
|
-
|
|
1227
|
+
|
|
1054
1228
|
except Exception as e:
|
|
1055
1229
|
logger.error(f"❌ КРИТИЧЕСКАЯ ОШИБКА в process_user_message: {e}")
|
|
1056
1230
|
logger.exception("Полный стек ошибки:")
|
|
1057
1231
|
try:
|
|
1058
|
-
await message.answer(
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1232
|
+
await message.answer(
|
|
1233
|
+
"Произошла критическая ошибка. Попробуйте написать /start для перезапуска."
|
|
1234
|
+
)
|
|
1235
|
+
except Exception:
|
|
1236
|
+
logger.error("❌ Не удалось отправить сообщение об критической ошибке", exc_info=True)
|
|
1237
|
+
|
|
1238
|
+
|
|
1239
|
+
async def process_voice_message(
|
|
1240
|
+
message: Message,
|
|
1241
|
+
state: FSMContext,
|
|
1242
|
+
session_id: str,
|
|
1243
|
+
system_prompt: str,
|
|
1244
|
+
recognized_text: str,
|
|
1245
|
+
):
|
|
1063
1246
|
"""Обработка распознанного голосового сообщения"""
|
|
1064
|
-
supabase_client = get_global_var(
|
|
1065
|
-
openai_client = get_global_var(
|
|
1066
|
-
config = get_global_var(
|
|
1067
|
-
bot = get_global_var(
|
|
1068
|
-
prompt_loader = get_global_var(
|
|
1069
|
-
|
|
1247
|
+
supabase_client = get_global_var("supabase_client")
|
|
1248
|
+
openai_client = get_global_var("openai_client")
|
|
1249
|
+
config = get_global_var("config")
|
|
1250
|
+
bot = get_global_var("bot")
|
|
1251
|
+
prompt_loader = get_global_var("prompt_loader")
|
|
1252
|
+
|
|
1070
1253
|
from datetime import datetime
|
|
1254
|
+
|
|
1071
1255
|
import pytz
|
|
1072
|
-
|
|
1256
|
+
|
|
1073
1257
|
try:
|
|
1074
1258
|
# Сохраняем распознанное сообщение как текст пользователя
|
|
1075
1259
|
await supabase_client.add_message(
|
|
1076
1260
|
session_id=session_id,
|
|
1077
|
-
role=
|
|
1261
|
+
role="user",
|
|
1078
1262
|
content=recognized_text,
|
|
1079
|
-
message_type=
|
|
1080
|
-
metadata={
|
|
1263
|
+
message_type="text", # БД разрешает только 'text'
|
|
1264
|
+
metadata={
|
|
1265
|
+
"original_type": "voice",
|
|
1266
|
+
"duration": message.voice.duration if message.voice else 0,
|
|
1267
|
+
},
|
|
1081
1268
|
)
|
|
1082
|
-
logger.info(
|
|
1083
|
-
|
|
1269
|
+
logger.info("✅ Распознанное сообщение сохранено в БД")
|
|
1270
|
+
|
|
1084
1271
|
# Получаем историю сообщений
|
|
1085
|
-
chat_history = await supabase_client.get_chat_history(
|
|
1272
|
+
chat_history = await supabase_client.get_chat_history(
|
|
1273
|
+
session_id, limit=config.MAX_CONTEXT_MESSAGES
|
|
1274
|
+
)
|
|
1086
1275
|
logger.info(f"📚 Загружена история: {len(chat_history)} сообщений")
|
|
1087
|
-
|
|
1276
|
+
|
|
1088
1277
|
# Добавляем текущее время
|
|
1089
|
-
moscow_tz = pytz.timezone(
|
|
1278
|
+
moscow_tz = pytz.timezone("Europe/Moscow")
|
|
1090
1279
|
current_time = datetime.now(moscow_tz)
|
|
1091
|
-
time_info = current_time.strftime(
|
|
1092
|
-
|
|
1280
|
+
time_info = current_time.strftime("%H:%M, %d.%m.%Y, %A")
|
|
1281
|
+
|
|
1093
1282
|
system_prompt_with_time = f"""
|
|
1094
1283
|
{system_prompt}
|
|
1095
1284
|
|
|
1096
1285
|
ТЕКУЩЕЕ ВРЕМЯ: {time_info} (московское время)
|
|
1097
1286
|
"""
|
|
1098
|
-
|
|
1287
|
+
|
|
1099
1288
|
# Формируем контекст для OpenAI
|
|
1100
1289
|
messages = [{"role": "system", "content": system_prompt_with_time}]
|
|
1101
|
-
|
|
1102
|
-
for msg in chat_history[-config.MAX_CONTEXT_MESSAGES:]:
|
|
1103
|
-
messages.append({
|
|
1104
|
-
|
|
1105
|
-
"content": msg['content']
|
|
1106
|
-
})
|
|
1107
|
-
|
|
1290
|
+
|
|
1291
|
+
for msg in chat_history[-config.MAX_CONTEXT_MESSAGES :]:
|
|
1292
|
+
messages.append({"role": msg["role"], "content": msg["content"]})
|
|
1293
|
+
|
|
1108
1294
|
# Добавляем финальные инструкции
|
|
1109
1295
|
final_instructions = await prompt_loader.load_final_instructions()
|
|
1110
1296
|
if final_instructions:
|
|
1111
1297
|
messages.append({"role": "system", "content": final_instructions})
|
|
1112
|
-
logger.info(
|
|
1113
|
-
|
|
1298
|
+
logger.info("🎯 Добавлены финальные инструкции")
|
|
1299
|
+
|
|
1114
1300
|
logger.info(f"📝 Контекст сформирован: {len(messages)} сообщений")
|
|
1115
|
-
|
|
1301
|
+
|
|
1116
1302
|
await bot.send_chat_action(message.chat.id, "typing")
|
|
1117
|
-
|
|
1303
|
+
|
|
1118
1304
|
import time
|
|
1305
|
+
|
|
1119
1306
|
start_time = time.time()
|
|
1120
1307
|
ai_response = await openai_client.get_completion(messages)
|
|
1121
1308
|
processing_time = int((time.time() - start_time) * 1000)
|
|
1122
1309
|
|
|
1123
1310
|
logger.info(f"🤖 OpenAI ответил за {processing_time}мс")
|
|
1124
|
-
|
|
1311
|
+
|
|
1125
1312
|
tokens_used = 0
|
|
1126
1313
|
ai_metadata = {}
|
|
1127
1314
|
response_text = ""
|
|
1128
1315
|
|
|
1129
1316
|
if not ai_response or not ai_response.strip():
|
|
1130
|
-
logger.warning(
|
|
1131
|
-
fallback_message =
|
|
1317
|
+
logger.warning("❌ OpenAI вернул пустой ответ!")
|
|
1318
|
+
fallback_message = (
|
|
1319
|
+
"Извините, произошла техническая ошибка. Попробуйте еще раз."
|
|
1320
|
+
)
|
|
1132
1321
|
ai_response = fallback_message
|
|
1133
1322
|
response_text = fallback_message
|
|
1134
1323
|
tokens_used = 0
|
|
@@ -1141,56 +1330,71 @@ async def process_voice_message(message: Message, state: FSMContext, session_id:
|
|
|
1141
1330
|
response_text = ai_response
|
|
1142
1331
|
ai_metadata = {}
|
|
1143
1332
|
elif not response_text.strip():
|
|
1144
|
-
logger.warning(
|
|
1333
|
+
logger.warning(
|
|
1334
|
+
"⚠️ JSON найден, но текст пустой! Используем исходный ответ."
|
|
1335
|
+
)
|
|
1145
1336
|
response_text = ai_response
|
|
1146
1337
|
|
|
1147
1338
|
# Обновляем этап и качество
|
|
1148
1339
|
if ai_metadata:
|
|
1149
|
-
stage = ai_metadata.get(
|
|
1150
|
-
quality = ai_metadata.get(
|
|
1151
|
-
|
|
1340
|
+
stage = ai_metadata.get("этап")
|
|
1341
|
+
quality = ai_metadata.get("качество")
|
|
1342
|
+
|
|
1152
1343
|
if stage or quality is not None:
|
|
1153
|
-
await supabase_client.update_session_stage(
|
|
1154
|
-
|
|
1344
|
+
await supabase_client.update_session_stage(
|
|
1345
|
+
session_id, stage, quality
|
|
1346
|
+
)
|
|
1347
|
+
|
|
1155
1348
|
# Обрабатываем события
|
|
1156
|
-
events = ai_metadata.get(
|
|
1349
|
+
events = ai_metadata.get("події", [])
|
|
1157
1350
|
if events:
|
|
1158
|
-
should_send_response = await process_events(
|
|
1351
|
+
should_send_response = await process_events(
|
|
1352
|
+
session_id, events, message.from_user.id
|
|
1353
|
+
)
|
|
1159
1354
|
|
|
1160
1355
|
# Сохраняем ответ ассистента
|
|
1161
1356
|
await supabase_client.add_message(
|
|
1162
1357
|
session_id=session_id,
|
|
1163
|
-
role=
|
|
1358
|
+
role="assistant",
|
|
1164
1359
|
content=response_text,
|
|
1165
|
-
message_type=
|
|
1360
|
+
message_type="text",
|
|
1166
1361
|
tokens_used=tokens_used,
|
|
1167
1362
|
processing_time_ms=processing_time,
|
|
1168
|
-
ai_metadata=ai_metadata
|
|
1363
|
+
ai_metadata=ai_metadata,
|
|
1169
1364
|
)
|
|
1170
1365
|
|
|
1171
1366
|
# Определяем финальный ответ
|
|
1172
1367
|
final_response = ai_response if config.DEBUG_MODE else response_text
|
|
1173
|
-
|
|
1368
|
+
|
|
1174
1369
|
if not final_response or not final_response.strip():
|
|
1175
1370
|
final_response = "Извините, произошла ошибка при формировании ответа. Попробуйте еще раз."
|
|
1176
1371
|
|
|
1177
1372
|
# Обрабатываем файлы
|
|
1178
|
-
files_list = ai_metadata.get(
|
|
1179
|
-
directories_list = ai_metadata.get(
|
|
1180
|
-
|
|
1373
|
+
files_list = ai_metadata.get("файлы", [])
|
|
1374
|
+
directories_list = ai_metadata.get("каталоги", [])
|
|
1375
|
+
|
|
1181
1376
|
# Проверяем, нужно ли отправлять сообщение от ИИ
|
|
1182
|
-
if
|
|
1183
|
-
logger.info(
|
|
1377
|
+
if "should_send_response" in locals() and not should_send_response:
|
|
1378
|
+
logger.info(
|
|
1379
|
+
"🔇 События запретили отправку сообщения от ИИ (voice), пропускаем отправку"
|
|
1380
|
+
)
|
|
1184
1381
|
return
|
|
1185
|
-
|
|
1382
|
+
|
|
1186
1383
|
# Отправляем ответ пользователю
|
|
1187
|
-
await send_message(
|
|
1384
|
+
await send_message(
|
|
1385
|
+
message,
|
|
1386
|
+
final_response,
|
|
1387
|
+
files_list=files_list,
|
|
1388
|
+
directories_list=directories_list,
|
|
1389
|
+
)
|
|
1188
1390
|
logger.info(f"✅ Ответ отправлен пользователю {message.from_user.id}")
|
|
1189
|
-
|
|
1391
|
+
|
|
1190
1392
|
except Exception as e:
|
|
1191
1393
|
logger.error(f"❌ КРИТИЧЕСКАЯ ОШИБКА в process_voice_message: {e}")
|
|
1192
1394
|
logger.exception("Полный стек ошибки:")
|
|
1193
1395
|
try:
|
|
1194
|
-
await message.answer(
|
|
1195
|
-
|
|
1196
|
-
|
|
1396
|
+
await message.answer(
|
|
1397
|
+
"Произошла критическая ошибка. Попробуйте написать /start"
|
|
1398
|
+
)
|
|
1399
|
+
except Exception:
|
|
1400
|
+
pass
|