smart-bot-factory 0.1.4__py3-none-any.whl → 0.1.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.

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