smart-bot-factory 0.3.5__py3-none-any.whl → 0.3.7__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.

@@ -255,6 +255,10 @@ async def process_events(session_id: str, events: list, user_id: int) -> bool:
255
255
  # if session_id:
256
256
  # check_query = check_query.eq('session_id', session_id)
257
257
 
258
+ # 🆕 Фильтруем по bot_id если указан
259
+ if supabase_client.bot_id:
260
+ check_query = check_query.eq('bot_id', supabase_client.bot_id)
261
+
258
262
  existing = check_query.execute()
259
263
 
260
264
  logger.info(f" 🔍 Проверка БД: найдено {len(existing.data) if existing.data else 0} выполненных событий '{event_type}' для user_id={user_id}")
@@ -271,6 +275,13 @@ async def process_events(session_id: str, events: list, user_id: int) -> bool:
271
275
  # Выполняем событие
272
276
  result = await execute_event_handler(event_type, user_id, event_info)
273
277
 
278
+ # Проверяем наличие поля 'info' для дашборда
279
+ import json
280
+ info_dashboard_json = None
281
+ if isinstance(result, dict) and 'info' in result:
282
+ info_dashboard_json = json.dumps(result['info'], ensure_ascii=False)
283
+ logger.info(f" 📊 Дашборд данные добавлены: {result['info'].get('title', 'N/A')}")
284
+
274
285
  # Сохраняем в БД УЖЕ со статусом completed (избегаем дублирования)
275
286
  event_record = {
276
287
  'event_type': event_type,
@@ -281,8 +292,14 @@ async def process_events(session_id: str, events: list, user_id: int) -> bool:
281
292
  'status': 'completed', # Сразу completed!
282
293
  'session_id': session_id,
283
294
  '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
295
+ 'result_data': __import__('json').dumps(result, ensure_ascii=False) if result else None,
296
+ 'info_dashboard': info_dashboard_json # Добавится только если есть поле 'info'
285
297
  }
298
+
299
+ # 🆕 Добавляем bot_id если указан
300
+ if supabase_client.bot_id:
301
+ event_record['bot_id'] = supabase_client.bot_id
302
+
286
303
  response = supabase_client.client.table('scheduled_events').insert(event_record).execute()
287
304
  event_id = response.data[0]['id']
288
305
 
@@ -303,6 +320,11 @@ async def process_events(session_id: str, events: list, user_id: int) -> bool:
303
320
  'session_id': session_id,
304
321
  'last_error': str(e)
305
322
  }
323
+
324
+ # 🆕 Добавляем bot_id если указан
325
+ if supabase_client.bot_id:
326
+ event_record['bot_id'] = supabase_client.bot_id
327
+
306
328
  supabase_client.client.table('scheduled_events').insert(event_record).execute()
307
329
  raise
308
330
 
@@ -1135,6 +1135,10 @@ async def save_immediate_event(
1135
1135
  'session_id': session_id
1136
1136
  }
1137
1137
 
1138
+ # 🆕 Добавляем bot_id если указан
1139
+ if supabase_client.bot_id:
1140
+ event_record['bot_id'] = supabase_client.bot_id
1141
+
1138
1142
  try:
1139
1143
  response = supabase_client.client.table('scheduled_events').insert(event_record).execute()
1140
1144
  event_id = response.data[0]['id']
@@ -1187,6 +1191,10 @@ async def save_scheduled_task(
1187
1191
  'session_id': session_id
1188
1192
  }
1189
1193
 
1194
+ # 🆕 Добавляем bot_id если указан
1195
+ if supabase_client.bot_id:
1196
+ event_record['bot_id'] = supabase_client.bot_id
1197
+
1190
1198
  try:
1191
1199
  response = supabase_client.client.table('scheduled_events').insert(event_record).execute()
1192
1200
  event_id = response.data[0]['id']
@@ -1241,6 +1249,10 @@ async def save_global_event(
1241
1249
  'status': status
1242
1250
  }
1243
1251
 
1252
+ # 🆕 Добавляем bot_id если указан (глобальные события тоже привязаны к боту)
1253
+ if supabase_client.bot_id:
1254
+ event_record['bot_id'] = supabase_client.bot_id
1255
+
1244
1256
  try:
1245
1257
  response = supabase_client.client.table('scheduled_events').insert(event_record).execute()
1246
1258
  event_id = response.data[0]['id']
@@ -1271,6 +1283,11 @@ async def update_event_result(
1271
1283
  if result_data:
1272
1284
  import json
1273
1285
  update_data['result_data'] = json.dumps(result_data, ensure_ascii=False)
1286
+
1287
+ # Проверяем наличие поля 'info' для дашборда
1288
+ if isinstance(result_data, dict) and 'info' in result_data:
1289
+ update_data['info_dashboard'] = json.dumps(result_data['info'], ensure_ascii=False)
1290
+ logger.info(f"📊 Дашборд данные добавлены в событие {event_id}")
1274
1291
 
1275
1292
  if error_message:
1276
1293
  update_data['last_error'] = error_message
@@ -1298,13 +1315,18 @@ async def get_pending_events(limit: int = 50) -> list:
1298
1315
  try:
1299
1316
  now = datetime.now(timezone.utc).isoformat()
1300
1317
 
1301
- response = supabase_client.client.table('scheduled_events')\
1318
+ query = supabase_client.client.table('scheduled_events')\
1302
1319
  .select('*')\
1303
1320
  .in_('status', ['pending', 'immediate'])\
1304
1321
  .or_(f'scheduled_at.is.null,scheduled_at.lte.{now}')\
1305
1322
  .order('created_at')\
1306
- .limit(limit)\
1307
- .execute()
1323
+ .limit(limit)
1324
+
1325
+ # 🆕 Фильтруем по bot_id если указан
1326
+ if supabase_client.bot_id:
1327
+ query = query.eq('bot_id', supabase_client.bot_id)
1328
+
1329
+ response = query.execute()
1308
1330
 
1309
1331
  return response.data
1310
1332
  except Exception as e:
@@ -1323,13 +1345,18 @@ async def get_pending_events_in_next_minute(limit: int = 100) -> list:
1323
1345
  now = datetime.now(timezone.utc)
1324
1346
  next_minute = now + timedelta(seconds=60)
1325
1347
 
1326
- response = supabase_client.client.table('scheduled_events')\
1348
+ query = supabase_client.client.table('scheduled_events')\
1327
1349
  .select('*')\
1328
1350
  .in_('status', ['pending', 'immediate'])\
1329
1351
  .or_(f'scheduled_at.is.null,scheduled_at.lte.{next_minute.isoformat()}')\
1330
1352
  .order('created_at')\
1331
- .limit(limit)\
1332
- .execute()
1353
+ .limit(limit)
1354
+
1355
+ # 🆕 Фильтруем по bot_id если указан
1356
+ if supabase_client.bot_id:
1357
+ query = query.eq('bot_id', supabase_client.bot_id)
1358
+
1359
+ response = query.execute()
1333
1360
 
1334
1361
  return response.data
1335
1362
  except Exception as e:
@@ -1471,8 +1498,16 @@ async def background_event_processor():
1471
1498
  continue
1472
1499
 
1473
1500
  # Выполняем событие
1474
- await process_scheduled_event(event)
1475
- await update_event_result(event['id'], 'completed', {"processed": True})
1501
+ result = await process_scheduled_event(event)
1502
+
1503
+ # Проверяем наличие поля 'info' для дашборда
1504
+ result_data = {"processed": True}
1505
+ if isinstance(result, dict):
1506
+ result_data.update(result)
1507
+ if 'info' in result:
1508
+ logger.info(f" 📊 Дашборд данные для задачи: {result['info'].get('title', 'N/A')}")
1509
+
1510
+ await update_event_result(event['id'], 'completed', result_data)
1476
1511
  logger.info(f"✅ Событие {event['id']} выполнено")
1477
1512
 
1478
1513
  except Exception as e:
@@ -1486,7 +1521,7 @@ async def background_event_processor():
1486
1521
  await asyncio.sleep(60)
1487
1522
 
1488
1523
  async def process_scheduled_event(event: Dict):
1489
- """Обрабатывает одно событие из БД"""
1524
+ """Обрабатывает одно событие из БД и возвращает результат"""
1490
1525
 
1491
1526
  event_type = event['event_type']
1492
1527
  event_category = event['event_category']
@@ -1495,14 +1530,17 @@ async def process_scheduled_event(event: Dict):
1495
1530
 
1496
1531
  logger.info(f"🔄 Обработка события {event['id']}: {event_category}/{event_type}")
1497
1532
 
1533
+ result = None
1498
1534
  if event_category == 'scheduled_task':
1499
- await execute_scheduled_task(event_type, user_id, event_data)
1535
+ result = await execute_scheduled_task(event_type, user_id, event_data)
1500
1536
  elif event_category == 'global_handler':
1501
- await execute_global_handler(event_type, event_data)
1537
+ result = await execute_global_handler(event_type, event_data)
1502
1538
  elif event_category == 'user_event':
1503
- await execute_event_handler(event_type, user_id, event_data)
1539
+ result = await execute_event_handler(event_type, user_id, event_data)
1504
1540
  else:
1505
1541
  logger.warning(f"⚠️ Неизвестная категория события: {event_category}")
1542
+
1543
+ return result
1506
1544
 
1507
1545
  # =============================================================================
1508
1546
  # ОБНОВЛЕННЫЕ ФУНКЦИИ С СОХРАНЕНИЕМ В БД
@@ -1689,6 +1727,10 @@ async def check_event_already_processed(event_type: str, user_id: int = None, se
1689
1727
  if session_id:
1690
1728
  query = query.eq('session_id', session_id)
1691
1729
 
1730
+ # 🆕 Фильтруем по bot_id если указан
1731
+ if supabase_client.bot_id:
1732
+ query = query.eq('bot_id', supabase_client.bot_id)
1733
+
1692
1734
  response = query.execute()
1693
1735
 
1694
1736
  if response.data:
@@ -1884,3 +1926,69 @@ async def process_admin_event(event: Dict):
1884
1926
  shutil.rmtree(temp_after_msg, ignore_errors=True)
1885
1927
  logger.error(f"❌ Критическая ошибка обработки события: {e}")
1886
1928
  raise
1929
+
1930
+ # =============================================================================
1931
+ # ФУНКЦИЯ ДЛЯ ПОДГОТОВКИ ДАННЫХ ДАШБОРДА
1932
+ # =============================================================================
1933
+
1934
+ async def prepare_dashboard_info(
1935
+ description_template: str,
1936
+ title: str,
1937
+ user_id: int
1938
+ ) -> Dict[str, Any]:
1939
+ """
1940
+ Подготавливает данные для дашборда (БЕЗ записи в БД)
1941
+
1942
+ Возвращаемый dict нужно поместить в поле 'info' результата обработчика.
1943
+ bot_utils.py автоматически запишет его в столбец info_dashboard таблицы.
1944
+
1945
+ Args:
1946
+ description_template: Строка с {username}, например "{username} купил подписку"
1947
+ title: Заголовок для дашборда
1948
+ user_id: Telegram ID
1949
+
1950
+ Returns:
1951
+ Dict с данными для дашборда
1952
+
1953
+ Example:
1954
+ @event_router.event_handler("collect_phone", notify=True)
1955
+ async def handle_phone_collection(user_id: int, phone_number: str):
1956
+ # ... бизнес-логика ...
1957
+
1958
+ return {
1959
+ "status": "success",
1960
+ "phone": phone_number,
1961
+ "info": await prepare_dashboard_info(
1962
+ description_template="{username} оставил телефон",
1963
+ title="Новый контакт",
1964
+ user_id=user_id
1965
+ )
1966
+ }
1967
+ """
1968
+ supabase_client = get_supabase_client()
1969
+
1970
+ # Получаем username из sales_users
1971
+ username = f"user_{user_id}" # fallback
1972
+ if supabase_client:
1973
+ try:
1974
+ query = supabase_client.client.table('sales_users').select('username').eq('telegram_id', user_id)
1975
+ if supabase_client.bot_id:
1976
+ query = query.eq('bot_id', supabase_client.bot_id)
1977
+ response = query.execute()
1978
+ if response.data:
1979
+ username = response.data[0].get('username') or username
1980
+ except Exception as e:
1981
+ logger.warning(f"⚠️ Не удалось получить username для дашборда: {e}")
1982
+
1983
+ # Форматируем строку
1984
+ description = description_template.format(username=username)
1985
+
1986
+ # Московское время (UTC+3)
1987
+ moscow_tz = timezone(timedelta(hours=3))
1988
+ moscow_time = datetime.now(moscow_tz)
1989
+
1990
+ return {
1991
+ 'title': title,
1992
+ 'description': description,
1993
+ 'created_at': moscow_time.isoformat()
1994
+ }
@@ -0,0 +1,5 @@
1
+ from ..core.decorators import prepare_dashboard_info
2
+
3
+ __all__ = [
4
+ 'prepare_dashboard_info'
5
+ ]
@@ -77,14 +77,26 @@ async def timeup_handler(message: Message, state: FSMContext):
77
77
 
78
78
  # Получаем события для этого пользователя И глобальные события (user_id = null)
79
79
  # 1. События пользователя
80
- user_events = supabase_client.client.table('scheduled_events').select(
80
+ user_events_query = supabase_client.client.table('scheduled_events').select(
81
81
  '*'
82
- ).eq('user_id', message.from_user.id).in_('status', ['pending', 'immediate']).execute()
82
+ ).eq('user_id', message.from_user.id).in_('status', ['pending', 'immediate'])
83
+
84
+ # 🆕 Фильтруем по bot_id если указан
85
+ if supabase_client.bot_id:
86
+ user_events_query = user_events_query.eq('bot_id', supabase_client.bot_id)
87
+
88
+ user_events = user_events_query.execute()
83
89
 
84
90
  # 2. Глобальные события (без user_id)
85
- global_events = supabase_client.client.table('scheduled_events').select(
91
+ global_events_query = supabase_client.client.table('scheduled_events').select(
86
92
  '*'
87
- ).is_('user_id', 'null').in_('status', ['pending', 'immediate']).execute()
93
+ ).is_('user_id', 'null').in_('status', ['pending', 'immediate'])
94
+
95
+ # 🆕 Фильтруем по bot_id если указан
96
+ if supabase_client.bot_id:
97
+ global_events_query = global_events_query.eq('bot_id', supabase_client.bot_id)
98
+
99
+ global_events = global_events_query.execute()
88
100
 
89
101
  # Объединяем события
90
102
  all_events = (user_events.data or []) + (global_events.data or [])
@@ -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.5
3
+ Version: 0.3.7
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 # Выполнять только 1 раз
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], # Задержка: "1h 30m" или секунды
252
- notify: bool = False, # Уведомлять админов
253
- smart_check: bool = True, # Умная проверка активности
254
- once_only: bool = True, # Выполнять только 1 раз
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, # Выполнять только 1 раз
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=4c98tVPThXW4ET6XJ0FYMePrx4gYrw_HNhx3YLep9hY,46976
32
+ smart_bot_factory/core/bot_utils.py,sha256=vblk94k8cXvc-3iS3UOZvnU2NtXXKYgt3vzkioca-ac,48395
34
33
  smart_bot_factory/core/conversation_manager.py,sha256=eoHL7MCEz68DRvTVwRwZgf2PWwGv4T6J9D-I-thETi8,28289
35
- smart_bot_factory/core/decorators.py,sha256=0fDgYNaXU3ZLhFJBxAognLWfy_Fz1F63wdUNaVziDQU,95141
34
+ smart_bot_factory/core/decorators.py,sha256=mYWiN9B0lrgV3uRAaFVkJTCKWZyTpVNM_AneuQcqifA,99872
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
- smart_bot_factory/handlers/handlers.py,sha256=v3ubszkN7ML-CXChveTdp68EdMjHl02NTJ3hMT2zXKA,61893
44
+ smart_bot_factory/handlers/handlers.py,sha256=haxyVvFuyMWvfGCOiAvXPrez96bchoHnwQEEURMqiMI,62409
45
45
  smart_bot_factory/integrations/openai_client.py,sha256=fwaJpwojFdLBWChcFWpFGOHK9upG-nCIwDochkCRRlY,24291
46
- smart_bot_factory/integrations/supabase_client.py,sha256=Rv0sZHXGSfm3UWodmaR1N-X5-2JbmynPWJKY0a0k_Tk,63557
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.5.dist-info/METADATA,sha256=xOG5tl-e-w8yfvxuZfiyVRnQGdEvUxYLMPbKVGTcbmY,31905
56
- smart_bot_factory-0.3.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
57
- smart_bot_factory-0.3.5.dist-info/entry_points.txt,sha256=ybKEAI0WSb7WoRiey7QE-HHfn88UGV7nxLDxXq7b7SU,50
58
- smart_bot_factory-0.3.5.dist-info/licenses/LICENSE,sha256=OrK3cwdUTzNzIhJvSPtJaVMoYIyC_sSx5EFE_FDMvGs,1092
59
- smart_bot_factory-0.3.5.dist-info/RECORD,,
55
+ smart_bot_factory-0.3.7.dist-info/METADATA,sha256=cfOUXXewYMK7EomidcIP78W_yjjCnWHtmQ7efPx8FMA,40662
56
+ smart_bot_factory-0.3.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
57
+ smart_bot_factory-0.3.7.dist-info/entry_points.txt,sha256=ybKEAI0WSb7WoRiey7QE-HHfn88UGV7nxLDxXq7b7SU,50
58
+ smart_bot_factory-0.3.7.dist-info/licenses/LICENSE,sha256=OrK3cwdUTzNzIhJvSPtJaVMoYIyC_sSx5EFE_FDMvGs,1092
59
+ smart_bot_factory-0.3.7.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;