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

Files changed (37) hide show
  1. smart_bot_factory/__init__.py +0 -48
  2. smart_bot_factory/admin/admin_logic.py +11 -11
  3. smart_bot_factory/cli.py +299 -106
  4. smart_bot_factory/clients/__init__.py +33 -0
  5. smart_bot_factory/configs/growthmed-october-24/prompts/final_instructions.txt +2 -0
  6. smart_bot_factory/configs/growthmed-october-24/tests/quick_scenarios.yaml +95 -28
  7. smart_bot_factory/core/__init__.py +43 -22
  8. smart_bot_factory/core/bot_utils.py +268 -95
  9. smart_bot_factory/core/conversation_manager.py +542 -535
  10. smart_bot_factory/core/decorators.py +943 -229
  11. smart_bot_factory/core/globals.py +68 -0
  12. smart_bot_factory/core/message_sender.py +6 -6
  13. smart_bot_factory/core/router.py +172 -0
  14. smart_bot_factory/core/router_manager.py +165 -0
  15. smart_bot_factory/creation/__init__.py +1 -2
  16. smart_bot_factory/creation/bot_builder.py +116 -8
  17. smart_bot_factory/creation/bot_testing.py +74 -13
  18. smart_bot_factory/handlers/handlers.py +10 -2
  19. smart_bot_factory/integrations/__init__.py +1 -0
  20. smart_bot_factory/integrations/supabase_client.py +272 -2
  21. smart_bot_factory/utm_link_generator.py +106 -0
  22. smart_bot_factory-0.1.5.dist-info/METADATA +466 -0
  23. {smart_bot_factory-0.1.3.dist-info → smart_bot_factory-0.1.5.dist-info}/RECORD +26 -31
  24. smart_bot_factory/configs/growthmed-helper/env_example.txt +0 -1
  25. smart_bot_factory/configs/growthmed-helper/prompts/1sales_context.txt +0 -9
  26. smart_bot_factory/configs/growthmed-helper/prompts/2product_info.txt +0 -582
  27. smart_bot_factory/configs/growthmed-helper/prompts/3objection_handling.txt +0 -66
  28. smart_bot_factory/configs/growthmed-helper/prompts/final_instructions.txt +0 -232
  29. smart_bot_factory/configs/growthmed-helper/prompts/help_message.txt +0 -28
  30. smart_bot_factory/configs/growthmed-helper/prompts/welcome_message.txt +0 -7
  31. smart_bot_factory/configs/growthmed-helper/welcome_file/welcome_file_msg.txt +0 -16
  32. smart_bot_factory/configs/growthmed-helper/welcome_file//342/225/250/320/267/342/225/250/342/225/241/342/225/250/342/225/221 /342/225/250/342/225/227/342/225/250/342/225/225/342/225/244/320/221/342/225/244/320/222 /342/225/250/342/224/220/342/225/250/342/225/233 152/342/225/250/320/264/342/225/250/320/247 /342/225/250/342/225/225 323/342/225/250/320/264/342/225/250/320/247 /342/225/250/342/224/244/342/225/250/342/225/227/342/225/244/320/237 /342/225/250/342/225/235/342/225/250/342/225/241/342/225/250/342/224/244/342/225/250/342/225/225/342/225/244/320/226/342/225/250/342/225/225/342/225/250/342/225/234/342/225/244/320/233.pdf +0 -0
  33. smart_bot_factory/uv.lock +0 -2004
  34. smart_bot_factory-0.1.3.dist-info/METADATA +0 -126
  35. {smart_bot_factory-0.1.3.dist-info → smart_bot_factory-0.1.5.dist-info}/WHEEL +0 -0
  36. {smart_bot_factory-0.1.3.dist-info → smart_bot_factory-0.1.5.dist-info}/entry_points.txt +0 -0
  37. {smart_bot_factory-0.1.3.dist-info → smart_bot_factory-0.1.5.dist-info}/licenses/LICENSE +0 -0
@@ -1,229 +1,943 @@
1
- """
2
- Декораторы для обработчиков событий и временных задач
3
- """
4
-
5
- import asyncio
6
- import logging
7
- from typing import Callable, Any, Dict, Optional
8
- from datetime import datetime, timedelta
9
- from functools import wraps
10
-
11
- logger = logging.getLogger(__name__)
12
-
13
- # Глобальный реестр обработчиков событий
14
- _event_handlers: Dict[str, Callable] = {}
15
- _scheduled_tasks: Dict[str, Dict[str, Any]] = {}
16
-
17
- def event_handler(event_type: str, description: str = ""):
18
- """
19
- Декоратор для регистрации обработчика события
20
-
21
- Args:
22
- event_type: Тип события (например, 'appointment_booking', 'phone_collection')
23
- description: Описание что делает обработчик (для добавления в промпт)
24
-
25
- Example:
26
- @event_handler("appointment_booking", "Записывает пользователя на прием")
27
- async def book_appointment(user_id: int, appointment_data: dict):
28
- # Логика записи на прием
29
- return {"status": "success", "appointment_id": "123"}
30
- """
31
- def decorator(func: Callable) -> Callable:
32
- _event_handlers[event_type] = {
33
- 'handler': func,
34
- 'description': description,
35
- 'name': func.__name__
36
- }
37
-
38
- logger.info(f"📝 Зарегистрирован обработчик события '{event_type}': {func.__name__}")
39
-
40
- @wraps(func)
41
- async def wrapper(*args, **kwargs):
42
- try:
43
- logger.info(f"🔧 Выполняем обработчик события '{event_type}'")
44
- result = await func(*args, **kwargs)
45
- logger.info(f"✅ Обработчик '{event_type}' выполнен успешно")
46
- return result
47
- except Exception as e:
48
- logger.error(f"❌ Ошибка в обработчике '{event_type}': {e}")
49
- raise
50
-
51
- return wrapper
52
- return decorator
53
-
54
- def schedule_task(task_name: str, description: str = ""):
55
- """
56
- Декоратор для регистрации задачи, которую можно запланировать на время
57
-
58
- Args:
59
- task_name: Название задачи (например, 'send_reminder', 'follow_up')
60
- description: Описание задачи (для добавления в промпт)
61
-
62
- Example:
63
- @schedule_task("send_reminder", "Отправляет напоминание пользователю")
64
- async def send_reminder(user_id: int, message: str):
65
- # Логика отправки напоминания
66
- return {"status": "sent"}
67
- """
68
- def decorator(func: Callable) -> Callable:
69
- _scheduled_tasks[task_name] = {
70
- 'handler': func,
71
- 'description': description,
72
- 'name': func.__name__
73
- }
74
-
75
- logger.info(f"⏰ Зарегистрирована задача '{task_name}': {func.__name__}")
76
-
77
- @wraps(func)
78
- async def wrapper(*args, **kwargs):
79
- try:
80
- logger.info(f"⏰ Выполняем запланированную задачу '{task_name}'")
81
- result = await func(*args, **kwargs)
82
- logger.info(f"✅ Задача '{task_name}' выполнена успешно")
83
- return result
84
- except Exception as e:
85
- logger.error(f"❌ Ошибка в задаче '{task_name}': {e}")
86
- raise
87
-
88
- return wrapper
89
- return decorator
90
-
91
- def get_event_handlers() -> Dict[str, Dict[str, Any]]:
92
- """Возвращает все зарегистрированные обработчики событий"""
93
- return _event_handlers.copy()
94
-
95
- def get_scheduled_tasks() -> Dict[str, Dict[str, Any]]:
96
- """Возвращает все зарегистрированные задачи"""
97
- return _scheduled_tasks.copy()
98
-
99
- def get_handlers_for_prompt() -> str:
100
- """
101
- Возвращает описание всех обработчиков для добавления в промпт
102
- """
103
- if not _event_handlers and not _scheduled_tasks:
104
- return ""
105
-
106
- prompt_parts = []
107
-
108
- if _event_handlers:
109
- prompt_parts.append("ДОСТУПНЫЕ ОБРАБОТЧИКИ СОБЫТИЙ:")
110
- for event_type, handler_info in _event_handlers.items():
111
- prompt_parts.append(f"- {event_type}: {handler_info['description']}")
112
-
113
- if _scheduled_tasks:
114
- prompt_parts.append("\nДОСТУПНЫЕ ЗАДАЧИ ДЛЯ ПЛАНИРОВАНИЯ:")
115
- for task_name, task_info in _scheduled_tasks.items():
116
- prompt_parts.append(f"- {task_name}: {task_info['description']}")
117
-
118
- return "\n".join(prompt_parts)
119
-
120
- async def execute_event_handler(event_type: str, *args, **kwargs) -> Any:
121
- """Выполняет обработчик события по типу"""
122
- if event_type not in _event_handlers:
123
- raise ValueError(f"Обработчик события '{event_type}' не найден")
124
-
125
- handler_info = _event_handlers[event_type]
126
- return await handler_info['handler'](*args, **kwargs)
127
-
128
- async def execute_scheduled_task(task_name: str, *args, **kwargs) -> Any:
129
- """Выполняет запланированную задачу по имени"""
130
- if task_name not in _scheduled_tasks:
131
- raise ValueError(f"Задача '{task_name}' не найдена")
132
-
133
- task_info = _scheduled_tasks[task_name]
134
- return await task_info['handler'](*args, **kwargs)
135
-
136
- async def schedule_task_for_later(task_name: str, delay_seconds: int, *args, **kwargs):
137
- """
138
- Планирует выполнение задачи через указанное время
139
-
140
- Args:
141
- task_name: Название задачи
142
- delay_seconds: Задержка в секундах
143
- *args, **kwargs: Аргументы для задачи
144
- """
145
- if task_name not in _scheduled_tasks:
146
- raise ValueError(f"Задача '{task_name}' не найдена")
147
-
148
- logger.info(f"⏰ Планируем задачу '{task_name}' через {delay_seconds} секунд")
149
-
150
- async def delayed_task():
151
- await asyncio.sleep(delay_seconds)
152
- await execute_scheduled_task(task_name, *args, **kwargs)
153
-
154
- # Запускаем задачу в фоне
155
- asyncio.create_task(delayed_task())
156
-
157
- return {
158
- "status": "scheduled",
159
- "task_name": task_name,
160
- "delay_seconds": delay_seconds,
161
- "scheduled_at": datetime.now().isoformat()
162
- }
163
-
164
- async def execute_scheduled_task_from_event(user_id: int, task_name: str, event_info: str):
165
- """
166
- Выполняет запланированную задачу на основе события от ИИ
167
-
168
- Args:
169
- user_id: ID пользователя
170
- task_name: Название задачи
171
- event_info: Информация от ИИ (содержит время и сообщение)
172
- """
173
- if task_name not in _scheduled_tasks:
174
- raise ValueError(f"Задача '{task_name}' не найдена")
175
-
176
- # Парсим event_info для извлечения времени и сообщения
177
- # Формат: "через 2 часа: напомнить о приеме"
178
- try:
179
- if ":" in event_info:
180
- time_part, message = event_info.split(":", 1)
181
- time_part = time_part.strip()
182
- message = message.strip()
183
- else:
184
- time_part = event_info
185
- message = "Напоминание"
186
-
187
- # Парсим время
188
- delay_seconds = _parse_time_to_seconds(time_part)
189
-
190
- # Планируем задачу
191
- result = await schedule_task_for_later(task_name, delay_seconds, user_id, message)
192
-
193
- return result
194
-
195
- except Exception as e:
196
- logger.error(f"Ошибка парсинга времени из event_info '{event_info}': {e}")
197
- # Fallback - планируем через 1 час
198
- return await schedule_task_for_later(task_name, 3600, user_id, event_info)
199
-
200
- def _parse_time_to_seconds(time_str: str) -> int:
201
- """
202
- Парсит строку времени в секунды
203
- Поддерживает форматы:
204
- - "через 2 часа"
205
- - "через 30 минут"
206
- - "через 1 день"
207
- - "через 2 часа 30 минут"
208
- """
209
- import re
210
-
211
- time_str = time_str.lower().strip()
212
-
213
- # Ищем часы
214
- hours_match = re.search(r'(\d+)\s*час', time_str)
215
- hours = int(hours_match.group(1)) if hours_match else 0
216
-
217
- # Ищем минуты
218
- minutes_match = re.search(r'(\d+)\s*минут', time_str)
219
- minutes = int(minutes_match.group(1)) if minutes_match else 0
220
-
221
- # Ищем дни
222
- days_match = re.search(r'(\d+)\s*дн', time_str)
223
- days = int(days_match.group(1)) if days_match else 0
224
-
225
- # Конвертируем в секунды
226
- total_seconds = (days * 24 * 3600) + (hours * 3600) + (minutes * 60)
227
-
228
- # Минимум 1 минута
229
- return max(total_seconds, 60)
1
+ """
2
+ Декораторы для обработчиков событий и временных задач
3
+ """
4
+
5
+ import asyncio
6
+ import logging
7
+ from typing import Callable, Any, Dict
8
+ from datetime import datetime, timedelta, timezone
9
+ from functools import wraps
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ # Глобальный реестр обработчиков событий
14
+ _event_handlers: Dict[str, Callable] = {}
15
+ _scheduled_tasks: Dict[str, Dict[str, Any]] = {}
16
+ _global_handlers: Dict[str, Dict[str, Any]] = {}
17
+
18
+ # Глобальный менеджер роутеров
19
+ _router_manager = None
20
+
21
+ def event_handler(event_type: str, notify: bool = False, once_only: bool = True):
22
+ """
23
+ Декоратор для регистрации обработчика события
24
+
25
+ Args:
26
+ event_type: Тип события (например, 'appointment_booking', 'phone_collection')
27
+ notify: Уведомлять ли админов о выполнении события (по умолчанию False)
28
+ once_only: Обрабатывать ли событие только один раз (по умолчанию True)
29
+
30
+ Example:
31
+ # Обработчик только один раз (по умолчанию)
32
+ @event_handler("appointment_booking", notify=True)
33
+ async def book_appointment(user_id: int, appointment_data: dict):
34
+ # Логика записи на прием
35
+ return {"status": "success", "appointment_id": "123"}
36
+
37
+ # Обработчик может выполняться многократно
38
+ @event_handler("phone_collection", once_only=False)
39
+ async def collect_phone(user_id: int, phone_data: dict):
40
+ # Логика сбора телефона
41
+ return {"status": "phone_collected"}
42
+ """
43
+ def decorator(func: Callable) -> Callable:
44
+ _event_handlers[event_type] = {
45
+ 'handler': func,
46
+ 'name': func.__name__,
47
+ 'notify': notify,
48
+ 'once_only': once_only
49
+ }
50
+
51
+ logger.info(f"📝 Зарегистрирован обработчик события '{event_type}': {func.__name__}")
52
+
53
+ @wraps(func)
54
+ async def wrapper(*args, **kwargs):
55
+ try:
56
+ logger.info(f"🔧 Выполняем обработчик события '{event_type}'")
57
+ result = await func(*args, **kwargs)
58
+ logger.info(f"✅ Обработчик '{event_type}' выполнен успешно")
59
+
60
+ # Автоматически добавляем флаг notify к результату
61
+ if isinstance(result, dict):
62
+ result['notify'] = notify
63
+ else:
64
+ # Если результат не словарь, создаем словарь
65
+ result = {
66
+ 'status': 'success',
67
+ 'result': result,
68
+ 'notify': notify
69
+ }
70
+
71
+ return result
72
+ except Exception as e:
73
+ logger.error(f"❌ Ошибка в обработчике '{event_type}': {e}")
74
+ raise
75
+
76
+ return wrapper
77
+ return decorator
78
+
79
+ def schedule_task(task_name: str, notify: bool = False, smart_check: bool = True, once_only: bool = True):
80
+ """
81
+ Декоратор для регистрации задачи, которую можно запланировать на время
82
+
83
+ Args:
84
+ task_name: Название задачи (например, 'send_reminder', 'follow_up')
85
+ notify: Уведомлять ли админов о выполнении задачи (по умолчанию False)
86
+ smart_check: Использовать ли умную проверку активности пользователя (по умолчанию True)
87
+ once_only: Выполнять ли задачу только один раз (по умолчанию True)
88
+
89
+ Example:
90
+ # С умной проверкой (по умолчанию)
91
+ @schedule_task("send_reminder", notify=False)
92
+ async def send_reminder(user_id: int, user_data: str):
93
+ # user_data содержит текст напоминания от ИИ
94
+ # Логика отправки напоминания (выполняется на фоне)
95
+ return {"status": "sent", "message": user_data}
96
+
97
+ # Без умной проверки (всегда выполняется по времени)
98
+ @schedule_task("system_notification", smart_check=False)
99
+ async def system_notification(user_id: int, user_data: str):
100
+ # Выполняется точно по времени, без проверки активности
101
+ return {"status": "sent", "message": user_data}
102
+
103
+ # Задача может выполняться многократно
104
+ @schedule_task("recurring_reminder", once_only=False)
105
+ async def recurring_reminder(user_id: int, user_data: str):
106
+ # Может запускаться несколько раз
107
+ return {"status": "sent", "message": user_data}
108
+
109
+ # ИИ может отправлять события в форматах:
110
+ # {"тип": "send_reminder", "инфо": "3600"} - просто время
111
+ # {"тип": "send_reminder", "инфо": "3600|Не забудьте про встречу!"} - время + текст
112
+ """
113
+ def decorator(func: Callable) -> Callable:
114
+ _scheduled_tasks[task_name] = {
115
+ 'handler': func,
116
+ 'name': func.__name__,
117
+ 'notify': notify,
118
+ 'smart_check': smart_check,
119
+ 'once_only': once_only
120
+ }
121
+
122
+ logger.info(f"⏰ Зарегистрирована задача '{task_name}': {func.__name__}")
123
+
124
+ @wraps(func)
125
+ async def wrapper(*args, **kwargs):
126
+ try:
127
+ logger.info(f"⏰ Выполняем запланированную задачу '{task_name}'")
128
+ result = await func(*args, **kwargs)
129
+ logger.info(f" Задача '{task_name}' выполнена успешно")
130
+
131
+ # Автоматически добавляем флаг notify к результату
132
+ if isinstance(result, dict):
133
+ result['notify'] = notify
134
+ else:
135
+ # Если результат не словарь, создаем словарь
136
+ result = {
137
+ 'status': 'success',
138
+ 'result': result,
139
+ 'notify': notify
140
+ }
141
+
142
+ return result
143
+ except Exception as e:
144
+ logger.error(f"❌ Ошибка в задаче '{task_name}': {e}")
145
+ raise
146
+
147
+ return wrapper
148
+ return decorator
149
+
150
+ def global_handler(handler_type: str, notify: bool = False, once_only: bool = True):
151
+ """
152
+ Декоратор для регистрации глобального обработчика (для всех пользователей)
153
+
154
+ Args:
155
+ handler_type: Тип глобального обработчика (например, 'global_announcement', 'mass_notification')
156
+ notify: Уведомлять ли админов о выполнении (по умолчанию False)
157
+ once_only: Выполнять ли обработчик только один раз (по умолчанию True)
158
+
159
+ Example:
160
+ # Глобальный обработчик только один раз (по умолчанию)
161
+ @global_handler("global_announcement", notify=True)
162
+ async def send_global_announcement(announcement_text: str):
163
+ # Логика отправки анонса всем пользователям
164
+ return {"status": "sent", "recipients_count": 150}
165
+
166
+ # Глобальный обработчик может выполняться многократно
167
+ @global_handler("daily_report", once_only=False)
168
+ async def send_daily_report(report_data: str):
169
+ # Может запускаться каждый день
170
+ return {"status": "sent", "report_type": "daily"}
171
+ """
172
+ def decorator(func: Callable) -> Callable:
173
+ _global_handlers[handler_type] = {
174
+ 'handler': func,
175
+ 'name': func.__name__,
176
+ 'notify': notify,
177
+ 'once_only': once_only
178
+ }
179
+
180
+ logger.info(f"🌍 Зарегистрирован глобальный обработчик '{handler_type}': {func.__name__}")
181
+
182
+ @wraps(func)
183
+ async def wrapper(*args, **kwargs):
184
+ try:
185
+ logger.info(f"🌍 Выполняем глобальный обработчик '{handler_type}'")
186
+ result = await func(*args, **kwargs)
187
+ logger.info(f"✅ Глобальный обработчик '{handler_type}' выполнен успешно")
188
+
189
+ # Автоматически добавляем флаг notify к результату
190
+ if isinstance(result, dict):
191
+ result['notify'] = notify
192
+ else:
193
+ # Если результат не словарь, создаем словарь
194
+ result = {
195
+ 'status': 'success',
196
+ 'result': result,
197
+ 'notify': notify
198
+ }
199
+
200
+ return result
201
+ except Exception as e:
202
+ logger.error(f"❌ Ошибка в глобальном обработчике '{handler_type}': {e}")
203
+ raise
204
+
205
+ return wrapper
206
+ return decorator
207
+
208
+ def get_event_handlers() -> Dict[str, Dict[str, Any]]:
209
+ """Возвращает все зарегистрированные обработчики событий"""
210
+ return _event_handlers.copy()
211
+
212
+ def get_scheduled_tasks() -> Dict[str, Dict[str, Any]]:
213
+ """Возвращает все зарегистрированные задачи"""
214
+ return _scheduled_tasks.copy()
215
+
216
+ def get_global_handlers() -> Dict[str, Dict[str, Any]]:
217
+ """Возвращает все зарегистрированные глобальные обработчики"""
218
+ return _global_handlers.copy()
219
+
220
+ def set_router_manager(router_manager):
221
+ """Устанавливает глобальный менеджер роутеров"""
222
+ global _router_manager
223
+ _router_manager = router_manager
224
+ logger.info("🔄 RouterManager установлен в decorators")
225
+
226
+ def get_router_manager():
227
+ """Получает глобальный менеджер роутеров"""
228
+ return _router_manager
229
+
230
+ def get_handlers_for_prompt() -> str:
231
+ """
232
+ Возвращает описание всех обработчиков для добавления в промпт
233
+ """
234
+ # Сначала пробуем получить из роутеров
235
+ if _router_manager:
236
+ return _router_manager.get_handlers_for_prompt()
237
+
238
+ # Fallback к старым декораторам
239
+ if not _event_handlers and not _scheduled_tasks and not _global_handlers:
240
+ return ""
241
+
242
+ prompt_parts = []
243
+
244
+ if _event_handlers:
245
+ prompt_parts.append("ДОСТУПНЫЕ ОБРАБОТЧИКИ СОБЫТИЙ:")
246
+ for event_type, handler_info in _event_handlers.items():
247
+ prompt_parts.append(f"- {event_type}: {handler_info['name']}")
248
+
249
+ if _scheduled_tasks:
250
+ prompt_parts.append("\nДОСТУПНЫЕ ЗАДАЧИ ДЛЯ ПЛАНИРОВАНИЯ:")
251
+ for task_name, task_info in _scheduled_tasks.items():
252
+ prompt_parts.append(f"- {task_name}: {task_info['name']}")
253
+
254
+ if _global_handlers:
255
+ prompt_parts.append("\nДОСТУПНЫЕ ГЛОБАЛЬНЫЕ ОБРАБОТЧИКИ:")
256
+ for handler_type, handler_info in _global_handlers.items():
257
+ prompt_parts.append(f"- {handler_type}: {handler_info['name']}")
258
+
259
+ return "\n".join(prompt_parts)
260
+
261
+ async def execute_event_handler(event_type: str, *args, **kwargs) -> Any:
262
+ """Выполняет обработчик события по типу"""
263
+ # Сначала пробуем получить из роутеров
264
+ if _router_manager:
265
+ event_handlers = _router_manager.get_event_handlers()
266
+ if event_type in event_handlers:
267
+ handler_info = event_handlers[event_type]
268
+ return await handler_info['handler'](*args, **kwargs)
269
+
270
+ # Fallback к старым декораторам
271
+ if event_type not in _event_handlers:
272
+ raise ValueError(f"Обработчик события '{event_type}' не найден")
273
+
274
+ handler_info = _event_handlers[event_type]
275
+ return await handler_info['handler'](*args, **kwargs)
276
+
277
+ async def execute_scheduled_task(task_name: str, user_id: int, user_data: str) -> Any:
278
+ """Выполняет запланированную задачу по имени"""
279
+ # Сначала пробуем получить из роутеров
280
+ if _router_manager:
281
+ scheduled_tasks = _router_manager.get_scheduled_tasks()
282
+ if task_name in scheduled_tasks:
283
+ task_info = scheduled_tasks[task_name]
284
+ return await task_info['handler'](user_id, user_data)
285
+
286
+ # Fallback к старым декораторам
287
+ if task_name not in _scheduled_tasks:
288
+ raise ValueError(f"Задача '{task_name}' не найдена")
289
+
290
+ task_info = _scheduled_tasks[task_name]
291
+ return await task_info['handler'](user_id, user_data)
292
+
293
+ async def execute_global_handler(handler_type: str, *args, **kwargs) -> Any:
294
+ """Выполняет глобальный обработчик по типу"""
295
+ # Сначала пробуем получить из роутеров
296
+ if _router_manager:
297
+ global_handlers = _router_manager.get_global_handlers()
298
+ if handler_type in global_handlers:
299
+ handler_info = global_handlers[handler_type]
300
+ return await handler_info['handler'](*args, **kwargs)
301
+
302
+ # Fallback к старым декораторам
303
+ if handler_type not in _global_handlers:
304
+ raise ValueError(f"Глобальный обработчик '{handler_type}' не найден")
305
+
306
+ handler_info = _global_handlers[handler_type]
307
+ return await handler_info['handler'](*args, **kwargs)
308
+
309
+ async def schedule_task_for_later(task_name: str, delay_seconds: int, user_id: int, user_data: str):
310
+ """
311
+ Планирует выполнение задачи через указанное время
312
+
313
+ Args:
314
+ task_name: Название задачи
315
+ delay_seconds: Задержка в секундах
316
+ user_id: ID пользователя
317
+ user_data: Простой текст для задачи
318
+ """
319
+ if task_name not in _scheduled_tasks:
320
+ raise ValueError(f"Задача '{task_name}' не найдена")
321
+
322
+ logger.info(f"⏰ Планируем задачу '{task_name}' через {delay_seconds} секунд")
323
+
324
+ async def delayed_task():
325
+ await asyncio.sleep(delay_seconds)
326
+ await execute_scheduled_task(task_name, user_id, user_data)
327
+
328
+ # Запускаем задачу в фоне
329
+ asyncio.create_task(delayed_task())
330
+
331
+ return {
332
+ "status": "scheduled",
333
+ "task_name": task_name,
334
+ "delay_seconds": delay_seconds,
335
+ "scheduled_at": datetime.now().isoformat()
336
+ }
337
+
338
+ async def execute_scheduled_task_from_event(user_id: int, task_name: str, event_info: str):
339
+ """
340
+ Выполняет запланированную задачу на основе события от ИИ
341
+
342
+ Args:
343
+ user_id: ID пользователя
344
+ task_name: Название задачи
345
+ event_info: Информация от ИИ (простой текст)
346
+ """
347
+ if task_name not in _scheduled_tasks:
348
+ raise ValueError(f"Задача '{task_name}' не найдена")
349
+
350
+ try:
351
+ # ИИ может присылать время в двух форматах:
352
+ # 1. Просто время: "3600"
353
+ # 2. Время с данными: "3600|Текст напоминания"
354
+
355
+ if '|' in event_info:
356
+ # Формат с дополнительными данными
357
+ delay_seconds_str, user_data = event_info.split('|', 1)
358
+ delay_seconds = int(delay_seconds_str.strip())
359
+ user_data = user_data.strip()
360
+ else:
361
+ # Просто время
362
+ delay_seconds = int(event_info)
363
+ user_data = f"Напоминание через {delay_seconds} секунд"
364
+
365
+ # Планируем задачу на фоне с сохранением в БД
366
+ result = await schedule_task_for_later_with_db(task_name, user_id, user_data, delay_seconds)
367
+
368
+ return result
369
+
370
+ except ValueError as e:
371
+ logger.error(f"Ошибка парсинга времени из event_info '{event_info}': {e}")
372
+ # Fallback - планируем через 1 час с сохранением в БД
373
+ return await schedule_task_for_later_with_db(task_name, user_id, "Напоминание через 1 час (fallback)", 3600)
374
+
375
+ async def schedule_global_handler_for_later(handler_type: str, delay_seconds: int, handler_data: str):
376
+ """
377
+ Планирует выполнение глобального обработчика через указанное время
378
+
379
+ Args:
380
+ handler_type: Тип глобального обработчика
381
+ delay_seconds: Задержка в секундах
382
+ handler_data: Данные для обработчика (время в секундах как строка)
383
+ """
384
+ if handler_type not in _global_handlers:
385
+ raise ValueError(f"Глобальный обработчик '{handler_type}' не найден")
386
+
387
+ logger.info(f"🌍 Планируем глобальный обработчик '{handler_type}' через {delay_seconds} секунд")
388
+
389
+ async def delayed_global_handler():
390
+ await asyncio.sleep(delay_seconds)
391
+ # Передаем данные обработчику (может быть текст анонса или другие данные)
392
+ await execute_global_handler(handler_type, handler_data)
393
+
394
+ # Запускаем задачу в фоне
395
+ asyncio.create_task(delayed_global_handler())
396
+
397
+ return {
398
+ "status": "scheduled",
399
+ "handler_type": handler_type,
400
+ "delay_seconds": delay_seconds,
401
+ "scheduled_at": datetime.now().isoformat()
402
+ }
403
+
404
+ async def execute_global_handler_from_event(handler_type: str, event_info: str):
405
+ """
406
+ Выполняет глобальный обработчик на основе события от ИИ
407
+
408
+ Args:
409
+ handler_type: Тип глобального обработчика
410
+ event_info: Информация от ИИ (содержит данные для обработчика и время)
411
+ """
412
+ if handler_type not in _global_handlers:
413
+ raise ValueError(f"Глобальный обработчик '{handler_type}' не найден")
414
+
415
+ try:
416
+ # ИИ присылает время в секундах, парсим его
417
+ delay_seconds = int(event_info)
418
+
419
+ # Планируем на будущее с сохранением в БД
420
+ result = await schedule_global_handler_for_later_with_db(handler_type, delay_seconds, event_info)
421
+ return result
422
+
423
+ except ValueError as e:
424
+ logger.error(f"Ошибка парсинга времени для глобального обработчика '{handler_type}': {e}")
425
+ # Fallback - планируем через 1 час с сохранением в БД
426
+ return await schedule_global_handler_for_later_with_db(handler_type, 3600, event_info)
427
+
428
+
429
+ # =============================================================================
430
+ # ФУНКЦИИ ДЛЯ РАБОТЫ С БД СОБЫТИЙ
431
+ # =============================================================================
432
+
433
+ def get_supabase_client():
434
+ """Получает клиент Supabase из глобальных переменных"""
435
+ import sys
436
+ current_module = sys.modules[__name__]
437
+ supabase_client = getattr(current_module, 'supabase_client', None)
438
+
439
+ # Если не найден в decorators, пробуем получить из bot_utils
440
+ if not supabase_client:
441
+ try:
442
+ bot_utils_module = sys.modules.get('smart_bot_factory.core.bot_utils')
443
+ if bot_utils_module:
444
+ supabase_client = getattr(bot_utils_module, 'supabase_client', None)
445
+ except:
446
+ pass
447
+
448
+ return supabase_client
449
+
450
+ async def save_immediate_event(
451
+ event_type: str,
452
+ user_id: int,
453
+ event_data: str,
454
+ session_id: str = None
455
+ ) -> str:
456
+ """Сохраняет событие для немедленного выполнения"""
457
+
458
+ supabase_client = get_supabase_client()
459
+ if not supabase_client:
460
+ logger.error("❌ Supabase клиент не найден")
461
+ raise RuntimeError("Supabase клиент не инициализирован")
462
+
463
+ # Проверяем, нужно ли предотвращать дублирование
464
+ # Получаем информацию об обработчике через роутер-менеджер или fallback
465
+ if _router_manager:
466
+ event_handlers = _router_manager.get_event_handlers()
467
+ event_handler_info = event_handlers.get(event_type, {})
468
+ else:
469
+ event_handler_info = _event_handlers.get(event_type, {})
470
+ once_only = event_handler_info.get('once_only', True)
471
+
472
+ if once_only:
473
+ # Проверяем, было ли уже обработано аналогичное событие
474
+ already_processed = await check_event_already_processed(event_type, user_id, session_id)
475
+ if already_processed:
476
+ logger.info(f"🔄 Событие '{event_type}' уже обрабатывалось для пользователя {user_id}, пропускаем")
477
+ raise ValueError(f"Событие '{event_type}' уже обрабатывалось (once_only=True)")
478
+
479
+ event_record = {
480
+ 'event_type': event_type,
481
+ 'event_category': 'user_event',
482
+ 'user_id': user_id,
483
+ 'event_data': event_data,
484
+ 'scheduled_at': None, # Немедленное выполнение
485
+ 'status': 'immediate',
486
+ 'session_id': session_id
487
+ }
488
+
489
+ try:
490
+ response = supabase_client.client.table('scheduled_events').insert(event_record).execute()
491
+ event_id = response.data[0]['id']
492
+ logger.info(f"💾 Событие сохранено в БД: {event_id}")
493
+ return event_id
494
+ except Exception as e:
495
+ logger.error(f"❌ Ошибка сохранения события в БД: {e}")
496
+ raise
497
+
498
+ async def save_scheduled_task(
499
+ task_name: str,
500
+ user_id: int,
501
+ user_data: str,
502
+ delay_seconds: int,
503
+ session_id: str = None
504
+ ) -> str:
505
+ """Сохраняет запланированную задачу"""
506
+
507
+ supabase_client = get_supabase_client()
508
+ if not supabase_client:
509
+ logger.error("❌ Supabase клиент не найден")
510
+ raise RuntimeError("Supabase клиент не инициализирован")
511
+
512
+ # Проверяем, нужно ли предотвращать дублирование
513
+ # Получаем информацию о задаче через роутер-менеджер или fallback
514
+ if _router_manager:
515
+ scheduled_tasks = _router_manager.get_scheduled_tasks()
516
+ task_info = scheduled_tasks.get(task_name, {})
517
+ else:
518
+ task_info = _scheduled_tasks.get(task_name, {})
519
+ once_only = task_info.get('once_only', True)
520
+
521
+ if once_only:
522
+ # Проверяем, была ли уже запланирована аналогичная задача
523
+ already_processed = await check_event_already_processed(task_name, user_id, session_id)
524
+ if already_processed:
525
+ logger.info(f"🔄 Задача '{task_name}' уже запланирована для пользователя {user_id}, пропускаем")
526
+ raise ValueError(f"Задача '{task_name}' уже запланирована (once_only=True)")
527
+
528
+ scheduled_at = datetime.now(timezone.utc) + timedelta(seconds=delay_seconds)
529
+
530
+ event_record = {
531
+ 'event_type': task_name,
532
+ 'event_category': 'scheduled_task',
533
+ 'user_id': user_id,
534
+ 'event_data': user_data,
535
+ 'scheduled_at': scheduled_at.isoformat(),
536
+ 'status': 'pending',
537
+ 'session_id': session_id
538
+ }
539
+
540
+ try:
541
+ response = supabase_client.client.table('scheduled_events').insert(event_record).execute()
542
+ event_id = response.data[0]['id']
543
+ logger.info(f"⏰ Запланированная задача сохранена в БД: {event_id} (через {delay_seconds}с)")
544
+ return event_id
545
+ except Exception as e:
546
+ logger.error(f"❌ Ошибка сохранения запланированной задачи в БД: {e}")
547
+ raise
548
+
549
+ async def save_global_event(
550
+ handler_type: str,
551
+ handler_data: str,
552
+ delay_seconds: int = 0
553
+ ) -> str:
554
+ """Сохраняет глобальное событие"""
555
+
556
+ supabase_client = get_supabase_client()
557
+ if not supabase_client:
558
+ logger.error("❌ Supabase клиент не найден")
559
+ raise RuntimeError("Supabase клиент не инициализирован")
560
+
561
+ # Проверяем, нужно ли предотвращать дублирование
562
+ # Получаем информацию о глобальном обработчике через роутер-менеджер или fallback
563
+ if _router_manager:
564
+ global_handlers = _router_manager.get_global_handlers()
565
+ handler_info = global_handlers.get(handler_type, {})
566
+ else:
567
+ handler_info = _global_handlers.get(handler_type, {})
568
+ once_only = handler_info.get('once_only', True)
569
+
570
+ if once_only:
571
+ # Проверяем, было ли уже запланировано аналогичное глобальное событие
572
+ already_processed = await check_event_already_processed(handler_type, user_id=None)
573
+ if already_processed:
574
+ logger.info(f"🔄 Глобальное событие '{handler_type}' уже запланировано, пропускаем")
575
+ raise ValueError(f"Глобальное событие '{handler_type}' уже запланировано (once_only=True)")
576
+
577
+ scheduled_at = None
578
+ status = 'immediate'
579
+
580
+ if delay_seconds > 0:
581
+ scheduled_at = datetime.now(timezone.utc) + timedelta(seconds=delay_seconds)
582
+ status = 'pending'
583
+
584
+ event_record = {
585
+ 'event_type': handler_type,
586
+ 'event_category': 'global_handler',
587
+ 'user_id': None, # Глобальное событие
588
+ 'event_data': handler_data,
589
+ 'scheduled_at': scheduled_at.isoformat() if scheduled_at else None,
590
+ 'status': status
591
+ }
592
+
593
+ try:
594
+ response = supabase_client.client.table('scheduled_events').insert(event_record).execute()
595
+ event_id = response.data[0]['id']
596
+ logger.info(f"🌍 Глобальное событие сохранено в БД: {event_id}")
597
+ return event_id
598
+ except Exception as e:
599
+ logger.error(f"❌ Ошибка сохранения глобального события в БД: {e}")
600
+ raise
601
+
602
+ async def update_event_result(
603
+ event_id: str,
604
+ status: str,
605
+ result_data: Any = None,
606
+ error_message: str = None
607
+ ):
608
+ """Обновляет результат выполнения события"""
609
+
610
+ supabase_client = get_supabase_client()
611
+ if not supabase_client:
612
+ logger.error("❌ Supabase клиент не найден")
613
+ return
614
+
615
+ update_data = {
616
+ 'status': status,
617
+ 'executed_at': datetime.now(timezone.utc).isoformat()
618
+ }
619
+
620
+ if result_data:
621
+ import json
622
+ update_data['result_data'] = json.dumps(result_data, ensure_ascii=False)
623
+
624
+ if error_message:
625
+ update_data['last_error'] = error_message
626
+ # Получаем текущее количество попыток
627
+ try:
628
+ current_retry = supabase_client.client.table('scheduled_events').select('retry_count').eq('id', event_id).execute().data[0]['retry_count']
629
+ update_data['retry_count'] = current_retry + 1
630
+ except:
631
+ update_data['retry_count'] = 1
632
+
633
+ try:
634
+ supabase_client.client.table('scheduled_events').update(update_data).eq('id', event_id).execute()
635
+ logger.info(f"📝 Результат события {event_id} обновлен: {status}")
636
+ except Exception as e:
637
+ logger.error(f"❌ Ошибка обновления результата события {event_id}: {e}")
638
+
639
+ async def get_pending_events(limit: int = 50) -> list:
640
+ """Получает события готовые к выполнению"""
641
+
642
+ supabase_client = get_supabase_client()
643
+ if not supabase_client:
644
+ logger.error("❌ Supabase клиент не найден")
645
+ return []
646
+
647
+ try:
648
+ now = datetime.now(timezone.utc).isoformat()
649
+
650
+ response = supabase_client.client.table('scheduled_events')\
651
+ .select('*')\
652
+ .in_('status', ['pending', 'immediate'])\
653
+ .or_(f'scheduled_at.is.null,scheduled_at.lte.{now}')\
654
+ .order('created_at')\
655
+ .limit(limit)\
656
+ .execute()
657
+
658
+ return response.data
659
+ except Exception as e:
660
+ logger.error(f"❌ Ошибка получения событий из БД: {e}")
661
+ return []
662
+
663
+ async def background_event_processor():
664
+ """Фоновый процессор для всех типов событий"""
665
+
666
+ logger.info("🔄 Запуск фонового процессора событий")
667
+
668
+ while True:
669
+ try:
670
+ # Получаем события готовые к выполнению
671
+ pending_events = await get_pending_events(limit=50)
672
+
673
+ if pending_events:
674
+ logger.info(f"📋 Найдено {len(pending_events)} событий для обработки")
675
+
676
+ for event in pending_events:
677
+ try:
678
+ await process_scheduled_event(event)
679
+ await update_event_result(event['id'], 'completed', {"processed": True})
680
+
681
+ except Exception as e:
682
+ logger.error(f"❌ Ошибка обработки события {event['id']}: {e}")
683
+ await update_event_result(event['id'], 'failed', None, str(e))
684
+
685
+ await asyncio.sleep(30)
686
+
687
+ except Exception as e:
688
+ logger.error(f"❌ Ошибка в фоновом процессоре: {e}")
689
+ await asyncio.sleep(60)
690
+
691
+ async def process_scheduled_event(event: Dict):
692
+ """Обрабатывает одно событие из БД"""
693
+
694
+ event_type = event['event_type']
695
+ event_category = event['event_category']
696
+ event_data = event['event_data']
697
+ user_id = event.get('user_id')
698
+
699
+ logger.info(f"🔄 Обработка события {event['id']}: {event_category}/{event_type}")
700
+
701
+ if event_category == 'scheduled_task':
702
+ await execute_scheduled_task(event_type, user_id, event_data)
703
+ elif event_category == 'global_handler':
704
+ await execute_global_handler(event_type, event_data)
705
+ elif event_category == 'user_event':
706
+ await execute_event_handler(event_type, user_id, event_data)
707
+ else:
708
+ logger.warning(f"⚠️ Неизвестная категория события: {event_category}")
709
+
710
+ # =============================================================================
711
+ # ОБНОВЛЕННЫЕ ФУНКЦИИ С СОХРАНЕНИЕМ В БД
712
+ # =============================================================================
713
+
714
+ async def schedule_task_for_later_with_db(task_name: str, user_id: int, user_data: str, delay_seconds: int, session_id: str = None):
715
+ """Планирует выполнение задачи через указанное время с сохранением в БД"""
716
+
717
+ # Проверяем через роутер-менеджер или fallback к старым декораторам
718
+ if _router_manager:
719
+ scheduled_tasks = _router_manager.get_scheduled_tasks()
720
+ if task_name not in scheduled_tasks:
721
+ raise ValueError(f"Задача '{task_name}' не найдена")
722
+ else:
723
+ if task_name not in _scheduled_tasks:
724
+ raise ValueError(f"Задача '{task_name}' не найдена")
725
+
726
+ logger.info(f"⏰ Планируем задачу '{task_name}' через {delay_seconds} секунд")
727
+
728
+ # Сохраняем в БД
729
+ event_id = await save_scheduled_task(task_name, user_id, user_data, delay_seconds, session_id)
730
+
731
+ async def delayed_task():
732
+ await asyncio.sleep(delay_seconds)
733
+
734
+ # Проверяем, нужна ли умная проверка
735
+ task_info = _scheduled_tasks.get(task_name, {})
736
+ use_smart_check = task_info.get('smart_check', True)
737
+
738
+ if use_smart_check:
739
+ # Умная проверка перед выполнением
740
+ try:
741
+ result = await smart_execute_check(event_id, user_id, session_id, task_name, user_data)
742
+ if result['action'] == 'execute':
743
+ await execute_scheduled_task(task_name, user_id, user_data)
744
+ await update_event_result(event_id, 'completed', {"executed": True, "reason": "scheduled_execution"})
745
+ elif result['action'] == 'cancel':
746
+ await update_event_result(event_id, 'cancelled', {"reason": result['reason']})
747
+ elif result['action'] == 'reschedule':
748
+ # Перепланируем задачу на новое время
749
+ new_delay = result['new_delay']
750
+ await update_event_result(event_id, 'rescheduled', {
751
+ "new_delay": new_delay,
752
+ "reason": result['reason']
753
+ })
754
+ # Запускаем новую задачу
755
+ await asyncio.sleep(new_delay)
756
+ await execute_scheduled_task(task_name, user_id, user_data)
757
+ await update_event_result(event_id, 'completed', {"executed": True, "reason": "rescheduled_execution"})
758
+
759
+ except Exception as e:
760
+ await update_event_result(event_id, 'failed', None, str(e))
761
+ raise
762
+ else:
763
+ # Простое выполнение без умной проверки
764
+ try:
765
+ await execute_scheduled_task(task_name, user_id, user_data)
766
+ await update_event_result(event_id, 'completed', {"executed": True, "reason": "simple_execution"})
767
+ except Exception as e:
768
+ await update_event_result(event_id, 'failed', None, str(e))
769
+ raise
770
+
771
+ # Запускаем задачу в фоне
772
+ asyncio.create_task(delayed_task())
773
+
774
+ return {
775
+ "status": "scheduled",
776
+ "task_name": task_name,
777
+ "delay_seconds": delay_seconds,
778
+ "event_id": event_id,
779
+ "scheduled_at": datetime.now(timezone.utc).isoformat()
780
+ }
781
+
782
+ async def schedule_global_handler_for_later_with_db(handler_type: str, delay_seconds: int, handler_data: str):
783
+ """Планирует выполнение глобального обработчика через указанное время с сохранением в БД"""
784
+
785
+ # Проверяем через роутер-менеджер или fallback к старым декораторам
786
+ if _router_manager:
787
+ global_handlers = _router_manager.get_global_handlers()
788
+ if handler_type not in global_handlers:
789
+ raise ValueError(f"Глобальный обработчик '{handler_type}' не найден")
790
+ else:
791
+ if handler_type not in _global_handlers:
792
+ raise ValueError(f"Глобальный обработчик '{handler_type}' не найден")
793
+
794
+ logger.info(f"🌍 Планируем глобальный обработчик '{handler_type}' через {delay_seconds} секунд")
795
+
796
+ # Сохраняем в БД
797
+ event_id = await save_global_event(handler_type, handler_data, delay_seconds)
798
+
799
+ async def delayed_global_handler():
800
+ await asyncio.sleep(delay_seconds)
801
+ try:
802
+ await execute_global_handler(handler_type, handler_data)
803
+ await update_event_result(event_id, 'completed', {"executed": True})
804
+ except Exception as e:
805
+ await update_event_result(event_id, 'failed', None, str(e))
806
+ raise
807
+
808
+ # Запускаем задачу в фоне
809
+ asyncio.create_task(delayed_global_handler())
810
+
811
+ return {
812
+ "status": "scheduled",
813
+ "handler_type": handler_type,
814
+ "delay_seconds": delay_seconds,
815
+ "event_id": event_id,
816
+ "scheduled_at": datetime.now(timezone.utc).isoformat()
817
+ }
818
+
819
+ async def smart_execute_check(event_id: str, user_id: int, session_id: str, task_name: str, user_data: str) -> Dict[str, Any]:
820
+ """
821
+ Умная проверка перед выполнением запланированной задачи
822
+
823
+ Логика:
824
+ 1. Если пользователь перешел на новый этап - отменяем событие
825
+ 2. Если прошло меньше времени чем планировалось - переносим на разницу
826
+ 3. Если прошло достаточно времени - выполняем
827
+
828
+ Returns:
829
+ Dict с action: 'execute', 'cancel', 'reschedule'
830
+ """
831
+ supabase_client = get_supabase_client()
832
+ if not supabase_client:
833
+ logger.error("❌ Supabase клиент не найден для умной проверки")
834
+ return {"action": "execute", "reason": "no_supabase_client"}
835
+
836
+ try:
837
+ # Получаем информацию о последнем сообщении пользователя
838
+ user_info = await supabase_client.get_user_last_message_info(user_id)
839
+
840
+ if not user_info:
841
+ logger.info(f"🔄 Пользователь {user_id} не найден, выполняем задачу")
842
+ return {"action": "execute", "reason": "user_not_found"}
843
+
844
+ # Проверяем, изменился ли этап
845
+ stage_changed = await supabase_client.check_user_stage_changed(user_id, session_id)
846
+ if stage_changed:
847
+ logger.info(f"🔄 Пользователь {user_id} перешел на новый этап, отменяем задачу {task_name}")
848
+ return {"action": "cancel", "reason": "user_stage_changed"}
849
+
850
+ # Получаем информацию о событии из БД
851
+ event_response = supabase_client.client.table('scheduled_events').select(
852
+ 'created_at', 'scheduled_at'
853
+ ).eq('id', event_id).execute()
854
+
855
+ if not event_response.data:
856
+ logger.error(f"❌ Событие {event_id} не найдено в БД")
857
+ return {"action": "execute", "reason": "event_not_found"}
858
+
859
+ event = event_response.data[0]
860
+ created_at = datetime.fromisoformat(event['created_at'].replace('Z', '+00:00'))
861
+ scheduled_at = datetime.fromisoformat(event['scheduled_at'].replace('Z', '+00:00'))
862
+ last_message_at = datetime.fromisoformat(user_info['last_message_at'].replace('Z', '+00:00'))
863
+
864
+ # Вычисляем разницу во времени
865
+ now = datetime.now(timezone.utc)
866
+ time_since_creation = (now - created_at).total_seconds()
867
+ time_since_last_message = (now - last_message_at).total_seconds()
868
+ planned_delay = (scheduled_at - created_at).total_seconds()
869
+
870
+ logger.info(f"🔄 Анализ для пользователя {user_id}:")
871
+ logger.info(f" Время с создания события: {time_since_creation:.0f}с")
872
+ logger.info(f" Время с последнего сообщения: {time_since_last_message:.0f}с")
873
+ logger.info(f" Запланированная задержка: {planned_delay:.0f}с")
874
+
875
+ # Если прошло меньше времени чем планировалось, но пользователь недавно писал
876
+ if time_since_creation < planned_delay and time_since_last_message < planned_delay:
877
+ # Пересчитываем время - отправляем через planned_delay после последнего сообщения
878
+ new_delay = max(0, planned_delay - time_since_last_message)
879
+ logger.info(f"🔄 Переносим задачу на {new_delay:.0f}с (через {planned_delay:.0f}с после последнего сообщения)")
880
+ return {
881
+ "action": "reschedule",
882
+ "new_delay": new_delay,
883
+ "reason": f"user_active_recently_{new_delay:.0f}s_delay"
884
+ }
885
+
886
+ # Если прошло достаточно времени - выполняем
887
+ if time_since_creation >= planned_delay:
888
+ logger.info(f"🔄 Выполняем задачу {task_name} для пользователя {user_id}")
889
+ return {"action": "execute", "reason": "time_expired"}
890
+
891
+ # Если что-то пошло не так - выполняем
892
+ logger.info(f"🔄 Неожиданная ситуация, выполняем задачу {task_name}")
893
+ return {"action": "execute", "reason": "unexpected_situation"}
894
+
895
+ except Exception as e:
896
+ logger.error(f"❌ Ошибка в умной проверке для пользователя {user_id}: {e}")
897
+ return {"action": "execute", "reason": f"error_in_check: {str(e)}"}
898
+
899
+ async def check_event_already_processed(event_type: str, user_id: int = None, session_id: str = None) -> bool:
900
+ """
901
+ Проверяет, был ли уже обработан аналогичный event_type для пользователя/сессии
902
+
903
+ Args:
904
+ event_type: Тип события
905
+ user_id: ID пользователя (для user_event и scheduled_task)
906
+ session_id: ID сессии (для дополнительной проверки)
907
+
908
+ Returns:
909
+ True если событие уже обрабатывалось или в процессе
910
+ """
911
+ supabase_client = get_supabase_client()
912
+ if not supabase_client:
913
+ logger.error("❌ Supabase клиент не найден для проверки дублирования")
914
+ return False
915
+
916
+ try:
917
+ # Строим запрос для поиска аналогичных событий
918
+ query = supabase_client.client.table('scheduled_events').select('id').eq('event_type', event_type)
919
+
920
+ # Для глобальных событий (user_id = None)
921
+ if user_id is None:
922
+ query = query.is_('user_id', 'null')
923
+ else:
924
+ query = query.eq('user_id', user_id)
925
+
926
+ # Добавляем фильтр по статусам (pending, immediate, completed)
927
+ query = query.in_('status', ['pending', 'immediate', 'completed'])
928
+
929
+ # Если есть session_id, добавляем его в фильтр
930
+ if session_id:
931
+ query = query.eq('session_id', session_id)
932
+
933
+ response = query.execute()
934
+
935
+ if response.data:
936
+ logger.info(f"🔄 Найдено {len(response.data)} аналогичных событий для '{event_type}'")
937
+ return True
938
+
939
+ return False
940
+
941
+ except Exception as e:
942
+ logger.error(f"❌ Ошибка проверки дублирования для '{event_type}': {e}")
943
+ return False