smart-bot-factory 0.1.4__py3-none-any.whl → 0.1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of smart-bot-factory might be problematic. Click here for more details.
- smart_bot_factory/__init__.py +0 -30
- smart_bot_factory/admin/admin_logic.py +11 -11
- smart_bot_factory/cli.py +138 -71
- smart_bot_factory/clients/__init__.py +33 -0
- smart_bot_factory/configs/growthmed-october-24/prompts/final_instructions.txt +2 -0
- smart_bot_factory/configs/growthmed-october-24/tests/quick_scenarios.yaml +95 -28
- smart_bot_factory/core/__init__.py +43 -22
- smart_bot_factory/core/bot_utils.py +251 -88
- smart_bot_factory/core/conversation_manager.py +542 -535
- smart_bot_factory/core/decorators.py +943 -230
- smart_bot_factory/core/globals.py +68 -0
- smart_bot_factory/core/message_sender.py +6 -6
- smart_bot_factory/core/router.py +172 -0
- smart_bot_factory/core/router_manager.py +165 -0
- smart_bot_factory/creation/__init__.py +1 -2
- smart_bot_factory/creation/bot_builder.py +116 -8
- smart_bot_factory/creation/bot_testing.py +74 -13
- smart_bot_factory/handlers/handlers.py +10 -2
- smart_bot_factory/integrations/__init__.py +1 -0
- smart_bot_factory/integrations/supabase_client.py +272 -2
- smart_bot_factory-0.1.5.dist-info/METADATA +466 -0
- {smart_bot_factory-0.1.4.dist-info → smart_bot_factory-0.1.5.dist-info}/RECORD +25 -30
- smart_bot_factory/configs/growthmed-helper/env_example.txt +0 -1
- smart_bot_factory/configs/growthmed-helper/prompts/1sales_context.txt +0 -9
- smart_bot_factory/configs/growthmed-helper/prompts/2product_info.txt +0 -582
- smart_bot_factory/configs/growthmed-helper/prompts/3objection_handling.txt +0 -66
- smart_bot_factory/configs/growthmed-helper/prompts/final_instructions.txt +0 -232
- smart_bot_factory/configs/growthmed-helper/prompts/help_message.txt +0 -28
- smart_bot_factory/configs/growthmed-helper/prompts/welcome_message.txt +0 -7
- smart_bot_factory/configs/growthmed-helper/welcome_file/welcome_file_msg.txt +0 -16
- 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
- smart_bot_factory-0.1.4.dist-info/METADATA +0 -126
- {smart_bot_factory-0.1.4.dist-info → smart_bot_factory-0.1.5.dist-info}/WHEEL +0 -0
- {smart_bot_factory-0.1.4.dist-info → smart_bot_factory-0.1.5.dist-info}/entry_points.txt +0 -0
- {smart_bot_factory-0.1.4.dist-info → smart_bot_factory-0.1.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -13,6 +13,12 @@
|
|
|
13
13
|
|
|
14
14
|
ОТЧЕТЫ:
|
|
15
15
|
bots/BOT_ID/reports/test_YYYYMMDD_HHMMSS.txt - подробные отчеты
|
|
16
|
+
|
|
17
|
+
ФОРМАТ СЦЕНАРИЕВ:
|
|
18
|
+
expected_keywords поддерживает синонимы:
|
|
19
|
+
- Одно слово: ["привет"] - проверяется только это слово
|
|
20
|
+
- Синонимы: [["привет", "здравствуйте", "добро пожаловать"]] - достаточно найти любое из слов
|
|
21
|
+
- Смешанный формат: ["привет", ["здравствуйте", "добро пожаловать"]] - комбинация одиночных слов и синонимов
|
|
16
22
|
"""
|
|
17
23
|
|
|
18
24
|
import asyncio
|
|
@@ -73,8 +79,25 @@ class TestStep:
|
|
|
73
79
|
def __init__(self, user_input: str, expected_keywords: List[str],
|
|
74
80
|
forbidden_keywords: List[str] = None):
|
|
75
81
|
self.user_input = user_input
|
|
76
|
-
|
|
82
|
+
# Поддержка синонимов: если элемент списка - список, то это синонимы
|
|
83
|
+
self.expected_keywords = self._process_keywords(expected_keywords)
|
|
77
84
|
self.forbidden_keywords = [kw.lower() for kw in (forbidden_keywords or [])]
|
|
85
|
+
|
|
86
|
+
def _process_keywords(self, keywords: List) -> List:
|
|
87
|
+
"""Обрабатывает ключевые слова, поддерживая синонимы"""
|
|
88
|
+
processed = []
|
|
89
|
+
for kw in keywords:
|
|
90
|
+
if isinstance(kw, list):
|
|
91
|
+
# Это группа синонимов
|
|
92
|
+
synonyms = [s.lower() for s in kw if isinstance(s, str)]
|
|
93
|
+
processed.append(synonyms)
|
|
94
|
+
elif isinstance(kw, str):
|
|
95
|
+
# Одно слово
|
|
96
|
+
processed.append([kw.lower()])
|
|
97
|
+
else:
|
|
98
|
+
# Игнорируем неверные типы
|
|
99
|
+
continue
|
|
100
|
+
return processed
|
|
78
101
|
|
|
79
102
|
|
|
80
103
|
class TestScenario:
|
|
@@ -343,7 +366,14 @@ class BotTester:
|
|
|
343
366
|
logging.info(f"💬 Ввод пользователя: '{step.user_input}'")
|
|
344
367
|
|
|
345
368
|
if step.expected_keywords:
|
|
346
|
-
|
|
369
|
+
# Форматируем ожидаемые ключевые слова для логов
|
|
370
|
+
expected_display = []
|
|
371
|
+
for group in step.expected_keywords:
|
|
372
|
+
if len(group) == 1:
|
|
373
|
+
expected_display.append(group[0])
|
|
374
|
+
else:
|
|
375
|
+
expected_display.append(f"[{'/'.join(group)}]")
|
|
376
|
+
logging.info(f"🎯 Ожидаемые слова: {expected_display}")
|
|
347
377
|
if step.forbidden_keywords:
|
|
348
378
|
logging.info(f"🚫 Запрещенные слова: {step.forbidden_keywords}")
|
|
349
379
|
|
|
@@ -358,14 +388,25 @@ class BotTester:
|
|
|
358
388
|
logging.info(f"🤖 Ответ бота: '{response_preview}'")
|
|
359
389
|
logging.info(f"⏱️ Время обработки: {step_duration}мс")
|
|
360
390
|
|
|
361
|
-
# Проверяем ожидаемые ключевые слова
|
|
362
|
-
|
|
391
|
+
# Проверяем ожидаемые ключевые слова (с поддержкой синонимов)
|
|
392
|
+
missing_keyword_groups = []
|
|
363
393
|
found_expected = []
|
|
364
|
-
for
|
|
365
|
-
|
|
366
|
-
|
|
394
|
+
for keyword_group in step.expected_keywords:
|
|
395
|
+
# keyword_group - это либо список синонимов, либо список с одним словом
|
|
396
|
+
found_in_group = False
|
|
397
|
+
found_synonym = None
|
|
398
|
+
|
|
399
|
+
for synonym in keyword_group:
|
|
400
|
+
if synonym in clean_response.lower():
|
|
401
|
+
found_in_group = True
|
|
402
|
+
found_synonym = synonym
|
|
403
|
+
break
|
|
404
|
+
|
|
405
|
+
if found_in_group:
|
|
406
|
+
found_expected.append(found_synonym)
|
|
367
407
|
else:
|
|
368
|
-
|
|
408
|
+
# Если группа синонимов не найдена, добавляем всю группу в missing
|
|
409
|
+
missing_keyword_groups.append(keyword_group)
|
|
369
410
|
|
|
370
411
|
# Проверяем запрещенные ключевые слова
|
|
371
412
|
found_forbidden = []
|
|
@@ -376,22 +417,34 @@ class BotTester:
|
|
|
376
417
|
# Выводим результаты проверки
|
|
377
418
|
if found_expected:
|
|
378
419
|
logging.info(f"✅ Найденные ожидаемые: {found_expected}")
|
|
379
|
-
if
|
|
380
|
-
|
|
420
|
+
if missing_keyword_groups:
|
|
421
|
+
# Показываем пропущенные группы синонимов в удобном формате
|
|
422
|
+
missing_display = []
|
|
423
|
+
for group in missing_keyword_groups:
|
|
424
|
+
if len(group) == 1:
|
|
425
|
+
missing_display.append(group[0])
|
|
426
|
+
else:
|
|
427
|
+
missing_display.append(f"[{'/'.join(group)}]")
|
|
428
|
+
logging.info(f"❌ НЕ найденные ожидаемые: {missing_display}")
|
|
381
429
|
if found_forbidden:
|
|
382
430
|
logging.info(f"🚫 Найденные запрещенные: {found_forbidden}")
|
|
383
431
|
|
|
384
432
|
# Определяем результат шага
|
|
385
|
-
passed = len(
|
|
433
|
+
passed = len(missing_keyword_groups) == 0 and len(found_forbidden) == 0
|
|
386
434
|
status_icon = "✅" if passed else "❌"
|
|
387
435
|
status_text = "ПРОЙДЕН" if passed else "ПРОВАЛЕН"
|
|
388
436
|
logging.info(f"🎯 Результат шага {step_num}: {status_icon} {status_text}")
|
|
389
437
|
|
|
438
|
+
# Преобразуем missing_keyword_groups в плоский список для обратной совместимости
|
|
439
|
+
missing_keywords_flat = []
|
|
440
|
+
for group in missing_keyword_groups:
|
|
441
|
+
missing_keywords_flat.extend(group)
|
|
442
|
+
|
|
390
443
|
step_result = StepResult(
|
|
391
444
|
step=step,
|
|
392
445
|
bot_response=clean_response,
|
|
393
446
|
passed=passed,
|
|
394
|
-
missing_keywords=
|
|
447
|
+
missing_keywords=missing_keywords_flat,
|
|
395
448
|
found_forbidden=found_forbidden
|
|
396
449
|
)
|
|
397
450
|
|
|
@@ -813,10 +866,18 @@ class ReportGenerator:
|
|
|
813
866
|
step_num = i + 1
|
|
814
867
|
status = "✅" if step_result.passed else "❌"
|
|
815
868
|
|
|
869
|
+
# Форматируем ожидаемые ключевые слова с учетом синонимов
|
|
870
|
+
expected_display = []
|
|
871
|
+
for group in step_result.step.expected_keywords:
|
|
872
|
+
if len(group) == 1:
|
|
873
|
+
expected_display.append(group[0])
|
|
874
|
+
else:
|
|
875
|
+
expected_display.append(f"[{'/'.join(group)}]")
|
|
876
|
+
|
|
816
877
|
report_lines.extend([
|
|
817
878
|
f"ШАГ {step_num} {status}:",
|
|
818
879
|
f" Ввод: \"{step_result.step.user_input}\"",
|
|
819
|
-
f" Ожидаемые: {
|
|
880
|
+
f" Ожидаемые: {expected_display}",
|
|
820
881
|
f" Запрещенные: {step_result.step.forbidden_keywords}",
|
|
821
882
|
""
|
|
822
883
|
])
|
|
@@ -134,8 +134,16 @@ async def user_start_handler(message: Message, state: FSMContext):
|
|
|
134
134
|
await state.set_state(UserStates.waiting_for_message)
|
|
135
135
|
|
|
136
136
|
# 6. ОТПРАВЛЯЕМ ПРИВЕТСТВЕННОЕ СООБЩЕНИЕ
|
|
137
|
-
|
|
138
|
-
|
|
137
|
+
try:
|
|
138
|
+
await send_message(message, welcome_message)
|
|
139
|
+
logger.info(f"Приветственное сообщение отправлено пользователю {message.from_user.id}")
|
|
140
|
+
except Exception as e:
|
|
141
|
+
if "Forbidden: bot was blocked by the user" in str(e):
|
|
142
|
+
logger.warning(f"🚫 Бот заблокирован пользователем {message.from_user.id}")
|
|
143
|
+
return
|
|
144
|
+
else:
|
|
145
|
+
logger.error(f"❌ Ошибка отправки приветственного сообщения: {e}")
|
|
146
|
+
raise
|
|
139
147
|
|
|
140
148
|
# 7. ЕСЛИ ЕСТЬ ФАЙЛ ОТПРАВЛЯЕМ ВМЕСТЕ С ПОДПИСЬЮ
|
|
141
149
|
logging.info(f"📎 Попытка отправки приветственного файла для сессии {session_id}")
|
|
@@ -421,7 +421,7 @@ class SupabaseClient:
|
|
|
421
421
|
logger.error(f"Ошибка при завершении диалогов: {e}")
|
|
422
422
|
return 0
|
|
423
423
|
|
|
424
|
-
async def
|
|
424
|
+
async def get_admin_active_conversation(self, admin_id: int) -> Optional[Dict[str, Any]]:
|
|
425
425
|
"""Получает активный диалог админа"""
|
|
426
426
|
try:
|
|
427
427
|
response = self.client.table('admin_user_conversations').select(
|
|
@@ -589,4 +589,274 @@ class SupabaseClient:
|
|
|
589
589
|
|
|
590
590
|
except APIError as e:
|
|
591
591
|
logger.error(f"Ошибка при архивировании сессий: {e}")
|
|
592
|
-
raise
|
|
592
|
+
raise
|
|
593
|
+
|
|
594
|
+
async def get_sent_files(self, user_id: int) -> List[str]:
|
|
595
|
+
"""Получает список отправленных файлов для пользователя
|
|
596
|
+
|
|
597
|
+
Args:
|
|
598
|
+
user_id: Telegram ID пользователя
|
|
599
|
+
|
|
600
|
+
Returns:
|
|
601
|
+
List[str]: Список имен файлов, разделенных запятой
|
|
602
|
+
"""
|
|
603
|
+
try:
|
|
604
|
+
query = self.client.table('sales_users').select('files').eq('telegram_id', user_id)
|
|
605
|
+
|
|
606
|
+
if self.bot_id:
|
|
607
|
+
query = query.eq('bot_id', self.bot_id)
|
|
608
|
+
|
|
609
|
+
response = query.execute()
|
|
610
|
+
|
|
611
|
+
if response.data and response.data[0].get('files'):
|
|
612
|
+
files_str = response.data[0]['files']
|
|
613
|
+
return [f.strip() for f in files_str.split(',') if f.strip()]
|
|
614
|
+
|
|
615
|
+
return []
|
|
616
|
+
|
|
617
|
+
except Exception as e:
|
|
618
|
+
logger.error(f"Ошибка получения отправленных файлов для пользователя {user_id}: {e}")
|
|
619
|
+
return []
|
|
620
|
+
|
|
621
|
+
async def get_sent_directories(self, user_id: int) -> List[str]:
|
|
622
|
+
"""Получает список отправленных каталогов для пользователя
|
|
623
|
+
|
|
624
|
+
Args:
|
|
625
|
+
user_id: Telegram ID пользователя
|
|
626
|
+
|
|
627
|
+
Returns:
|
|
628
|
+
List[str]: Список путей каталогов, разделенных запятой
|
|
629
|
+
"""
|
|
630
|
+
try:
|
|
631
|
+
query = self.client.table('sales_users').select('directories').eq('telegram_id', user_id)
|
|
632
|
+
|
|
633
|
+
if self.bot_id:
|
|
634
|
+
query = query.eq('bot_id', self.bot_id)
|
|
635
|
+
|
|
636
|
+
response = query.execute()
|
|
637
|
+
|
|
638
|
+
if response.data and response.data[0].get('directories'):
|
|
639
|
+
dirs_str = response.data[0]['directories']
|
|
640
|
+
return [d.strip() for d in dirs_str.split(',') if d.strip()]
|
|
641
|
+
|
|
642
|
+
return []
|
|
643
|
+
|
|
644
|
+
except Exception as e:
|
|
645
|
+
logger.error(f"Ошибка получения отправленных каталогов для пользователя {user_id}: {e}")
|
|
646
|
+
return []
|
|
647
|
+
|
|
648
|
+
async def add_sent_files(self, user_id: int, files_list: List[str]):
|
|
649
|
+
"""Добавляет файлы в список отправленных для пользователя
|
|
650
|
+
|
|
651
|
+
Args:
|
|
652
|
+
user_id: Telegram ID пользователя
|
|
653
|
+
files_list: Список имен файлов для добавления
|
|
654
|
+
"""
|
|
655
|
+
try:
|
|
656
|
+
logger.info(f"Добавление файлов для пользователя {user_id}: {files_list}")
|
|
657
|
+
|
|
658
|
+
# Получаем текущий список
|
|
659
|
+
current_files = await self.get_sent_files(user_id)
|
|
660
|
+
logger.info(f"Текущие файлы в БД: {current_files}")
|
|
661
|
+
|
|
662
|
+
# Объединяем с новыми файлами (без дубликатов)
|
|
663
|
+
all_files = list(set(current_files + files_list))
|
|
664
|
+
logger.info(f"Объединенный список файлов: {all_files}")
|
|
665
|
+
|
|
666
|
+
# Сохраняем обратно
|
|
667
|
+
files_str = ', '.join(all_files)
|
|
668
|
+
logger.info(f"Сохраняем строку: {files_str}")
|
|
669
|
+
|
|
670
|
+
query = self.client.table('sales_users').update({
|
|
671
|
+
'files': files_str
|
|
672
|
+
}).eq('telegram_id', user_id)
|
|
673
|
+
|
|
674
|
+
if self.bot_id:
|
|
675
|
+
query = query.eq('bot_id', self.bot_id)
|
|
676
|
+
logger.info(f"Фильтр по bot_id: {self.bot_id}")
|
|
677
|
+
|
|
678
|
+
response = query.execute()
|
|
679
|
+
logger.info(f"Ответ от БД: {response.data}")
|
|
680
|
+
|
|
681
|
+
logger.info(f"✅ Добавлено {len(files_list)} файлов для пользователя {user_id}")
|
|
682
|
+
|
|
683
|
+
except Exception as e:
|
|
684
|
+
logger.error(f"❌ Ошибка добавления отправленных файлов для пользователя {user_id}: {e}")
|
|
685
|
+
logger.exception("Полный стек ошибки:")
|
|
686
|
+
|
|
687
|
+
async def add_sent_directories(self, user_id: int, dirs_list: List[str]):
|
|
688
|
+
"""Добавляет каталоги в список отправленных для пользователя
|
|
689
|
+
|
|
690
|
+
Args:
|
|
691
|
+
user_id: Telegram ID пользователя
|
|
692
|
+
dirs_list: Список путей каталогов для добавления
|
|
693
|
+
"""
|
|
694
|
+
try:
|
|
695
|
+
logger.info(f"Добавление каталогов для пользователя {user_id}: {dirs_list}")
|
|
696
|
+
|
|
697
|
+
# Получаем текущий список
|
|
698
|
+
current_dirs = await self.get_sent_directories(user_id)
|
|
699
|
+
logger.info(f"Текущие каталоги в БД: {current_dirs}")
|
|
700
|
+
|
|
701
|
+
# Объединяем с новыми каталогами (без дубликатов)
|
|
702
|
+
all_dirs = list(set(current_dirs + dirs_list))
|
|
703
|
+
logger.info(f"Объединенный список каталогов: {all_dirs}")
|
|
704
|
+
|
|
705
|
+
# Сохраняем обратно
|
|
706
|
+
dirs_str = ', '.join(all_dirs)
|
|
707
|
+
logger.info(f"Сохраняем строку: {dirs_str}")
|
|
708
|
+
|
|
709
|
+
query = self.client.table('sales_users').update({
|
|
710
|
+
'directories': dirs_str
|
|
711
|
+
}).eq('telegram_id', user_id)
|
|
712
|
+
|
|
713
|
+
if self.bot_id:
|
|
714
|
+
query = query.eq('bot_id', self.bot_id)
|
|
715
|
+
logger.info(f"Фильтр по bot_id: {self.bot_id}")
|
|
716
|
+
|
|
717
|
+
response = query.execute()
|
|
718
|
+
logger.info(f"Ответ от БД: {response.data}")
|
|
719
|
+
|
|
720
|
+
logger.info(f"✅ Добавлено {len(dirs_list)} каталогов для пользователя {user_id}")
|
|
721
|
+
|
|
722
|
+
except Exception as e:
|
|
723
|
+
logger.error(f"❌ Ошибка добавления отправленных каталогов для пользователя {user_id}: {e}")
|
|
724
|
+
logger.exception("Полный стек ошибки:")
|
|
725
|
+
|
|
726
|
+
# =============================================================================
|
|
727
|
+
# МЕТОДЫ ДЛЯ АНАЛИТИКИ
|
|
728
|
+
# =============================================================================
|
|
729
|
+
|
|
730
|
+
async def get_funnel_stats(self, days: int = 7) -> Dict[str, Any]:
|
|
731
|
+
"""Получает статистику воронки продаж"""
|
|
732
|
+
try:
|
|
733
|
+
cutoff_date = datetime.now(timezone.utc) - timedelta(days=days)
|
|
734
|
+
|
|
735
|
+
# Общее количество сессий
|
|
736
|
+
sessions_response = self.client.table('sales_chat_sessions').select('id').gte(
|
|
737
|
+
'created_at', cutoff_date.isoformat()
|
|
738
|
+
)
|
|
739
|
+
if self.bot_id:
|
|
740
|
+
sessions_response = sessions_response.eq('bot_id', self.bot_id)
|
|
741
|
+
|
|
742
|
+
total_sessions = len(sessions_response.execute().data)
|
|
743
|
+
|
|
744
|
+
# Статистика по этапам
|
|
745
|
+
stages_response = self.client.table('sales_chat_sessions').select(
|
|
746
|
+
'current_stage', 'quality_score'
|
|
747
|
+
).gte('created_at', cutoff_date.isoformat())
|
|
748
|
+
|
|
749
|
+
if self.bot_id:
|
|
750
|
+
stages_response = stages_response.eq('bot_id', self.bot_id)
|
|
751
|
+
|
|
752
|
+
sessions_data = stages_response.execute().data
|
|
753
|
+
|
|
754
|
+
stages = {}
|
|
755
|
+
quality_scores = []
|
|
756
|
+
|
|
757
|
+
for session in sessions_data:
|
|
758
|
+
stage = session.get('current_stage', 'unknown')
|
|
759
|
+
stages[stage] = stages.get(stage, 0) + 1
|
|
760
|
+
|
|
761
|
+
if session.get('quality_score'):
|
|
762
|
+
quality_scores.append(session['quality_score'])
|
|
763
|
+
|
|
764
|
+
avg_quality = sum(quality_scores) / len(quality_scores) if quality_scores else 0
|
|
765
|
+
|
|
766
|
+
return {
|
|
767
|
+
'total_sessions': total_sessions,
|
|
768
|
+
'stages': stages,
|
|
769
|
+
'avg_quality': round(avg_quality, 2),
|
|
770
|
+
'period_days': days
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
except Exception as e:
|
|
774
|
+
logger.error(f"Ошибка получения статистики воронки: {e}")
|
|
775
|
+
return {
|
|
776
|
+
'total_sessions': 0,
|
|
777
|
+
'stages': {},
|
|
778
|
+
'avg_quality': 0,
|
|
779
|
+
'period_days': days
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
async def get_events_stats(self, days: int = 7) -> Dict[str, int]:
|
|
783
|
+
"""Получает статистику событий"""
|
|
784
|
+
try:
|
|
785
|
+
cutoff_date = datetime.now(timezone.utc) - timedelta(days=days)
|
|
786
|
+
|
|
787
|
+
response = self.client.table('scheduled_events').select(
|
|
788
|
+
'event_type', 'status'
|
|
789
|
+
).gte('created_at', cutoff_date.isoformat())
|
|
790
|
+
|
|
791
|
+
events_data = response.execute().data
|
|
792
|
+
|
|
793
|
+
stats = {}
|
|
794
|
+
for event in events_data:
|
|
795
|
+
event_type = event.get('event_type', 'unknown')
|
|
796
|
+
status = event.get('status', 'unknown')
|
|
797
|
+
|
|
798
|
+
key = f"{event_type}_{status}"
|
|
799
|
+
stats[key] = stats.get(key, 0) + 1
|
|
800
|
+
|
|
801
|
+
return stats
|
|
802
|
+
|
|
803
|
+
except Exception as e:
|
|
804
|
+
logger.error(f"Ошибка получения статистики событий: {e}")
|
|
805
|
+
return {}
|
|
806
|
+
|
|
807
|
+
async def get_user_last_message_info(self, user_id: int) -> Optional[Dict[str, Any]]:
|
|
808
|
+
"""Получает информацию о последнем сообщении пользователя"""
|
|
809
|
+
try:
|
|
810
|
+
# Получаем последнее сообщение пользователя
|
|
811
|
+
response = self.client.table('sales_messages').select(
|
|
812
|
+
'id', 'created_at', 'session_id', 'sender_type'
|
|
813
|
+
).eq('sender_telegram_id', user_id).order('created_at', desc=True).limit(1).execute()
|
|
814
|
+
|
|
815
|
+
if not response.data:
|
|
816
|
+
return None
|
|
817
|
+
|
|
818
|
+
last_message = response.data[0]
|
|
819
|
+
|
|
820
|
+
# Получаем информацию о сессии
|
|
821
|
+
session_response = self.client.table('sales_chat_sessions').select(
|
|
822
|
+
'id', 'current_stage', 'updated_at'
|
|
823
|
+
).eq('id', last_message['session_id']).execute()
|
|
824
|
+
|
|
825
|
+
if not session_response.data:
|
|
826
|
+
return None
|
|
827
|
+
|
|
828
|
+
session = session_response.data[0]
|
|
829
|
+
|
|
830
|
+
return {
|
|
831
|
+
'last_message_at': last_message['created_at'],
|
|
832
|
+
'session_id': last_message['session_id'],
|
|
833
|
+
'current_stage': session['current_stage'],
|
|
834
|
+
'session_updated_at': session['updated_at']
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
except Exception as e:
|
|
838
|
+
logger.error(f"Ошибка получения информации о последнем сообщении пользователя {user_id}: {e}")
|
|
839
|
+
return None
|
|
840
|
+
|
|
841
|
+
async def check_user_stage_changed(self, user_id: int, original_session_id: str) -> bool:
|
|
842
|
+
"""Проверяет, изменился ли этап пользователя с момента планирования события"""
|
|
843
|
+
try:
|
|
844
|
+
# Получаем текущую информацию о сессии
|
|
845
|
+
response = self.client.table('sales_chat_sessions').select(
|
|
846
|
+
'id', 'current_stage'
|
|
847
|
+
).eq('user_telegram_id', user_id).order('created_at', desc=True).limit(1).execute()
|
|
848
|
+
|
|
849
|
+
if not response.data:
|
|
850
|
+
return False
|
|
851
|
+
|
|
852
|
+
current_session = response.data[0]
|
|
853
|
+
|
|
854
|
+
# Если сессия изменилась - этап точно изменился
|
|
855
|
+
if current_session['id'] != original_session_id:
|
|
856
|
+
return True
|
|
857
|
+
|
|
858
|
+
return False
|
|
859
|
+
|
|
860
|
+
except Exception as e:
|
|
861
|
+
logger.error(f"Ошибка проверки изменения этапа пользователя {user_id}: {e}")
|
|
862
|
+
return False
|