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

@@ -28,24 +28,6 @@ __all__ = [
28
28
  'schedule_task',
29
29
  'send_message_by_ai',
30
30
  'send_message_by_human',
31
- 'Config',
32
31
  'OpenAIClient',
33
- 'SupabaseClient',
34
- 'ConversationManager',
35
- 'AdminManager',
36
- 'PromptLoader',
37
- 'setup_handlers',
38
- 'setup_admin_handlers',
39
- 'setup_utils_handlers',
40
- 'parse_ai_response',
41
- 'process_events',
42
- 'setup_debug_handlers',
43
- 'UserStates',
44
- 'AdminStates',
45
- 'bot_testing_main',
46
- 'AnalyticsManager',
47
- 'check_timeouts',
48
- 'setup_bot_environment',
49
- 'check_setup',
50
- 'test_admin_system',
32
+ 'SupabaseClient'
51
33
  ]
smart_bot_factory/cli.py CHANGED
@@ -295,18 +295,58 @@ def rm(bot_id: str, force: bool = False):
295
295
  sys.exit(1)
296
296
 
297
297
 
298
+ @cli.command()
299
+ @click.argument("source_bot_id")
300
+ @click.argument("new_bot_id")
301
+ @click.option("--force", "-f", is_flag=True, help="Перезаписать существующего бота без подтверждения")
302
+ def copy(source_bot_id: str, new_bot_id: str, force: bool = False):
303
+ """Скопировать существующего бота как шаблон"""
304
+ try:
305
+ # Проверяем существование исходного бота
306
+ source_bot_path = PROJECT_ROOT / "bots" / source_bot_id
307
+ if not source_bot_path.exists():
308
+ raise click.ClickException(f"Исходный бот {source_bot_id} не найден в папке bots/")
309
+
310
+ # Проверяем наличие файла запускалки исходного бота
311
+ source_bot_file = PROJECT_ROOT / f"{source_bot_id}.py"
312
+ if not source_bot_file.exists():
313
+ raise click.ClickException(f"Файл запускалки {source_bot_id}.py не найден в корневой директории")
314
+
315
+ # Проверяем, не существует ли уже новый бот
316
+ new_bot_path = PROJECT_ROOT / "bots" / new_bot_id
317
+ new_bot_file = PROJECT_ROOT / f"{new_bot_id}.py"
318
+
319
+ if new_bot_path.exists() or new_bot_file.exists():
320
+ if not force:
321
+ if not click.confirm(f"Бот {new_bot_id} уже существует. Перезаписать?"):
322
+ click.echo("Копирование отменено")
323
+ return
324
+ else:
325
+ click.echo(f"⚠️ Перезаписываем существующего бота {new_bot_id}")
326
+
327
+ # Копируем бота
328
+ click.echo(f"📋 Копируем бота {source_bot_id} → {new_bot_id}...")
329
+ copy_bot_template(source_bot_id, new_bot_id)
330
+
331
+ click.echo(f"✅ Бот {new_bot_id} успешно скопирован из {source_bot_id}")
332
+ click.echo(f"📝 Не забудьте настроить .env файл для нового бота")
333
+
334
+ except Exception as e:
335
+ click.echo(f"Ошибка при копировании бота: {e}", err=True)
336
+ sys.exit(1)
337
+
298
338
  @cli.command()
299
339
  def link():
300
340
  """Создать UTM-ссылку для бота"""
301
341
  try:
302
342
  # Проверяем наличие скрипта генерации ссылок
303
- link_script = Path("utm_link_generator.py")
343
+ link_script = Path(__file__).parent / "utm_link_generator.py"
304
344
  if not link_script.exists():
305
345
  raise click.ClickException("Скрипт utm_link_generator.py не найден")
306
346
 
307
- # Запускаем ваш скрипт генерации ссылок
308
- click.echo("Запускаем генератор UTM-ссылок...")
309
- subprocess.run([sys.executable, "utm_link_generator.py"], check=True)
347
+ # Запускаем скрипт генерации ссылок
348
+ click.echo("🔗 Запускаем генератор UTM-ссылок...")
349
+ subprocess.run([sys.executable, str(link_script)], check=True)
310
350
 
311
351
  except subprocess.CalledProcessError as e:
312
352
  click.echo(f"Ошибка при запуске генератора ссылок: {e}", err=True)
@@ -335,6 +375,7 @@ def create_new_bot_structure(template: str, bot_id: str) -> bool:
335
375
  (bot_dir / "tests").mkdir()
336
376
  (bot_dir / "reports").mkdir()
337
377
  (bot_dir / "welcome_files").mkdir()
378
+ (bot_dir / "files").mkdir()
338
379
 
339
380
  if template == "base":
340
381
  # Используем growthmed-october-24 как базовый шаблон
@@ -372,31 +413,6 @@ def create_bot_template(bot_id: str) -> str:
372
413
  """
373
414
 
374
415
  import asyncio
375
- import sys
376
- import os
377
- from pathlib import Path
378
-
379
- # Добавляем корень проекта в путь
380
- project_root = Path(__file__).parent
381
- sys.path.insert(0, str(project_root))
382
-
383
- # Устанавливаем переменные окружения ДО импорта библиотеки
384
- bot_id = "{bot_id}"
385
- config_dir = Path("bots") / bot_id
386
- prompts_dir = config_dir / "prompts"
387
-
388
- if prompts_dir.exists():
389
- os.environ["PROMT_FILES_DIR"] = str(prompts_dir)
390
- print(f"📁 Установлен путь к промптам: {{prompts_dir}}")
391
-
392
- # Загружаем .env файл ДО импорта библиотеки
393
- env_file = config_dir / ".env"
394
- if env_file.exists():
395
- from dotenv import load_dotenv
396
- load_dotenv(env_file)
397
- print(f"📄 Загружен .env файл: {{env_file}}")
398
- else:
399
- print(f"⚠️ .env файл не найден: {{env_file}}")
400
416
 
401
417
  from smart_bot_factory import (
402
418
  BotBuilder,
@@ -516,12 +532,12 @@ def copy_from_growthmed_template(bot_dir: Path, bot_id: str):
516
532
  bot_file = PROJECT_ROOT / Path(f"{bot_id}.py")
517
533
  bot_file.write_text(create_bot_template(bot_id), encoding='utf-8')
518
534
 
519
- # Копируем .env файл в папку бота
535
+ # Создаем .env файл в папке бота (НЕ копируем из шаблона)
520
536
  env_file = bot_dir / ".env"
521
537
  env_file.write_text(create_env_template(bot_id), encoding='utf-8')
522
538
 
523
539
  # Копируем промпты из growthmed-october-24
524
- source_prompts = Path("configs/growthmed-october-24/prompts")
540
+ source_prompts = Path(__file__).parent / "configs" / "growthmed-october-24" / "prompts"
525
541
  target_prompts = bot_dir / "prompts"
526
542
 
527
543
  if source_prompts.exists():
@@ -529,17 +545,134 @@ def copy_from_growthmed_template(bot_dir: Path, bot_id: str):
529
545
  shutil.copy2(prompt_file, target_prompts / prompt_file.name)
530
546
  click.echo("Промпты скопированы из growthmed-october-24")
531
547
  else:
548
+ click.echo(f"⚠️ Папка промптов не найдена: {source_prompts}")
532
549
  # Fallback к базовым промптам
533
550
  create_basic_prompts(target_prompts)
534
551
  click.echo("Созданы базовые промпты")
552
+
553
+ # Копируем тесты из growthmed-october-24
554
+ source_tests = Path(__file__).parent / "configs" / "growthmed-october-24" / "tests"
555
+ target_tests = bot_dir / "tests"
556
+
557
+ if source_tests.exists():
558
+ for test_file in source_tests.glob("*"):
559
+ if test_file.is_file():
560
+ shutil.copy2(test_file, target_tests / test_file.name)
561
+ click.echo("Тесты скопированы из growthmed-october-24")
562
+
563
+ # Копируем welcome_files из growthmed-october-24
564
+ source_welcome = Path(__file__).parent / "configs" / "growthmed-october-24" / "welcome_file"
565
+ target_welcome = bot_dir / "welcome_files"
566
+
567
+ if source_welcome.exists():
568
+ for welcome_file in source_welcome.glob("*"):
569
+ if welcome_file.is_file():
570
+ shutil.copy2(welcome_file, target_welcome / welcome_file.name)
571
+ click.echo("Welcome файлы скопированы из growthmed-october-24")
572
+
573
+ # Копируем files из growthmed-october-24
574
+ source_files = Path(__file__).parent / "configs" / "growthmed-october-24" / "files"
575
+ target_files = bot_dir / "files"
576
+
577
+ if source_files.exists():
578
+ for file_item in source_files.glob("*"):
579
+ if file_item.is_file():
580
+ shutil.copy2(file_item, target_files / file_item.name)
581
+ click.echo("Файлы скопированы из growthmed-october-24")
535
582
 
536
583
  except Exception as e:
537
584
  click.echo(f"Ошибка при копировании шаблона: {e}")
538
585
  # Fallback к базовым промптам
539
586
  create_basic_prompts(bot_dir / "prompts")
540
587
 
588
+ def copy_bot_template(source_bot_id: str, new_bot_id: str):
589
+ """Копирует существующего бота как шаблон для нового бота"""
590
+ try:
591
+ source_dir = PROJECT_ROOT / "bots" / source_bot_id
592
+ new_dir = PROJECT_ROOT / "bots" / new_bot_id
593
+
594
+ # Создаем папку для нового бота
595
+ new_dir.mkdir(exist_ok=True)
596
+
597
+ # Создаем структуру папок
598
+ (new_dir / "prompts").mkdir(exist_ok=True)
599
+ (new_dir / "tests").mkdir(exist_ok=True)
600
+ (new_dir / "reports").mkdir(exist_ok=True)
601
+ (new_dir / "welcome_files").mkdir(exist_ok=True)
602
+ (new_dir / "files").mkdir(exist_ok=True)
603
+
604
+ # Копируем основной файл бота в корневую директорию
605
+ source_bot_file = PROJECT_ROOT / f"{source_bot_id}.py"
606
+ new_bot_file = PROJECT_ROOT / f"{new_bot_id}.py"
607
+
608
+ if source_bot_file.exists():
609
+ shutil.copy2(source_bot_file, new_bot_file)
610
+
611
+ # Заменяем название бота в файле
612
+ content = new_bot_file.read_text(encoding='utf-8')
613
+ content = content.replace(f'BotBuilder("{source_bot_id}")', f'BotBuilder("{new_bot_id}")')
614
+ content = content.replace(f'bot_id="{source_bot_id}"', f'bot_id="{new_bot_id}"')
615
+ new_bot_file.write_text(content, encoding='utf-8')
616
+ click.echo(f" 📄 Файл запускалки скопирован: {new_bot_id}.py")
617
+
618
+ # Копируем .env файл
619
+ source_env = source_dir / ".env"
620
+ new_env = new_dir / ".env"
621
+
622
+ if source_env.exists():
623
+ shutil.copy2(source_env, new_env)
624
+
625
+ # Заменяем BOT_ID в .env
626
+ env_content = new_env.read_text(encoding='utf-8')
627
+ env_content = env_content.replace(f'BOT_ID={source_bot_id}', f'BOT_ID={new_bot_id}')
628
+ new_env.write_text(env_content, encoding='utf-8')
629
+ click.echo(f" ⚙️ .env файл скопирован и обновлен")
630
+
631
+ # Копируем промпты
632
+ source_prompts = source_dir / "prompts"
633
+ new_prompts = new_dir / "prompts"
634
+
635
+ if source_prompts.exists():
636
+ for prompt_file in source_prompts.glob("*.txt"):
637
+ shutil.copy2(prompt_file, new_prompts / prompt_file.name)
638
+ click.echo(f" 📝 Промпты скопированы")
639
+
640
+ # Копируем тесты
641
+ source_tests = source_dir / "tests"
642
+ new_tests = new_dir / "tests"
643
+
644
+ if source_tests.exists():
645
+ for test_file in source_tests.glob("*"):
646
+ if test_file.is_file():
647
+ shutil.copy2(test_file, new_tests / test_file.name)
648
+ click.echo(f" 🧪 Тесты скопированы")
649
+
650
+ # Копируем welcome_files
651
+ source_welcome = source_dir / "welcome_files"
652
+ new_welcome = new_dir / "welcome_files"
653
+
654
+ if source_welcome.exists():
655
+ for welcome_file in source_welcome.glob("*"):
656
+ if welcome_file.is_file():
657
+ shutil.copy2(welcome_file, new_welcome / welcome_file.name)
658
+ click.echo(f" 📁 Welcome файлы скопированы")
659
+
660
+ # Копируем files
661
+ source_files = source_dir / "files"
662
+ new_files = new_dir / "files"
663
+
664
+ if source_files.exists():
665
+ for file_item in source_files.glob("*"):
666
+ if file_item.is_file():
667
+ shutil.copy2(file_item, new_files / file_item.name)
668
+ click.echo(f" 📎 Файлы скопированы")
669
+
670
+ except Exception as e:
671
+ click.echo(f"Ошибка при копировании бота: {e}")
672
+ raise
673
+
541
674
  def copy_from_bot_template(template: str, bot_dir: Path, bot_id: str):
542
- """Копирует шаблон из существующего бота"""
675
+ """Копирует шаблон из существующего бота (для команды create)"""
543
676
  try:
544
677
  template_dir = PROJECT_ROOT / Path("bots") / template
545
678
  if not template_dir.exists():
@@ -557,16 +690,9 @@ def copy_from_bot_template(template: str, bot_dir: Path, bot_id: str):
557
690
  content = content.replace(f'bot_id="{template}"', f'bot_id="{bot_id}"')
558
691
  bot_file.write_text(content, encoding='utf-8')
559
692
 
560
- # Копируем .env файл
561
- template_env = template_dir / ".env"
562
- if template_env.exists():
563
- env_file = bot_dir / ".env"
564
- shutil.copy2(template_env, env_file)
565
-
566
- # Заменяем BOT_ID в .env
567
- env_content = env_file.read_text(encoding='utf-8')
568
- env_content = env_content.replace(f'BOT_ID={template}', f'BOT_ID={bot_id}')
569
- env_file.write_text(env_content, encoding='utf-8')
693
+ # Создаем .env файл в папке бота (НЕ копируем из шаблона)
694
+ env_file = bot_dir / ".env"
695
+ env_file.write_text(create_env_template(bot_id), encoding='utf-8')
570
696
 
571
697
  # Копируем промпты
572
698
  template_prompts = template_dir / "prompts"
@@ -186,28 +186,38 @@ async def process_events(session_id: str, events: list, user_id: int):
186
186
  await supabase_client.add_session_event(session_id, event_type, event_info)
187
187
  logger.info(f" ✅ Событие сохранено в БД")
188
188
 
189
- # Уведомляем админов
190
- await notify_admins_about_event(user_id, event)
191
- logger.info(f" ✅ Админы уведомлены")
192
-
193
189
  # Вызываем зарегистрированный обработчик события или задачи
190
+ should_notify = False
194
191
  try:
195
192
  # Сначала пробуем как обычное событие
196
193
  try:
197
194
  logger.info(f" 🎯 Вызываем обработчик события '{event_type}'")
198
195
  result = await execute_event_handler(event_type, user_id, event_info)
199
196
  logger.info(f" ✅ Обработчик события вернул: {result}")
197
+
198
+ should_notify = result.get('notify', False)
199
+
200
200
  except ValueError:
201
201
  # Если обработчик события не найден, пробуем как запланированную задачу
202
202
  logger.info(f" ⏰ Пробуем как запланированную задачу '{event_type}'")
203
203
  result = await execute_scheduled_task(event_type, user_id, event_info)
204
204
  logger.info(f" ✅ Задача выполнена: {result}")
205
+
206
+ should_notify = result.get('notify', False)
207
+
205
208
  except ValueError as e:
206
209
  logger.warning(f" ⚠️ Обработчик/задача не найдены: {e}")
207
210
  except Exception as e:
208
211
  logger.error(f" ❌ Ошибка в обработчике/задаче: {e}")
209
212
  logger.exception(" Стек ошибки:")
210
213
 
214
+ # Уведомляем админов только если result.notify = True
215
+ if should_notify:
216
+ await notify_admins_about_event(user_id, event)
217
+ logger.info(f" ✅ Админы уведомлены")
218
+ else:
219
+ logger.info(f" 🔕 Уведомления админам отключены для '{event_type}'")
220
+
211
221
  except Exception as e:
212
222
  logger.error(f"❌ Ошибка обработки события {event}: {e}")
213
223
  logger.exception("Стек ошибки:")
@@ -647,19 +657,19 @@ OpenAI API: {'✅' if openai_status else '❌'}
647
657
 
648
658
 
649
659
  def parse_utm_from_start_param(start_param: str) -> dict:
650
- """Парсит UTM-метки из start параметра в формате utmSource-vk_utmCampaign-summer2025
660
+ """Парсит UTM-метки из start параметра в формате source-vk_campaign-summer2025
651
661
 
652
662
  Args:
653
- start_param: строка вида 'utmSource-vk_utmCampaign-summer2025' или полная ссылка
663
+ start_param: строка вида 'source-vk_campaign-summer2025' или полная ссылка
654
664
 
655
665
  Returns:
656
666
  dict: {'utm_source': 'vk', 'utm_campaign': 'summer2025'}
657
667
 
658
668
  Examples:
659
- >>> parse_utm_from_start_param('utmSource-vk_utmCampaign-summer2025')
669
+ >>> parse_utm_from_start_param('source-vk_campaign-summer2025')
660
670
  {'utm_source': 'vk', 'utm_campaign': 'summer2025'}
661
671
 
662
- >>> parse_utm_from_start_param('https://t.me/bot?start=utmSource-vk_utmCampaign-summer2025')
672
+ >>> parse_utm_from_start_param('https://t.me/bot?start=source-vk_campaign-summer2025')
663
673
  {'utm_source': 'vk', 'utm_campaign': 'summer2025'}
664
674
  """
665
675
  import re
@@ -676,15 +686,15 @@ def parse_utm_from_start_param(start_param: str) -> dict:
676
686
  else:
677
687
  return {}
678
688
 
679
- # Парсим формат: utmSource-vk_utmCampaign-summer2025
689
+ # Парсим новый формат: source-vk_campaign-summer2025
680
690
  if '_' in start_param and '-' in start_param:
681
691
  parts = start_param.split('_')
682
692
  for part in parts:
683
693
  if '-' in part:
684
694
  key, value = part.split('-', 1)
685
- # Преобразуем utmSource в utm_source
686
- if key.startswith('utm'):
687
- key = 'utm_' + key[3:].lower()
695
+ # Преобразуем source в utm_source
696
+ if key in ['source', 'medium', 'campaign', 'content', 'term']:
697
+ key = 'utm_' + key
688
698
  utm_data[key] = value
689
699
 
690
700
  except Exception as e:
@@ -14,16 +14,17 @@ logger = logging.getLogger(__name__)
14
14
  _event_handlers: Dict[str, Callable] = {}
15
15
  _scheduled_tasks: Dict[str, Dict[str, Any]] = {}
16
16
 
17
- def event_handler(event_type: str, description: str = ""):
17
+ def event_handler(event_type: str, description: str = "", notify: bool = False):
18
18
  """
19
19
  Декоратор для регистрации обработчика события
20
20
 
21
21
  Args:
22
22
  event_type: Тип события (например, 'appointment_booking', 'phone_collection')
23
23
  description: Описание что делает обработчик (для добавления в промпт)
24
+ notify: Уведомлять ли админов о выполнении события (по умолчанию False)
24
25
 
25
26
  Example:
26
- @event_handler("appointment_booking", "Записывает пользователя на прием")
27
+ @event_handler("appointment_booking", "Записывает пользователя на прием", notify=True)
27
28
  async def book_appointment(user_id: int, appointment_data: dict):
28
29
  # Логика записи на прием
29
30
  return {"status": "success", "appointment_id": "123"}
@@ -32,7 +33,8 @@ def event_handler(event_type: str, description: str = ""):
32
33
  _event_handlers[event_type] = {
33
34
  'handler': func,
34
35
  'description': description,
35
- 'name': func.__name__
36
+ 'name': func.__name__,
37
+ 'notify': notify
36
38
  }
37
39
 
38
40
  logger.info(f"📝 Зарегистрирован обработчик события '{event_type}': {func.__name__}")
@@ -43,6 +45,18 @@ def event_handler(event_type: str, description: str = ""):
43
45
  logger.info(f"🔧 Выполняем обработчик события '{event_type}'")
44
46
  result = await func(*args, **kwargs)
45
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
+
46
60
  return result
47
61
  except Exception as e:
48
62
  logger.error(f"❌ Ошибка в обработчике '{event_type}': {e}")
@@ -51,25 +65,29 @@ def event_handler(event_type: str, description: str = ""):
51
65
  return wrapper
52
66
  return decorator
53
67
 
54
- def schedule_task(task_name: str, description: str = ""):
68
+ def schedule_task(task_name: str, description: str = "", notify: bool = False):
55
69
  """
56
70
  Декоратор для регистрации задачи, которую можно запланировать на время
57
71
 
58
72
  Args:
59
73
  task_name: Название задачи (например, 'send_reminder', 'follow_up')
60
74
  description: Описание задачи (для добавления в промпт)
75
+ notify: Уведомлять ли админов о выполнении задачи (по умолчанию False)
61
76
 
62
77
  Example:
63
- @schedule_task("send_reminder", "Отправляет напоминание пользователю")
64
- async def send_reminder(user_id: int, message: str):
65
- # Логика отправки напоминания
66
- return {"status": "sent"}
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}
67
84
  """
68
85
  def decorator(func: Callable) -> Callable:
69
86
  _scheduled_tasks[task_name] = {
70
87
  'handler': func,
71
88
  'description': description,
72
- 'name': func.__name__
89
+ 'name': func.__name__,
90
+ 'notify': notify
73
91
  }
74
92
 
75
93
  logger.info(f"⏰ Зарегистрирована задача '{task_name}': {func.__name__}")
@@ -80,6 +98,18 @@ def schedule_task(task_name: str, description: str = ""):
80
98
  logger.info(f"⏰ Выполняем запланированную задачу '{task_name}'")
81
99
  result = await func(*args, **kwargs)
82
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
+
83
113
  return result
84
114
  except Exception as e:
85
115
  logger.error(f"❌ Ошибка в задаче '{task_name}': {e}")
@@ -125,22 +155,23 @@ async def execute_event_handler(event_type: str, *args, **kwargs) -> Any:
125
155
  handler_info = _event_handlers[event_type]
126
156
  return await handler_info['handler'](*args, **kwargs)
127
157
 
128
- async def execute_scheduled_task(task_name: str, *args, **kwargs) -> Any:
158
+ async def execute_scheduled_task(task_name: str, user_id: int, user_data: dict) -> Any:
129
159
  """Выполняет запланированную задачу по имени"""
130
160
  if task_name not in _scheduled_tasks:
131
161
  raise ValueError(f"Задача '{task_name}' не найдена")
132
162
 
133
163
  task_info = _scheduled_tasks[task_name]
134
- return await task_info['handler'](*args, **kwargs)
164
+ return await task_info['handler'](user_id, user_data)
135
165
 
136
- async def schedule_task_for_later(task_name: str, delay_seconds: int, *args, **kwargs):
166
+ async def schedule_task_for_later(task_name: str, delay_seconds: int, user_id: int, user_data: dict):
137
167
  """
138
168
  Планирует выполнение задачи через указанное время
139
169
 
140
170
  Args:
141
171
  task_name: Название задачи
142
172
  delay_seconds: Задержка в секундах
143
- *args, **kwargs: Аргументы для задачи
173
+ user_id: ID пользователя
174
+ user_data: Данные для задачи
144
175
  """
145
176
  if task_name not in _scheduled_tasks:
146
177
  raise ValueError(f"Задача '{task_name}' не найдена")
@@ -149,7 +180,7 @@ async def schedule_task_for_later(task_name: str, delay_seconds: int, *args, **k
149
180
 
150
181
  async def delayed_task():
151
182
  await asyncio.sleep(delay_seconds)
152
- await execute_scheduled_task(task_name, *args, **kwargs)
183
+ await execute_scheduled_task(task_name, user_id, user_data)
153
184
 
154
185
  # Запускаем задачу в фоне
155
186
  asyncio.create_task(delayed_task())
@@ -168,62 +199,32 @@ async def execute_scheduled_task_from_event(user_id: int, task_name: str, event_
168
199
  Args:
169
200
  user_id: ID пользователя
170
201
  task_name: Название задачи
171
- event_info: Информация от ИИ (содержит время и сообщение)
202
+ event_info: Информация от ИИ (содержит время в секундах и сообщение)
172
203
  """
173
204
  if task_name not in _scheduled_tasks:
174
205
  raise ValueError(f"Задача '{task_name}' не найдена")
175
206
 
176
- # Парсим event_info для извлечения времени и сообщения
177
- # Формат: "через 2 часа: напомнить о приеме"
178
207
  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 = "Напоминание"
208
+ # ИИ присылает время в секундах, парсим его
209
+ delay_seconds = int(event_info)
186
210
 
187
- # Парсим время
188
- delay_seconds = _parse_time_to_seconds(time_part)
211
+ # Создаем user_data с временем в секундах
212
+ user_data = {
213
+ "delay_seconds": delay_seconds,
214
+ "scheduled_at": datetime.now().isoformat()
215
+ }
189
216
 
190
- # Планируем задачу
191
- result = await schedule_task_for_later(task_name, delay_seconds, user_id, message)
217
+ # Планируем задачу на фоне
218
+ result = await schedule_task_for_later(task_name, delay_seconds, user_id, user_data)
192
219
 
193
220
  return result
194
221
 
195
- except Exception as e:
222
+ except ValueError as e:
196
223
  logger.error(f"Ошибка парсинга времени из event_info '{event_info}': {e}")
197
224
  # Fallback - планируем через 1 час
198
- return await schedule_task_for_later(task_name, 3600, user_id, event_info)
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)
199
230
 
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)