smart-bot-factory 0.3.4__py3-none-any.whl → 0.3.6__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/core/bot_utils.py +43 -10
- smart_bot_factory/core/decorators.py +116 -22
- smart_bot_factory/core/message_sender.py +11 -2
- smart_bot_factory/core/router.py +59 -8
- smart_bot_factory/dashboard/__init__.py +5 -0
- smart_bot_factory/handlers/handlers.py +17 -4
- smart_bot_factory/integrations/supabase_client.py +4 -3
- smart_bot_factory/utils/prompt_loader.py +12 -8
- {smart_bot_factory-0.3.4.dist-info → smart_bot_factory-0.3.6.dist-info}/METADATA +233 -22
- {smart_bot_factory-0.3.4.dist-info → smart_bot_factory-0.3.6.dist-info}/RECORD +13 -13
- smart_bot_factory/admin/admin_migration.sql +0 -136
- {smart_bot_factory-0.3.4.dist-info → smart_bot_factory-0.3.6.dist-info}/WHEEL +0 -0
- {smart_bot_factory-0.3.4.dist-info → smart_bot_factory-0.3.6.dist-info}/entry_points.txt +0 -0
- {smart_bot_factory-0.3.4.dist-info → smart_bot_factory-0.3.6.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import json
|
|
3
3
|
import logging
|
|
4
|
-
import re
|
|
5
4
|
from datetime import datetime
|
|
6
5
|
from aiogram import Router
|
|
7
6
|
from aiogram.filters import Command
|
|
@@ -170,8 +169,13 @@ def parse_ai_response_method2(ai_response: str) -> tuple[str, dict]:
|
|
|
170
169
|
logger.warning(f"Ошибка резервного метода: {e}")
|
|
171
170
|
return ai_response, {}
|
|
172
171
|
|
|
173
|
-
async def process_events(session_id: str, events: list, user_id: int):
|
|
174
|
-
"""
|
|
172
|
+
async def process_events(session_id: str, events: list, user_id: int) -> bool:
|
|
173
|
+
"""
|
|
174
|
+
Обрабатывает события из ответа ИИ
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
bool: True если нужно отправить сообщение от ИИ, False если не нужно
|
|
178
|
+
"""
|
|
175
179
|
|
|
176
180
|
# Проверяем кастомный процессор
|
|
177
181
|
custom_processor = get_global_var('custom_event_processor')
|
|
@@ -180,11 +184,14 @@ async def process_events(session_id: str, events: list, user_id: int):
|
|
|
180
184
|
# Используем кастомную функцию обработки событий
|
|
181
185
|
logger.info(f"🔄 Используется кастомная обработка событий: {custom_processor.__name__}")
|
|
182
186
|
await custom_processor(session_id, events, user_id)
|
|
183
|
-
return
|
|
187
|
+
return True # По умолчанию отправляем сообщение
|
|
184
188
|
|
|
185
189
|
# Стандартная обработка
|
|
186
190
|
supabase_client = get_global_var('supabase_client')
|
|
187
191
|
|
|
192
|
+
# Флаг для отслеживания, нужно ли отправлять сообщение от ИИ
|
|
193
|
+
should_send_ai_response = True
|
|
194
|
+
|
|
188
195
|
for event in events:
|
|
189
196
|
try:
|
|
190
197
|
event_type = event.get('тип', '')
|
|
@@ -200,7 +207,6 @@ async def process_events(session_id: str, events: list, user_id: int):
|
|
|
200
207
|
|
|
201
208
|
# Определяем категорию события и сохраняем в БД
|
|
202
209
|
event_id = None
|
|
203
|
-
should_execute_immediately = False
|
|
204
210
|
should_notify = False
|
|
205
211
|
|
|
206
212
|
try:
|
|
@@ -228,8 +234,14 @@ async def process_events(session_id: str, events: list, user_id: int):
|
|
|
228
234
|
|
|
229
235
|
event_handler_info = event_handlers.get(event_type, {})
|
|
230
236
|
once_only = event_handler_info.get('once_only', True)
|
|
237
|
+
send_ai_response_flag = event_handler_info.get('send_ai_response', True)
|
|
231
238
|
|
|
232
|
-
logger.info(f" 🔍 Обработчик '{event_type}': once_only={once_only}")
|
|
239
|
+
logger.info(f" 🔍 Обработчик '{event_type}': once_only={once_only}, send_ai_response={send_ai_response_flag}")
|
|
240
|
+
|
|
241
|
+
# Проверяем флаг send_ai_response ИЗ ДЕКОРАТОРА
|
|
242
|
+
if not send_ai_response_flag:
|
|
243
|
+
should_send_ai_response = False
|
|
244
|
+
logger.warning(f" 🔇🔇🔇 ОБРАБОТЧИК '{event_type}' ЗАПРЕТИЛ ОТПРАВКУ СООБЩЕНИЯ ОТ ИИ (send_ai_response=False) 🔇🔇🔇")
|
|
233
245
|
|
|
234
246
|
# Если once_only=True - проверяем в БД наличие выполненных событий
|
|
235
247
|
if once_only:
|
|
@@ -256,9 +268,16 @@ async def process_events(session_id: str, events: list, user_id: int):
|
|
|
256
268
|
logger.info(f" 🎯 Немедленно выполняем user_event: '{event_type}'")
|
|
257
269
|
|
|
258
270
|
try:
|
|
259
|
-
# Выполняем
|
|
271
|
+
# Выполняем событие
|
|
260
272
|
result = await execute_event_handler(event_type, user_id, event_info)
|
|
261
273
|
|
|
274
|
+
# Проверяем наличие поля 'info' для дашборда
|
|
275
|
+
import json
|
|
276
|
+
info_dashboard_json = None
|
|
277
|
+
if isinstance(result, dict) and 'info' in result:
|
|
278
|
+
info_dashboard_json = json.dumps(result['info'], ensure_ascii=False)
|
|
279
|
+
logger.info(f" 📊 Дашборд данные добавлены: {result['info'].get('title', 'N/A')}")
|
|
280
|
+
|
|
262
281
|
# Сохраняем в БД УЖЕ со статусом completed (избегаем дублирования)
|
|
263
282
|
event_record = {
|
|
264
283
|
'event_type': event_type,
|
|
@@ -269,13 +288,13 @@ async def process_events(session_id: str, events: list, user_id: int):
|
|
|
269
288
|
'status': 'completed', # Сразу completed!
|
|
270
289
|
'session_id': session_id,
|
|
271
290
|
'executed_at': __import__('datetime').datetime.now(__import__('datetime').timezone.utc).isoformat(),
|
|
272
|
-
'result_data': __import__('json').dumps(result, ensure_ascii=False) if result else None
|
|
291
|
+
'result_data': __import__('json').dumps(result, ensure_ascii=False) if result else None,
|
|
292
|
+
'info_dashboard': info_dashboard_json # Добавится только если есть поле 'info'
|
|
273
293
|
}
|
|
274
294
|
response = supabase_client.client.table('scheduled_events').insert(event_record).execute()
|
|
275
295
|
event_id = response.data[0]['id']
|
|
276
296
|
|
|
277
297
|
should_notify = event_handler_info.get('notify', False)
|
|
278
|
-
should_execute_immediately = True
|
|
279
298
|
|
|
280
299
|
logger.info(f" ✅ Событие {event_id} выполнено и сохранено как completed")
|
|
281
300
|
|
|
@@ -298,8 +317,18 @@ async def process_events(session_id: str, events: list, user_id: int):
|
|
|
298
317
|
# Если не user_event, пробуем как запланированную задачу
|
|
299
318
|
elif event_type in scheduled_tasks:
|
|
300
319
|
try:
|
|
320
|
+
# Достаем метаданные задачи
|
|
321
|
+
task_info = scheduled_tasks.get(event_type, {})
|
|
322
|
+
send_ai_response_flag = task_info.get('send_ai_response', True)
|
|
323
|
+
|
|
324
|
+
logger.info(f" ⏰ Планируем scheduled_task: '{event_type}', send_ai_response={send_ai_response_flag}")
|
|
325
|
+
|
|
326
|
+
# Проверяем флаг send_ai_response ИЗ ДЕКОРАТОРА
|
|
327
|
+
if not send_ai_response_flag:
|
|
328
|
+
should_send_ai_response = False
|
|
329
|
+
logger.warning(f" 🔇🔇🔇 ЗАДАЧА '{event_type}' ЗАПРЕТИЛА ОТПРАВКУ СООБЩЕНИЯ ОТ ИИ (send_ai_response=False) 🔇🔇🔇")
|
|
330
|
+
|
|
301
331
|
# Используем новую логику - время берется из декоратора
|
|
302
|
-
logger.info(f" ⏰ Планируем scheduled_task: '{event_type}' с данными: '{event_info}'")
|
|
303
332
|
result = await execute_scheduled_task_from_event(user_id, event_type, event_info, session_id)
|
|
304
333
|
event_id = result.get('event_id', 'unknown')
|
|
305
334
|
should_notify = result.get('notify', False)
|
|
@@ -354,6 +383,10 @@ async def process_events(session_id: str, events: list, user_id: int):
|
|
|
354
383
|
except Exception as e:
|
|
355
384
|
logger.error(f"❌ Ошибка обработки события {event}: {e}")
|
|
356
385
|
logger.exception("Стек ошибки:")
|
|
386
|
+
|
|
387
|
+
# Возвращаем флаг, нужно ли отправлять сообщение от ИИ
|
|
388
|
+
logger.warning(f"🔊🔊🔊 ИТОГОВЫЙ ФЛАГ send_ai_response: {should_send_ai_response} 🔊🔊🔊")
|
|
389
|
+
return should_send_ai_response
|
|
357
390
|
|
|
358
391
|
async def notify_admins_about_event(user_id: int, event: dict):
|
|
359
392
|
"""Отправляем уведомление админам о событии с явным указанием ID пользователя"""
|
|
@@ -377,7 +377,7 @@ _global_handlers: Dict[str, Dict[str, Any]] = {}
|
|
|
377
377
|
# Глобальный менеджер роутеров
|
|
378
378
|
_router_manager = None
|
|
379
379
|
|
|
380
|
-
def event_handler(event_type: str, notify: bool = False, once_only: bool = True):
|
|
380
|
+
def event_handler(event_type: str, notify: bool = False, once_only: bool = True, send_ai_response: bool = True):
|
|
381
381
|
"""
|
|
382
382
|
Декоратор для регистрации обработчика события
|
|
383
383
|
|
|
@@ -385,18 +385,19 @@ def event_handler(event_type: str, notify: bool = False, once_only: bool = True)
|
|
|
385
385
|
event_type: Тип события (например, 'appointment_booking', 'phone_collection')
|
|
386
386
|
notify: Уведомлять ли админов о выполнении события (по умолчанию False)
|
|
387
387
|
once_only: Обрабатывать ли событие только один раз (по умолчанию True)
|
|
388
|
+
send_ai_response: Отправлять ли сообщение от ИИ после обработки события (по умолчанию True)
|
|
388
389
|
|
|
389
390
|
Example:
|
|
390
|
-
# Обработчик
|
|
391
|
+
# Обработчик с отправкой сообщения от ИИ
|
|
391
392
|
@event_handler("appointment_booking", notify=True)
|
|
392
393
|
async def book_appointment(user_id: int, appointment_data: dict):
|
|
393
394
|
# Логика записи на прием
|
|
394
395
|
return {"status": "success", "appointment_id": "123"}
|
|
395
396
|
|
|
396
|
-
# Обработчик
|
|
397
|
-
@event_handler("phone_collection", once_only=False)
|
|
397
|
+
# Обработчик БЕЗ отправки сообщения от ИИ
|
|
398
|
+
@event_handler("phone_collection", once_only=False, send_ai_response=False)
|
|
398
399
|
async def collect_phone(user_id: int, phone_data: dict):
|
|
399
|
-
# Логика сбора телефона
|
|
400
|
+
# Логика сбора телефона - ИИ не отправит сообщение
|
|
400
401
|
return {"status": "phone_collected"}
|
|
401
402
|
"""
|
|
402
403
|
def decorator(func: Callable) -> Callable:
|
|
@@ -404,7 +405,8 @@ def event_handler(event_type: str, notify: bool = False, once_only: bool = True)
|
|
|
404
405
|
'handler': func,
|
|
405
406
|
'name': func.__name__,
|
|
406
407
|
'notify': notify,
|
|
407
|
-
'once_only': once_only
|
|
408
|
+
'once_only': once_only,
|
|
409
|
+
'send_ai_response': send_ai_response
|
|
408
410
|
}
|
|
409
411
|
|
|
410
412
|
logger.info(f"📝 Зарегистрирован обработчик события '{event_type}': {func.__name__}")
|
|
@@ -416,15 +418,17 @@ def event_handler(event_type: str, notify: bool = False, once_only: bool = True)
|
|
|
416
418
|
result = await func(*args, **kwargs)
|
|
417
419
|
logger.info(f"✅ Обработчик '{event_type}' выполнен успешно")
|
|
418
420
|
|
|
419
|
-
# Автоматически добавляем
|
|
421
|
+
# Автоматически добавляем флаги notify и send_ai_response к результату
|
|
420
422
|
if isinstance(result, dict):
|
|
421
423
|
result['notify'] = notify
|
|
424
|
+
result['send_ai_response'] = send_ai_response
|
|
422
425
|
else:
|
|
423
426
|
# Если результат не словарь, создаем словарь
|
|
424
427
|
result = {
|
|
425
428
|
'status': 'success',
|
|
426
429
|
'result': result,
|
|
427
|
-
'notify': notify
|
|
430
|
+
'notify': notify,
|
|
431
|
+
'send_ai_response': send_ai_response
|
|
428
432
|
}
|
|
429
433
|
|
|
430
434
|
return result
|
|
@@ -435,7 +439,7 @@ def event_handler(event_type: str, notify: bool = False, once_only: bool = True)
|
|
|
435
439
|
return wrapper
|
|
436
440
|
return decorator
|
|
437
441
|
|
|
438
|
-
def schedule_task(task_name: str, notify: bool = False, smart_check: bool = True, once_only: bool = True, delay: Union[str, int] = None, event_type: Union[str, Callable] = None):
|
|
442
|
+
def schedule_task(task_name: str, notify: bool = False, smart_check: bool = True, once_only: bool = True, delay: Union[str, int] = None, event_type: Union[str, Callable] = None, send_ai_response: bool = True):
|
|
439
443
|
"""
|
|
440
444
|
Декоратор для регистрации задачи, которую можно запланировать на время
|
|
441
445
|
|
|
@@ -448,6 +452,7 @@ def schedule_task(task_name: str, notify: bool = False, smart_check: bool = True
|
|
|
448
452
|
event_type: Источник времени события - ОПЦИОНАЛЬНО:
|
|
449
453
|
- str: Тип события для поиска в БД (например, 'appointment_booking')
|
|
450
454
|
- Callable: Функция для получения datetime (например, async def(user_id, user_data) -> datetime)
|
|
455
|
+
send_ai_response: Отправлять ли сообщение от ИИ после выполнения задачи (по умолчанию True)
|
|
451
456
|
|
|
452
457
|
Example:
|
|
453
458
|
# Обычная задача с фиксированным временем
|
|
@@ -509,7 +514,8 @@ def schedule_task(task_name: str, notify: bool = False, smart_check: bool = True
|
|
|
509
514
|
'smart_check': smart_check,
|
|
510
515
|
'once_only': once_only,
|
|
511
516
|
'default_delay': default_delay_seconds,
|
|
512
|
-
'event_type': event_type # Новое поле для типа события
|
|
517
|
+
'event_type': event_type, # Новое поле для типа события
|
|
518
|
+
'send_ai_response': send_ai_response
|
|
513
519
|
}
|
|
514
520
|
|
|
515
521
|
if event_type:
|
|
@@ -524,15 +530,17 @@ def schedule_task(task_name: str, notify: bool = False, smart_check: bool = True
|
|
|
524
530
|
result = await func(*args, **kwargs)
|
|
525
531
|
logger.info(f"✅ Задача '{task_name}' выполнена успешно")
|
|
526
532
|
|
|
527
|
-
# Автоматически добавляем
|
|
533
|
+
# Автоматически добавляем флаги notify и send_ai_response к результату
|
|
528
534
|
if isinstance(result, dict):
|
|
529
535
|
result['notify'] = notify
|
|
536
|
+
result['send_ai_response'] = send_ai_response
|
|
530
537
|
else:
|
|
531
538
|
# Если результат не словарь, создаем словарь
|
|
532
539
|
result = {
|
|
533
540
|
'status': 'success',
|
|
534
541
|
'result': result,
|
|
535
|
-
'notify': notify
|
|
542
|
+
'notify': notify,
|
|
543
|
+
'send_ai_response': send_ai_response
|
|
536
544
|
}
|
|
537
545
|
|
|
538
546
|
return result
|
|
@@ -543,7 +551,7 @@ def schedule_task(task_name: str, notify: bool = False, smart_check: bool = True
|
|
|
543
551
|
return wrapper
|
|
544
552
|
return decorator
|
|
545
553
|
|
|
546
|
-
def global_handler(handler_type: str, notify: bool = False, once_only: bool = True, delay: Union[str, int] = None, event_type: Union[str, Callable] = None):
|
|
554
|
+
def global_handler(handler_type: str, notify: bool = False, once_only: bool = True, delay: Union[str, int] = None, event_type: Union[str, Callable] = None, send_ai_response: bool = True):
|
|
547
555
|
"""
|
|
548
556
|
Декоратор для регистрации глобального обработчика (для всех пользователей)
|
|
549
557
|
|
|
@@ -555,6 +563,7 @@ def global_handler(handler_type: str, notify: bool = False, once_only: bool = Tr
|
|
|
555
563
|
event_type: Источник времени события - ОПЦИОНАЛЬНО:
|
|
556
564
|
- str: Тип события для поиска в БД
|
|
557
565
|
- Callable: Функция для получения datetime (например, async def(handler_data: str) -> datetime)
|
|
566
|
+
send_ai_response: Отправлять ли сообщение от ИИ после выполнения обработчика (по умолчанию True)
|
|
558
567
|
|
|
559
568
|
Example:
|
|
560
569
|
# Глобальный обработчик с задержкой
|
|
@@ -610,7 +619,8 @@ def global_handler(handler_type: str, notify: bool = False, once_only: bool = Tr
|
|
|
610
619
|
'notify': notify,
|
|
611
620
|
'once_only': once_only,
|
|
612
621
|
'default_delay': default_delay_seconds,
|
|
613
|
-
'event_type': event_type # Добавляем event_type для глобальных обработчиков
|
|
622
|
+
'event_type': event_type, # Добавляем event_type для глобальных обработчиков
|
|
623
|
+
'send_ai_response': send_ai_response
|
|
614
624
|
}
|
|
615
625
|
|
|
616
626
|
logger.info(f"🌍 Зарегистрирован глобальный обработчик '{handler_type}': {func.__name__}")
|
|
@@ -622,15 +632,17 @@ def global_handler(handler_type: str, notify: bool = False, once_only: bool = Tr
|
|
|
622
632
|
result = await func(*args, **kwargs)
|
|
623
633
|
logger.info(f"✅ Глобальный обработчик '{handler_type}' выполнен успешно")
|
|
624
634
|
|
|
625
|
-
# Автоматически добавляем
|
|
635
|
+
# Автоматически добавляем флаги notify и send_ai_response к результату
|
|
626
636
|
if isinstance(result, dict):
|
|
627
637
|
result['notify'] = notify
|
|
638
|
+
result['send_ai_response'] = send_ai_response
|
|
628
639
|
else:
|
|
629
640
|
# Если результат не словарь, создаем словарь
|
|
630
641
|
result = {
|
|
631
642
|
'status': 'success',
|
|
632
643
|
'result': result,
|
|
633
|
-
'notify': notify
|
|
644
|
+
'notify': notify,
|
|
645
|
+
'send_ai_response': send_ai_response
|
|
634
646
|
}
|
|
635
647
|
|
|
636
648
|
return result
|
|
@@ -1259,6 +1271,11 @@ async def update_event_result(
|
|
|
1259
1271
|
if result_data:
|
|
1260
1272
|
import json
|
|
1261
1273
|
update_data['result_data'] = json.dumps(result_data, ensure_ascii=False)
|
|
1274
|
+
|
|
1275
|
+
# Проверяем наличие поля 'info' для дашборда
|
|
1276
|
+
if isinstance(result_data, dict) and 'info' in result_data:
|
|
1277
|
+
update_data['info_dashboard'] = json.dumps(result_data['info'], ensure_ascii=False)
|
|
1278
|
+
logger.info(f"📊 Дашборд данные добавлены в событие {event_id}")
|
|
1262
1279
|
|
|
1263
1280
|
if error_message:
|
|
1264
1281
|
update_data['last_error'] = error_message
|
|
@@ -1459,8 +1476,16 @@ async def background_event_processor():
|
|
|
1459
1476
|
continue
|
|
1460
1477
|
|
|
1461
1478
|
# Выполняем событие
|
|
1462
|
-
await process_scheduled_event(event)
|
|
1463
|
-
|
|
1479
|
+
result = await process_scheduled_event(event)
|
|
1480
|
+
|
|
1481
|
+
# Проверяем наличие поля 'info' для дашборда
|
|
1482
|
+
result_data = {"processed": True}
|
|
1483
|
+
if isinstance(result, dict):
|
|
1484
|
+
result_data.update(result)
|
|
1485
|
+
if 'info' in result:
|
|
1486
|
+
logger.info(f" 📊 Дашборд данные для задачи: {result['info'].get('title', 'N/A')}")
|
|
1487
|
+
|
|
1488
|
+
await update_event_result(event['id'], 'completed', result_data)
|
|
1464
1489
|
logger.info(f"✅ Событие {event['id']} выполнено")
|
|
1465
1490
|
|
|
1466
1491
|
except Exception as e:
|
|
@@ -1474,7 +1499,7 @@ async def background_event_processor():
|
|
|
1474
1499
|
await asyncio.sleep(60)
|
|
1475
1500
|
|
|
1476
1501
|
async def process_scheduled_event(event: Dict):
|
|
1477
|
-
"""Обрабатывает одно событие из БД"""
|
|
1502
|
+
"""Обрабатывает одно событие из БД и возвращает результат"""
|
|
1478
1503
|
|
|
1479
1504
|
event_type = event['event_type']
|
|
1480
1505
|
event_category = event['event_category']
|
|
@@ -1483,14 +1508,17 @@ async def process_scheduled_event(event: Dict):
|
|
|
1483
1508
|
|
|
1484
1509
|
logger.info(f"🔄 Обработка события {event['id']}: {event_category}/{event_type}")
|
|
1485
1510
|
|
|
1511
|
+
result = None
|
|
1486
1512
|
if event_category == 'scheduled_task':
|
|
1487
|
-
await execute_scheduled_task(event_type, user_id, event_data)
|
|
1513
|
+
result = await execute_scheduled_task(event_type, user_id, event_data)
|
|
1488
1514
|
elif event_category == 'global_handler':
|
|
1489
|
-
await execute_global_handler(event_type, event_data)
|
|
1515
|
+
result = await execute_global_handler(event_type, event_data)
|
|
1490
1516
|
elif event_category == 'user_event':
|
|
1491
|
-
await execute_event_handler(event_type, user_id, event_data)
|
|
1517
|
+
result = await execute_event_handler(event_type, user_id, event_data)
|
|
1492
1518
|
else:
|
|
1493
1519
|
logger.warning(f"⚠️ Неизвестная категория события: {event_category}")
|
|
1520
|
+
|
|
1521
|
+
return result
|
|
1494
1522
|
|
|
1495
1523
|
# =============================================================================
|
|
1496
1524
|
# ОБНОВЛЕННЫЕ ФУНКЦИИ С СОХРАНЕНИЕМ В БД
|
|
@@ -1872,3 +1900,69 @@ async def process_admin_event(event: Dict):
|
|
|
1872
1900
|
shutil.rmtree(temp_after_msg, ignore_errors=True)
|
|
1873
1901
|
logger.error(f"❌ Критическая ошибка обработки события: {e}")
|
|
1874
1902
|
raise
|
|
1903
|
+
|
|
1904
|
+
# =============================================================================
|
|
1905
|
+
# ФУНКЦИЯ ДЛЯ ПОДГОТОВКИ ДАННЫХ ДАШБОРДА
|
|
1906
|
+
# =============================================================================
|
|
1907
|
+
|
|
1908
|
+
async def prepare_dashboard_info(
|
|
1909
|
+
description_template: str,
|
|
1910
|
+
title: str,
|
|
1911
|
+
user_id: int
|
|
1912
|
+
) -> Dict[str, Any]:
|
|
1913
|
+
"""
|
|
1914
|
+
Подготавливает данные для дашборда (БЕЗ записи в БД)
|
|
1915
|
+
|
|
1916
|
+
Возвращаемый dict нужно поместить в поле 'info' результата обработчика.
|
|
1917
|
+
bot_utils.py автоматически запишет его в столбец info_dashboard таблицы.
|
|
1918
|
+
|
|
1919
|
+
Args:
|
|
1920
|
+
description_template: Строка с {username}, например "{username} купил подписку"
|
|
1921
|
+
title: Заголовок для дашборда
|
|
1922
|
+
user_id: Telegram ID
|
|
1923
|
+
|
|
1924
|
+
Returns:
|
|
1925
|
+
Dict с данными для дашборда
|
|
1926
|
+
|
|
1927
|
+
Example:
|
|
1928
|
+
@event_router.event_handler("collect_phone", notify=True)
|
|
1929
|
+
async def handle_phone_collection(user_id: int, phone_number: str):
|
|
1930
|
+
# ... бизнес-логика ...
|
|
1931
|
+
|
|
1932
|
+
return {
|
|
1933
|
+
"status": "success",
|
|
1934
|
+
"phone": phone_number,
|
|
1935
|
+
"info": await prepare_dashboard_info(
|
|
1936
|
+
description_template="{username} оставил телефон",
|
|
1937
|
+
title="Новый контакт",
|
|
1938
|
+
user_id=user_id
|
|
1939
|
+
)
|
|
1940
|
+
}
|
|
1941
|
+
"""
|
|
1942
|
+
supabase_client = get_supabase_client()
|
|
1943
|
+
|
|
1944
|
+
# Получаем username из sales_users
|
|
1945
|
+
username = f"user_{user_id}" # fallback
|
|
1946
|
+
if supabase_client:
|
|
1947
|
+
try:
|
|
1948
|
+
query = supabase_client.client.table('sales_users').select('username').eq('telegram_id', user_id)
|
|
1949
|
+
if supabase_client.bot_id:
|
|
1950
|
+
query = query.eq('bot_id', supabase_client.bot_id)
|
|
1951
|
+
response = query.execute()
|
|
1952
|
+
if response.data:
|
|
1953
|
+
username = response.data[0].get('username') or username
|
|
1954
|
+
except Exception as e:
|
|
1955
|
+
logger.warning(f"⚠️ Не удалось получить username для дашборда: {e}")
|
|
1956
|
+
|
|
1957
|
+
# Форматируем строку
|
|
1958
|
+
description = description_template.format(username=username)
|
|
1959
|
+
|
|
1960
|
+
# Московское время (UTC+3)
|
|
1961
|
+
moscow_tz = timezone(timedelta(hours=3))
|
|
1962
|
+
moscow_time = datetime.now(moscow_tz)
|
|
1963
|
+
|
|
1964
|
+
return {
|
|
1965
|
+
'title': title,
|
|
1966
|
+
'description': description,
|
|
1967
|
+
'created_at': moscow_time.isoformat()
|
|
1968
|
+
}
|
|
@@ -143,10 +143,10 @@ async def send_message_by_ai(
|
|
|
143
143
|
logger.info(f"✅ Этап и качество обновлены в БД")
|
|
144
144
|
|
|
145
145
|
# Обрабатываем события
|
|
146
|
-
events = ai_metadata.get('
|
|
146
|
+
events = ai_metadata.get('събития', [])
|
|
147
147
|
if events:
|
|
148
148
|
logger.info(f"🔔 Обрабатываем {len(events)} событий")
|
|
149
|
-
await process_events(session_id, events, user_id)
|
|
149
|
+
should_send_response = await process_events(session_id, events, user_id)
|
|
150
150
|
|
|
151
151
|
# Сохраняем ответ ассистента
|
|
152
152
|
await supabase_client.add_message(
|
|
@@ -165,6 +165,15 @@ async def send_message_by_ai(
|
|
|
165
165
|
else:
|
|
166
166
|
final_response = response_text
|
|
167
167
|
|
|
168
|
+
# Проверяем, нужно ли отправлять сообщение от ИИ
|
|
169
|
+
if 'should_send_response' in locals() and not should_send_response:
|
|
170
|
+
logger.info("🔇 События запретили отправку сообщения от ИИ (message_sender), пропускаем отправку")
|
|
171
|
+
return {
|
|
172
|
+
"status": "skipped",
|
|
173
|
+
"reason": "send_ai_response=False",
|
|
174
|
+
"user_id": user_id
|
|
175
|
+
}
|
|
176
|
+
|
|
168
177
|
# Отправляем ответ пользователю напрямую через бота
|
|
169
178
|
await bot.send_message(
|
|
170
179
|
chat_id=user_id,
|
smart_bot_factory/core/router.py
CHANGED
|
@@ -26,7 +26,7 @@ class EventRouter:
|
|
|
26
26
|
|
|
27
27
|
logger.info(f"🔄 Создан роутер: {self.name}")
|
|
28
28
|
|
|
29
|
-
def event_handler(self, event_type: str, notify: bool = False, once_only: bool = True):
|
|
29
|
+
def event_handler(self, event_type: str, notify: bool = False, once_only: bool = True, send_ai_response: bool = True):
|
|
30
30
|
"""
|
|
31
31
|
Декоратор для регистрации обработчика события в роутере
|
|
32
32
|
|
|
@@ -34,6 +34,7 @@ class EventRouter:
|
|
|
34
34
|
event_type: Тип события
|
|
35
35
|
notify: Уведомлять ли админов
|
|
36
36
|
once_only: Выполнять ли только один раз
|
|
37
|
+
send_ai_response: Отправлять ли сообщение от ИИ после обработки события (по умолчанию True)
|
|
37
38
|
"""
|
|
38
39
|
def decorator(func: Callable) -> Callable:
|
|
39
40
|
self._event_handlers[event_type] = {
|
|
@@ -41,6 +42,7 @@ class EventRouter:
|
|
|
41
42
|
'name': func.__name__,
|
|
42
43
|
'notify': notify,
|
|
43
44
|
'once_only': once_only,
|
|
45
|
+
'send_ai_response': send_ai_response,
|
|
44
46
|
'router': self.name
|
|
45
47
|
}
|
|
46
48
|
|
|
@@ -50,14 +52,29 @@ class EventRouter:
|
|
|
50
52
|
@wraps(func)
|
|
51
53
|
async def wrapper(*args, **kwargs):
|
|
52
54
|
try:
|
|
53
|
-
|
|
55
|
+
result = await func(*args, **kwargs)
|
|
56
|
+
|
|
57
|
+
# Автоматически добавляем флаги notify и send_ai_response к результату
|
|
58
|
+
if isinstance(result, dict):
|
|
59
|
+
result['notify'] = notify
|
|
60
|
+
result['send_ai_response'] = send_ai_response
|
|
61
|
+
else:
|
|
62
|
+
# Если результат не словарь, создаем словарь
|
|
63
|
+
result = {
|
|
64
|
+
'status': 'success',
|
|
65
|
+
'result': result,
|
|
66
|
+
'notify': notify,
|
|
67
|
+
'send_ai_response': send_ai_response
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return result
|
|
54
71
|
except Exception as e:
|
|
55
72
|
logger.error(f"Ошибка выполнения обработчика '{event_type}' в роутере {self.name}: {e}")
|
|
56
73
|
raise
|
|
57
74
|
return wrapper
|
|
58
75
|
return decorator
|
|
59
76
|
|
|
60
|
-
def schedule_task(self, task_name: str, notify: bool = False, smart_check: bool = True, once_only: bool = True, delay: Union[str, int] = None, event_type: Union[str, Callable] = None):
|
|
77
|
+
def schedule_task(self, task_name: str, notify: bool = False, smart_check: bool = True, once_only: bool = True, delay: Union[str, int] = None, event_type: Union[str, Callable] = None, send_ai_response: bool = True):
|
|
61
78
|
"""
|
|
62
79
|
Декоратор для регистрации запланированной задачи в роутере
|
|
63
80
|
|
|
@@ -70,6 +87,7 @@ class EventRouter:
|
|
|
70
87
|
event_type: Источник времени события - ОПЦИОНАЛЬНО:
|
|
71
88
|
- str: Тип события для поиска в БД (например, 'appointment_booking')
|
|
72
89
|
- Callable: Функция async def(user_id, user_data) -> datetime
|
|
90
|
+
send_ai_response: Отправлять ли сообщение от ИИ после выполнения задачи (по умолчанию True)
|
|
73
91
|
"""
|
|
74
92
|
def decorator(func: Callable) -> Callable:
|
|
75
93
|
# Время ОБЯЗАТЕЛЬНО должно быть указано
|
|
@@ -98,7 +116,8 @@ class EventRouter:
|
|
|
98
116
|
'once_only': once_only,
|
|
99
117
|
'router': self.name,
|
|
100
118
|
'default_delay': default_delay_seconds,
|
|
101
|
-
'event_type': event_type # Новое поле для типа события
|
|
119
|
+
'event_type': event_type, # Новое поле для типа события
|
|
120
|
+
'send_ai_response': send_ai_response
|
|
102
121
|
}
|
|
103
122
|
|
|
104
123
|
if event_type:
|
|
@@ -110,14 +129,29 @@ class EventRouter:
|
|
|
110
129
|
@wraps(func)
|
|
111
130
|
async def wrapper(*args, **kwargs):
|
|
112
131
|
try:
|
|
113
|
-
|
|
132
|
+
result = await func(*args, **kwargs)
|
|
133
|
+
|
|
134
|
+
# Автоматически добавляем флаги notify и send_ai_response к результату
|
|
135
|
+
if isinstance(result, dict):
|
|
136
|
+
result['notify'] = notify
|
|
137
|
+
result['send_ai_response'] = send_ai_response
|
|
138
|
+
else:
|
|
139
|
+
# Если результат не словарь, создаем словарь
|
|
140
|
+
result = {
|
|
141
|
+
'status': 'success',
|
|
142
|
+
'result': result,
|
|
143
|
+
'notify': notify,
|
|
144
|
+
'send_ai_response': send_ai_response
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return result
|
|
114
148
|
except Exception as e:
|
|
115
149
|
logger.error(f"Ошибка выполнения задачи '{task_name}' в роутере {self.name}: {e}")
|
|
116
150
|
raise
|
|
117
151
|
return wrapper
|
|
118
152
|
return decorator
|
|
119
153
|
|
|
120
|
-
def global_handler(self, handler_type: str, notify: bool = False, once_only: bool = True, delay: Union[str, int] = None, event_type: Union[str, Callable] = None):
|
|
154
|
+
def global_handler(self, handler_type: str, notify: bool = False, once_only: bool = True, delay: Union[str, int] = None, event_type: Union[str, Callable] = None, send_ai_response: bool = True):
|
|
121
155
|
"""
|
|
122
156
|
Декоратор для регистрации глобального обработчика в роутере
|
|
123
157
|
|
|
@@ -126,6 +160,7 @@ class EventRouter:
|
|
|
126
160
|
notify: Уведомлять ли админов
|
|
127
161
|
once_only: Выполнять ли только один раз
|
|
128
162
|
delay: Время задержки в удобном формате (например, "1h 30m", "45m", 3600) - ОБЯЗАТЕЛЬНО
|
|
163
|
+
send_ai_response: Отправлять ли сообщение от ИИ после выполнения обработчика (по умолчанию True)
|
|
129
164
|
"""
|
|
130
165
|
def decorator(func: Callable) -> Callable:
|
|
131
166
|
# Время ОБЯЗАТЕЛЬНО должно быть указано
|
|
@@ -150,7 +185,8 @@ class EventRouter:
|
|
|
150
185
|
'once_only': once_only,
|
|
151
186
|
'router': self.name,
|
|
152
187
|
'default_delay': default_delay_seconds,
|
|
153
|
-
'event_type': event_type # Добавляем event_type для глобальных обработчиков
|
|
188
|
+
'event_type': event_type, # Добавляем event_type для глобальных обработчиков
|
|
189
|
+
'send_ai_response': send_ai_response
|
|
154
190
|
}
|
|
155
191
|
|
|
156
192
|
logger.info(f"🌍 Роутер {self.name}: зарегистрирован глобальный обработчик '{handler_type}': {func.__name__}")
|
|
@@ -159,7 +195,22 @@ class EventRouter:
|
|
|
159
195
|
@wraps(func)
|
|
160
196
|
async def wrapper(*args, **kwargs):
|
|
161
197
|
try:
|
|
162
|
-
|
|
198
|
+
result = await func(*args, **kwargs)
|
|
199
|
+
|
|
200
|
+
# Автоматически добавляем флаги notify и send_ai_response к результату
|
|
201
|
+
if isinstance(result, dict):
|
|
202
|
+
result['notify'] = notify
|
|
203
|
+
result['send_ai_response'] = send_ai_response
|
|
204
|
+
else:
|
|
205
|
+
# Если результат не словарь, создаем словарь
|
|
206
|
+
result = {
|
|
207
|
+
'status': 'success',
|
|
208
|
+
'result': result,
|
|
209
|
+
'notify': notify,
|
|
210
|
+
'send_ai_response': send_ai_response
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return result
|
|
163
214
|
except Exception as e:
|
|
164
215
|
logger.error(f"Ошибка выполнения глобального обработчика '{handler_type}' в роутере {self.name}: {e}")
|
|
165
216
|
raise
|
|
@@ -942,8 +942,8 @@ async def process_user_message(message: Message, state: FSMContext, session_id:
|
|
|
942
942
|
logger.info(f" {idx}. {event_emoji} {event_type}: {event_info}")
|
|
943
943
|
|
|
944
944
|
# Обрабатываем события в системе
|
|
945
|
-
await process_events(session_id, events, message.from_user.id)
|
|
946
|
-
logger.
|
|
945
|
+
should_send_response = await process_events(session_id, events, message.from_user.id)
|
|
946
|
+
logger.warning(f" ✅ События обработаны, should_send_response = {should_send_response}")
|
|
947
947
|
|
|
948
948
|
# Обрабатываем файлы и каталоги
|
|
949
949
|
files_list = ai_metadata.get('файлы', [])
|
|
@@ -1007,6 +1007,14 @@ async def process_user_message(message: Message, state: FSMContext, session_id:
|
|
|
1007
1007
|
|
|
1008
1008
|
logger.info(f"📱 Отправляем пользователю: {len(final_response)} символов")
|
|
1009
1009
|
|
|
1010
|
+
# ============ ПРОВЕРКА: НУЖНО ЛИ ОТПРАВЛЯТЬ СООБЩЕНИЕ ОТ ИИ ============
|
|
1011
|
+
# Проверяем флаг из событий (если события запретили отправку)
|
|
1012
|
+
logger.warning(f"🔍 Проверка should_send_response: exists={('should_send_response' in locals())}, value={locals().get('should_send_response', 'NOT_SET')}")
|
|
1013
|
+
|
|
1014
|
+
if 'should_send_response' in locals() and not should_send_response:
|
|
1015
|
+
logger.warning("🔇🔇🔇 СОБЫТИЯ ЗАПРЕТИЛИ ОТПРАВКУ СООБЩЕНИЯ ОТ ИИ, ПРОПУСКАЕМ ОТПРАВКУ 🔇🔇🔇")
|
|
1016
|
+
return
|
|
1017
|
+
|
|
1010
1018
|
# ============ ХУК 5: ФИЛЬТРЫ ОТПРАВКИ ============
|
|
1011
1019
|
send_filters = message_hooks.get('send_filters', [])
|
|
1012
1020
|
for filter_func in send_filters:
|
|
@@ -1133,9 +1141,9 @@ async def process_voice_message(message: Message, state: FSMContext, session_id:
|
|
|
1133
1141
|
await supabase_client.update_session_stage(session_id, stage, quality)
|
|
1134
1142
|
|
|
1135
1143
|
# Обрабатываем события
|
|
1136
|
-
events = ai_metadata.get('
|
|
1144
|
+
events = ai_metadata.get('події', [])
|
|
1137
1145
|
if events:
|
|
1138
|
-
await process_events(session_id, events, message.from_user.id)
|
|
1146
|
+
should_send_response = await process_events(session_id, events, message.from_user.id)
|
|
1139
1147
|
|
|
1140
1148
|
# Сохраняем ответ ассистента
|
|
1141
1149
|
await supabase_client.add_message(
|
|
@@ -1158,6 +1166,11 @@ async def process_voice_message(message: Message, state: FSMContext, session_id:
|
|
|
1158
1166
|
files_list = ai_metadata.get('файлы', [])
|
|
1159
1167
|
directories_list = ai_metadata.get('каталоги', [])
|
|
1160
1168
|
|
|
1169
|
+
# Проверяем, нужно ли отправлять сообщение от ИИ
|
|
1170
|
+
if 'should_send_response' in locals() and not should_send_response:
|
|
1171
|
+
logger.info("🔇 События запретили отправку сообщения от ИИ (voice), пропускаем отправку")
|
|
1172
|
+
return
|
|
1173
|
+
|
|
1161
1174
|
# Отправляем ответ пользователю
|
|
1162
1175
|
await send_message(message, final_response, files_list=files_list, directories_list=directories_list)
|
|
1163
1176
|
logger.info(f"✅ Ответ отправлен пользователю {message.from_user.id}")
|
|
@@ -398,8 +398,8 @@ class SupabaseClient:
|
|
|
398
398
|
try:
|
|
399
399
|
# Проверяем существует ли админ
|
|
400
400
|
response = self.client.table('sales_admins').select('telegram_id').eq(
|
|
401
|
-
'telegram_id', admin_data['telegram_id']
|
|
402
|
-
).execute()
|
|
401
|
+
'telegram_id', admin_data['telegram_id'],
|
|
402
|
+
).eq('bot_id', self.bot_id).execute()
|
|
403
403
|
|
|
404
404
|
if response.data:
|
|
405
405
|
# Обновляем существующего
|
|
@@ -408,13 +408,14 @@ class SupabaseClient:
|
|
|
408
408
|
'first_name': admin_data.get('first_name'),
|
|
409
409
|
'last_name': admin_data.get('last_name'),
|
|
410
410
|
'is_active': True
|
|
411
|
-
}).eq('telegram_id', admin_data['telegram_id']).execute()
|
|
411
|
+
}).eq('telegram_id', admin_data['telegram_id']).eq('bot_id', self.bot_id).execute()
|
|
412
412
|
|
|
413
413
|
logger.debug(f"Обновлен админ {admin_data['telegram_id']}")
|
|
414
414
|
else:
|
|
415
415
|
# Создаем нового
|
|
416
416
|
self.client.table('sales_admins').insert({
|
|
417
417
|
'telegram_id': admin_data['telegram_id'],
|
|
418
|
+
'bot_id': self.bot_id,
|
|
418
419
|
'username': admin_data.get('username'),
|
|
419
420
|
'first_name': admin_data.get('first_name'),
|
|
420
421
|
'last_name': admin_data.get('last_name'),
|
|
@@ -107,6 +107,7 @@ class PromptLoader:
|
|
|
107
107
|
{
|
|
108
108
|
"этап": "introduction|consult|offer|contacts",
|
|
109
109
|
"качество": 1-10,
|
|
110
|
+
"ссылка": 1,
|
|
110
111
|
"события": [
|
|
111
112
|
{
|
|
112
113
|
"тип": "телефон|консультация|покупка|отказ",
|
|
@@ -126,9 +127,10 @@ class PromptLoader:
|
|
|
126
127
|
СИСТЕМА ОЦЕНКИ КАЧЕСТВА (1-10):
|
|
127
128
|
1-3: низкий интерес, много возражений, скептически настроен
|
|
128
129
|
4-6: средний интерес, есть вопросы, обдумывает
|
|
129
|
-
7-8: высокий интерес, готов к покупке, активно интересуется
|
|
130
|
+
7-8: высокий интерес, готов к покупке, активно интересуется
|
|
130
131
|
9-10: горячий лид, предоставил контакты или готов к действию
|
|
131
132
|
|
|
133
|
+
|
|
132
134
|
СОБЫТИЯ - добавляй ТОЛЬКО когда происходит что-то из этого:
|
|
133
135
|
- "телефон": пользователь предоставил номер телефона
|
|
134
136
|
- "консультация": пользователь просит живую консультацию по телефону
|
|
@@ -140,7 +142,7 @@ class PromptLoader:
|
|
|
140
142
|
|
|
141
143
|
ПРИМЕРЫ ПРАВИЛЬНОГО ИСПОЛЬЗОВАНИЯ:
|
|
142
144
|
|
|
143
|
-
Пример 1 - обычный
|
|
145
|
+
Пример 1 - обычный диалог (без ссылки):
|
|
144
146
|
"Расскажу подробнее о конференции GrowthMED. Она пройдет 24-25 октября..."
|
|
145
147
|
|
|
146
148
|
{
|
|
@@ -151,11 +153,11 @@ class PromptLoader:
|
|
|
151
153
|
"каталоги": []
|
|
152
154
|
}
|
|
153
155
|
|
|
154
|
-
Пример 2 - получен
|
|
156
|
+
Пример 2 - получен телефон (без ссылки):
|
|
155
157
|
"Отлично! Записал ваш номер. Мы перезвоним в течение 10 минут!"
|
|
156
158
|
|
|
157
159
|
{
|
|
158
|
-
"этап": "contacts",
|
|
160
|
+
"этап": "contacts",
|
|
159
161
|
"качество": 9,
|
|
160
162
|
"события": [
|
|
161
163
|
{
|
|
@@ -167,7 +169,7 @@ class PromptLoader:
|
|
|
167
169
|
"каталоги": []
|
|
168
170
|
}
|
|
169
171
|
|
|
170
|
-
Пример 3 - отправка
|
|
172
|
+
Пример 3 - отправка презентации (без ссылки):
|
|
171
173
|
"Отправляю вам презентацию о нашей компании и прайс-лист с актуальными ценами."
|
|
172
174
|
|
|
173
175
|
{
|
|
@@ -175,7 +177,7 @@ class PromptLoader:
|
|
|
175
177
|
"качество": 7,
|
|
176
178
|
"события": [
|
|
177
179
|
{
|
|
178
|
-
"тип": "консультация",
|
|
180
|
+
"тип": "консультация",
|
|
179
181
|
"инфо": "Запросил материалы"
|
|
180
182
|
}
|
|
181
183
|
],
|
|
@@ -183,7 +185,7 @@ class PromptLoader:
|
|
|
183
185
|
"каталоги": []
|
|
184
186
|
}
|
|
185
187
|
|
|
186
|
-
Пример 4 - отправка файлов из
|
|
188
|
+
Пример 4 - отправка файлов из каталога (без ссылки):
|
|
187
189
|
"В каталоге 'примеры_работ' вы можете посмотреть наши последние проекты."
|
|
188
190
|
|
|
189
191
|
{
|
|
@@ -194,7 +196,7 @@ class PromptLoader:
|
|
|
194
196
|
"каталоги": ["примеры_работ"]
|
|
195
197
|
}
|
|
196
198
|
|
|
197
|
-
Пример 5 - комбинированная
|
|
199
|
+
Пример 5 - комбинированная отправка (без ссылки):
|
|
198
200
|
"Отправляю вам коммерческое предложение и примеры похожих проектов из нашего портфолио."
|
|
199
201
|
|
|
200
202
|
{
|
|
@@ -210,6 +212,7 @@ class PromptLoader:
|
|
|
210
212
|
"каталоги": ["портфолио_2023"]
|
|
211
213
|
}
|
|
212
214
|
|
|
215
|
+
|
|
213
216
|
ТРЕБОВАНИЯ К JSON:
|
|
214
217
|
- JSON должен быть валидным и находиться в самом конце ответа
|
|
215
218
|
- Всегда используй кавычки для строк
|
|
@@ -217,6 +220,7 @@ class PromptLoader:
|
|
|
217
220
|
- Если событий нет - не добавляй их в массив
|
|
218
221
|
- Качество должно быть числом от 1 до 10
|
|
219
222
|
|
|
223
|
+
|
|
220
224
|
ПОМНИ: Этот JSON критически важен для работы системы администрирования и аналитики!
|
|
221
225
|
"""
|
|
222
226
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: smart-bot-factory
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.6
|
|
4
4
|
Summary: Библиотека для создания умных чат-ботов
|
|
5
5
|
Author-email: Kopatych <eserov73@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -21,12 +21,10 @@ Classifier: Topic :: Communications :: Chat
|
|
|
21
21
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
22
|
Requires-Python: >=3.9
|
|
23
23
|
Requires-Dist: aiofiles>=23.0.0
|
|
24
|
-
Requires-Dist: aiogram-media-group>=0.5.1
|
|
25
24
|
Requires-Dist: aiogram>=3.4.1
|
|
26
25
|
Requires-Dist: click>=8.0.0
|
|
27
26
|
Requires-Dist: openai>=1.12.0
|
|
28
27
|
Requires-Dist: project-root-finder>=1.9
|
|
29
|
-
Requires-Dist: python-dateutil>=2.9.0.post0
|
|
30
28
|
Requires-Dist: python-dotenv>=1.0.1
|
|
31
29
|
Requires-Dist: pytz>=2023.3
|
|
32
30
|
Requires-Dist: pyyaml>=6.0.2
|
|
@@ -46,6 +44,7 @@ Description-Content-Type: text/markdown
|
|
|
46
44
|
- [event_handler](#event_handler---обработчики-событий)
|
|
47
45
|
- [schedule_task](#schedule_task---запланированные-задачи)
|
|
48
46
|
- [global_handler](#global_handler---глобальные-обработчики)
|
|
47
|
+
- [Dashboard Info](#-dashboard-info---отправка-данных-в-дашборд)
|
|
49
48
|
- [Хуки для кастомизации](#-хуки-для-кастомизации)
|
|
50
49
|
- [Telegram роутеры](#-telegram-роутеры)
|
|
51
50
|
- [Расширенные возможности](#-расширенные-возможности)
|
|
@@ -189,9 +188,10 @@ sbf link
|
|
|
189
188
|
|
|
190
189
|
```python
|
|
191
190
|
@event_router.event_handler(
|
|
192
|
-
event_type: str,
|
|
193
|
-
notify: bool = False,
|
|
194
|
-
once_only: bool = True
|
|
191
|
+
event_type: str, # Тип события
|
|
192
|
+
notify: bool = False, # Уведомлять админов
|
|
193
|
+
once_only: bool = True, # Выполнять только 1 раз
|
|
194
|
+
send_ai_response: bool = True # Отправлять ответ от ИИ
|
|
195
195
|
)
|
|
196
196
|
async def handler(user_id: int, event_data: str):
|
|
197
197
|
# Ваш код
|
|
@@ -203,6 +203,7 @@ async def handler(user_id: int, event_data: str):
|
|
|
203
203
|
- **`event_type`** (обязательный) - Уникальное имя события
|
|
204
204
|
- **`notify`** (по умолчанию `False`) - Отправлять уведомление админам после выполнения
|
|
205
205
|
- **`once_only`** (по умолчанию `True`) - Если `True`, событие выполнится только 1 раз для пользователя
|
|
206
|
+
- **`send_ai_response`** (по умолчанию `True`) - Если `False`, ИИ НЕ отправит сообщение после выполнения обработчика
|
|
206
207
|
|
|
207
208
|
**Как работает:**
|
|
208
209
|
|
|
@@ -237,6 +238,16 @@ async def handle_question(user_id: int, question: str):
|
|
|
237
238
|
"""Обрабатывает вопросы (может быть много)"""
|
|
238
239
|
# Логика обработки
|
|
239
240
|
return {"status": "answered"}
|
|
241
|
+
|
|
242
|
+
# БЕЗ отправки ответа от ИИ
|
|
243
|
+
@event_router.event_handler("silent_event", send_ai_response=False)
|
|
244
|
+
async def handle_silent(user_id: int, event_data: str):
|
|
245
|
+
"""
|
|
246
|
+
Выполняет логику БЕЗ отправки сообщения от ИИ
|
|
247
|
+
Используйте когда хотите только собрать данные без ответа пользователю
|
|
248
|
+
"""
|
|
249
|
+
await send_message_by_human(user_id, "✅ Данные сохранены")
|
|
250
|
+
return {"status": "saved"}
|
|
240
251
|
```
|
|
241
252
|
|
|
242
253
|
---
|
|
@@ -249,12 +260,13 @@ async def handle_question(user_id: int, question: str):
|
|
|
249
260
|
|
|
250
261
|
```python
|
|
251
262
|
@event_router.schedule_task(
|
|
252
|
-
task_name: str,
|
|
253
|
-
delay: Union[str, int],
|
|
254
|
-
notify: bool = False,
|
|
255
|
-
smart_check: bool = True,
|
|
256
|
-
once_only: bool = True,
|
|
257
|
-
event_type: Union[str, Callable] = None # Источник времени события
|
|
263
|
+
task_name: str, # Название задачи
|
|
264
|
+
delay: Union[str, int], # Задержка: "1h 30m" или секунды
|
|
265
|
+
notify: bool = False, # Уведомлять админов
|
|
266
|
+
smart_check: bool = True, # Умная проверка активности
|
|
267
|
+
once_only: bool = True, # Выполнять только 1 раз
|
|
268
|
+
event_type: Union[str, Callable] = None, # Источник времени события
|
|
269
|
+
send_ai_response: bool = True # Отправлять ответ от ИИ
|
|
258
270
|
)
|
|
259
271
|
async def handler(user_id: int, user_data: str):
|
|
260
272
|
# Ваш код
|
|
@@ -275,6 +287,7 @@ async def handler(user_id: int, user_data: str):
|
|
|
275
287
|
- **`event_type`** (опционально) - Источник времени события:
|
|
276
288
|
- **Строка**: `"appointment_booking"` - ищет событие в БД и вычисляет время
|
|
277
289
|
- **Функция**: `async def(user_id, user_data) -> datetime` - кастомная логика
|
|
290
|
+
- **`send_ai_response`** (по умолчанию `True`) - Если `False`, ИИ НЕ отправит сообщение после выполнения задачи
|
|
278
291
|
|
|
279
292
|
**Формула времени с `event_type`:**
|
|
280
293
|
|
|
@@ -364,11 +377,12 @@ async def important_reminder(user_id: int, text: str):
|
|
|
364
377
|
|
|
365
378
|
```python
|
|
366
379
|
@event_router.global_handler(
|
|
367
|
-
handler_type: str,
|
|
368
|
-
delay: Union[str, int],
|
|
369
|
-
notify: bool = False,
|
|
370
|
-
once_only: bool = True,
|
|
371
|
-
event_type: Union[str, Callable] = None # Источник времени
|
|
380
|
+
handler_type: str, # Тип обработчика
|
|
381
|
+
delay: Union[str, int], # Задержка
|
|
382
|
+
notify: bool = False, # Уведомлять админов
|
|
383
|
+
once_only: bool = True, # Выполнять только 1 раз
|
|
384
|
+
event_type: Union[str, Callable] = None, # Источник времени
|
|
385
|
+
send_ai_response: bool = True # Отправлять ответ от ИИ
|
|
372
386
|
)
|
|
373
387
|
async def handler(handler_data: str):
|
|
374
388
|
# Ваш код
|
|
@@ -422,6 +436,173 @@ async def notify_promo_ending(handler_data: str):
|
|
|
422
436
|
|
|
423
437
|
---
|
|
424
438
|
|
|
439
|
+
## 📊 Dashboard Info - Отправка данных в дашборд
|
|
440
|
+
|
|
441
|
+
**Назначение:** Позволяет отправлять информацию о событиях в дашборд (таблица `scheduled_events`, столбец `info_dashboard`) для аналитики и мониторинга.
|
|
442
|
+
|
|
443
|
+
### Как работает
|
|
444
|
+
|
|
445
|
+
1. Обработчик события возвращает результат с полем `'info'`
|
|
446
|
+
2. Система автоматически извлекает это поле и записывает в `info_dashboard` таблицы
|
|
447
|
+
3. Функция `prepare_dashboard_info` автоматически:
|
|
448
|
+
- Получает `username` из таблицы `sales_users`
|
|
449
|
+
- Форматирует строку с подстановкой данных
|
|
450
|
+
- Добавляет московское время (UTC+3)
|
|
451
|
+
|
|
452
|
+
### Сигнатура
|
|
453
|
+
|
|
454
|
+
```python
|
|
455
|
+
from smart_bot_factory.dashboard import prepare_dashboard_info
|
|
456
|
+
|
|
457
|
+
dashboard_data = await prepare_dashboard_info(
|
|
458
|
+
description_template: str, # Строка с {username}, например "{username} купил подписку"
|
|
459
|
+
title: str, # Заголовок для дашборда
|
|
460
|
+
user_id: int # Telegram ID пользователя
|
|
461
|
+
)
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
**Возвращает:**
|
|
465
|
+
|
|
466
|
+
```python
|
|
467
|
+
{
|
|
468
|
+
'title': 'Заголовок',
|
|
469
|
+
'description': '@username123 купил подписку', # С подстановкой реального username
|
|
470
|
+
'created_at': '2025-10-18T15:30:45.123456+03:00' # Московское время
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### Примеры использования
|
|
475
|
+
|
|
476
|
+
#### С event_handler
|
|
477
|
+
|
|
478
|
+
```python
|
|
479
|
+
from smart_bot_factory.dashboard import prepare_dashboard_info
|
|
480
|
+
|
|
481
|
+
@event_router.event_handler("collect_phone", notify=True, once_only=True)
|
|
482
|
+
async def handle_phone_collection(user_id: int, phone_number: str):
|
|
483
|
+
"""Сохраняет телефон клиента"""
|
|
484
|
+
|
|
485
|
+
# Ваша бизнес-логика
|
|
486
|
+
session = await supabase_client.get_active_session(user_id)
|
|
487
|
+
if session:
|
|
488
|
+
metadata = session.get('metadata', {})
|
|
489
|
+
metadata['phone'] = phone_number
|
|
490
|
+
await supabase_client.update_session_metadata(session['id'], metadata)
|
|
491
|
+
|
|
492
|
+
await send_message_by_human(
|
|
493
|
+
user_id=user_id,
|
|
494
|
+
message_text=f"✅ Спасибо! Ваш номер {phone_number} сохранен"
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
# 📊 Возвращаем результат С данными для дашборда
|
|
498
|
+
return {
|
|
499
|
+
"status": "success",
|
|
500
|
+
"phone": phone_number,
|
|
501
|
+
"info": await prepare_dashboard_info(
|
|
502
|
+
description_template="{username} оставил номер телефона",
|
|
503
|
+
title="Новый контакт",
|
|
504
|
+
user_id=user_id
|
|
505
|
+
)
|
|
506
|
+
}
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
#### С schedule_task
|
|
510
|
+
|
|
511
|
+
```python
|
|
512
|
+
@event_router.schedule_task("follow_up", delay="24h", smart_check=True)
|
|
513
|
+
async def send_follow_up(user_id: int, reminder_text: str):
|
|
514
|
+
"""Напоминание через 24 часа"""
|
|
515
|
+
|
|
516
|
+
await send_message_by_human(
|
|
517
|
+
user_id=user_id,
|
|
518
|
+
message_text=f"👋 {reminder_text}"
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
# 📊 Работает и для задач!
|
|
522
|
+
return {
|
|
523
|
+
"status": "sent",
|
|
524
|
+
"type": "follow_up",
|
|
525
|
+
"info": await prepare_dashboard_info(
|
|
526
|
+
description_template="{username} получил напоминание",
|
|
527
|
+
title="Напоминание отправлено",
|
|
528
|
+
user_id=user_id
|
|
529
|
+
)
|
|
530
|
+
}
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
#### БЕЗ дашборда
|
|
534
|
+
|
|
535
|
+
Если не нужно отправлять данные в дашборд - просто не добавляйте поле `'info'`:
|
|
536
|
+
|
|
537
|
+
```python
|
|
538
|
+
@event_router.event_handler("collect_name", once_only=False)
|
|
539
|
+
async def handle_name_collection(user_id: int, client_name: str):
|
|
540
|
+
"""БЕЗ дашборда - просто сохраняем имя"""
|
|
541
|
+
|
|
542
|
+
await send_message_by_human(
|
|
543
|
+
user_id=user_id,
|
|
544
|
+
message_text=f"✅ Спасибо! Ваше имя {client_name} сохранено"
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
# Возвращаем БЕЗ поля 'info' - дашборд останется пустым
|
|
548
|
+
return {"status": "success"}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
### Что попадает в БД
|
|
552
|
+
|
|
553
|
+
**События С дашбордом:**
|
|
554
|
+
|
|
555
|
+
```sql
|
|
556
|
+
SELECT * FROM scheduled_events WHERE id = '123';
|
|
557
|
+
|
|
558
|
+
id: 123
|
|
559
|
+
event_type: collect_phone
|
|
560
|
+
event_category: user_event
|
|
561
|
+
user_id: 12345
|
|
562
|
+
status: completed
|
|
563
|
+
result_data: {"status": "success", "phone": "+79001234567", "info": {...}}
|
|
564
|
+
info_dashboard: {
|
|
565
|
+
"title": "Новый контакт",
|
|
566
|
+
"description": "@username123 оставил номер телефона",
|
|
567
|
+
"created_at": "2025-10-18T15:30:45+03:00"
|
|
568
|
+
}
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
**События БЕЗ дашборда:**
|
|
572
|
+
|
|
573
|
+
```sql
|
|
574
|
+
SELECT * FROM scheduled_events WHERE id = '124';
|
|
575
|
+
|
|
576
|
+
id: 124
|
|
577
|
+
event_type: collect_name
|
|
578
|
+
event_category: user_event
|
|
579
|
+
user_id: 12345
|
|
580
|
+
status: completed
|
|
581
|
+
result_data: {"status": "success"}
|
|
582
|
+
info_dashboard: NULL ← Остается пустым
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
### Форматирование строк
|
|
586
|
+
|
|
587
|
+
Функция `prepare_dashboard_info` поддерживает подстановку `{username}`:
|
|
588
|
+
|
|
589
|
+
```python
|
|
590
|
+
# Примеры шаблонов:
|
|
591
|
+
"{username} купил подписку на 1 год"
|
|
592
|
+
"{username} оставил контакт"
|
|
593
|
+
"{username} записался на консультацию"
|
|
594
|
+
"{username} задал вопрос о продукте"
|
|
595
|
+
"{username} завершил оплату"
|
|
596
|
+
|
|
597
|
+
# После подстановки:
|
|
598
|
+
"@user123 купил подписку на 1 год"
|
|
599
|
+
"@ivan_petrov оставил контакт"
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
Если пользователь не найден в `sales_users` - будет использован fallback: `user_12345`
|
|
603
|
+
|
|
604
|
+
---
|
|
605
|
+
|
|
425
606
|
## 🎣 Хуки для кастомизации
|
|
426
607
|
|
|
427
608
|
Хуки позволяют внедрять свою логику в стандартную обработку сообщений без переписывания всей функции.
|
|
@@ -815,16 +996,46 @@ DEBUG_MODE=false
|
|
|
815
996
|
|
|
816
997
|
## 🎯 Сравнение декораторов
|
|
817
998
|
|
|
818
|
-
| Декоратор | Когда выполняется | Для кого |
|
|
819
|
-
|
|
820
|
-
| `@event_handler` | Немедленно | 1 пользователь | `event_type`, `notify`, `once_only` |
|
|
821
|
-
| `@schedule_task` | Через время | 1 пользователь | `task_name`, `delay`, `event_type`, `smart_check`, `once_only`, `notify` |
|
|
822
|
-
| `@global_handler` | Через время | Все пользователи | `handler_type`, `delay`, `event_type`, `once_only`, `notify` |
|
|
999
|
+
| Декоратор | Когда выполняется | Для кого | Ключевые параметры |
|
|
1000
|
+
|-----------|-------------------|----------|--------------------|
|
|
1001
|
+
| `@event_handler` | Немедленно | 1 пользователь | `event_type`, `notify`, `once_only`, `send_ai_response` |
|
|
1002
|
+
| `@schedule_task` | Через время | 1 пользователь | `task_name`, `delay`, `event_type`, `smart_check`, `once_only`, `notify`, `send_ai_response` |
|
|
1003
|
+
| `@global_handler` | Через время | Все пользователи | `handler_type`, `delay`, `event_type`, `once_only`, `notify`, `send_ai_response` |
|
|
823
1004
|
|
|
824
1005
|
---
|
|
825
1006
|
|
|
826
1007
|
## 🔑 Ключевые концепции
|
|
827
1008
|
|
|
1009
|
+
### `send_ai_response=True`
|
|
1010
|
+
|
|
1011
|
+
Контролирует отправку сообщения от ИИ после выполнения обработчика:
|
|
1012
|
+
|
|
1013
|
+
- **`True`** (по умолчанию) - ИИ отправит сообщение пользователю после выполнения обработчика
|
|
1014
|
+
- **`False`** - ИИ НЕ отправит сообщение (используйте когда нужна только фоновая обработка или когда отправляете сообщение вручную)
|
|
1015
|
+
|
|
1016
|
+
**Когда использовать `send_ai_response=False`:**
|
|
1017
|
+
|
|
1018
|
+
- Когда нужно только собрать данные без ответа пользователю
|
|
1019
|
+
- Когда вы сами отправляете сообщение через `send_message_by_human()`
|
|
1020
|
+
- Для фоновых задач без взаимодействия с пользователем
|
|
1021
|
+
|
|
1022
|
+
```python
|
|
1023
|
+
# ИИ отправит сообщение (по умолчанию)
|
|
1024
|
+
@event_router.event_handler("collect_phone")
|
|
1025
|
+
async def save_phone(user_id: int, phone: str):
|
|
1026
|
+
# Сохраняем телефон
|
|
1027
|
+
# ИИ автоматически отправит сообщение после выполнения
|
|
1028
|
+
return {"status": "success"}
|
|
1029
|
+
|
|
1030
|
+
# ИИ НЕ отправит сообщение
|
|
1031
|
+
@event_router.event_handler("collect_name", send_ai_response=False)
|
|
1032
|
+
async def save_name(user_id: int, name: str):
|
|
1033
|
+
# Сохраняем имя
|
|
1034
|
+
await send_message_by_human(user_id, f"✅ Имя {name} сохранено")
|
|
1035
|
+
# ИИ не будет отправлять свое сообщение
|
|
1036
|
+
return {"status": "success"}
|
|
1037
|
+
```
|
|
1038
|
+
|
|
828
1039
|
### `once_only=True`
|
|
829
1040
|
|
|
830
1041
|
Гарантирует выполнение события только 1 раз для пользователя:
|
|
@@ -7,7 +7,6 @@ smart_bot_factory/admin/__init__.py,sha256=vdsMTpt_LiXkY-awFu_X9e2Zt7CV50PwmsWkF
|
|
|
7
7
|
smart_bot_factory/admin/admin_events.py,sha256=QCosyTbJgrU8daWSK_bQgf8UZoJSIrV6xyO0R3XV2j0,43289
|
|
8
8
|
smart_bot_factory/admin/admin_logic.py,sha256=vPkNk86bdPsjNUNlZ3qfKtbRr9UuJy2oG54cYUGGNmg,23107
|
|
9
9
|
smart_bot_factory/admin/admin_manager.py,sha256=xlyG9mIjPmtUhS4E9lp36T7o5Kfp5PZpJ-r1QjnSn5g,6394
|
|
10
|
-
smart_bot_factory/admin/admin_migration.sql,sha256=kleMPJBSe2Z7ZZz7rNyOX_yoh4GZivGesqAX90U5PGs,5667
|
|
11
10
|
smart_bot_factory/admin/admin_tester.py,sha256=PGFpf7fmD5Wxea31xR2ZM_A_QpvrB73gsbxvUrHQBkg,6463
|
|
12
11
|
smart_bot_factory/admin/timeout_checker.py,sha256=TzA2FGrxwE8fuhKerGnGrt4qYMEZdIR8x3SQAnIW5YQ,24490
|
|
13
12
|
smart_bot_factory/aiogram_calendar/__init__.py,sha256=_IzB_HJIZuMs__7xBBsYVG20GbRNFw2VowvhOEyiGGc,349
|
|
@@ -30,30 +29,31 @@ smart_bot_factory/configs/growthmed-october-24/tests/realistic_scenarios.yaml,sh
|
|
|
30
29
|
smart_bot_factory/configs/growthmed-october-24/tests/scenario_examples.yaml,sha256=bzDulOU4a2LyWlcHzlQU8GYhOky2WTfyizGfjX4ioMY,2436
|
|
31
30
|
smart_bot_factory/configs/growthmed-october-24/welcome_file/welcome_file_msg.txt,sha256=Db21Mm0r8SBWFdX9EeIF2FZtLQ2cvuwVlSRJd2KEYCg,922
|
|
32
31
|
smart_bot_factory/configs/growthmed-october-24/welcome_file/Чек лист по 152ФЗ и 323ФЗ для медицины.pdf,sha256=BiAiQHNnQXJPMsks9AeL6s0beEjRFkRMJLMlAn4WorA,5284954
|
|
33
|
-
smart_bot_factory/core/bot_utils.py,sha256=
|
|
32
|
+
smart_bot_factory/core/bot_utils.py,sha256=29y9LrwM4yLYqBqiCld4RmNW0q3cHL8psuOl4PiDEjs,47632
|
|
34
33
|
smart_bot_factory/core/conversation_manager.py,sha256=eoHL7MCEz68DRvTVwRwZgf2PWwGv4T6J9D-I-thETi8,28289
|
|
35
|
-
smart_bot_factory/core/decorators.py,sha256=
|
|
36
|
-
smart_bot_factory/core/message_sender.py,sha256=
|
|
37
|
-
smart_bot_factory/core/router.py,sha256=
|
|
34
|
+
smart_bot_factory/core/decorators.py,sha256=wiYyFTai7NebH7wQuxEsLbYGOExiCUmpXqz9V2FkXpM,98782
|
|
35
|
+
smart_bot_factory/core/message_sender.py,sha256=0-SQcK4W1x__VgvyaeVRuFlXcxV56TsR_nNK07Nr4b4,32763
|
|
36
|
+
smart_bot_factory/core/router.py,sha256=ji7rzpuKaO8yKaxFW58WhlgG5ExXlbCgqCTONxAyqL4,15022
|
|
38
37
|
smart_bot_factory/core/router_manager.py,sha256=dUwesog-oHk1U2EDdS8p0e4MTSkwtx5_qXn6nrJ9l9I,9700
|
|
39
38
|
smart_bot_factory/core/states.py,sha256=L8qp1UmYFuxTN5U9tY076rDuKgxtFbpSGqBpva2eWbo,895
|
|
40
39
|
smart_bot_factory/creation/__init__.py,sha256=IgDk8GDS3pg7Pw_Et41J33ZmeZIU5dRwQdTmYKXfJfE,128
|
|
41
40
|
smart_bot_factory/creation/bot_builder.py,sha256=yGRmOPD7qCMbhcBiltHWISoKxWx8eqjDSnZXpwhqnUs,43115
|
|
42
41
|
smart_bot_factory/creation/bot_testing.py,sha256=JDWXyJfZmbgo-DLdAPk8Sd9FiehtHHa4sLD17lBrTOc,55669
|
|
42
|
+
smart_bot_factory/dashboard/__init__.py,sha256=bDBOWQbcAL1Bmz4KVFouAKg8FN-c6EsC_-YthTt_mP4,100
|
|
43
43
|
smart_bot_factory/event/__init__.py,sha256=hPL449RULIOB-OXv1ZbGNiHctAYaOMUqhSWGPrDHYBM,212
|
|
44
|
-
smart_bot_factory/handlers/handlers.py,sha256=
|
|
44
|
+
smart_bot_factory/handlers/handlers.py,sha256=v3ubszkN7ML-CXChveTdp68EdMjHl02NTJ3hMT2zXKA,61893
|
|
45
45
|
smart_bot_factory/integrations/openai_client.py,sha256=fwaJpwojFdLBWChcFWpFGOHK9upG-nCIwDochkCRRlY,24291
|
|
46
|
-
smart_bot_factory/integrations/supabase_client.py,sha256=
|
|
46
|
+
smart_bot_factory/integrations/supabase_client.py,sha256=XV1_caDAADnXLwCM7CkUdGfUNBNHBHYz-HBPSYmdGv4,63653
|
|
47
47
|
smart_bot_factory/message/__init__.py,sha256=-ehDZweUc3uKgmLLxFVsD-KWrDtnHpHms7pCrDelWo0,1950
|
|
48
48
|
smart_bot_factory/router/__init__.py,sha256=5gEbpG3eylOyow5NmidzGUy0K-AZq7RhYLVu9OaUT6c,270
|
|
49
49
|
smart_bot_factory/supabase/__init__.py,sha256=XmZP6yM9ffERM5ddAWyJnrNzEhCYtMu3AcjVCi1rOf8,179
|
|
50
50
|
smart_bot_factory/supabase/client.py,sha256=lWIzfOgoSvU7xPhYLoJtM5GnbWdoWsvHcRFC22sFBMU,25637
|
|
51
51
|
smart_bot_factory/utils/__init__.py,sha256=UhsJXEHfrIK8h1AHsroHSwAriijk-LvnqLyvgzi2VYs,273
|
|
52
52
|
smart_bot_factory/utils/debug_routing.py,sha256=BOoDhKBg7UXe5uHQxRk3TSfPfLPOFqt0N7lAo6kjCOo,4719
|
|
53
|
-
smart_bot_factory/utils/prompt_loader.py,sha256=
|
|
53
|
+
smart_bot_factory/utils/prompt_loader.py,sha256=HS_6Vf-qvRBkhvyzu-HNVS1swFgmqWOKNNv0F6We_AQ,20060
|
|
54
54
|
smart_bot_factory/utils/user_prompt_loader.py,sha256=dk6P0X_3UcNqxjRtuIvb0LcPrp03zIIsstZwdmeCPaE,2519
|
|
55
|
-
smart_bot_factory-0.3.
|
|
56
|
-
smart_bot_factory-0.3.
|
|
57
|
-
smart_bot_factory-0.3.
|
|
58
|
-
smart_bot_factory-0.3.
|
|
59
|
-
smart_bot_factory-0.3.
|
|
55
|
+
smart_bot_factory-0.3.6.dist-info/METADATA,sha256=1szKQIjRny6BWbsR5rNpeDUXZikcp7g4I9E8LIBYeqg,40662
|
|
56
|
+
smart_bot_factory-0.3.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
57
|
+
smart_bot_factory-0.3.6.dist-info/entry_points.txt,sha256=ybKEAI0WSb7WoRiey7QE-HHfn88UGV7nxLDxXq7b7SU,50
|
|
58
|
+
smart_bot_factory-0.3.6.dist-info/licenses/LICENSE,sha256=OrK3cwdUTzNzIhJvSPtJaVMoYIyC_sSx5EFE_FDMvGs,1092
|
|
59
|
+
smart_bot_factory-0.3.6.dist-info/RECORD,,
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
-- ФИНАЛЬНАЯ МИГРАЦИЯ АДМИНСКОЙ СИСТЕМЫ
|
|
2
|
-
-- Выполните ПОСЛЕ исправления уникальности telegram_id
|
|
3
|
-
|
|
4
|
-
-- 1. Расширяем существующие таблицы
|
|
5
|
-
ALTER TABLE sales_chat_sessions
|
|
6
|
-
ADD COLUMN IF NOT EXISTS current_stage TEXT,
|
|
7
|
-
ADD COLUMN IF NOT EXISTS lead_quality_score INTEGER;
|
|
8
|
-
|
|
9
|
-
ALTER TABLE sales_messages
|
|
10
|
-
ADD COLUMN IF NOT EXISTS ai_metadata JSONB DEFAULT '{}'::jsonb;
|
|
11
|
-
|
|
12
|
-
-- 2. Создаем функцию обновления updated_at
|
|
13
|
-
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
|
14
|
-
RETURNS TRIGGER AS $$
|
|
15
|
-
BEGIN
|
|
16
|
-
NEW.updated_at = NOW();
|
|
17
|
-
RETURN NEW;
|
|
18
|
-
END;
|
|
19
|
-
$$ LANGUAGE plpgsql;
|
|
20
|
-
|
|
21
|
-
-- 3. Таблица администраторов
|
|
22
|
-
CREATE TABLE IF NOT EXISTS sales_admins (
|
|
23
|
-
id BIGSERIAL PRIMARY KEY,
|
|
24
|
-
telegram_id BIGINT UNIQUE NOT NULL,
|
|
25
|
-
username TEXT,
|
|
26
|
-
first_name TEXT,
|
|
27
|
-
last_name TEXT,
|
|
28
|
-
role TEXT DEFAULT 'admin',
|
|
29
|
-
is_active BOOLEAN DEFAULT TRUE,
|
|
30
|
-
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
31
|
-
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
-- 4. Диалоги админов с пользователями
|
|
35
|
-
CREATE TABLE IF NOT EXISTS admin_user_conversations (
|
|
36
|
-
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
37
|
-
admin_id BIGINT REFERENCES sales_admins(telegram_id) ON DELETE CASCADE,
|
|
38
|
-
user_id BIGINT REFERENCES sales_users(telegram_id) ON DELETE CASCADE,
|
|
39
|
-
session_id UUID REFERENCES sales_chat_sessions(id) ON DELETE CASCADE,
|
|
40
|
-
status TEXT DEFAULT 'active' CHECK (status IN ('active', 'completed')),
|
|
41
|
-
started_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
42
|
-
ended_at TIMESTAMP WITH TIME ZONE,
|
|
43
|
-
auto_end_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() + INTERVAL '30 minutes'
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
-- 5. События из ответов ИИ
|
|
47
|
-
CREATE TABLE IF NOT EXISTS session_events (
|
|
48
|
-
id BIGSERIAL PRIMARY KEY,
|
|
49
|
-
session_id UUID REFERENCES sales_chat_sessions(id) ON DELETE CASCADE,
|
|
50
|
-
event_type TEXT NOT NULL,
|
|
51
|
-
event_info TEXT,
|
|
52
|
-
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
53
|
-
notified_admins BIGINT[] DEFAULT '{}'
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
-- 6. Индексы
|
|
57
|
-
CREATE INDEX IF NOT EXISTS idx_sales_admins_telegram_id ON sales_admins(telegram_id);
|
|
58
|
-
CREATE INDEX IF NOT EXISTS idx_admin_conversations_status ON admin_user_conversations(status);
|
|
59
|
-
CREATE INDEX IF NOT EXISTS idx_admin_conversations_admin ON admin_user_conversations(admin_id);
|
|
60
|
-
CREATE INDEX IF NOT EXISTS idx_admin_conversations_user ON admin_user_conversations(user_id);
|
|
61
|
-
CREATE INDEX IF NOT EXISTS idx_session_events_type ON session_events(event_type);
|
|
62
|
-
CREATE INDEX IF NOT EXISTS idx_session_events_session ON session_events(session_id);
|
|
63
|
-
CREATE INDEX IF NOT EXISTS idx_sales_chat_sessions_stage ON sales_chat_sessions(current_stage);
|
|
64
|
-
CREATE INDEX IF NOT EXISTS idx_sales_messages_metadata ON sales_messages USING gin(ai_metadata);
|
|
65
|
-
|
|
66
|
-
-- 7. Триггер для sales_admins
|
|
67
|
-
DROP TRIGGER IF EXISTS update_sales_admins_updated_at ON sales_admins;
|
|
68
|
-
CREATE TRIGGER update_sales_admins_updated_at
|
|
69
|
-
BEFORE UPDATE ON sales_admins
|
|
70
|
-
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
|
71
|
-
|
|
72
|
-
-- 8. Функция завершения просроченных диалогов
|
|
73
|
-
CREATE OR REPLACE FUNCTION end_expired_admin_conversations()
|
|
74
|
-
RETURNS INTEGER AS $$
|
|
75
|
-
DECLARE
|
|
76
|
-
ended_count INTEGER;
|
|
77
|
-
BEGIN
|
|
78
|
-
UPDATE admin_user_conversations
|
|
79
|
-
SET status = 'completed', ended_at = NOW()
|
|
80
|
-
WHERE status = 'active' AND auto_end_at < NOW();
|
|
81
|
-
|
|
82
|
-
GET DIAGNOSTICS ended_count = ROW_COUNT;
|
|
83
|
-
RETURN ended_count;
|
|
84
|
-
END;
|
|
85
|
-
$$ LANGUAGE plpgsql;
|
|
86
|
-
|
|
87
|
-
-- 9. Представления для аналитики
|
|
88
|
-
CREATE OR REPLACE VIEW funnel_stats AS
|
|
89
|
-
SELECT
|
|
90
|
-
current_stage,
|
|
91
|
-
COUNT(*) as count,
|
|
92
|
-
AVG(lead_quality_score) as avg_quality,
|
|
93
|
-
ROUND(COUNT(*) * 100.0 / NULLIF(SUM(COUNT(*)) OVER(), 0), 1) as percentage
|
|
94
|
-
FROM sales_chat_sessions
|
|
95
|
-
WHERE created_at > NOW() - INTERVAL '7 days'
|
|
96
|
-
AND current_stage IS NOT NULL
|
|
97
|
-
GROUP BY current_stage;
|
|
98
|
-
|
|
99
|
-
CREATE OR REPLACE VIEW daily_events AS
|
|
100
|
-
SELECT
|
|
101
|
-
DATE(created_at) as event_date,
|
|
102
|
-
event_type,
|
|
103
|
-
COUNT(*) as count
|
|
104
|
-
FROM session_events
|
|
105
|
-
WHERE created_at > NOW() - INTERVAL '30 days'
|
|
106
|
-
GROUP BY DATE(created_at), event_type
|
|
107
|
-
ORDER BY event_date DESC, event_type;
|
|
108
|
-
|
|
109
|
-
-- 10. RLS политики
|
|
110
|
-
ALTER TABLE sales_admins ENABLE ROW LEVEL SECURITY;
|
|
111
|
-
ALTER TABLE admin_user_conversations ENABLE ROW LEVEL SECURITY;
|
|
112
|
-
ALTER TABLE session_events ENABLE ROW LEVEL SECURITY;
|
|
113
|
-
|
|
114
|
-
DROP POLICY IF EXISTS "Service role can manage all admins" ON sales_admins;
|
|
115
|
-
DROP POLICY IF EXISTS "Service role can manage all conversations" ON admin_user_conversations;
|
|
116
|
-
DROP POLICY IF EXISTS "Service role can manage all events" ON session_events;
|
|
117
|
-
|
|
118
|
-
CREATE POLICY "Service role can manage all admins" ON sales_admins
|
|
119
|
-
FOR ALL USING (current_setting('role') = 'service_role');
|
|
120
|
-
|
|
121
|
-
CREATE POLICY "Service role can manage all conversations" ON admin_user_conversations
|
|
122
|
-
FOR ALL USING (current_setting('role') = 'service_role');
|
|
123
|
-
|
|
124
|
-
CREATE POLICY "Service role can manage all events" ON session_events
|
|
125
|
-
FOR ALL USING (current_setting('role') = 'service_role');
|
|
126
|
-
|
|
127
|
-
-- 11. Комментарии
|
|
128
|
-
COMMENT ON TABLE sales_admins IS 'Администраторы бота';
|
|
129
|
-
COMMENT ON TABLE admin_user_conversations IS 'Активные диалоги админов с пользователями';
|
|
130
|
-
COMMENT ON TABLE session_events IS 'События из ответов ИИ для уведомлений';
|
|
131
|
-
|
|
132
|
-
-- Финальная проверка
|
|
133
|
-
SELECT
|
|
134
|
-
'АДМИНСКАЯ СИСТЕМА СОЗДАНА!' AS status,
|
|
135
|
-
(SELECT COUNT(*) FROM information_schema.tables
|
|
136
|
-
WHERE table_name IN ('sales_admins', 'admin_user_conversations', 'session_events')) AS tables_created;
|
|
File without changes
|
|
File without changes
|
|
File without changes
|