smart-bot-factory 0.1.2__py3-none-any.whl → 0.1.3__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/__init__.py +51 -0
- smart_bot_factory/admin/__init__.py +16 -0
- smart_bot_factory/admin/admin_logic.py +430 -0
- smart_bot_factory/admin/admin_manager.py +141 -0
- smart_bot_factory/admin/admin_migration.sql +136 -0
- smart_bot_factory/admin/admin_tester.py +151 -0
- smart_bot_factory/admin/timeout_checker.py +499 -0
- smart_bot_factory/analytics/__init__.py +7 -0
- smart_bot_factory/analytics/analytics_manager.py +355 -0
- smart_bot_factory/cli.py +642 -0
- smart_bot_factory/config.py +235 -0
- smart_bot_factory/configs/growthmed-helper/env_example.txt +1 -0
- smart_bot_factory/configs/growthmed-helper/prompts/1sales_context.txt +9 -0
- smart_bot_factory/configs/growthmed-helper/prompts/2product_info.txt +582 -0
- smart_bot_factory/configs/growthmed-helper/prompts/3objection_handling.txt +66 -0
- smart_bot_factory/configs/growthmed-helper/prompts/final_instructions.txt +232 -0
- smart_bot_factory/configs/growthmed-helper/prompts/help_message.txt +28 -0
- smart_bot_factory/configs/growthmed-helper/prompts/welcome_message.txt +7 -0
- smart_bot_factory/configs/growthmed-helper/welcome_file/welcome_file_msg.txt +16 -0
- smart_bot_factory/configs/growthmed-helper/welcome_file//342/225/250/320/267/342/225/250/342/225/241/342/225/250/342/225/221 /342/225/250/342/225/227/342/225/250/342/225/225/342/225/244/320/221/342/225/244/320/222 /342/225/250/342/224/220/342/225/250/342/225/233 152/342/225/250/320/264/342/225/250/320/247 /342/225/250/342/225/225 323/342/225/250/320/264/342/225/250/320/247 /342/225/250/342/224/244/342/225/250/342/225/227/342/225/244/320/237 /342/225/250/342/225/235/342/225/250/342/225/241/342/225/250/342/224/244/342/225/250/342/225/225/342/225/244/320/226/342/225/250/342/225/225/342/225/250/342/225/234/342/225/244/320/233.pdf +0 -0
- smart_bot_factory/configs/growthmed-october-24/prompts/1sales_context.txt +16 -0
- smart_bot_factory/configs/growthmed-october-24/prompts/2product_info.txt +582 -0
- smart_bot_factory/configs/growthmed-october-24/prompts/3objection_handling.txt +66 -0
- smart_bot_factory/configs/growthmed-october-24/prompts/final_instructions.txt +212 -0
- smart_bot_factory/configs/growthmed-october-24/prompts/help_message.txt +28 -0
- smart_bot_factory/configs/growthmed-october-24/prompts/welcome_message.txt +8 -0
- smart_bot_factory/configs/growthmed-october-24/reports/test_20250924_064229.txt +818 -0
- smart_bot_factory/configs/growthmed-october-24/reports/test_20250924_064335.txt +32 -0
- smart_bot_factory/configs/growthmed-october-24/reports/test_20250924_064638.txt +35 -0
- smart_bot_factory/configs/growthmed-october-24/tests/quick_scenarios.yaml +66 -0
- smart_bot_factory/configs/growthmed-october-24/tests/realistic_scenarios.yaml +108 -0
- smart_bot_factory/configs/growthmed-october-24/tests/scenario_examples.yaml +46 -0
- smart_bot_factory/configs/growthmed-october-24/welcome_file/welcome_file_msg.txt +16 -0
- smart_bot_factory/configs/growthmed-october-24/welcome_file//342/225/250/320/267/342/225/250/342/225/241/342/225/250/342/225/221 /342/225/250/342/225/227/342/225/250/342/225/225/342/225/244/320/221/342/225/244/320/222 /342/225/250/342/224/220/342/225/250/342/225/233 152/342/225/250/320/264/342/225/250/320/247 /342/225/250/342/225/225 323/342/225/250/320/264/342/225/250/320/247 /342/225/250/342/224/244/342/225/250/342/225/227/342/225/244/320/237 /342/225/250/342/225/235/342/225/250/342/225/241/342/225/250/342/224/244/342/225/250/342/225/225/342/225/244/320/226/342/225/250/342/225/225/342/225/250/342/225/234/342/225/244/320/233.pdf +0 -0
- smart_bot_factory/core/__init__.py +22 -0
- smart_bot_factory/core/bot_utils.py +693 -0
- smart_bot_factory/core/conversation_manager.py +536 -0
- smart_bot_factory/core/decorators.py +229 -0
- smart_bot_factory/core/message_sender.py +249 -0
- smart_bot_factory/core/states.py +14 -0
- smart_bot_factory/creation/__init__.py +8 -0
- smart_bot_factory/creation/bot_builder.py +329 -0
- smart_bot_factory/creation/bot_testing.py +986 -0
- smart_bot_factory/database/database_structure.sql +57 -0
- smart_bot_factory/database/schema.sql +1094 -0
- smart_bot_factory/handlers/handlers.py +583 -0
- smart_bot_factory/integrations/__init__.py +9 -0
- smart_bot_factory/integrations/openai_client.py +435 -0
- smart_bot_factory/integrations/supabase_client.py +592 -0
- smart_bot_factory/setup_checker.py +476 -0
- smart_bot_factory/utils/__init__.py +9 -0
- smart_bot_factory/utils/debug_routing.py +103 -0
- smart_bot_factory/utils/prompt_loader.py +427 -0
- smart_bot_factory/uv.lock +2004 -0
- smart_bot_factory-0.1.3.dist-info/METADATA +126 -0
- smart_bot_factory-0.1.3.dist-info/RECORD +59 -0
- smart_bot_factory-0.1.3.dist-info/licenses/LICENSE +24 -0
- smart_bot_factory-0.1.2.dist-info/METADATA +0 -31
- smart_bot_factory-0.1.2.dist-info/RECORD +0 -4
- {smart_bot_factory-0.1.2.dist-info → smart_bot_factory-0.1.3.dist-info}/WHEEL +0 -0
- {smart_bot_factory-0.1.2.dist-info → smart_bot_factory-0.1.3.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
# Исправленный handlers.py с отладкой маршрутизации
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
from aiogram import Router, F
|
|
6
|
+
from aiogram.filters import Command, StateFilter
|
|
7
|
+
from aiogram.fsm.context import FSMContext
|
|
8
|
+
from aiogram.types import Message
|
|
9
|
+
|
|
10
|
+
from ..core.bot_utils import send_message, parse_ai_response, process_events, send_welcome_file
|
|
11
|
+
from ..core.states import UserStates, AdminStates
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
# Создаем роутер для обработчиков
|
|
16
|
+
router = Router()
|
|
17
|
+
|
|
18
|
+
def setup_handlers(dp):
|
|
19
|
+
"""Настройка основных обработчиков"""
|
|
20
|
+
# Подключаем middleware
|
|
21
|
+
router.message.middleware()(admin_middleware)
|
|
22
|
+
|
|
23
|
+
# Регистрируем роутер
|
|
24
|
+
dp.include_router(router)
|
|
25
|
+
|
|
26
|
+
# Функция для получения глобальных переменных
|
|
27
|
+
def get_global_var(var_name):
|
|
28
|
+
"""Получает глобальную переменную из модуля handlers"""
|
|
29
|
+
import sys
|
|
30
|
+
current_module = sys.modules[__name__]
|
|
31
|
+
return getattr(current_module, var_name, None)
|
|
32
|
+
|
|
33
|
+
# Middleware для проверки админов
|
|
34
|
+
async def admin_middleware(handler, event: Message, data: dict):
|
|
35
|
+
"""Middleware для обновления информации об админах"""
|
|
36
|
+
admin_manager = get_global_var('admin_manager')
|
|
37
|
+
|
|
38
|
+
if admin_manager and admin_manager.is_admin(event.from_user.id):
|
|
39
|
+
await admin_manager.update_admin_info(event.from_user)
|
|
40
|
+
|
|
41
|
+
return await handler(event, data)
|
|
42
|
+
|
|
43
|
+
@router.message(Command(commands=["start", "старт", "ст"]))
|
|
44
|
+
async def start_handler(message: Message, state: FSMContext):
|
|
45
|
+
"""Обработчик команды /start - сброс сессии и начало заново"""
|
|
46
|
+
admin_manager = get_global_var('admin_manager')
|
|
47
|
+
from ..admin.admin_logic import admin_start_handler
|
|
48
|
+
from ..utils.debug_routing import debug_user_state
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
await debug_user_state(message, state, "START_COMMAND")
|
|
52
|
+
|
|
53
|
+
# Проверяем, админ ли это и в каком режиме
|
|
54
|
+
if admin_manager.is_admin(message.from_user.id):
|
|
55
|
+
if admin_manager.is_in_admin_mode(message.from_user.id):
|
|
56
|
+
# Админ в режиме администратора - работаем как админ
|
|
57
|
+
await admin_start_handler(message, state)
|
|
58
|
+
return
|
|
59
|
+
# Админ в режиме пользователя - работаем как обычный пользователь
|
|
60
|
+
|
|
61
|
+
await user_start_handler(message, state)
|
|
62
|
+
|
|
63
|
+
except Exception as e:
|
|
64
|
+
logger.error(f"Ошибка при обработке /start: {e}")
|
|
65
|
+
await send_message(message, "Произошла ошибка при инициализации. Попробуйте позже.")
|
|
66
|
+
|
|
67
|
+
async def user_start_handler(message: Message, state: FSMContext):
|
|
68
|
+
"""Обработчик /start для обычных пользователей"""
|
|
69
|
+
supabase_client = get_global_var('supabase_client')
|
|
70
|
+
prompt_loader = get_global_var('prompt_loader')
|
|
71
|
+
from ..core.bot_utils import parse_utm_from_start_param
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
# 0. ПОЛУЧАЕМ UTM ДАННЫЕ
|
|
75
|
+
start_param = message.text.split(' ', 1)[1] if len(message.text.split()) > 1 else None
|
|
76
|
+
|
|
77
|
+
# Логируем входящий start параметр
|
|
78
|
+
# Пример поддерживаемого формата: @https://t.me/bot?start=utmSource-vk_utmCampaign-summer2025 не более 64 символов после strat=
|
|
79
|
+
|
|
80
|
+
logger.info(f"📥 Получен start параметр: '{start_param}'")
|
|
81
|
+
|
|
82
|
+
utm_data = {}
|
|
83
|
+
if start_param:
|
|
84
|
+
# Парсим UTM данные
|
|
85
|
+
utm_data = parse_utm_from_start_param(start_param)
|
|
86
|
+
|
|
87
|
+
# Подробное логирование UTM
|
|
88
|
+
logger.info(f"📊 UTM данные для пользователя {message.from_user.id}:")
|
|
89
|
+
if utm_data:
|
|
90
|
+
for key, value in utm_data.items():
|
|
91
|
+
logger.info(f" • {key}: {value}")
|
|
92
|
+
logger.info(f"✅ UTM данные успешно распознаны")
|
|
93
|
+
else:
|
|
94
|
+
logger.warning(f"⚠️ UTM данные не найдены в параметре: '{start_param}'")
|
|
95
|
+
else:
|
|
96
|
+
logger.info("ℹ️ Start параметр отсутствует (обычный /start)")
|
|
97
|
+
|
|
98
|
+
# 1. ЯВНО ОЧИЩАЕМ СОСТОЯНИЕ FSM
|
|
99
|
+
await state.clear()
|
|
100
|
+
logger.info(f"🔄 Состояние FSM очищено для пользователя {message.from_user.id}")
|
|
101
|
+
|
|
102
|
+
# 2. ЗАГРУЖАЕМ ПРОМПТЫ
|
|
103
|
+
logger.info(f"Загрузка промптов для пользователя {message.from_user.id}")
|
|
104
|
+
system_prompt = await prompt_loader.load_system_prompt()
|
|
105
|
+
|
|
106
|
+
# Загружаем приветственное сообщение
|
|
107
|
+
welcome_message = await prompt_loader.load_welcome_message()
|
|
108
|
+
|
|
109
|
+
# 3. ПОЛУЧАЕМ ДАННЫЕ ПОЛЬЗОВАТЕЛЯ
|
|
110
|
+
user_data = {
|
|
111
|
+
'telegram_id': message.from_user.id,
|
|
112
|
+
'username': message.from_user.username,
|
|
113
|
+
'first_name': message.from_user.first_name,
|
|
114
|
+
'last_name': message.from_user.last_name,
|
|
115
|
+
'language_code': message.from_user.language_code,
|
|
116
|
+
'source': utm_data.get('utm_source'),
|
|
117
|
+
'medium': utm_data.get('utm_medium'),
|
|
118
|
+
'campaign': utm_data.get('utm_campaign'),
|
|
119
|
+
'content': utm_data.get('utm_content'),
|
|
120
|
+
'term': utm_data.get('utm_term')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# 4. СОЗДАЕМ НОВУЮ СЕССИЮ (автоматически закроет активные)
|
|
124
|
+
# Добавляем UTM данные в метаданные пользователя
|
|
125
|
+
if utm_data:
|
|
126
|
+
user_data['metadata'] = {'utm_data': utm_data}
|
|
127
|
+
logger.info(f"📈 UTM данные добавлены в метаданные пользователя")
|
|
128
|
+
|
|
129
|
+
session_id = await supabase_client.create_chat_session(user_data, system_prompt)
|
|
130
|
+
logger.info(f"✅ Создана новая сессия {session_id} для пользователя {message.from_user.id}")
|
|
131
|
+
|
|
132
|
+
# 5. УСТАНАВЛИВАЕМ НОВОЕ СОСТОЯНИЕ
|
|
133
|
+
await state.update_data(session_id=session_id, system_prompt=system_prompt)
|
|
134
|
+
await state.set_state(UserStates.waiting_for_message)
|
|
135
|
+
|
|
136
|
+
# 6. ОТПРАВЛЯЕМ ПРИВЕТСТВЕННОЕ СООБЩЕНИЕ
|
|
137
|
+
await send_message(message, welcome_message)
|
|
138
|
+
logger.info(f"Приветственное сообщение отправлено пользователю {message.from_user.id}")
|
|
139
|
+
|
|
140
|
+
# 7. ЕСЛИ ЕСТЬ ФАЙЛ ОТПРАВЛЯЕМ ВМЕСТЕ С ПОДПИСЬЮ
|
|
141
|
+
logging.info(f"📎 Попытка отправки приветственного файла для сессии {session_id}")
|
|
142
|
+
caption = await send_welcome_file(message)
|
|
143
|
+
|
|
144
|
+
# 8. СОХРАНЯЕМ ПРИВЕТСТВЕННОЕ СООБЩЕНИЕ В БД
|
|
145
|
+
if caption:
|
|
146
|
+
logging.info(f"📄 Добавление подписи к файлу в приветственное сообщение для сессии {session_id}")
|
|
147
|
+
welcome_message = f"{welcome_message}\n\nПодпись к файлу:\n\n{caption}"
|
|
148
|
+
else:
|
|
149
|
+
logging.info(f"📄 Приветственный файл отправлен без подписи для сессии {session_id}")
|
|
150
|
+
|
|
151
|
+
logging.info(f"💾 Сохранение приветственного сообщения в БД для сессии {session_id}")
|
|
152
|
+
|
|
153
|
+
await supabase_client.add_message(
|
|
154
|
+
session_id=session_id,
|
|
155
|
+
role='assistant',
|
|
156
|
+
content=welcome_message,
|
|
157
|
+
message_type='text'
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
logging.info(f"✅ Приветственное сообщение успешно сохранено в БД для сессии {session_id}")
|
|
161
|
+
|
|
162
|
+
except Exception as e:
|
|
163
|
+
logger.error(f"Ошибка при обработке user /start: {e}")
|
|
164
|
+
await send_message(message, "Произошла ошибка при инициализации. Попробуйте позже.")
|
|
165
|
+
|
|
166
|
+
@router.message(StateFilter(None))
|
|
167
|
+
async def message_without_state_handler(message: Message, state: FSMContext):
|
|
168
|
+
"""Обработчик сообщений без состояния (после перезапуска бота)"""
|
|
169
|
+
admin_manager = get_global_var('admin_manager')
|
|
170
|
+
supabase_client = get_global_var('supabase_client')
|
|
171
|
+
conversation_manager = get_global_var('conversation_manager')
|
|
172
|
+
from ..admin.admin_logic import AdminStates
|
|
173
|
+
from ..utils.debug_routing import debug_user_state
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
await debug_user_state(message, state, "NO_STATE")
|
|
177
|
+
|
|
178
|
+
# СНАЧАЛА проверяем диалог с админом
|
|
179
|
+
conversation = await conversation_manager.is_user_in_admin_chat(message.from_user.id)
|
|
180
|
+
|
|
181
|
+
if conversation:
|
|
182
|
+
logger.info(f"✅ Найден диалог с админом {conversation['admin_id']}, устанавливаем состояние admin_chat")
|
|
183
|
+
|
|
184
|
+
# Устанавливаем состояние admin_chat
|
|
185
|
+
await state.set_state(UserStates.admin_chat)
|
|
186
|
+
await state.update_data(admin_conversation=conversation)
|
|
187
|
+
|
|
188
|
+
# Сразу пересылаем сообщение админу
|
|
189
|
+
await conversation_manager.forward_message_to_admin(message, conversation)
|
|
190
|
+
|
|
191
|
+
# Сохраняем сообщение в БД
|
|
192
|
+
session_info = await supabase_client.get_active_session(message.from_user.id)
|
|
193
|
+
if session_info:
|
|
194
|
+
await supabase_client.add_message(
|
|
195
|
+
session_id=session_info['id'],
|
|
196
|
+
role='user',
|
|
197
|
+
content=message.text,
|
|
198
|
+
message_type='text',
|
|
199
|
+
metadata={'in_admin_chat': True, 'admin_id': conversation['admin_id']}
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
# Проверяем, админ ли это
|
|
205
|
+
if admin_manager.is_admin(message.from_user.id):
|
|
206
|
+
logger.info(f"👑 Админ в режиме администратора без состояния")
|
|
207
|
+
await state.set_state(AdminStates.admin_mode)
|
|
208
|
+
await message.answer("👑 Режим администратора\nИспользуйте /start для панели управления")
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
logger.info(f"👤 Обычный пользователь без состояния, ищем активную сессию")
|
|
212
|
+
|
|
213
|
+
# Ищем активную сессию в БД
|
|
214
|
+
session_info = await supabase_client.get_active_session(message.from_user.id)
|
|
215
|
+
|
|
216
|
+
if session_info:
|
|
217
|
+
logger.info(f"📝 Восстанавливаем сессию {session_info['id']}")
|
|
218
|
+
# Восстанавливаем сессию из БД
|
|
219
|
+
session_id = session_info['id']
|
|
220
|
+
system_prompt = session_info['system_prompt']
|
|
221
|
+
|
|
222
|
+
# Сохраняем в состояние
|
|
223
|
+
await state.update_data(session_id=session_id, system_prompt=system_prompt)
|
|
224
|
+
await state.set_state(UserStates.waiting_for_message)
|
|
225
|
+
|
|
226
|
+
logger.info(f"✅ Сессия восстановлена, обрабатываем сообщение")
|
|
227
|
+
|
|
228
|
+
# Теперь обрабатываем сообщение как обычно
|
|
229
|
+
await process_user_message(message, state, session_id, system_prompt)
|
|
230
|
+
else:
|
|
231
|
+
logger.info(f"❌ Нет активной сессии, просим написать /start")
|
|
232
|
+
await send_message(message, "Привет! Напишите /start для начала диалога.")
|
|
233
|
+
|
|
234
|
+
except Exception as e:
|
|
235
|
+
logger.error(f"❌ Ошибка при обработке сообщения без состояния: {e}")
|
|
236
|
+
await send_message(message, "Произошла ошибка. Попробуйте написать /start для начала диалога.")
|
|
237
|
+
|
|
238
|
+
# ✅ ИСПРАВЛЕНИЕ: Обработчик admin_chat должен быть ПЕРВЫМ и более приоритетным
|
|
239
|
+
@router.message(StateFilter(UserStates.admin_chat))
|
|
240
|
+
async def user_in_admin_chat_handler(message: Message, state: FSMContext):
|
|
241
|
+
"""ПРИОРИТЕТНЫЙ обработчик сообщений пользователей в диалоге с админом"""
|
|
242
|
+
conversation_manager = get_global_var('conversation_manager')
|
|
243
|
+
supabase_client = get_global_var('supabase_client')
|
|
244
|
+
from ..utils.debug_routing import debug_user_state
|
|
245
|
+
|
|
246
|
+
await debug_user_state(message, state, "ADMIN_CHAT_HANDLER")
|
|
247
|
+
|
|
248
|
+
user_id = message.from_user.id
|
|
249
|
+
logger.info(f"🎯 ADMIN_CHAT HANDLER: сообщение от {user_id}: '{message.text}'")
|
|
250
|
+
|
|
251
|
+
# Проверяем, есть ли еще активный диалог
|
|
252
|
+
conversation = await conversation_manager.is_user_in_admin_chat(user_id)
|
|
253
|
+
|
|
254
|
+
if conversation:
|
|
255
|
+
logger.info(f"✅ Диалог активен, пересылаем админу {conversation['admin_id']}")
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
# Сохраняем сообщение в БД
|
|
259
|
+
session_info = await supabase_client.get_active_session(user_id)
|
|
260
|
+
if session_info:
|
|
261
|
+
await supabase_client.add_message(
|
|
262
|
+
session_id=session_info['id'],
|
|
263
|
+
role='user',
|
|
264
|
+
content=message.text,
|
|
265
|
+
message_type='text',
|
|
266
|
+
metadata={'in_admin_chat': True, 'admin_id': conversation['admin_id']}
|
|
267
|
+
)
|
|
268
|
+
logger.info(f"💾 Сообщение сохранено в БД")
|
|
269
|
+
|
|
270
|
+
# Пересылаем админу
|
|
271
|
+
await conversation_manager.forward_message_to_admin(message, conversation)
|
|
272
|
+
logger.info(f"📤 Сообщение переслано админу")
|
|
273
|
+
|
|
274
|
+
except Exception as e:
|
|
275
|
+
logger.error(f"❌ Ошибка обработки admin_chat: {e}")
|
|
276
|
+
await message.answer("Произошла ошибка. Попробуйте позже.")
|
|
277
|
+
else:
|
|
278
|
+
logger.info(f"💬 Диалог завершен, возвращаем к обычному режиму")
|
|
279
|
+
# Диалог завершен, возвращаем к обычному режиму
|
|
280
|
+
await state.set_state(UserStates.waiting_for_message)
|
|
281
|
+
|
|
282
|
+
# Обрабатываем как обычное сообщение
|
|
283
|
+
data = await state.get_data()
|
|
284
|
+
session_id = data.get('session_id')
|
|
285
|
+
system_prompt = data.get('system_prompt')
|
|
286
|
+
|
|
287
|
+
if session_id:
|
|
288
|
+
await process_user_message(message, state, session_id, system_prompt)
|
|
289
|
+
else:
|
|
290
|
+
await send_message(message, "Сессия не найдена. Пожалуйста, напишите /start")
|
|
291
|
+
|
|
292
|
+
# Обработчик для обычных сообщений (НЕ в admin_chat)
|
|
293
|
+
@router.message(StateFilter(UserStates.waiting_for_message), ~F.text.startswith('/'))
|
|
294
|
+
async def user_message_handler(message: Message, state: FSMContext):
|
|
295
|
+
"""Обработчик сообщений пользователей (исключая admin_chat)"""
|
|
296
|
+
conversation_manager = get_global_var('conversation_manager')
|
|
297
|
+
from ..utils.debug_routing import debug_user_state
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
await debug_user_state(message, state, "USER_MESSAGE_HANDLER")
|
|
301
|
+
|
|
302
|
+
# ✅ ВАЖНО: Сначала проверяем диалог с админом
|
|
303
|
+
conversation = await conversation_manager.is_user_in_admin_chat(message.from_user.id)
|
|
304
|
+
|
|
305
|
+
if conversation:
|
|
306
|
+
logger.info(f"⚠️ НЕОЖИДАННО: пользователь в waiting_for_message, но есть диалог с админом!")
|
|
307
|
+
logger.info(f"🔄 Принудительно переключаем в admin_chat состояние")
|
|
308
|
+
|
|
309
|
+
# Принудительно переключаем состояние
|
|
310
|
+
await state.set_state(UserStates.admin_chat)
|
|
311
|
+
await state.update_data(admin_conversation=conversation)
|
|
312
|
+
|
|
313
|
+
# Обрабатываем сообщение как admin_chat
|
|
314
|
+
await user_in_admin_chat_handler(message, state)
|
|
315
|
+
return
|
|
316
|
+
|
|
317
|
+
logger.info(f"🤖 Обычный диалог с ботом")
|
|
318
|
+
data = await state.get_data()
|
|
319
|
+
session_id = data.get('session_id')
|
|
320
|
+
system_prompt = data.get('system_prompt')
|
|
321
|
+
|
|
322
|
+
if not session_id:
|
|
323
|
+
logger.warning(f"❌ Нет session_id в состоянии")
|
|
324
|
+
await send_message(message, "Сессия не найдена. Пожалуйста, напишите /start")
|
|
325
|
+
return
|
|
326
|
+
|
|
327
|
+
logger.info(f"📝 Обрабатываем сообщение с session_id: {session_id}")
|
|
328
|
+
await process_user_message(message, state, session_id, system_prompt)
|
|
329
|
+
|
|
330
|
+
except Exception as e:
|
|
331
|
+
logger.error(f"❌ Ошибка при обработке сообщения пользователя: {e}")
|
|
332
|
+
await send_message(message, "Произошла ошибка. Попробуйте еще раз или напишите /start для перезапуска.")
|
|
333
|
+
|
|
334
|
+
@router.message()
|
|
335
|
+
async def catch_all_handler(message: Message, state: FSMContext):
|
|
336
|
+
"""Перехватчик всех необработанных сообщений"""
|
|
337
|
+
admin_manager = get_global_var('admin_manager')
|
|
338
|
+
from ..utils.debug_routing import debug_user_state
|
|
339
|
+
|
|
340
|
+
await debug_user_state(message, state, "CATCH_ALL")
|
|
341
|
+
|
|
342
|
+
current_state = await state.get_state()
|
|
343
|
+
logger.warning(f"⚠️ НЕОБРАБОТАННОЕ СООБЩЕНИЕ от {message.from_user.id}: '{message.text}', состояние: {current_state}")
|
|
344
|
+
|
|
345
|
+
# Проверяем, админ ли это
|
|
346
|
+
if admin_manager.is_admin(message.from_user.id):
|
|
347
|
+
logger.info(f"👑 Необработанное сообщение админа")
|
|
348
|
+
await message.answer("Команда не распознана. Используйте /help для справки.")
|
|
349
|
+
else:
|
|
350
|
+
logger.info(f"👤 Необработанное сообщение пользователя")
|
|
351
|
+
await message.answer("Не понимаю. Напишите /start для начала диалога.")
|
|
352
|
+
|
|
353
|
+
async def process_user_message(message: Message, state: FSMContext, session_id: str, system_prompt: str):
|
|
354
|
+
"""Общая функция для обработки сообщений пользователя"""
|
|
355
|
+
supabase_client = get_global_var('supabase_client')
|
|
356
|
+
openai_client = get_global_var('openai_client')
|
|
357
|
+
config = get_global_var('config')
|
|
358
|
+
bot = get_global_var('bot')
|
|
359
|
+
prompt_loader = get_global_var('prompt_loader')
|
|
360
|
+
from datetime import datetime
|
|
361
|
+
import pytz # Добавляем импорт для работы с временными зонами
|
|
362
|
+
|
|
363
|
+
try:
|
|
364
|
+
# Сохраняем сообщение пользователя
|
|
365
|
+
await supabase_client.add_message(
|
|
366
|
+
session_id=session_id,
|
|
367
|
+
role='user',
|
|
368
|
+
content=message.text,
|
|
369
|
+
message_type='text'
|
|
370
|
+
)
|
|
371
|
+
logger.info(f"✅ Сообщение пользователя сохранено в БД")
|
|
372
|
+
|
|
373
|
+
# Получаем историю сообщений
|
|
374
|
+
chat_history = await supabase_client.get_chat_history(session_id, limit=config.MAX_CONTEXT_MESSAGES)
|
|
375
|
+
logger.info(f"📚 Загружена история: {len(chat_history)} сообщений")
|
|
376
|
+
|
|
377
|
+
# ДОБАВЛЯЕМ ПОЛУЧЕНИЕ ТЕКУЩЕГО ВРЕМЕНИ
|
|
378
|
+
moscow_tz = pytz.timezone('Europe/Moscow')
|
|
379
|
+
current_time = datetime.now(moscow_tz)
|
|
380
|
+
time_info = current_time.strftime('%H:%M, %d.%m.%Y, %A')
|
|
381
|
+
|
|
382
|
+
# Модифицируем системный промпт, добавляя время
|
|
383
|
+
system_prompt_with_time = f"""
|
|
384
|
+
{system_prompt}
|
|
385
|
+
|
|
386
|
+
ТЕКУЩЕЕ ВРЕМЯ: {time_info} (московское время)
|
|
387
|
+
"""
|
|
388
|
+
|
|
389
|
+
# Формируем контекст для OpenAI с обновленным системным промптом
|
|
390
|
+
messages = [{"role": "system", "content": system_prompt_with_time}]
|
|
391
|
+
|
|
392
|
+
for msg in chat_history[-config.MAX_CONTEXT_MESSAGES:]: # Ограничиваем контекст
|
|
393
|
+
messages.append({
|
|
394
|
+
"role": msg['role'],
|
|
395
|
+
"content": msg['content']
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
# Добавляем финальные инструкции в конец контекста
|
|
399
|
+
final_instructions = await prompt_loader.load_final_instructions()
|
|
400
|
+
if final_instructions:
|
|
401
|
+
messages.append({"role": "system", "content": final_instructions})
|
|
402
|
+
logger.info(f"🎯 Добавлены финальные инструкции ({len(final_instructions)} символов)")
|
|
403
|
+
|
|
404
|
+
logger.info(f"📝 Контекст сформирован: {len(messages)} сообщений (включая время: {time_info})")
|
|
405
|
+
|
|
406
|
+
await bot.send_chat_action(message.chat.id, "typing")
|
|
407
|
+
|
|
408
|
+
start_time = time.time()
|
|
409
|
+
ai_response = await openai_client.get_completion(messages)
|
|
410
|
+
processing_time = int((time.time() - start_time) * 1000)
|
|
411
|
+
|
|
412
|
+
logger.info(f"🤖 OpenAI ответил за {processing_time}мс, длина ответа: {len(ai_response) if ai_response else 0}")
|
|
413
|
+
|
|
414
|
+
# ИСПРАВЛЕННАЯ ЛОГИКА: инициализируем все переменные заранее
|
|
415
|
+
tokens_used = 0
|
|
416
|
+
ai_metadata = {}
|
|
417
|
+
response_text = ""
|
|
418
|
+
|
|
419
|
+
# Проверяем ответ
|
|
420
|
+
if not ai_response or not ai_response.strip():
|
|
421
|
+
logger.warning(f"❌ OpenAI вернул пустой/пробельный ответ!")
|
|
422
|
+
|
|
423
|
+
# Проверяем, были ли использованы токены при пустом ответе
|
|
424
|
+
if hasattr(openai_client, 'last_completion_tokens'):
|
|
425
|
+
logger.warning(f"⚠️ Токены использованы ({openai_client.last_completion_tokens}), но ответ пустой")
|
|
426
|
+
|
|
427
|
+
# Устанавливаем fallback ответ
|
|
428
|
+
fallback_message = "Извините, произошла техническая ошибка. Попробуйте переформулировать вопрос или напишите /start для перезапуска."
|
|
429
|
+
ai_response = fallback_message
|
|
430
|
+
response_text = fallback_message
|
|
431
|
+
tokens_used = 0
|
|
432
|
+
ai_metadata = {}
|
|
433
|
+
|
|
434
|
+
else:
|
|
435
|
+
logger.info(f"📤 Сырой ответ OpenAI получен, обрабатываем...")
|
|
436
|
+
|
|
437
|
+
tokens_used = openai_client.estimate_tokens(ai_response)
|
|
438
|
+
|
|
439
|
+
# Парсим JSON метаданные
|
|
440
|
+
response_text, ai_metadata = parse_ai_response(ai_response)
|
|
441
|
+
|
|
442
|
+
logger.info(f"🔍 После парсинга JSON:")
|
|
443
|
+
logger.info(f" 📝 Текст ответа: {len(response_text)} символов: '{response_text[:100]}...'")
|
|
444
|
+
logger.info(f" 📊 Метаданные: {ai_metadata}")
|
|
445
|
+
|
|
446
|
+
# Более надежная проверка
|
|
447
|
+
if not ai_metadata:
|
|
448
|
+
logger.info("ℹ️ JSON не найден, используем исходный ответ")
|
|
449
|
+
response_text = ai_response
|
|
450
|
+
ai_metadata = {}
|
|
451
|
+
elif not response_text.strip():
|
|
452
|
+
logger.warning("⚠️ JSON найден, но текст ответа пустой! Используем исходный ответ.")
|
|
453
|
+
response_text = ai_response
|
|
454
|
+
|
|
455
|
+
logger.info(f"✅ Финальный текст для отправки: {len(response_text)} символов")
|
|
456
|
+
|
|
457
|
+
# Обновляем этап сессии и качество лида
|
|
458
|
+
if ai_metadata:
|
|
459
|
+
logger.info("🔍 Анализ метаданных от ИИ:")
|
|
460
|
+
|
|
461
|
+
# Вывод информации об этапе
|
|
462
|
+
stage = ai_metadata.get('этап')
|
|
463
|
+
if stage:
|
|
464
|
+
logger.info(f" 📈 Этап диалога: {stage}")
|
|
465
|
+
|
|
466
|
+
# Вывод информации о качестве лида
|
|
467
|
+
quality = ai_metadata.get('качество')
|
|
468
|
+
if quality is not None:
|
|
469
|
+
quality_emoji = "⭐" * min(quality, 5) # Максимум 5 звезд
|
|
470
|
+
logger.info(f" {quality_emoji} Качество лида: {quality}/10")
|
|
471
|
+
|
|
472
|
+
# Обновляем в базе данных
|
|
473
|
+
if stage or quality is not None:
|
|
474
|
+
await supabase_client.update_session_stage(session_id, stage, quality)
|
|
475
|
+
logger.info(f" ✅ Этап и качество обновлены в БД")
|
|
476
|
+
|
|
477
|
+
# Обрабатываем события
|
|
478
|
+
events = ai_metadata.get('события', [])
|
|
479
|
+
if events:
|
|
480
|
+
logger.info(f"\n🔔 События в диалоге ({len(events)}):")
|
|
481
|
+
for idx, event in enumerate(events, 1):
|
|
482
|
+
event_type = event.get('тип', 'неизвестно')
|
|
483
|
+
event_info = event.get('инфо', 'нет информации')
|
|
484
|
+
|
|
485
|
+
# Подбираем эмодзи для разных типов событий
|
|
486
|
+
event_emoji = {
|
|
487
|
+
'телефон': '📱',
|
|
488
|
+
'email': '📧',
|
|
489
|
+
'встреча': '📅',
|
|
490
|
+
'заказ': '🛍️',
|
|
491
|
+
'вопрос': '❓',
|
|
492
|
+
'консультация': '💬',
|
|
493
|
+
'жалоба': '⚠️',
|
|
494
|
+
'отзыв': '💭'
|
|
495
|
+
}.get(event_type.lower(), '📌')
|
|
496
|
+
|
|
497
|
+
logger.info(f" {idx}. {event_emoji} {event_type}: {event_info}")
|
|
498
|
+
|
|
499
|
+
# Обрабатываем события в системе
|
|
500
|
+
await process_events(session_id, events, message.from_user.id)
|
|
501
|
+
logger.info(" ✅ События обработаны")
|
|
502
|
+
|
|
503
|
+
# Обрабатываем файлы и каталоги
|
|
504
|
+
files_list = ai_metadata.get('файлы', [])
|
|
505
|
+
directories_list = ai_metadata.get('каталоги', [])
|
|
506
|
+
|
|
507
|
+
# Форматируем информацию о файлах
|
|
508
|
+
if files_list:
|
|
509
|
+
logger.info("📎 Найденные файлы:")
|
|
510
|
+
for idx, file in enumerate(files_list, 1):
|
|
511
|
+
logger.info(f" {idx}. 📄 {file}")
|
|
512
|
+
|
|
513
|
+
# Форматируем информацию о каталогах
|
|
514
|
+
if directories_list:
|
|
515
|
+
logger.info("📂 Найденные каталоги:")
|
|
516
|
+
for idx, directory in enumerate(directories_list, 1):
|
|
517
|
+
logger.info(f" {idx}. 📁 {directory}")
|
|
518
|
+
|
|
519
|
+
# Добавляем информацию в текст ответа
|
|
520
|
+
if files_list or directories_list:
|
|
521
|
+
files_info = []
|
|
522
|
+
if files_list:
|
|
523
|
+
files_str = "\n".join(f"• {file}" for file in files_list)
|
|
524
|
+
files_info.append(f"\n\n📎 Доступные файлы:\n{files_str}")
|
|
525
|
+
|
|
526
|
+
if directories_list:
|
|
527
|
+
dirs_str = "\n".join(f"• {directory}" for directory in directories_list)
|
|
528
|
+
files_info.append(f"\n\n📂 Доступные каталоги:\n{dirs_str}")
|
|
529
|
+
|
|
530
|
+
else:
|
|
531
|
+
logger.info("📎 Файлы и каталоги не указаны")
|
|
532
|
+
|
|
533
|
+
# Сохраняем ответ ассистента с метаданными
|
|
534
|
+
try:
|
|
535
|
+
await supabase_client.add_message(
|
|
536
|
+
session_id=session_id,
|
|
537
|
+
role='assistant',
|
|
538
|
+
content=response_text,
|
|
539
|
+
message_type='text',
|
|
540
|
+
tokens_used=tokens_used,
|
|
541
|
+
processing_time_ms=processing_time,
|
|
542
|
+
ai_metadata=ai_metadata
|
|
543
|
+
)
|
|
544
|
+
logger.info(f"✅ Ответ ассистента сохранен в БД")
|
|
545
|
+
except Exception as e:
|
|
546
|
+
logger.error(f"❌ Ошибка сохранения ответа в БД: {e}")
|
|
547
|
+
|
|
548
|
+
# Определяем финальный ответ для пользователя
|
|
549
|
+
if config.DEBUG_MODE:
|
|
550
|
+
# В режиме отладки показываем полный ответ с JSON
|
|
551
|
+
final_response = ai_response
|
|
552
|
+
logger.info(f"🐛 Режим отладки: отправляем полный ответ с JSON")
|
|
553
|
+
else:
|
|
554
|
+
# В обычном режиме показываем только текст без JSON
|
|
555
|
+
final_response = response_text
|
|
556
|
+
logger.info(f"👤 Обычный режим: отправляем очищенный текст")
|
|
557
|
+
|
|
558
|
+
# Проверяем, что есть что отправлять
|
|
559
|
+
if not final_response or not final_response.strip():
|
|
560
|
+
logger.error(f"❌ КРИТИЧЕСКАЯ ОШИБКА: Финальный ответ пуст!")
|
|
561
|
+
final_response = "Извините, произошла ошибка при формировании ответа. Попробуйте еще раз."
|
|
562
|
+
|
|
563
|
+
logger.info(f"📱 Отправляем пользователю: {len(final_response)} символов")
|
|
564
|
+
|
|
565
|
+
# Отправляем ответ пользователю
|
|
566
|
+
try:
|
|
567
|
+
await send_message(message, final_response, files_list=files_list, directories_list=directories_list)
|
|
568
|
+
logger.info(f"✅ Ответ успешно отправлен пользователю {message.from_user.id}")
|
|
569
|
+
except Exception as e:
|
|
570
|
+
logger.error(f"❌ ОШИБКА ОТПРАВКИ СООБЩЕНИЯ: {e}")
|
|
571
|
+
# Пытаемся отправить простое сообщение об ошибке
|
|
572
|
+
try:
|
|
573
|
+
await message.answer("Произошла ошибка при отправке ответа. Попробуйте еще раз.")
|
|
574
|
+
except Exception as e2:
|
|
575
|
+
logger.error(f"❌ Не удалось отправить даже сообщение об ошибке: {e2}")
|
|
576
|
+
|
|
577
|
+
except Exception as e:
|
|
578
|
+
logger.error(f"❌ КРИТИЧЕСКАЯ ОШИБКА в process_user_message: {e}")
|
|
579
|
+
logger.exception("Полный стек ошибки:")
|
|
580
|
+
try:
|
|
581
|
+
await message.answer("Произошла критическая ошибка. Попробуйте написать /start для перезапуска.")
|
|
582
|
+
except:
|
|
583
|
+
logger.error(f"❌ Не удалось отправить сообщение об критической ошибке")
|