smart-bot-factory 0.3.5__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 +9 -1
- smart_bot_factory/core/decorators.py +88 -6
- smart_bot_factory/dashboard/__init__.py +5 -0
- smart_bot_factory/integrations/supabase_client.py +4 -3
- {smart_bot_factory-0.3.5.dist-info → smart_bot_factory-0.3.6.dist-info}/METADATA +233 -20
- {smart_bot_factory-0.3.5.dist-info → smart_bot_factory-0.3.6.dist-info}/RECORD +9 -9
- smart_bot_factory/admin/admin_migration.sql +0 -136
- {smart_bot_factory-0.3.5.dist-info → smart_bot_factory-0.3.6.dist-info}/WHEEL +0 -0
- {smart_bot_factory-0.3.5.dist-info → smart_bot_factory-0.3.6.dist-info}/entry_points.txt +0 -0
- {smart_bot_factory-0.3.5.dist-info → smart_bot_factory-0.3.6.dist-info}/licenses/LICENSE +0 -0
|
@@ -271,6 +271,13 @@ async def process_events(session_id: str, events: list, user_id: int) -> bool:
|
|
|
271
271
|
# Выполняем событие
|
|
272
272
|
result = await execute_event_handler(event_type, user_id, event_info)
|
|
273
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
|
+
|
|
274
281
|
# Сохраняем в БД УЖЕ со статусом completed (избегаем дублирования)
|
|
275
282
|
event_record = {
|
|
276
283
|
'event_type': event_type,
|
|
@@ -281,7 +288,8 @@ async def process_events(session_id: str, events: list, user_id: int) -> bool:
|
|
|
281
288
|
'status': 'completed', # Сразу completed!
|
|
282
289
|
'session_id': session_id,
|
|
283
290
|
'executed_at': __import__('datetime').datetime.now(__import__('datetime').timezone.utc).isoformat(),
|
|
284
|
-
'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'
|
|
285
293
|
}
|
|
286
294
|
response = supabase_client.client.table('scheduled_events').insert(event_record).execute()
|
|
287
295
|
event_id = response.data[0]['id']
|
|
@@ -1271,6 +1271,11 @@ async def update_event_result(
|
|
|
1271
1271
|
if result_data:
|
|
1272
1272
|
import json
|
|
1273
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}")
|
|
1274
1279
|
|
|
1275
1280
|
if error_message:
|
|
1276
1281
|
update_data['last_error'] = error_message
|
|
@@ -1471,8 +1476,16 @@ async def background_event_processor():
|
|
|
1471
1476
|
continue
|
|
1472
1477
|
|
|
1473
1478
|
# Выполняем событие
|
|
1474
|
-
await process_scheduled_event(event)
|
|
1475
|
-
|
|
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)
|
|
1476
1489
|
logger.info(f"✅ Событие {event['id']} выполнено")
|
|
1477
1490
|
|
|
1478
1491
|
except Exception as e:
|
|
@@ -1486,7 +1499,7 @@ async def background_event_processor():
|
|
|
1486
1499
|
await asyncio.sleep(60)
|
|
1487
1500
|
|
|
1488
1501
|
async def process_scheduled_event(event: Dict):
|
|
1489
|
-
"""Обрабатывает одно событие из БД"""
|
|
1502
|
+
"""Обрабатывает одно событие из БД и возвращает результат"""
|
|
1490
1503
|
|
|
1491
1504
|
event_type = event['event_type']
|
|
1492
1505
|
event_category = event['event_category']
|
|
@@ -1495,14 +1508,17 @@ async def process_scheduled_event(event: Dict):
|
|
|
1495
1508
|
|
|
1496
1509
|
logger.info(f"🔄 Обработка события {event['id']}: {event_category}/{event_type}")
|
|
1497
1510
|
|
|
1511
|
+
result = None
|
|
1498
1512
|
if event_category == 'scheduled_task':
|
|
1499
|
-
await execute_scheduled_task(event_type, user_id, event_data)
|
|
1513
|
+
result = await execute_scheduled_task(event_type, user_id, event_data)
|
|
1500
1514
|
elif event_category == 'global_handler':
|
|
1501
|
-
await execute_global_handler(event_type, event_data)
|
|
1515
|
+
result = await execute_global_handler(event_type, event_data)
|
|
1502
1516
|
elif event_category == 'user_event':
|
|
1503
|
-
await execute_event_handler(event_type, user_id, event_data)
|
|
1517
|
+
result = await execute_event_handler(event_type, user_id, event_data)
|
|
1504
1518
|
else:
|
|
1505
1519
|
logger.warning(f"⚠️ Неизвестная категория события: {event_category}")
|
|
1520
|
+
|
|
1521
|
+
return result
|
|
1506
1522
|
|
|
1507
1523
|
# =============================================================================
|
|
1508
1524
|
# ОБНОВЛЕННЫЕ ФУНКЦИИ С СОХРАНЕНИЕМ В БД
|
|
@@ -1884,3 +1900,69 @@ async def process_admin_event(event: Dict):
|
|
|
1884
1900
|
shutil.rmtree(temp_after_msg, ignore_errors=True)
|
|
1885
1901
|
logger.error(f"❌ Критическая ошибка обработки события: {e}")
|
|
1886
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
|
+
}
|
|
@@ -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'),
|
|
@@ -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
|
|
@@ -44,6 +44,7 @@ Description-Content-Type: text/markdown
|
|
|
44
44
|
- [event_handler](#event_handler---обработчики-событий)
|
|
45
45
|
- [schedule_task](#schedule_task---запланированные-задачи)
|
|
46
46
|
- [global_handler](#global_handler---глобальные-обработчики)
|
|
47
|
+
- [Dashboard Info](#-dashboard-info---отправка-данных-в-дашборд)
|
|
47
48
|
- [Хуки для кастомизации](#-хуки-для-кастомизации)
|
|
48
49
|
- [Telegram роутеры](#-telegram-роутеры)
|
|
49
50
|
- [Расширенные возможности](#-расширенные-возможности)
|
|
@@ -187,9 +188,10 @@ sbf link
|
|
|
187
188
|
|
|
188
189
|
```python
|
|
189
190
|
@event_router.event_handler(
|
|
190
|
-
event_type: str,
|
|
191
|
-
notify: bool = False,
|
|
192
|
-
once_only: bool = True
|
|
191
|
+
event_type: str, # Тип события
|
|
192
|
+
notify: bool = False, # Уведомлять админов
|
|
193
|
+
once_only: bool = True, # Выполнять только 1 раз
|
|
194
|
+
send_ai_response: bool = True # Отправлять ответ от ИИ
|
|
193
195
|
)
|
|
194
196
|
async def handler(user_id: int, event_data: str):
|
|
195
197
|
# Ваш код
|
|
@@ -201,6 +203,7 @@ async def handler(user_id: int, event_data: str):
|
|
|
201
203
|
- **`event_type`** (обязательный) - Уникальное имя события
|
|
202
204
|
- **`notify`** (по умолчанию `False`) - Отправлять уведомление админам после выполнения
|
|
203
205
|
- **`once_only`** (по умолчанию `True`) - Если `True`, событие выполнится только 1 раз для пользователя
|
|
206
|
+
- **`send_ai_response`** (по умолчанию `True`) - Если `False`, ИИ НЕ отправит сообщение после выполнения обработчика
|
|
204
207
|
|
|
205
208
|
**Как работает:**
|
|
206
209
|
|
|
@@ -235,6 +238,16 @@ async def handle_question(user_id: int, question: str):
|
|
|
235
238
|
"""Обрабатывает вопросы (может быть много)"""
|
|
236
239
|
# Логика обработки
|
|
237
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"}
|
|
238
251
|
```
|
|
239
252
|
|
|
240
253
|
---
|
|
@@ -247,12 +260,13 @@ async def handle_question(user_id: int, question: str):
|
|
|
247
260
|
|
|
248
261
|
```python
|
|
249
262
|
@event_router.schedule_task(
|
|
250
|
-
task_name: str,
|
|
251
|
-
delay: Union[str, int],
|
|
252
|
-
notify: bool = False,
|
|
253
|
-
smart_check: bool = True,
|
|
254
|
-
once_only: bool = True,
|
|
255
|
-
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 # Отправлять ответ от ИИ
|
|
256
270
|
)
|
|
257
271
|
async def handler(user_id: int, user_data: str):
|
|
258
272
|
# Ваш код
|
|
@@ -273,6 +287,7 @@ async def handler(user_id: int, user_data: str):
|
|
|
273
287
|
- **`event_type`** (опционально) - Источник времени события:
|
|
274
288
|
- **Строка**: `"appointment_booking"` - ищет событие в БД и вычисляет время
|
|
275
289
|
- **Функция**: `async def(user_id, user_data) -> datetime` - кастомная логика
|
|
290
|
+
- **`send_ai_response`** (по умолчанию `True`) - Если `False`, ИИ НЕ отправит сообщение после выполнения задачи
|
|
276
291
|
|
|
277
292
|
**Формула времени с `event_type`:**
|
|
278
293
|
|
|
@@ -362,11 +377,12 @@ async def important_reminder(user_id: int, text: str):
|
|
|
362
377
|
|
|
363
378
|
```python
|
|
364
379
|
@event_router.global_handler(
|
|
365
|
-
handler_type: str,
|
|
366
|
-
delay: Union[str, int],
|
|
367
|
-
notify: bool = False,
|
|
368
|
-
once_only: bool = True,
|
|
369
|
-
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 # Отправлять ответ от ИИ
|
|
370
386
|
)
|
|
371
387
|
async def handler(handler_data: str):
|
|
372
388
|
# Ваш код
|
|
@@ -420,6 +436,173 @@ async def notify_promo_ending(handler_data: str):
|
|
|
420
436
|
|
|
421
437
|
---
|
|
422
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
|
+
|
|
423
606
|
## 🎣 Хуки для кастомизации
|
|
424
607
|
|
|
425
608
|
Хуки позволяют внедрять свою логику в стандартную обработку сообщений без переписывания всей функции.
|
|
@@ -813,16 +996,46 @@ DEBUG_MODE=false
|
|
|
813
996
|
|
|
814
997
|
## 🎯 Сравнение декораторов
|
|
815
998
|
|
|
816
|
-
| Декоратор | Когда выполняется | Для кого |
|
|
817
|
-
|
|
818
|
-
| `@event_handler` | Немедленно | 1 пользователь | `event_type`, `notify`, `once_only` |
|
|
819
|
-
| `@schedule_task` | Через время | 1 пользователь | `task_name`, `delay`, `event_type`, `smart_check`, `once_only`, `notify` |
|
|
820
|
-
| `@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` |
|
|
821
1004
|
|
|
822
1005
|
---
|
|
823
1006
|
|
|
824
1007
|
## 🔑 Ключевые концепции
|
|
825
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
|
+
|
|
826
1039
|
### `once_only=True`
|
|
827
1040
|
|
|
828
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,9 +29,9 @@ 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=
|
|
34
|
+
smart_bot_factory/core/decorators.py,sha256=wiYyFTai7NebH7wQuxEsLbYGOExiCUmpXqz9V2FkXpM,98782
|
|
36
35
|
smart_bot_factory/core/message_sender.py,sha256=0-SQcK4W1x__VgvyaeVRuFlXcxV56TsR_nNK07Nr4b4,32763
|
|
37
36
|
smart_bot_factory/core/router.py,sha256=ji7rzpuKaO8yKaxFW58WhlgG5ExXlbCgqCTONxAyqL4,15022
|
|
38
37
|
smart_bot_factory/core/router_manager.py,sha256=dUwesog-oHk1U2EDdS8p0e4MTSkwtx5_qXn6nrJ9l9I,9700
|
|
@@ -40,10 +39,11 @@ smart_bot_factory/core/states.py,sha256=L8qp1UmYFuxTN5U9tY076rDuKgxtFbpSGqBpva2e
|
|
|
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
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
|
|
@@ -52,8 +52,8 @@ smart_bot_factory/utils/__init__.py,sha256=UhsJXEHfrIK8h1AHsroHSwAriijk-LvnqLyvg
|
|
|
52
52
|
smart_bot_factory/utils/debug_routing.py,sha256=BOoDhKBg7UXe5uHQxRk3TSfPfLPOFqt0N7lAo6kjCOo,4719
|
|
53
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
|