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.
- smart_bot_factory/__init__.py +1 -19
- smart_bot_factory/cli.py +168 -42
- smart_bot_factory/core/bot_utils.py +22 -12
- smart_bot_factory/core/decorators.py +61 -60
- smart_bot_factory/utm_link_generator.py +106 -0
- {smart_bot_factory-0.1.3.dist-info → smart_bot_factory-0.1.4.dist-info}/METADATA +1 -1
- {smart_bot_factory-0.1.3.dist-info → smart_bot_factory-0.1.4.dist-info}/RECORD +10 -10
- smart_bot_factory/uv.lock +0 -2004
- {smart_bot_factory-0.1.3.dist-info → smart_bot_factory-0.1.4.dist-info}/WHEEL +0 -0
- {smart_bot_factory-0.1.3.dist-info → smart_bot_factory-0.1.4.dist-info}/entry_points.txt +0 -0
- {smart_bot_factory-0.1.3.dist-info → smart_bot_factory-0.1.4.dist-info}/licenses/LICENSE +0 -0
smart_bot_factory/__init__.py
CHANGED
|
@@ -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,
|
|
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
|
-
#
|
|
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
|
-
#
|
|
561
|
-
|
|
562
|
-
|
|
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 параметра в формате
|
|
660
|
+
"""Парсит UTM-метки из start параметра в формате source-vk_campaign-summer2025
|
|
651
661
|
|
|
652
662
|
Args:
|
|
653
|
-
start_param: строка вида '
|
|
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('
|
|
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=
|
|
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
|
-
# Парсим формат:
|
|
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
|
-
# Преобразуем
|
|
686
|
-
if key
|
|
687
|
-
key = 'utm_' + key
|
|
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,
|
|
65
|
-
#
|
|
66
|
-
|
|
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,
|
|
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'](
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
180
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
222
|
+
except ValueError as e:
|
|
196
223
|
logger.error(f"Ошибка парсинга времени из event_info '{event_info}': {e}")
|
|
197
224
|
# Fallback - планируем через 1 час
|
|
198
|
-
|
|
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)
|