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

@@ -242,3 +242,210 @@ async def send_message_by_human(
242
242
  "error": str(e),
243
243
  "user_id": user_id
244
244
  }
245
+
246
+ async def send_message_to_users_by_stage(
247
+ stage: str,
248
+ message_text: str,
249
+ bot_id: str
250
+ ) -> Dict[str, Any]:
251
+ """
252
+ Отправляет сообщение всем пользователям, находящимся на определенной стадии
253
+
254
+ Args:
255
+ stage: Стадия диалога (например, 'introduction', 'qualification', 'closing')
256
+ message_text: Текст сообщения для отправки
257
+ bot_id: ID бота (если не указан, используется текущий бот)
258
+
259
+ Returns:
260
+ Результат отправки с количеством отправленных сообщений
261
+ """
262
+ try:
263
+ # Импортируем необходимые компоненты
264
+ from ..handlers.handlers import get_global_var
265
+ bot = get_global_var('bot')
266
+ supabase_client = get_global_var('supabase_client')
267
+ current_bot_id = get_global_var('config').BOT_ID if get_global_var('config') else bot_id
268
+
269
+ if not current_bot_id:
270
+ return {
271
+ "status": "error",
272
+ "error": "Не удалось определить bot_id"
273
+ }
274
+
275
+ logger.info(f"🔍 Ищем пользователей на стадии '{stage}' для бота '{current_bot_id}'")
276
+
277
+ # Получаем последние сессии для каждого пользователя с нужной стадией
278
+ # Сначала получаем все активные сессии с нужной стадией
279
+ sessions_query = supabase_client.client.table('sales_chat_sessions').select(
280
+ 'user_id, id, current_stage, created_at'
281
+ ).eq('status', 'active').eq('current_stage', stage)
282
+
283
+ # Фильтруем по bot_id если указан
284
+ if current_bot_id:
285
+ sessions_query = sessions_query.eq('bot_id', current_bot_id)
286
+
287
+ # Сортируем по дате создания (последние сначала)
288
+ sessions_query = sessions_query.order('created_at', desc=True)
289
+
290
+ sessions_data = sessions_query.execute()
291
+
292
+ if not sessions_data.data:
293
+ logger.info(f"📭 Пользователи на стадии '{stage}' не найдены")
294
+ return {
295
+ "status": "success",
296
+ "stage": stage,
297
+ "users_found": 0,
298
+ "messages_sent": 0,
299
+ "errors": []
300
+ }
301
+
302
+ # Выбираем уникальные user_id (берем только последнюю сессию для каждого пользователя)
303
+ unique_users = {}
304
+ for session in sessions_data.data:
305
+ user_id = session['user_id']
306
+ # Если пользователь еще не добавлен, добавляем его (так как сессии отсортированы по дате, первая будет самой последней)
307
+ if user_id not in unique_users:
308
+ unique_users[user_id] = {
309
+ 'session_id': session['id'],
310
+ 'current_stage': session['current_stage']
311
+ }
312
+
313
+ logger.info(f"👥 Найдено {len(unique_users)} уникальных пользователей на стадии '{stage}'")
314
+
315
+ # Отправляем сообщения
316
+ messages_sent = 0
317
+ errors = []
318
+
319
+ for user_id, user_data in unique_users.items():
320
+ session_id = user_data['session_id']
321
+
322
+ try:
323
+ # Отправляем сообщение пользователю
324
+ await bot.send_message(
325
+ chat_id=user_id,
326
+ text=message_text
327
+ )
328
+
329
+ # Сохраняем сообщение в БД
330
+ await supabase_client.add_message(
331
+ session_id=session_id,
332
+ role='assistant',
333
+ content=message_text,
334
+ message_type='text',
335
+ metadata={
336
+ 'sent_by_stage_broadcast': True,
337
+ 'target_stage': stage,
338
+ 'broadcast_timestamp': datetime.now().isoformat()
339
+ }
340
+ )
341
+
342
+ messages_sent += 1
343
+ logger.info(f"✅ Сообщение отправлено пользователю {user_id} (стадия: {stage})")
344
+
345
+ except Exception as e:
346
+ error_msg = f"Ошибка отправки пользователю {user_id}: {str(e)}"
347
+ errors.append(error_msg)
348
+ logger.error(f"❌ {error_msg}")
349
+
350
+ result = {
351
+ "status": "success",
352
+ "stage": stage,
353
+ "users_found": len(unique_users),
354
+ "messages_sent": messages_sent,
355
+ "errors": errors
356
+ }
357
+
358
+ logger.info(f"📊 Результат рассылки по стадии '{stage}': {messages_sent}/{len(unique_users)} сообщений отправлено")
359
+
360
+ return result
361
+
362
+ except Exception as e:
363
+ logger.error(f"❌ Ошибка в send_message_to_users_by_stage: {e}")
364
+ return {
365
+ "status": "error",
366
+ "error": str(e),
367
+ "stage": stage
368
+ }
369
+
370
+ async def get_users_by_stage_stats(
371
+ bot_id: Optional[str] = None
372
+ ) -> Dict[str, Any]:
373
+ """
374
+ Получает статистику пользователей по стадиям
375
+
376
+ Args:
377
+ bot_id: ID бота (если не указан, используется текущий бот)
378
+
379
+ Returns:
380
+ Статистика по стадиям с количеством пользователей
381
+ """
382
+ try:
383
+ # Импортируем необходимые компоненты
384
+ from ..handlers.handlers import get_global_var
385
+ supabase_client = get_global_var('supabase_client')
386
+ current_bot_id = get_global_var('config').BOT_ID if get_global_var('config') else bot_id
387
+
388
+ if not current_bot_id:
389
+ return {
390
+ "status": "error",
391
+ "error": "Не удалось определить bot_id"
392
+ }
393
+
394
+ logger.info(f"📊 Получаем статистику по стадиям для бота '{current_bot_id}'")
395
+
396
+ # Получаем статистику по стадиям с user_id для подсчета уникальных пользователей
397
+ stats_query = supabase_client.client.table('sales_chat_sessions').select(
398
+ 'user_id, current_stage, created_at'
399
+ ).eq('status', 'active')
400
+
401
+ # Фильтруем по bot_id если указан
402
+ if current_bot_id:
403
+ stats_query = stats_query.eq('bot_id', current_bot_id)
404
+
405
+ # Сортируем по дате создания (последние сначала)
406
+ stats_query = stats_query.order('created_at', desc=True)
407
+
408
+ sessions_data = stats_query.execute()
409
+
410
+ # Подсчитываем уникальных пользователей по стадиям (берем последнюю сессию каждого пользователя)
411
+ user_stages = {} # {user_id: stage}
412
+
413
+ for session in sessions_data.data:
414
+ user_id = session['user_id']
415
+ stage = session['current_stage'] or 'unknown'
416
+
417
+ # Если пользователь еще не добавлен, добавляем его стадию (первая встреченная - самая последняя)
418
+ if user_id not in user_stages:
419
+ user_stages[user_id] = stage
420
+
421
+ # Подсчитываем количество пользователей по стадиям
422
+ stage_stats = {}
423
+ for stage in user_stages.values():
424
+ stage_stats[stage] = stage_stats.get(stage, 0) + 1
425
+
426
+ total_users = len(user_stages)
427
+
428
+ # Сортируем по количеству пользователей (по убыванию)
429
+ sorted_stages = sorted(stage_stats.items(), key=lambda x: x[1], reverse=True)
430
+
431
+ result = {
432
+ "status": "success",
433
+ "bot_id": current_bot_id,
434
+ "total_active_users": total_users,
435
+ "stages": dict(sorted_stages),
436
+ "stages_list": sorted_stages
437
+ }
438
+
439
+ logger.info(f"📊 Статистика по стадиям: {total_users} активных пользователей")
440
+ for stage, count in sorted_stages:
441
+ logger.info(f" {stage}: {count} пользователей")
442
+
443
+ return result
444
+
445
+ except Exception as e:
446
+ logger.error(f"❌ Ошибка в get_users_by_stage_stats: {e}")
447
+ return {
448
+ "status": "error",
449
+ "error": str(e),
450
+ "bot_id": bot_id
451
+ }
@@ -2,7 +2,7 @@
2
2
  Роутер для Smart Bot Factory - аналог aiogram Router
3
3
  """
4
4
 
5
- from typing import Dict, Any, Callable
5
+ from typing import Dict, Any, Callable, Union
6
6
  import logging
7
7
 
8
8
  logger = logging.getLogger(__name__)
@@ -57,7 +57,7 @@ class Router:
57
57
  return wrapper
58
58
  return decorator
59
59
 
60
- def schedule_task(self, task_name: str, notify: bool = False, smart_check: bool = True, once_only: bool = True):
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: str = None):
61
61
  """
62
62
  Декоратор для регистрации запланированной задачи в роутере
63
63
 
@@ -66,18 +66,43 @@ class Router:
66
66
  notify: Уведомлять ли админов
67
67
  smart_check: Использовать ли умную проверку
68
68
  once_only: Выполнять ли только один раз
69
+ delay: Время задержки в удобном формате (например, "1h 30m", "45m", 3600) - ОБЯЗАТЕЛЬНО
70
+ event_type: Тип события для напоминания (например, 'appointment_booking') - ОПЦИОНАЛЬНО
69
71
  """
70
72
  def decorator(func: Callable) -> Callable:
73
+ # Время ОБЯЗАТЕЛЬНО должно быть указано
74
+ if delay is None:
75
+ raise ValueError(f"Для задачи '{task_name}' в роутере {self.name} ОБЯЗАТЕЛЬНО нужно указать параметр delay")
76
+
77
+ # Импортируем функцию парсинга времени
78
+ from .decorators import parse_time_string
79
+
80
+ # Парсим время
81
+ try:
82
+ default_delay_seconds = parse_time_string(delay)
83
+ if event_type:
84
+ logger.info(f"⏰ Роутер {self.name}: задача '{task_name}' настроена как напоминание о событии '{event_type}' за {delay} ({default_delay_seconds}с)")
85
+ else:
86
+ logger.info(f"⏰ Роутер {self.name}: задача '{task_name}' настроена с задержкой: {delay} ({default_delay_seconds}с)")
87
+ except ValueError as e:
88
+ logger.error(f"❌ Ошибка парсинга времени для задачи '{task_name}' в роутере {self.name}: {e}")
89
+ raise
90
+
71
91
  self._scheduled_tasks[task_name] = {
72
92
  'handler': func,
73
93
  'name': func.__name__,
74
94
  'notify': notify,
75
95
  'smart_check': smart_check,
76
96
  'once_only': once_only,
77
- 'router': self.name
97
+ 'router': self.name,
98
+ 'default_delay': default_delay_seconds,
99
+ 'event_type': event_type # Новое поле для типа события
78
100
  }
79
101
 
80
- logger.info(f"⏰ Роутер {self.name}: зарегистрирована задача '{task_name}': {func.__name__}")
102
+ if event_type:
103
+ logger.info(f"⏰ Роутер {self.name}: зарегистрирована задача-напоминание '{task_name}' для события '{event_type}': {func.__name__}")
104
+ else:
105
+ logger.info(f"⏰ Роутер {self.name}: зарегистрирована задача '{task_name}': {func.__name__}")
81
106
 
82
107
  from functools import wraps
83
108
  @wraps(func)
@@ -90,7 +115,7 @@ class Router:
90
115
  return wrapper
91
116
  return decorator
92
117
 
93
- def global_handler(self, handler_type: str, notify: bool = False, once_only: bool = True):
118
+ def global_handler(self, handler_type: str, notify: bool = False, once_only: bool = True, delay: Union[str, int] = None):
94
119
  """
95
120
  Декоратор для регистрации глобального обработчика в роутере
96
121
 
@@ -98,14 +123,31 @@ class Router:
98
123
  handler_type: Тип глобального обработчика
99
124
  notify: Уведомлять ли админов
100
125
  once_only: Выполнять ли только один раз
126
+ delay: Время задержки в удобном формате (например, "1h 30m", "45m", 3600) - ОБЯЗАТЕЛЬНО
101
127
  """
102
128
  def decorator(func: Callable) -> Callable:
129
+ # Время ОБЯЗАТЕЛЬНО должно быть указано
130
+ if delay is None:
131
+ raise ValueError(f"Для глобального обработчика '{handler_type}' в роутере {self.name} ОБЯЗАТЕЛЬНО нужно указать параметр delay")
132
+
133
+ # Импортируем функцию парсинга времени
134
+ from .decorators import parse_time_string
135
+
136
+ # Парсим время
137
+ try:
138
+ default_delay_seconds = parse_time_string(delay)
139
+ logger.info(f"🌍 Роутер {self.name}: глобальный обработчик '{handler_type}' настроен с задержкой: {delay} ({default_delay_seconds}с)")
140
+ except ValueError as e:
141
+ logger.error(f"❌ Ошибка парсинга времени для глобального обработчика '{handler_type}' в роутере {self.name}: {e}")
142
+ raise
143
+
103
144
  self._global_handlers[handler_type] = {
104
145
  'handler': func,
105
146
  'name': func.__name__,
106
147
  'notify': notify,
107
148
  'once_only': once_only,
108
- 'router': self.name
149
+ 'router': self.name,
150
+ 'default_delay': default_delay_seconds
109
151
  }
110
152
 
111
153
  logger.info(f"🌍 Роутер {self.name}: зарегистрирован глобальный обработчик '{handler_type}': {func.__name__}")
@@ -120,7 +162,7 @@ class Router:
120
162
  raise
121
163
  return wrapper
122
164
  return decorator
123
-
165
+
124
166
  def get_event_handlers(self) -> Dict[str, Dict[str, Any]]:
125
167
  """Получает все обработчики событий роутера"""
126
168
  return self._event_handlers.copy()
@@ -169,5 +211,4 @@ class Router:
169
211
  logger.info(f"🔗 Роутер {self.name}: включен роутер {router.name}")
170
212
 
171
213
  def __repr__(self):
172
- return f"Router(name='{self.name}', events={len(self._event_handlers)}, tasks={len(self._scheduled_tasks)}, globals={len(self._global_handlers)})"
173
-
214
+ return f"Router(name='{self.name}', events={len(self._event_handlers)}, tasks={len(self._scheduled_tasks)}, globals={len(self._global_handlers)})"
@@ -60,29 +60,41 @@ class RouterManager:
60
60
  'global_handlers': {}
61
61
  }
62
62
 
63
+ logger.debug(f"🔍 RouterManager._update_combined_handlers(): обновляем обработчики для {len(self._routers)} роутеров")
64
+
63
65
  # Собираем обработчики из всех роутеров
64
66
  for router in self._routers:
67
+ logger.debug(f"🔍 Обрабатываем роутер: {router.name}")
68
+
65
69
  # Обработчики событий
66
- for event_type, handler_info in router.get_event_handlers().items():
70
+ event_handlers = router.get_event_handlers()
71
+ logger.debug(f"🔍 Роутер {router.name}: {len(event_handlers)} обработчиков событий")
72
+ for event_type, handler_info in event_handlers.items():
67
73
  if event_type in self._combined_handlers['event_handlers']:
68
74
  existing_router = self._combined_handlers['event_handlers'][event_type]['router']
69
75
  logger.warning(f"⚠️ Конфликт обработчиков событий '{event_type}' между роутерами {existing_router} и {router.name}")
70
76
  self._combined_handlers['event_handlers'][event_type] = handler_info
71
77
 
72
78
  # Запланированные задачи
73
- for task_name, task_info in router.get_scheduled_tasks().items():
79
+ scheduled_tasks = router.get_scheduled_tasks()
80
+ logger.debug(f"🔍 Роутер {router.name}: {len(scheduled_tasks)} запланированных задач: {list(scheduled_tasks.keys())}")
81
+ for task_name, task_info in scheduled_tasks.items():
74
82
  if task_name in self._combined_handlers['scheduled_tasks']:
75
83
  existing_router = self._combined_handlers['scheduled_tasks'][task_name]['router']
76
84
  logger.warning(f"⚠️ Конфликт задач '{task_name}' между роутерами {existing_router} и {router.name}")
77
85
  self._combined_handlers['scheduled_tasks'][task_name] = task_info
78
86
 
79
87
  # Глобальные обработчики
80
- for handler_type, handler_info in router.get_global_handlers().items():
88
+ global_handlers = router.get_global_handlers()
89
+ logger.debug(f"🔍 Роутер {router.name}: {len(global_handlers)} глобальных обработчиков")
90
+ for handler_type, handler_info in global_handlers.items():
81
91
  if handler_type in self._combined_handlers['global_handlers']:
82
92
  existing_router = self._combined_handlers['global_handlers'][handler_type]['router']
83
93
  logger.warning(f"⚠️ Конфликт глобальных обработчиков '{handler_type}' между роутерами {existing_router} и {router.name}")
84
94
  self._combined_handlers['global_handlers'][handler_type] = handler_info
85
95
 
96
+ logger.debug(f"🔍 RouterManager._update_combined_handlers(): итого - {len(self._combined_handlers['scheduled_tasks'])} задач: {list(self._combined_handlers['scheduled_tasks'].keys())}")
97
+
86
98
  total_handlers = (len(self._combined_handlers['event_handlers']) +
87
99
  len(self._combined_handlers['scheduled_tasks']) +
88
100
  len(self._combined_handlers['global_handlers']))
@@ -95,7 +107,9 @@ class RouterManager:
95
107
 
96
108
  def get_scheduled_tasks(self) -> Dict[str, Dict[str, Any]]:
97
109
  """Получает все запланированные задачи"""
98
- return self._combined_handlers['scheduled_tasks'].copy()
110
+ tasks = self._combined_handlers['scheduled_tasks'].copy()
111
+ logger.debug(f"🔍 RouterManager.get_scheduled_tasks(): возвращаем {len(tasks)} задач: {list(tasks.keys())}")
112
+ return tasks
99
113
 
100
114
  def get_global_handlers(self) -> Dict[str, Dict[str, Any]]:
101
115
  """Получает все глобальные обработчики"""
@@ -383,11 +383,16 @@ class BotBuilder:
383
383
  # Устанавливаем глобальные переменные в модуле бота для удобного доступа
384
384
  self.set_global_vars_in_module(self.bot_id)
385
385
 
386
-
387
- # Устанавливаем роутер-менеджер в декораторы
386
+ # Устанавливаем роутер-менеджер в декораторы ПЕРЕД настройкой обработчиков
388
387
  if self.router_manager:
389
388
  from ..core.decorators import set_router_manager
390
389
  set_router_manager(self.router_manager)
390
+ logger.info("✅ RouterManager установлен в decorators")
391
+
392
+ # Обновляем обработчики после установки RouterManager
393
+ # (на случай если декораторы выполнялись после добавления роутера)
394
+ self.router_manager._update_combined_handlers()
395
+ logger.info("✅ RouterManager обработчики обновлены")
391
396
 
392
397
  # Фоновые задачи выполняются через asyncio.create_task в decorators.py
393
398
 
@@ -64,6 +64,116 @@ async def start_handler(message: Message, state: FSMContext):
64
64
  logger.error(f"Ошибка при обработке /start: {e}")
65
65
  await send_message(message, "Произошла ошибка при инициализации. Попробуйте позже.")
66
66
 
67
+ @router.message(Command(commands=["timeup"]))
68
+ async def timeup_handler(message: Message, state: FSMContext):
69
+ """Обработчик команды /timeup - тестирование запланированных событий"""
70
+ from ..core.decorators import process_scheduled_event, update_event_result
71
+ from datetime import datetime
72
+
73
+ supabase_client = get_global_var('supabase_client')
74
+
75
+ try:
76
+ await message.answer("🔄 Запускаю тестирование запланированных событий...")
77
+
78
+ # Получаем события для этого пользователя И глобальные события (user_id = null)
79
+ # 1. События пользователя
80
+ user_events = supabase_client.client.table('scheduled_events').select(
81
+ '*'
82
+ ).eq('user_id', message.from_user.id).in_('status', ['pending', 'immediate']).execute()
83
+
84
+ # 2. Глобальные события (без user_id)
85
+ global_events = supabase_client.client.table('scheduled_events').select(
86
+ '*'
87
+ ).is_('user_id', 'null').in_('status', ['pending', 'immediate']).execute()
88
+
89
+ # Объединяем события
90
+ all_events = (user_events.data or []) + (global_events.data or [])
91
+
92
+ if not all_events:
93
+ await message.answer("📭 Нет запланированных событий для тестирования")
94
+ return
95
+
96
+ total_events = len(all_events)
97
+ user_count = len(user_events.data or [])
98
+ global_count = len(global_events.data or [])
99
+
100
+ status_msg = f"📋 Найдено {total_events} событий:"
101
+ if user_count > 0:
102
+ status_msg += f"\n 👤 Ваших: {user_count}"
103
+ if global_count > 0:
104
+ status_msg += f"\n 🌍 Глобальных: {global_count}"
105
+ status_msg += "\n\nВыполняю их немедленно..."
106
+
107
+ await message.answer(status_msg)
108
+
109
+ # Выполняем каждое событие
110
+ success_count = 0
111
+ failed_count = 0
112
+ results = []
113
+
114
+ for event in all_events:
115
+ event_id = event['id']
116
+ event_type = event['event_type']
117
+ event_category = event['event_category']
118
+ is_global = event.get('user_id') is None
119
+
120
+ try:
121
+ event_label = f"🌍 {event_type}" if is_global else f"👤 {event_type}"
122
+ logger.info(f"🧪 Тестируем событие {event_id}: {event_category}/{event_type} ({'глобальное' if is_global else f'пользователя {message.from_user.id}'})")
123
+
124
+ # Выполняем событие
125
+ await process_scheduled_event(event)
126
+
127
+ # Помечаем как выполненное
128
+ await update_event_result(event_id, 'completed', {
129
+ "executed": True,
130
+ "test_mode": True,
131
+ "tested_by_user": message.from_user.id,
132
+ "tested_at": datetime.now().isoformat()
133
+ })
134
+
135
+ success_count += 1
136
+ results.append(f"✅ {event_label}")
137
+ logger.info(f"✅ Событие {event_id} успешно выполнено")
138
+
139
+ except Exception as e:
140
+ failed_count += 1
141
+ error_msg = str(e)
142
+ event_label = f"🌍 {event_type}" if is_global else f"👤 {event_type}"
143
+ results.append(f"❌ {event_label}: {error_msg[:50]}")
144
+ logger.error(f"❌ Ошибка выполнения события {event_id}: {error_msg}")
145
+
146
+ # Помечаем как failed
147
+ await update_event_result(event_id, 'failed', None, error_msg)
148
+
149
+ # Отправляем итоговую статистику
150
+ result_text = [
151
+ "📊 **Результаты тестирования:**",
152
+ "",
153
+ f"✅ Успешно: {success_count}",
154
+ f"❌ Ошибок: {failed_count}",
155
+ f"📋 Всего: {total_events}",
156
+ "",
157
+ "**События:**",
158
+ f"👤 - ваши события",
159
+ f"🌍 - глобальные события",
160
+ ""
161
+ ]
162
+
163
+ # Добавляем результаты (максимум 10 событий)
164
+ for result in results[:10]:
165
+ result_text.append(result)
166
+
167
+ if len(results) > 10:
168
+ result_text.append(f"... и еще {len(results) - 10} событий")
169
+
170
+ await message.answer("\n".join(result_text))
171
+
172
+ except Exception as e:
173
+ logger.error(f"❌ Критическая ошибка в timeup_handler: {e}")
174
+ await message.answer(f"❌ Ошибка тестирования: {str(e)}")
175
+
176
+
67
177
  async def user_start_handler(message: Message, state: FSMContext):
68
178
  """Обработчик /start для обычных пользователей"""
69
179
  supabase_client = get_global_var('supabase_client')
@@ -842,21 +842,91 @@ class SupabaseClient:
842
842
  """Проверяет, изменился ли этап пользователя с момента планирования события"""
843
843
  try:
844
844
  # Получаем текущую информацию о сессии
845
- response = self.client.table('sales_chat_sessions').select(
845
+ current_response = self.client.table('sales_chat_sessions').select(
846
846
  'id', 'current_stage'
847
847
  ).eq('user_telegram_id', user_id).order('created_at', desc=True).limit(1).execute()
848
848
 
849
- if not response.data:
849
+ if not current_response.data:
850
850
  return False
851
851
 
852
- current_session = response.data[0]
852
+ current_session = current_response.data[0]
853
853
 
854
854
  # Если сессия изменилась - этап точно изменился
855
855
  if current_session['id'] != original_session_id:
856
856
  return True
857
857
 
858
+ # Если сессия та же, получаем оригинальный этап из scheduled_events
859
+ # и сравниваем с текущим
860
+ original_response = self.client.table('sales_chat_sessions').select(
861
+ 'current_stage'
862
+ ).eq('id', original_session_id).execute()
863
+
864
+ if not original_response.data:
865
+ # Если не нашли оригинальную сессию, считаем что этап не изменился
866
+ return False
867
+
868
+ original_stage = original_response.data[0]['current_stage']
869
+ current_stage = current_session['current_stage']
870
+
871
+ # Проверяем, изменился ли этап внутри той же сессии
872
+ if original_stage != current_stage:
873
+ logger.info(f"🔄 Этап изменился: {original_stage} -> {current_stage} (сессия {original_session_id})")
874
+ return True
875
+
858
876
  return False
859
877
 
860
878
  except Exception as e:
861
879
  logger.error(f"Ошибка проверки изменения этапа пользователя {user_id}: {e}")
862
- return False
880
+ return False
881
+
882
+ async def get_last_event_info_by_user_and_type(self, user_id: int, event_type: str) -> Optional[str]:
883
+ """
884
+ Получает event_info последнего события определенного типа для пользователя
885
+
886
+ Args:
887
+ user_id: Telegram ID пользователя
888
+ event_type: Тип события для поиска
889
+
890
+ Returns:
891
+ str: event_info последнего найденного события или None если не найдено
892
+ """
893
+ try:
894
+ # 1. Получаем последнюю сессию пользователя
895
+ sessions_query = self.client.table('sales_chat_sessions').select(
896
+ 'id'
897
+ ).eq('user_id', user_id).order('created_at', desc=True).limit(1)
898
+
899
+ # Фильтруем по bot_id если указан
900
+ if self.bot_id:
901
+ sessions_query = sessions_query.eq('bot_id', self.bot_id)
902
+
903
+ sessions_response = sessions_query.execute()
904
+
905
+ if not sessions_response.data:
906
+ logger.info(f"Пользователь {user_id} не найден в сессиях")
907
+ return None
908
+
909
+ session_id = sessions_response.data[0]['id']
910
+ logger.info(f"Найдена последняя сессия {session_id} для пользователя {user_id}")
911
+
912
+ # 2. Ищем последнее событие с этим session_id и event_type
913
+ events_response = self.client.table('session_events').select(
914
+ 'event_info', 'created_at'
915
+ ).eq('session_id', session_id).eq('event_type', event_type).order(
916
+ 'created_at', desc=True
917
+ ).limit(1).execute()
918
+
919
+ if not events_response.data:
920
+ logger.info(f"События типа '{event_type}' не найдены для сессии {session_id}")
921
+ return None
922
+
923
+ event_info = events_response.data[0]['event_info']
924
+ created_at = events_response.data[0]['created_at']
925
+
926
+ logger.info(f"Найдено последнее событие '{event_type}' для пользователя {user_id}: {event_info[:50]}... (создано: {created_at})")
927
+
928
+ return event_info
929
+
930
+ except Exception as e:
931
+ logger.error(f"Ошибка получения последнего события для пользователя {user_id}, тип '{event_type}': {e}")
932
+ return None
@@ -1,12 +1,16 @@
1
- """
2
- Message модули smart_bot_factory
3
- """
4
-
5
-
6
- from ..core.message_sender import send_message_by_human, send_message_by_ai
7
-
8
- __all__ = [
9
- 'send_message_by_human',
10
- 'send_message_by_ai',
11
- 'send_message_to_users_by_stage'
1
+ """
2
+ Message модули smart_bot_factory
3
+ """
4
+
5
+
6
+ from ..core.message_sender import (
7
+ send_message_by_human,
8
+ send_message_by_ai,
9
+ send_message_to_users_by_stage,
10
+ )
11
+
12
+ __all__ = [
13
+ 'send_message_by_human',
14
+ 'send_message_by_ai',
15
+ 'send_message_to_users_by_stage',
12
16
  ]
@@ -4,4 +4,4 @@ Supabase клиент с автоматической загрузкой нас
4
4
 
5
5
  from .client import SupabaseClient
6
6
 
7
- __all__ = ['SupabaseClient']
7
+ __all__ = ['SupabaseClient']