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.

@@ -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
- # Автоматически добавляем флаг notify к результату
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
- # Автоматически добавляем флаг notify к результату
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
- # Автоматически добавляем флаг notify к результату
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
- await update_event_result(event['id'], 'completed', {"processed": True})
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,
@@ -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
- return await func(*args, **kwargs)
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
- return await func(*args, **kwargs)
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
- return await func(*args, **kwargs)
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
@@ -0,0 +1,5 @@
1
+ from ..core.decorators import prepare_dashboard_info
2
+
3
+ __all__ = [
4
+ 'prepare_dashboard_info'
5
+ ]
@@ -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.info(" ✅ События обработаны")
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.4
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 # Выполнять только 1 раз
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], # Задержка: "1h 30m" или секунды
254
- notify: bool = False, # Уведомлять админов
255
- smart_check: bool = True, # Умная проверка активности
256
- once_only: bool = True, # Выполнять только 1 раз
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, # Выполнять только 1 раз
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=Be334BPspBgHNO9OpEiPlrCc0Gmy7_8sox5WpV8XHic,45174
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=oSsX1yCULoUfVf2aNynBmvap7zDEh5-cEcW4yoRv3dc,93903
36
- smart_bot_factory/core/message_sender.py,sha256=J4b6n8nXVjqf-qzL6URRSvc-FVnQfShwujVSM6qv26w,32232
37
- smart_bot_factory/core/router.py,sha256=03fbysaj0LR96p4-8iiml8dTmEHCkR-AaTposSv8q8o,11898
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=ymPCQPjA0uQc9deiwiVrMhxpBsqdKsAfeajHzK74seA,60739
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=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
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=JSn7CsWnToSbHYtURdeuZn7ectyDqQGrPGHN2ixIGkw,19930
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.4.dist-info/METADATA,sha256=lO_csJRkPwV6c8SUmBUEN4WBl9X8LTx_lSt_FALl6J0,31991
56
- smart_bot_factory-0.3.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
57
- smart_bot_factory-0.3.4.dist-info/entry_points.txt,sha256=ybKEAI0WSb7WoRiey7QE-HHfn88UGV7nxLDxXq7b7SU,50
58
- smart_bot_factory-0.3.4.dist-info/licenses/LICENSE,sha256=OrK3cwdUTzNzIhJvSPtJaVMoYIyC_sSx5EFE_FDMvGs,1092
59
- smart_bot_factory-0.3.4.dist-info/RECORD,,
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;