smart-bot-factory 0.1.6__py3-none-any.whl → 0.1.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of smart-bot-factory might be problematic. Click here for more details.

@@ -4,12 +4,339 @@
4
4
 
5
5
  import asyncio
6
6
  import logging
7
- from typing import Callable, Any, Dict
7
+ import re
8
+ from typing import Callable, Any, Dict, Union
8
9
  from datetime import datetime, timedelta, timezone
9
10
  from functools import wraps
10
11
 
11
12
  logger = logging.getLogger(__name__)
12
13
 
14
+ def parse_time_string(time_str: Union[str, int]) -> int:
15
+ """
16
+ Парсит время в удобном формате и возвращает секунды
17
+
18
+ Args:
19
+ time_str: Время в формате "1h 30m 45s" или число (секунды)
20
+
21
+ Returns:
22
+ int: Количество секунд
23
+
24
+ Examples:
25
+ parse_time_string("1h 30m 45s") -> 5445
26
+ parse_time_string("2h") -> 7200
27
+ parse_time_string("45m") -> 2700
28
+ parse_time_string("30s") -> 30
29
+ parse_time_string(3600) -> 3600
30
+ """
31
+ if isinstance(time_str, int):
32
+ return time_str
33
+
34
+ # Убираем лишние пробелы и приводим к нижнему регистру
35
+ time_str = time_str.strip().lower()
36
+
37
+ # Если это просто число - возвращаем как секунды
38
+ if time_str.isdigit():
39
+ return int(time_str)
40
+
41
+ total_seconds = 0
42
+
43
+ # Регулярное выражение для поиска времени: число + единица (h, m, s)
44
+ pattern = r'(\d+)\s*(h|m|s)'
45
+ matches = re.findall(pattern, time_str)
46
+
47
+ if not matches:
48
+ raise ValueError(f"Неверный формат времени: '{time_str}'. Используйте формат '1h 30m 45s'")
49
+
50
+ for value, unit in matches:
51
+ value = int(value)
52
+
53
+ if unit == 'h': # часы
54
+ total_seconds += value * 3600
55
+ elif unit == 'm': # минуты
56
+ total_seconds += value * 60
57
+ elif unit == 's': # секунды
58
+ total_seconds += value
59
+
60
+ if total_seconds <= 0:
61
+ raise ValueError(f"Время должно быть больше 0: '{time_str}'")
62
+
63
+ return total_seconds
64
+
65
+ def parse_supabase_datetime(datetime_str: str) -> datetime:
66
+ """
67
+ Парсит дату и время из формата Supabase в объект datetime
68
+
69
+ Args:
70
+ datetime_str: Строка даты и времени из Supabase (ISO 8601 формат)
71
+
72
+ Returns:
73
+ datetime: Объект datetime с timezone
74
+
75
+ Examples:
76
+ parse_supabase_datetime("2024-01-15T10:30:45.123456Z") -> datetime(2024, 1, 15, 10, 30, 45, 123456, tzinfo=timezone.utc)
77
+ parse_supabase_datetime("2024-01-15T10:30:45+00:00") -> datetime(2024, 1, 15, 10, 30, 45, tzinfo=timezone.utc)
78
+ parse_supabase_datetime("2024-01-15T10:30:45") -> datetime(2024, 1, 15, 10, 30, 45, tzinfo=timezone.utc)
79
+ """
80
+ if not datetime_str:
81
+ raise ValueError("Пустая строка даты и времени")
82
+
83
+ # Убираем лишние пробелы
84
+ datetime_str = datetime_str.strip()
85
+
86
+ try:
87
+ # Пробуем парсить ISO 8601 формат с Z в конце
88
+ if datetime_str.endswith('Z'):
89
+ # Заменяем Z на +00:00 для корректного парсинга
90
+ datetime_str = datetime_str[:-1] + '+00:00'
91
+ return datetime.fromisoformat(datetime_str)
92
+
93
+ # Пробуем парсить ISO 8601 формат с timezone
94
+ if '+' in datetime_str or datetime_str.count('-') > 2:
95
+ return datetime.fromisoformat(datetime_str)
96
+
97
+ # Если нет timezone, добавляем UTC
98
+ if 'T' in datetime_str:
99
+ return datetime.fromisoformat(datetime_str + '+00:00')
100
+
101
+ # Если это только дата, добавляем время 00:00:00 и UTC
102
+ return datetime.fromisoformat(datetime_str + 'T00:00:00+00:00')
103
+
104
+ except ValueError as e:
105
+ raise ValueError(f"Неверный формат даты и времени: '{datetime_str}'. Ошибка: {e}")
106
+
107
+ def format_datetime_for_supabase(dt: datetime) -> str:
108
+ """
109
+ Форматирует объект datetime в формат для Supabase
110
+
111
+ Args:
112
+ dt: Объект datetime
113
+
114
+ Returns:
115
+ str: Строка в формате ISO 8601 для Supabase
116
+
117
+ Examples:
118
+ format_datetime_for_supabase(datetime.now(timezone.utc)) -> "2024-01-15T10:30:45.123456+00:00"
119
+ """
120
+ if not isinstance(dt, datetime):
121
+ raise ValueError("Ожидается объект datetime")
122
+
123
+ # Если нет timezone, добавляем UTC
124
+ if dt.tzinfo is None:
125
+ dt = dt.replace(tzinfo=timezone.utc)
126
+
127
+ return dt.isoformat()
128
+
129
+ def get_time_difference_seconds(dt1: datetime, dt2: datetime) -> int:
130
+ """
131
+ Вычисляет разность между двумя датами в секундах
132
+
133
+ Args:
134
+ dt1: Первая дата
135
+ dt2: Вторая дата
136
+
137
+ Returns:
138
+ int: Разность в секундах (dt2 - dt1)
139
+
140
+ Examples:
141
+ get_time_difference_seconds(datetime1, datetime2) -> 3600 # 1 час
142
+ """
143
+
144
+ # Если у дат нет timezone, добавляем UTC
145
+ if dt1.tzinfo is None:
146
+ dt1 = dt1.replace(tzinfo=timezone.utc)
147
+ if dt2.tzinfo is None:
148
+ dt2 = dt2.replace(tzinfo=timezone.utc)
149
+
150
+ return int((dt2 - dt1).total_seconds())
151
+
152
+ def is_datetime_recent(dt: datetime, max_age_seconds: int = 3600) -> bool:
153
+ """
154
+ Проверяет, является ли дата недавней (не старше указанного времени)
155
+
156
+ Args:
157
+ dt: Дата для проверки
158
+ max_age_seconds: Максимальный возраст в секундах (по умолчанию 1 час)
159
+
160
+ Returns:
161
+ bool: True если дата недавняя, False если старая
162
+
163
+ Examples:
164
+ is_datetime_recent(datetime.now(), 1800) -> True # если дата сейчас
165
+ is_datetime_recent(datetime.now() - timedelta(hours=2), 3600) -> False # если дата 2 часа назад
166
+ """
167
+ if not isinstance(dt, datetime):
168
+ raise ValueError("Ожидается объект datetime")
169
+
170
+ now = datetime.now(timezone.utc)
171
+
172
+ # Если у даты нет timezone, добавляем UTC
173
+ if dt.tzinfo is None:
174
+ dt = dt.replace(tzinfo=timezone.utc)
175
+
176
+ age_seconds = (now - dt).total_seconds()
177
+ return age_seconds <= max_age_seconds
178
+
179
+ def parse_appointment_data(data_str: str) -> Dict[str, Any]:
180
+ """
181
+ Парсит данные записи на прием из строки формата "ключ: значение, ключ: значение"
182
+
183
+ Args:
184
+ data_str: Строка с данными записи
185
+
186
+ Returns:
187
+ Dict[str, Any]: Словарь с распарсенными данными
188
+
189
+ Examples:
190
+ parse_appointment_data("имя: Михаил, телефон: +79965214968, процедура: Ламинирование + окрашивание, мастер: Софья, дата: 2025-10-01, время: 19:00")
191
+ -> {
192
+ 'имя': 'Михаил',
193
+ 'телефон': '+79965214968',
194
+ 'процедура': 'Ламинирование + окрашивание',
195
+ 'мастер': 'Софья',
196
+ 'дата': '2025-10-01',
197
+ 'время': '19:00'
198
+ }
199
+ """
200
+ if not data_str or not isinstance(data_str, str):
201
+ return {}
202
+
203
+ result = {}
204
+
205
+ try:
206
+ # Разделяем по запятым, но учитываем что внутри значений могут быть запятые
207
+ # Используем более умный подход - ищем паттерн "ключ: значение"
208
+ pattern = r'([^:]+):\s*([^,]+?)(?=,\s*[^:]+:|$)'
209
+ matches = re.findall(pattern, data_str.strip())
210
+
211
+ for key, value in matches:
212
+ # Очищаем ключ и значение от лишних пробелов
213
+ clean_key = key.strip()
214
+ clean_value = value.strip()
215
+
216
+ # Убираем запятые в конце значения если есть
217
+ if clean_value.endswith(','):
218
+ clean_value = clean_value[:-1].strip()
219
+
220
+ result[clean_key] = clean_value
221
+
222
+ # Дополнительная обработка для даты и времени
223
+ if 'дата' in result and 'время' in result:
224
+ try:
225
+ # Создаем полную дату и время
226
+ date_str = result['дата']
227
+ time_str = result['время']
228
+
229
+ # Парсим дату и время
230
+ appointment_datetime = datetime.strptime(f"{date_str} {time_str}", "%Y-%m-%d %H:%M")
231
+
232
+ # Добавляем в результат
233
+ result['datetime'] = appointment_datetime
234
+ result['datetime_str'] = appointment_datetime.strftime("%Y-%m-%d %H:%M")
235
+
236
+ # Проверяем, не в прошлом ли запись
237
+ now = datetime.now()
238
+ if appointment_datetime < now:
239
+ result['is_past'] = True
240
+ else:
241
+ result['is_past'] = False
242
+
243
+ except ValueError as e:
244
+ logger.warning(f"Ошибка парсинга даты/времени: {e}")
245
+ result['datetime_error'] = str(e)
246
+
247
+ logger.info(f"Распарсены данные записи: {list(result.keys())}")
248
+ return result
249
+
250
+ except Exception as e:
251
+ logger.error(f"Ошибка парсинга данных записи: {e}")
252
+ return {'error': str(e), 'raw_data': data_str}
253
+
254
+ def format_appointment_data(appointment_data: Dict[str, Any]) -> str:
255
+ """
256
+ Форматирует данные записи обратно в строку
257
+
258
+ Args:
259
+ appointment_data: Словарь с данными записи
260
+
261
+ Returns:
262
+ str: Отформатированная строка
263
+
264
+ Examples:
265
+ format_appointment_data({
266
+ 'имя': 'Михаил',
267
+ 'телефон': '+79965214968',
268
+ 'процедура': 'Ламинирование + окрашивание',
269
+ 'мастер': 'Софья',
270
+ 'дата': '2025-10-01',
271
+ 'время': '19:00'
272
+ })
273
+ -> "имя: Михаил, телефон: +79965214968, процедура: Ламинирование + окрашивание, мастер: Софья, дата: 2025-10-01, время: 19:00"
274
+ """
275
+ if not appointment_data or not isinstance(appointment_data, dict):
276
+ return ""
277
+
278
+ # Исключаем служебные поля
279
+ exclude_fields = {'datetime', 'datetime_str', 'is_past', 'datetime_error', 'error', 'raw_data'}
280
+
281
+ parts = []
282
+ for key, value in appointment_data.items():
283
+ if key not in exclude_fields and value is not None:
284
+ parts.append(f"{key}: {value}")
285
+
286
+ return ", ".join(parts)
287
+
288
+ def validate_appointment_data(appointment_data: Dict[str, Any]) -> Dict[str, Any]:
289
+ """
290
+ Валидирует данные записи на прием
291
+
292
+ Args:
293
+ appointment_data: Словарь с данными записи
294
+
295
+ Returns:
296
+ Dict[str, Any]: Результат валидации с полями 'valid', 'errors', 'warnings'
297
+ """
298
+ result = {
299
+ 'valid': True,
300
+ 'errors': [],
301
+ 'warnings': []
302
+ }
303
+
304
+ # Проверяем обязательные поля
305
+ required_fields = ['имя', 'телефон', 'процедура', 'мастер', 'дата', 'время']
306
+
307
+ for field in required_fields:
308
+ if field not in appointment_data or not appointment_data[field]:
309
+ result['errors'].append(f"Отсутствует обязательное поле: {field}")
310
+ result['valid'] = False
311
+
312
+ # Проверяем формат телефона
313
+ if 'телефон' in appointment_data:
314
+ phone = appointment_data['телефон']
315
+ if not re.match(r'^\+?[1-9]\d{10,14}$', phone.replace(' ', '').replace('-', '')):
316
+ result['warnings'].append(f"Неверный формат телефона: {phone}")
317
+
318
+ # Проверяем дату
319
+ if 'дата' in appointment_data:
320
+ try:
321
+ datetime.strptime(appointment_data['дата'], "%Y-%m-%d")
322
+ except ValueError:
323
+ result['errors'].append(f"Неверный формат даты: {appointment_data['дата']}")
324
+ result['valid'] = False
325
+
326
+ # Проверяем время
327
+ if 'время' in appointment_data:
328
+ try:
329
+ datetime.strptime(appointment_data['время'], "%H:%M")
330
+ except ValueError:
331
+ result['errors'].append(f"Неверный формат времени: {appointment_data['время']}")
332
+ result['valid'] = False
333
+
334
+ # Проверяем, не в прошлом ли запись
335
+ if 'is_past' in appointment_data and appointment_data['is_past']:
336
+ result['warnings'].append("Запись назначена на прошедшую дату")
337
+
338
+ return result
339
+
13
340
  # Глобальный реестр обработчиков событий
14
341
  _event_handlers: Dict[str, Callable] = {}
15
342
  _scheduled_tasks: Dict[str, Dict[str, Any]] = {}
@@ -76,7 +403,7 @@ def event_handler(event_type: str, notify: bool = False, once_only: bool = True)
76
403
  return wrapper
77
404
  return decorator
78
405
 
79
- def schedule_task(task_name: str, notify: bool = False, smart_check: bool = True, once_only: bool = True):
406
+ def schedule_task(task_name: str, notify: bool = False, smart_check: bool = True, once_only: bool = True, delay: Union[str, int] = None, event_type: str = None):
80
407
  """
81
408
  Декоратор для регистрации задачи, которую можно запланировать на время
82
409
 
@@ -85,41 +412,68 @@ def schedule_task(task_name: str, notify: bool = False, smart_check: bool = True
85
412
  notify: Уведомлять ли админов о выполнении задачи (по умолчанию False)
86
413
  smart_check: Использовать ли умную проверку активности пользователя (по умолчанию True)
87
414
  once_only: Выполнять ли задачу только один раз (по умолчанию True)
415
+ delay: Время задержки в удобном формате (например, "1h 30m", "45m", 3600) - ОБЯЗАТЕЛЬНО
416
+ event_type: Тип события для напоминания (например, 'appointment_booking') - ОПЦИОНАЛЬНО
88
417
 
89
418
  Example:
90
- # С умной проверкой (по умолчанию)
91
- @schedule_task("send_reminder", notify=False)
419
+ # Обычная задача с фиксированным временем
420
+ @schedule_task("send_reminder", delay="1h 30m")
92
421
  async def send_reminder(user_id: int, user_data: str):
93
- # user_data содержит текст напоминания от ИИ
94
- # Логика отправки напоминания (выполняется на фоне)
422
+ # Задача будет запланирована на 1 час 30 минут
95
423
  return {"status": "sent", "message": user_data}
96
424
 
97
- # Без умной проверки (всегда выполняется по времени)
98
- @schedule_task("system_notification", smart_check=False)
99
- async def system_notification(user_id: int, user_data: str):
100
- # Выполняется точно по времени, без проверки активности
425
+ # Напоминание о событии (за delay времени до события)
426
+ @schedule_task("appointment_reminder", delay="2h", event_type="appointment_booking")
427
+ async def appointment_reminder(user_id: int, user_data: str):
428
+ # Напоминание будет отправлено за 2 часа до события appointment_booking
101
429
  return {"status": "sent", "message": user_data}
102
430
 
103
- # Задача может выполняться многократно
104
- @schedule_task("recurring_reminder", once_only=False)
105
- async def recurring_reminder(user_id: int, user_data: str):
106
- # Может запускаться несколько раз
431
+ # Напоминание о процедуре
432
+ @schedule_task("procedure_reminder", delay="1d", event_type="procedure_booking")
433
+ async def procedure_reminder(user_id: int, user_data: str):
434
+ # Напоминание будет отправлено за 1 день до процедуры
107
435
  return {"status": "sent", "message": user_data}
108
436
 
109
- # ИИ может отправлять события в форматах:
110
- # {"тип": "send_reminder", "инфо": "3600"} - просто время
111
- # {"тип": "send_reminder", "инфо": "3600|Не забудьте про встречу!"} - время + текст
437
+ # Форматы времени:
438
+ # delay="1h 30m 45s" - 1 час 30 минут 45 секунд
439
+ # delay="2h" - 2 часа
440
+ # delay="30m" - 30 минут
441
+ # delay=3600 - 3600 секунд (число)
442
+
443
+ # ИИ может передавать только данные (текст):
444
+ # {"тип": "send_reminder", "инфо": "Текст напоминания"} - только текст
445
+ # {"тип": "appointment_reminder", "инфо": ""} - пустой текст, время берется из события
112
446
  """
113
447
  def decorator(func: Callable) -> Callable:
448
+ # Время ОБЯЗАТЕЛЬНО должно быть указано
449
+ if delay is None:
450
+ raise ValueError(f"Для задачи '{task_name}' ОБЯЗАТЕЛЬНО нужно указать параметр delay")
451
+
452
+ # Парсим время
453
+ try:
454
+ default_delay_seconds = parse_time_string(delay)
455
+ if event_type:
456
+ logger.info(f"⏰ Задача '{task_name}' настроена как напоминание о событии '{event_type}' за {delay} ({default_delay_seconds}с)")
457
+ else:
458
+ logger.info(f"⏰ Задача '{task_name}' настроена с задержкой: {delay} ({default_delay_seconds}с)")
459
+ except ValueError as e:
460
+ logger.error(f"❌ Ошибка парсинга времени для задачи '{task_name}': {e}")
461
+ raise
462
+
114
463
  _scheduled_tasks[task_name] = {
115
464
  'handler': func,
116
465
  'name': func.__name__,
117
466
  'notify': notify,
118
467
  'smart_check': smart_check,
119
- 'once_only': once_only
468
+ 'once_only': once_only,
469
+ 'default_delay': default_delay_seconds,
470
+ 'event_type': event_type # Новое поле для типа события
120
471
  }
121
472
 
122
- logger.info(f"⏰ Зарегистрирована задача '{task_name}': {func.__name__}")
473
+ if event_type:
474
+ logger.info(f"⏰ Зарегистрирована задача-напоминание '{task_name}' для события '{event_type}': {func.__name__}")
475
+ else:
476
+ logger.info(f"⏰ Зарегистрирована задача '{task_name}': {func.__name__}")
123
477
 
124
478
  @wraps(func)
125
479
  async def wrapper(*args, **kwargs):
@@ -147,7 +501,7 @@ def schedule_task(task_name: str, notify: bool = False, smart_check: bool = True
147
501
  return wrapper
148
502
  return decorator
149
503
 
150
- def global_handler(handler_type: str, notify: bool = False, once_only: bool = True):
504
+ def global_handler(handler_type: str, notify: bool = False, once_only: bool = True, delay: Union[str, int] = None):
151
505
  """
152
506
  Декоратор для регистрации глобального обработчика (для всех пользователей)
153
507
 
@@ -155,26 +509,50 @@ def global_handler(handler_type: str, notify: bool = False, once_only: bool = Tr
155
509
  handler_type: Тип глобального обработчика (например, 'global_announcement', 'mass_notification')
156
510
  notify: Уведомлять ли админов о выполнении (по умолчанию False)
157
511
  once_only: Выполнять ли обработчик только один раз (по умолчанию True)
512
+ delay: Время задержки в удобном формате (например, "1h 30m", "45m", 3600) - ОБЯЗАТЕЛЬНО
158
513
 
159
514
  Example:
160
- # Глобальный обработчик только один раз (по умолчанию)
161
- @global_handler("global_announcement", notify=True)
515
+ # Глобальный обработчик с задержкой
516
+ @global_handler("global_announcement", delay="2h", notify=True)
162
517
  async def send_global_announcement(announcement_text: str):
163
- # Логика отправки анонса всем пользователям
518
+ # Выполнится через 2 часа
164
519
  return {"status": "sent", "recipients_count": 150}
165
520
 
166
521
  # Глобальный обработчик может выполняться многократно
167
- @global_handler("daily_report", once_only=False)
522
+ @global_handler("daily_report", delay="24h", once_only=False)
168
523
  async def send_daily_report(report_data: str):
169
- # Может запускаться каждый день
524
+ # Может запускаться каждый день через 24 часа
170
525
  return {"status": "sent", "report_type": "daily"}
526
+
527
+ # Форматы времени:
528
+ # delay="1h 30m 45s" - 1 час 30 минут 45 секунд
529
+ # delay="2h" - 2 часа
530
+ # delay="45m" - 45 минут
531
+ # delay=3600 - 3600 секунд (число)
532
+
533
+ # ИИ может передавать только данные (текст):
534
+ # {"тип": "global_announcement", "инфо": "Важное объявление!"} - только текст
535
+ # {"тип": "global_announcement", "инфо": ""} - пустой текст
171
536
  """
172
537
  def decorator(func: Callable) -> Callable:
538
+ # Время ОБЯЗАТЕЛЬНО должно быть указано
539
+ if delay is None:
540
+ raise ValueError(f"Для глобального обработчика '{handler_type}' ОБЯЗАТЕЛЬНО нужно указать параметр delay")
541
+
542
+ # Парсим время
543
+ try:
544
+ default_delay_seconds = parse_time_string(delay)
545
+ logger.info(f"🌍 Глобальный обработчик '{handler_type}' настроен с задержкой: {delay} ({default_delay_seconds}с)")
546
+ except ValueError as e:
547
+ logger.error(f"❌ Ошибка парсинга времени для глобального обработчика '{handler_type}': {e}")
548
+ raise
549
+
173
550
  _global_handlers[handler_type] = {
174
551
  'handler': func,
175
552
  'name': func.__name__,
176
553
  'notify': notify,
177
- 'once_only': once_only
554
+ 'once_only': once_only,
555
+ 'default_delay': default_delay_seconds
178
556
  }
179
557
 
180
558
  logger.info(f"🌍 Зарегистрирован глобальный обработчик '{handler_type}': {func.__name__}")
@@ -269,13 +647,17 @@ async def execute_event_handler(event_type: str, *args, **kwargs) -> Any:
269
647
 
270
648
  # Fallback к старым декораторам
271
649
  if event_type not in _event_handlers:
650
+ import inspect
651
+ frame = inspect.currentframe()
652
+ line_no = frame.f_lineno if frame else "unknown"
653
+ logger.error(f"❌ [decorators.py:{line_no}] Обработчик события '{event_type}' не найден")
272
654
  raise ValueError(f"Обработчик события '{event_type}' не найден")
273
655
 
274
656
  handler_info = _event_handlers[event_type]
275
657
  return await handler_info['handler'](*args, **kwargs)
276
658
 
277
659
  async def execute_scheduled_task(task_name: str, user_id: int, user_data: str) -> Any:
278
- """Выполняет запланированную задачу по имени"""
660
+ """Выполняет запланированную задачу по имени (без планирования, только выполнение)"""
279
661
  # Сначала пробуем получить из роутеров
280
662
  if _router_manager:
281
663
  scheduled_tasks = _router_manager.get_scheduled_tasks()
@@ -283,9 +665,6 @@ async def execute_scheduled_task(task_name: str, user_id: int, user_data: str) -
283
665
  task_info = scheduled_tasks[task_name]
284
666
  return await task_info['handler'](user_id, user_data)
285
667
 
286
- # Fallback к старым декораторам
287
- if task_name not in _scheduled_tasks:
288
- raise ValueError(f"Задача '{task_name}' не найдена")
289
668
 
290
669
  task_info = _scheduled_tasks[task_name]
291
670
  return await task_info['handler'](user_id, user_data)
@@ -314,8 +693,19 @@ async def schedule_task_for_later(task_name: str, delay_seconds: int, user_id: i
314
693
  user_id: ID пользователя
315
694
  user_data: Простой текст для задачи
316
695
  """
317
- if task_name not in _scheduled_tasks:
318
- raise ValueError(f"Задача '{task_name}' не найдена")
696
+ # Ищем задачу через RouterManager (новая логика)
697
+ router_manager = get_router_manager()
698
+ if router_manager:
699
+ scheduled_tasks = router_manager.get_scheduled_tasks()
700
+ logger.debug(f"🔍 Поиск задачи '{task_name}' через RouterManager")
701
+ else:
702
+ scheduled_tasks = _scheduled_tasks
703
+ logger.debug(f"🔍 Поиск задачи '{task_name}' через глобальный реестр")
704
+
705
+ if task_name not in scheduled_tasks:
706
+ available_tasks = list(scheduled_tasks.keys())
707
+ logger.error(f"❌ Задача '{task_name}' не найдена. Доступные задачи: {available_tasks}")
708
+ raise ValueError(f"Задача '{task_name}' не найдена. Доступные: {available_tasks}")
319
709
 
320
710
  logger.info(f"⏰ Планируем задачу '{task_name}' через {delay_seconds} секунд")
321
711
 
@@ -333,42 +723,113 @@ async def schedule_task_for_later(task_name: str, delay_seconds: int, user_id: i
333
723
  "scheduled_at": datetime.now().isoformat()
334
724
  }
335
725
 
336
- async def execute_scheduled_task_from_event(user_id: int, task_name: str, event_info: str):
726
+ async def execute_scheduled_task_from_event(user_id: int, task_name: str, event_info: str, session_id: str = None):
337
727
  """
338
728
  Выполняет запланированную задачу на основе события от ИИ
339
729
 
340
730
  Args:
341
731
  user_id: ID пользователя
342
732
  task_name: Название задачи
343
- event_info: Информация от ИИ (простой текст)
733
+ event_info: Информация от ИИ (только текст, время задается в декораторе или событии)
734
+ session_id: ID сессии для отслеживания
344
735
  """
345
- if task_name not in _scheduled_tasks:
346
- raise ValueError(f"Задача '{task_name}' не найдена")
736
+ router_manager = get_router_manager()
737
+ if router_manager:
738
+ scheduled_tasks = router_manager.get_scheduled_tasks()
739
+ logger.debug(f"🔍 RouterManager найден, доступные задачи: {list(scheduled_tasks.keys())}")
740
+ else:
741
+ scheduled_tasks = _scheduled_tasks
742
+ logger.debug(f"🔍 RouterManager не найден, старые задачи: {list(scheduled_tasks.keys())}")
347
743
 
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} секунд"
744
+ if task_name not in scheduled_tasks:
745
+ available_tasks = list(scheduled_tasks.keys())
746
+ logger.error(f"❌ Задача '{task_name}' не найдена. Доступные задачи: {available_tasks}")
747
+ logger.error(f"❌ RouterManager статус: {'найден' if router_manager else 'НЕ НАЙДЕН'}")
748
+ raise ValueError(f"Задача '{task_name}' не найдена. Доступные задачи: {available_tasks}")
749
+
750
+ task_info = scheduled_tasks[task_name]
751
+ default_delay = task_info.get('default_delay')
752
+ event_type = task_info.get('event_type')
753
+
754
+ # Время всегда берется из декоратора, ИИ может передавать только текст
755
+ if default_delay is None:
756
+ raise ValueError(f"Для задачи '{task_name}' не указано время в декораторе (параметр delay)")
757
+
758
+ # event_info содержит только текст для задачи
759
+ user_data = event_info.strip() if event_info else f"Напоминание через {default_delay} секунд"
760
+
761
+ # Если указан event_type, то это напоминание о событии
762
+ if event_type:
763
+ logger.info(f"⏰ Задача '{task_name}' - напоминание о событии '{event_type}' за {default_delay}с")
764
+
765
+ # Получаем клиент Supabase
766
+ supabase_client = get_supabase_client()
767
+ if not supabase_client:
768
+ raise RuntimeError("Supabase клиент не найден для получения времени события")
769
+
770
+ try:
771
+ # Получаем данные события
772
+ event_data_str = await supabase_client.get_last_event_info_by_user_and_type(user_id, event_type)
773
+
774
+ if not event_data_str:
775
+ logger.warning(f"Событие '{event_type}' не найдено для пользователя {user_id}")
776
+ # Fallback - планируем через default_delay
777
+ result = await schedule_task_for_later_with_db(task_name, user_id, user_data, default_delay, session_id)
778
+ return result
779
+
780
+ # Парсим данные события
781
+ event_data = parse_appointment_data(event_data_str)
782
+
783
+ if 'datetime' not in event_data:
784
+ logger.warning(f"Не удалось распарсить дату/время из события '{event_type}'")
785
+ # Fallback - планируем через default_delay
786
+ result = await schedule_task_for_later_with_db(task_name, user_id, user_data, default_delay, session_id)
787
+ return result
788
+
789
+ event_datetime = event_data['datetime']
790
+ now = datetime.now()
791
+
792
+ # Вычисляем время напоминания (за default_delay до события)
793
+ reminder_datetime = event_datetime - timedelta(seconds=default_delay)
794
+
795
+ # Проверяем, не в прошлом ли напоминание
796
+ if reminder_datetime <= now:
797
+ logger.warning(f"Напоминание о событии '{event_type}' уже в прошлом, отправляем немедленно")
798
+ # Выполняем задачу немедленно
799
+ result = await execute_scheduled_task(task_name, user_id, user_data)
800
+ return {
801
+ "status": "executed_immediately",
802
+ "task_name": task_name,
803
+ "reason": "reminder_time_passed",
804
+ "event_datetime": event_datetime.isoformat(),
805
+ "result": result
806
+ }
807
+
808
+ # Вычисляем задержку до напоминания
809
+ delay_seconds = int((reminder_datetime - now).total_seconds())
810
+
811
+ logger.info(f"⏰ Планируем напоминание '{task_name}' за {default_delay}с до события '{event_type}' (через {delay_seconds}с)")
812
+
813
+ # Планируем напоминание
814
+ result = await schedule_task_for_later_with_db(task_name, user_id, user_data, delay_seconds, session_id)
815
+ result['event_datetime'] = event_datetime.isoformat()
816
+ result['reminder_type'] = 'event_reminder'
817
+
818
+ return result
819
+
820
+ except Exception as e:
821
+ logger.error(f"Ошибка при работе с событием '{event_type}': {e}")
822
+ # Fallback - планируем через default_delay
823
+ result = await schedule_task_for_later_with_db(task_name, user_id, user_data, default_delay, session_id)
824
+ return result
825
+ else:
826
+ # Обычная задача с фиксированным временем
827
+ logger.info(f"⏰ Планируем задачу '{task_name}' через {default_delay}с с текстом: '{user_data}'")
362
828
 
363
829
  # Планируем задачу на фоне с сохранением в БД
364
- result = await schedule_task_for_later_with_db(task_name, user_id, user_data, delay_seconds)
830
+ result = await schedule_task_for_later_with_db(task_name, user_id, user_data, default_delay, session_id)
365
831
 
366
832
  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
833
 
373
834
  async def schedule_global_handler_for_later(handler_type: str, delay_seconds: int, handler_data: str):
374
835
  """
@@ -379,8 +840,19 @@ async def schedule_global_handler_for_later(handler_type: str, delay_seconds: in
379
840
  delay_seconds: Задержка в секундах
380
841
  handler_data: Данные для обработчика (время в секундах как строка)
381
842
  """
382
- if handler_type not in _global_handlers:
383
- raise ValueError(f"Глобальный обработчик '{handler_type}' не найден")
843
+ # Ищем глобальный обработчик через RouterManager (новая логика)
844
+ router_manager = get_router_manager()
845
+ if router_manager:
846
+ global_handlers = router_manager.get_global_handlers()
847
+ logger.debug(f"🔍 Поиск глобального обработчика '{handler_type}' через RouterManager")
848
+ else:
849
+ global_handlers = _global_handlers
850
+ logger.debug(f"🔍 Поиск глобального обработчика '{handler_type}' через глобальный реестр")
851
+
852
+ if handler_type not in global_handlers:
853
+ available_handlers = list(global_handlers.keys())
854
+ logger.error(f"❌ Глобальный обработчик '{handler_type}' не найден. Доступные: {available_handlers}")
855
+ raise ValueError(f"Глобальный обработчик '{handler_type}' не найден. Доступные: {available_handlers}")
384
856
 
385
857
  logger.info(f"🌍 Планируем глобальный обработчик '{handler_type}' через {delay_seconds} секунд")
386
858
 
@@ -405,23 +877,33 @@ async def execute_global_handler_from_event(handler_type: str, event_info: str):
405
877
 
406
878
  Args:
407
879
  handler_type: Тип глобального обработчика
408
- event_info: Информация от ИИ (содержит данные для обработчика и время)
880
+ event_info: Информация от ИИ (только текст, время задается в декораторе)
409
881
  """
410
- if handler_type not in _global_handlers:
882
+ router_manager = get_router_manager()
883
+ if router_manager:
884
+ global_handlers = router_manager.get_global_handlers()
885
+ else:
886
+ global_handlers = _global_handlers
887
+
888
+ if handler_type not in global_handlers:
411
889
  raise ValueError(f"Глобальный обработчик '{handler_type}' не найден")
412
890
 
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)
891
+ handler_info = global_handlers[handler_type]
892
+ default_delay = handler_info.get('default_delay')
893
+
894
+ # Время всегда берется из декоратора, ИИ может передавать только текст
895
+ if default_delay is None:
896
+ raise ValueError(f"Для глобального обработчика '{handler_type}' не указано время в декораторе (параметр delay)")
897
+
898
+ # event_info содержит только текст для обработчика
899
+ handler_data = event_info.strip() if event_info else f"Глобальное событие через {default_delay} секунд"
900
+
901
+ logger.info(f"🌍 Планируем глобальный обработчик '{handler_type}' через {default_delay}с с данными: '{handler_data}'")
902
+
903
+ # Планируем обработчик на фоне с сохранением в БД
904
+ result = await schedule_global_handler_for_later_with_db(handler_type, default_delay, handler_data)
905
+
906
+ return result
425
907
 
426
908
 
427
909
  # =============================================================================
@@ -703,7 +1185,19 @@ async def process_scheduled_event(event: Dict):
703
1185
  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
1186
  """Планирует выполнение задачи через указанное время с сохранением в БД"""
705
1187
 
706
- if task_name not in _scheduled_tasks:
1188
+ # Проверяем через RouterManager или fallback к старым декораторам
1189
+ router_manager = get_router_manager()
1190
+ if router_manager:
1191
+ scheduled_tasks = router_manager.get_scheduled_tasks()
1192
+ else:
1193
+ scheduled_tasks = _scheduled_tasks
1194
+
1195
+ if task_name not in scheduled_tasks:
1196
+ import inspect
1197
+ frame = inspect.currentframe()
1198
+ line_no = frame.f_lineno if frame else "unknown"
1199
+ available_tasks = list(scheduled_tasks.keys())
1200
+ logger.error(f"❌ [decorators.py:{line_no}] Задача '{task_name}' не найдена. Доступные: {available_tasks}")
707
1201
  raise ValueError(f"Задача '{task_name}' не найдена")
708
1202
 
709
1203
  logger.info(f"⏰ Планируем задачу '{task_name}' через {delay_seconds} секунд")
@@ -714,8 +1208,8 @@ async def schedule_task_for_later_with_db(task_name: str, user_id: int, user_dat
714
1208
  async def delayed_task():
715
1209
  await asyncio.sleep(delay_seconds)
716
1210
 
717
- # Проверяем, нужна ли умная проверка
718
- task_info = _scheduled_tasks.get(task_name, {})
1211
+ # Получаем информацию о задаче
1212
+ task_info = scheduled_tasks.get(task_name, {})
719
1213
  use_smart_check = task_info.get('smart_check', True)
720
1214
 
721
1215
  if use_smart_check:
@@ -851,26 +1345,31 @@ async def smart_execute_check(event_id: str, user_id: int, session_id: str, task
851
1345
  time_since_last_message = (now - last_message_at).total_seconds()
852
1346
  planned_delay = (scheduled_at - created_at).total_seconds()
853
1347
 
1348
+ # Проверяем, писал ли пользователь после создания события
1349
+ time_between_creation_and_last_message = (last_message_at - created_at).total_seconds()
1350
+
854
1351
  logger.info(f"🔄 Анализ для пользователя {user_id}:")
855
1352
  logger.info(f" Время с создания события: {time_since_creation:.0f}с")
856
1353
  logger.info(f" Время с последнего сообщения: {time_since_last_message:.0f}с")
857
1354
  logger.info(f" Запланированная задержка: {planned_delay:.0f}с")
1355
+ logger.info(f" Пользователь писал после создания события: {time_between_creation_and_last_message > 0}")
858
1356
 
859
- # Если прошло меньше времени чем планировалось, но пользователь недавно писал
860
- if time_since_creation < planned_delay and time_since_last_message < planned_delay:
1357
+ # Если пользователь писал ПОСЛЕ создания события (недавно активен)
1358
+ # И с момента его последнего сообщения прошло меньше planned_delay
1359
+ if time_between_creation_and_last_message > 0 and time_since_last_message < planned_delay:
861
1360
  # Пересчитываем время - отправляем через planned_delay после последнего сообщения
862
1361
  new_delay = max(0, planned_delay - time_since_last_message)
863
- logger.info(f"🔄 Переносим задачу на {new_delay:.0f}с (через {planned_delay:.0f}с после последнего сообщения)")
1362
+ logger.info(f"🔄 Переносим задачу на {new_delay:.0f}с (пользователь был активен, через {planned_delay:.0f}с после последнего сообщения)")
864
1363
  return {
865
1364
  "action": "reschedule",
866
1365
  "new_delay": new_delay,
867
- "reason": f"user_active_recently_{new_delay:.0f}s_delay"
1366
+ "reason": f"user_active_after_event_creation_{new_delay:.0f}s_delay"
868
1367
  }
869
1368
 
870
- # Если прошло достаточно времени - выполняем
871
- if time_since_creation >= planned_delay:
872
- logger.info(f"🔄 Выполняем задачу {task_name} для пользователя {user_id}")
873
- return {"action": "execute", "reason": "time_expired"}
1369
+ # Если прошло достаточно времени с последнего сообщения - выполняем
1370
+ if time_since_last_message >= planned_delay:
1371
+ logger.info(f"🔄 Выполняем задачу {task_name} для пользователя {user_id} (прошло {time_since_last_message:.0f}с с последнего сообщения)")
1372
+ return {"action": "execute", "reason": "time_expired_since_last_message"}
874
1373
 
875
1374
  # Если что-то пошло не так - выполняем
876
1375
  logger.info(f"🔄 Неожиданная ситуация, выполняем задачу {task_name}")