smart-bot-factory 1.1.1__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.
Files changed (73) hide show
  1. smart_bot_factory/__init__.py +3 -0
  2. smart_bot_factory/admin/__init__.py +18 -0
  3. smart_bot_factory/admin/admin_events.py +1223 -0
  4. smart_bot_factory/admin/admin_logic.py +553 -0
  5. smart_bot_factory/admin/admin_manager.py +156 -0
  6. smart_bot_factory/admin/admin_tester.py +157 -0
  7. smart_bot_factory/admin/timeout_checker.py +547 -0
  8. smart_bot_factory/aiogram_calendar/__init__.py +14 -0
  9. smart_bot_factory/aiogram_calendar/common.py +64 -0
  10. smart_bot_factory/aiogram_calendar/dialog_calendar.py +259 -0
  11. smart_bot_factory/aiogram_calendar/schemas.py +99 -0
  12. smart_bot_factory/aiogram_calendar/simple_calendar.py +224 -0
  13. smart_bot_factory/analytics/analytics_manager.py +414 -0
  14. smart_bot_factory/cli.py +806 -0
  15. smart_bot_factory/config.py +258 -0
  16. smart_bot_factory/configs/growthmed-october-24/prompts/1sales_context.txt +16 -0
  17. smart_bot_factory/configs/growthmed-october-24/prompts/2product_info.txt +582 -0
  18. smart_bot_factory/configs/growthmed-october-24/prompts/3objection_handling.txt +66 -0
  19. smart_bot_factory/configs/growthmed-october-24/prompts/final_instructions.txt +212 -0
  20. smart_bot_factory/configs/growthmed-october-24/prompts/help_message.txt +28 -0
  21. smart_bot_factory/configs/growthmed-october-24/prompts/welcome_message.txt +8 -0
  22. smart_bot_factory/configs/growthmed-october-24/reports/test_20250924_064229.txt +818 -0
  23. smart_bot_factory/configs/growthmed-october-24/reports/test_20250924_064335.txt +32 -0
  24. smart_bot_factory/configs/growthmed-october-24/reports/test_20250924_064638.txt +35 -0
  25. smart_bot_factory/configs/growthmed-october-24/tests/quick_scenarios.yaml +133 -0
  26. smart_bot_factory/configs/growthmed-october-24/tests/realistic_scenarios.yaml +108 -0
  27. smart_bot_factory/configs/growthmed-october-24/tests/scenario_examples.yaml +46 -0
  28. smart_bot_factory/configs/growthmed-october-24/welcome_file/welcome_file_msg.txt +16 -0
  29. smart_bot_factory/configs/growthmed-october-24/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
  30. smart_bot_factory/core/bot_utils.py +1108 -0
  31. smart_bot_factory/core/conversation_manager.py +653 -0
  32. smart_bot_factory/core/decorators.py +2464 -0
  33. smart_bot_factory/core/message_sender.py +729 -0
  34. smart_bot_factory/core/router.py +347 -0
  35. smart_bot_factory/core/router_manager.py +218 -0
  36. smart_bot_factory/core/states.py +27 -0
  37. smart_bot_factory/creation/__init__.py +7 -0
  38. smart_bot_factory/creation/bot_builder.py +1093 -0
  39. smart_bot_factory/creation/bot_testing.py +1122 -0
  40. smart_bot_factory/dashboard/__init__.py +3 -0
  41. smart_bot_factory/event/__init__.py +7 -0
  42. smart_bot_factory/handlers/handlers.py +2013 -0
  43. smart_bot_factory/integrations/langchain_openai.py +542 -0
  44. smart_bot_factory/integrations/openai_client.py +513 -0
  45. smart_bot_factory/integrations/supabase_client.py +1678 -0
  46. smart_bot_factory/memory/__init__.py +8 -0
  47. smart_bot_factory/memory/memory_manager.py +299 -0
  48. smart_bot_factory/memory/static_memory.py +214 -0
  49. smart_bot_factory/message/__init__.py +56 -0
  50. smart_bot_factory/rag/__init__.py +5 -0
  51. smart_bot_factory/rag/decorators.py +29 -0
  52. smart_bot_factory/rag/router.py +54 -0
  53. smart_bot_factory/rag/templates/__init__.py +3 -0
  54. smart_bot_factory/rag/templates/create_table.sql +7 -0
  55. smart_bot_factory/rag/templates/create_table_and_function_template.py +94 -0
  56. smart_bot_factory/rag/templates/match_function.sql +61 -0
  57. smart_bot_factory/rag/templates/match_services_template.py +82 -0
  58. smart_bot_factory/rag/vectorstore.py +449 -0
  59. smart_bot_factory/router/__init__.py +10 -0
  60. smart_bot_factory/setup_checker.py +512 -0
  61. smart_bot_factory/supabase/__init__.py +7 -0
  62. smart_bot_factory/supabase/client.py +631 -0
  63. smart_bot_factory/utils/__init__.py +11 -0
  64. smart_bot_factory/utils/debug_routing.py +114 -0
  65. smart_bot_factory/utils/prompt_loader.py +529 -0
  66. smart_bot_factory/utils/tool_router.py +68 -0
  67. smart_bot_factory/utils/user_prompt_loader.py +55 -0
  68. smart_bot_factory/utm_link_generator.py +123 -0
  69. smart_bot_factory-1.1.1.dist-info/METADATA +1135 -0
  70. smart_bot_factory-1.1.1.dist-info/RECORD +73 -0
  71. smart_bot_factory-1.1.1.dist-info/WHEEL +4 -0
  72. smart_bot_factory-1.1.1.dist-info/entry_points.txt +2 -0
  73. smart_bot_factory-1.1.1.dist-info/licenses/LICENSE +24 -0
@@ -0,0 +1,547 @@
1
+ """
2
+ Модуль для проверки корректности таймаутов диалогов админов
3
+ """
4
+
5
+ import asyncio
6
+ import logging
7
+ import os
8
+ import sys
9
+ from datetime import datetime, timezone, timedelta
10
+ from pathlib import Path
11
+ from typing import Optional
12
+
13
+ from ..admin.admin_manager import AdminManager
14
+ from ..config import Config
15
+ from ..core.conversation_manager import ConversationManager
16
+ from ..integrations.supabase_client import SupabaseClient
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ def setup_bot_environment(bot_name: str = "growthmed-october-24") -> Optional[Path]:
22
+ """Настраивает окружение для указанного бота с автоопределением BOT_ID"""
23
+ root_dir = Path(os.getcwd()) # Используем текущую директорию как корневую
24
+ config_dir = root_dir / "bots" / bot_name
25
+
26
+ logger.info(f"🔍 Ищем конфигурацию бота в: {config_dir}")
27
+
28
+ # Сохраняем оригинальный путь для возврата
29
+ original_cwd = os.getcwd()
30
+
31
+ if not config_dir.exists():
32
+ logger.error(f"❌ Папка конфигурации не найдена: {config_dir}")
33
+ logger.info(" Доступные боты:")
34
+ bots_dir = root_dir / "bots"
35
+ if bots_dir.exists():
36
+ for bot_dir in bots_dir.iterdir():
37
+ if bot_dir.is_dir():
38
+ logger.info(f" - {bot_dir.name}")
39
+ return None
40
+
41
+ # Проверяем наличие промптов
42
+ prompts_dir = config_dir / "prompts"
43
+ if not prompts_dir.exists():
44
+ logger.error(f"❌ Папка с промптами не найдена: {prompts_dir}")
45
+ return None
46
+
47
+ logger.info(f"✅ Найдена папка промптов: {prompts_dir}")
48
+
49
+ # Устанавливаем BOT_ID из имени бота
50
+ os.environ["BOT_ID"] = bot_name
51
+ logger.info(f"🤖 Автоматически установлен BOT_ID: {bot_name}")
52
+
53
+ # Загружаем .env из конфигурации бота
54
+ env_file = config_dir / ".env"
55
+ if env_file.exists():
56
+ logger.info(f"🔧 Загружаем .env из: {env_file}")
57
+ from dotenv import load_dotenv
58
+
59
+ load_dotenv(env_file)
60
+ else:
61
+ logger.error(f"❌ Файл .env не найден: {env_file}")
62
+ return None
63
+
64
+ # Сохраняем текущую директорию и меняем её
65
+ original_cwd = os.getcwd()
66
+ os.chdir(str(config_dir))
67
+ logger.info(f"📁 Изменена рабочая директория: {os.getcwd()}")
68
+
69
+ # Проверяем что промпты доступны относительно новой директории
70
+ local_prompts = Path("prompts")
71
+ if local_prompts.exists():
72
+ logger.info(
73
+ f"✅ Промпты доступны из рабочей директории: {local_prompts.absolute()}"
74
+ )
75
+ else:
76
+ logger.error(
77
+ f"❌ Промпты не найдены в рабочей директории: {local_prompts.absolute()}"
78
+ )
79
+ os.chdir(original_cwd) # Восстанавливаем директорию
80
+ return None
81
+
82
+ return config_dir
83
+
84
+
85
+ async def debug_timeout_issue(bot_name: str = "growthmed-october-24") -> bool:
86
+ """
87
+ Диагностирует проблему с таймаутом диалогов
88
+
89
+ Args:
90
+ bot_name: Имя бота для диагностики
91
+
92
+ Returns:
93
+ bool: True если диагностика прошла успешно, False если найдены проблемы
94
+ """
95
+ logger.info("🔍 Диагностика проблемы с таймаутом диалогов\n")
96
+ logger.info(f"🚀 Диагностика для бота: {bot_name}")
97
+ logger.info(f"🤖 Bot ID будет автоопределен как: {bot_name}\n")
98
+
99
+ # Настраиваем окружение для бота (автоматически устанавливает BOT_ID)
100
+ config_dir = setup_bot_environment(bot_name)
101
+ if not config_dir:
102
+ return False
103
+
104
+ # Инициализируем конфигурацию
105
+ config = Config()
106
+ logger.info("📋 Конфигурация:")
107
+ logger.info(f" BOT_ID: {config.BOT_ID}")
108
+ logger.info(
109
+ f" ADMIN_SESSION_TIMEOUT_MINUTES: {config.ADMIN_SESSION_TIMEOUT_MINUTES}"
110
+ )
111
+ logger.info(f" PROMT_FILES_DIR: {config.PROMT_FILES_DIR}")
112
+ logger.info(f" Найдено промпт-файлов: {len(config.PROMPT_FILES)}")
113
+ logger.info("")
114
+
115
+ # Проверяем часовые пояса
116
+ logger.info("🕐 Временные зоны:")
117
+ now_naive = datetime.now()
118
+ now_utc = datetime.now(timezone.utc)
119
+ logger.info(f" datetime.now() (локальное): {now_naive}")
120
+ logger.info(f" datetime.now(timezone.utc): {now_utc}")
121
+ logger.info(
122
+ f" Разница: {(now_naive.replace(tzinfo=timezone.utc) - now_utc).total_seconds() / 3600:.1f} часов"
123
+ )
124
+ logger.info("")
125
+
126
+ # Проверяем активные диалоги в БД
127
+ try:
128
+ supabase_client = SupabaseClient(config.SUPABASE_URL, config.SUPABASE_KEY)
129
+ await supabase_client.initialize()
130
+
131
+ response = (
132
+ supabase_client.client.table("admin_user_conversations")
133
+ .select("id", "admin_id", "user_id", "started_at", "auto_end_at")
134
+ .eq("status", "active")
135
+ .execute()
136
+ )
137
+
138
+ conversations = response.data
139
+
140
+ logger.info(f"📊 Активные диалоги в БД: {len(conversations)}")
141
+
142
+ problems_found = 0
143
+
144
+ for i, conv in enumerate(conversations, 1):
145
+ logger.info(f"\n{i}. Диалог ID: {conv['id']}")
146
+ logger.info(
147
+ f" Админ: {conv['admin_id']}, Пользователь: {conv['user_id']}"
148
+ )
149
+
150
+ # Парсим времена
151
+ started_at = conv["started_at"]
152
+ auto_end_at = conv["auto_end_at"]
153
+
154
+ logger.info(f" started_at (сырое): {started_at}")
155
+ logger.info(f" auto_end_at (сырое): {auto_end_at}")
156
+
157
+ try:
158
+ # Парсим как делает код
159
+ if started_at.endswith("Z"):
160
+ start_time = datetime.fromisoformat(
161
+ started_at.replace("Z", "+00:00")
162
+ )
163
+ elif "+" in started_at or started_at.count(":") >= 3:
164
+ start_time = datetime.fromisoformat(started_at)
165
+ else:
166
+ naive_time = datetime.fromisoformat(started_at)
167
+ start_time = naive_time.replace(tzinfo=timezone.utc)
168
+
169
+ if auto_end_at.endswith("Z"):
170
+ end_time = datetime.fromisoformat(
171
+ auto_end_at.replace("Z", "+00:00")
172
+ )
173
+ elif "+" in auto_end_at or auto_end_at.count(":") >= 3:
174
+ end_time = datetime.fromisoformat(auto_end_at)
175
+ else:
176
+ naive_time = datetime.fromisoformat(auto_end_at)
177
+ end_time = naive_time.replace(tzinfo=timezone.utc)
178
+
179
+ logger.info(f" start_time (парсед): {start_time}")
180
+ logger.info(f" end_time (парсед): {end_time}")
181
+
182
+ # Вычисляем длительность диалога
183
+ planned_duration = end_time - start_time
184
+ planned_minutes = int(planned_duration.total_seconds() / 60)
185
+ logger.info(f" Запланированная длительность: {planned_minutes} минут")
186
+
187
+ # Проверяем соответствие конфигу
188
+ expected = config.ADMIN_SESSION_TIMEOUT_MINUTES
189
+ if planned_minutes == expected:
190
+ logger.info(f" ✅ Соответствует конфигу ({expected} мин)")
191
+ else:
192
+ logger.error(
193
+ f" ❌ НЕ соответствует конфигу! Ожидалось {expected} мин, получили {planned_minutes} мин"
194
+ )
195
+ problems_found += 1
196
+
197
+ # Вычисляем текущее время до автозавершения
198
+ now_utc = datetime.now(timezone.utc)
199
+
200
+ # Приводим к UTC
201
+ if end_time.tzinfo != timezone.utc:
202
+ end_time_utc = end_time.astimezone(timezone.utc)
203
+ else:
204
+ end_time_utc = end_time
205
+
206
+ remaining = end_time_utc - now_utc
207
+ remaining_minutes = max(0, int(remaining.total_seconds() / 60))
208
+
209
+ logger.info(f" now_utc: {now_utc}")
210
+ logger.info(f" end_time_utc: {end_time_utc}")
211
+ logger.info(f" Оставшееся время: {remaining_minutes} минут")
212
+
213
+ # Вычисляем сколько уже прошло
214
+ if start_time.tzinfo != timezone.utc:
215
+ start_time_utc = start_time.astimezone(timezone.utc)
216
+ else:
217
+ start_time_utc = start_time
218
+
219
+ elapsed = now_utc - start_time_utc
220
+ elapsed_minutes = max(0, int(elapsed.total_seconds() / 60))
221
+ logger.info(f" Прошло времени: {elapsed_minutes} минут")
222
+
223
+ # Проверяем математику
224
+ total_check = elapsed_minutes + remaining_minutes
225
+ logger.info(
226
+ f" Проверка: {elapsed_minutes} + {remaining_minutes} = {total_check} мин (должно быть ~{planned_minutes})"
227
+ )
228
+
229
+ if abs(total_check - planned_minutes) > 2:
230
+ logger.warning(
231
+ " ⚠️ ПРОБЛЕМА: сумма не сходится! Возможная проблема с timezone"
232
+ )
233
+ problems_found += 1
234
+
235
+ except Exception as e:
236
+ logger.error(f" ❌ Ошибка парсинга времени: {e}")
237
+ problems_found += 1
238
+
239
+ if not conversations:
240
+ logger.info(" Нет активных диалогов для анализа")
241
+ logger.info(" 💡 Создайте диалог командой /чат USER_ID для тестирования")
242
+
243
+ return problems_found == 0
244
+
245
+ except Exception as e:
246
+ logger.error(f"❌ Ошибка подключения к БД: {e}")
247
+ return False
248
+
249
+
250
+ async def test_conversation_creation(config: Config) -> bool:
251
+ """
252
+ Тестирует создание нового диалога с правильным таймаутом
253
+
254
+ Args:
255
+ config: Конфигурация бота
256
+
257
+ Returns:
258
+ bool: True если тест прошел успешно, False если найдены проблемы
259
+ """
260
+ logger.info(f"\n{'='*50}")
261
+ logger.info("🧪 ТЕСТ СОЗДАНИЯ ДИАЛОГА")
262
+ logger.info(f"{'='*50}")
263
+
264
+ timeout_minutes = config.ADMIN_SESSION_TIMEOUT_MINUTES
265
+ logger.info(f"📋 Конфигурация таймаута: {timeout_minutes} минут")
266
+
267
+ # Эмулируем создание диалога
268
+ now_utc = datetime.now(timezone.utc)
269
+ auto_end_utc = now_utc + timedelta(minutes=timeout_minutes)
270
+
271
+ logger.info(f"🕐 now_utc: {now_utc}")
272
+ logger.info(f"⏰ auto_end_utc: {auto_end_utc}")
273
+ logger.info(
274
+ f"📏 Разница: {int((auto_end_utc - now_utc).total_seconds() / 60)} минут"
275
+ )
276
+
277
+ # Проверяем ISO формат
278
+ auto_end_iso = auto_end_utc.isoformat()
279
+ logger.info(f"📝 ISO формат: {auto_end_iso}")
280
+
281
+ # Проверяем парсинг обратно
282
+ try:
283
+ if auto_end_iso.endswith("Z"):
284
+ parsed_back = datetime.fromisoformat(auto_end_iso.replace("Z", "+00:00"))
285
+ elif "+" in auto_end_iso:
286
+ parsed_back = datetime.fromisoformat(auto_end_iso)
287
+ else:
288
+ parsed_back = datetime.fromisoformat(auto_end_iso).replace(
289
+ tzinfo=timezone.utc
290
+ )
291
+
292
+ logger.info(f"🔄 Парсед обратно: {parsed_back}")
293
+
294
+ # Проверяем что время совпадает
295
+ if abs((parsed_back - auto_end_utc).total_seconds()) < 1:
296
+ logger.info("✅ Парсинг работает корректно")
297
+ return True
298
+ else:
299
+ logger.error("❌ Проблема с парсингом времени")
300
+ return False
301
+
302
+ except Exception as e:
303
+ logger.error(f"❌ Ошибка парсинга: {e}")
304
+ return False
305
+
306
+
307
+ async def check_timeouts(bot_name: str = "growthmed-october-24") -> bool:
308
+ """
309
+ Проверяет корректность таймаутов диалогов админов
310
+
311
+ Args:
312
+ bot_name: Имя бота для проверки
313
+
314
+ Returns:
315
+ bool: True если все таймауты корректны, False если найдены проблемы
316
+ """
317
+ logger.info("🔍 Проверка таймаутов диалогов админов\n")
318
+ logger.info(f"🚀 Проверка для бота: {bot_name}")
319
+ logger.info(f"🤖 Bot ID будет автоопределен как: {bot_name}\n")
320
+
321
+ # Настраиваем окружение для бота (автоматически устанавливает BOT_ID)
322
+ config_dir = setup_bot_environment(bot_name)
323
+ if not config_dir:
324
+ logger.error("❌ Не удалось настроить окружение бота")
325
+ return False
326
+
327
+ logger.info(f"📁 Текущая рабочая директория: {os.getcwd()}")
328
+ logger.info("📂 Содержимое рабочей директории:")
329
+ for item in Path(".").iterdir():
330
+ if item.is_dir():
331
+ logger.info(f" 📁 {item.name}/")
332
+ else:
333
+ logger.info(f" 📄 {item.name}")
334
+ logger.info("")
335
+
336
+ # Инициализация
337
+ try:
338
+ logger.info("⚙️ Инициализация конфигурации...")
339
+ config = Config()
340
+ logger.info("✅ Конфигурация загружена")
341
+
342
+ logger.info("🔗 Подключение к Supabase...")
343
+ supabase_client = SupabaseClient(config.SUPABASE_URL, config.SUPABASE_KEY)
344
+ await supabase_client.initialize()
345
+ logger.info("✅ Supabase подключен")
346
+
347
+ logger.info("👑 Инициализация менеджеров...")
348
+ admin_manager = AdminManager(config, supabase_client)
349
+ conversation_manager = ConversationManager(supabase_client, admin_manager)
350
+ logger.info("✅ Менеджеры инициализированы\n")
351
+
352
+ except Exception as e:
353
+ logger.error(f"❌ Ошибка инициализации: {e}")
354
+ logger.exception("Стек ошибки:")
355
+ return False
356
+
357
+ logger.info("⚙️ Конфигурация:")
358
+ logger.info(f" BOT_ID: {config.BOT_ID}")
359
+ logger.info(
360
+ f" ADMIN_SESSION_TIMEOUT_MINUTES: {config.ADMIN_SESSION_TIMEOUT_MINUTES}"
361
+ )
362
+ logger.info(f" PROMT_FILES_DIR: {config.PROMT_FILES_DIR}")
363
+ logger.info(f" Найдено промпт-файлов: {len(config.PROMPT_FILES)}")
364
+ logger.info(f" Админов: {len(config.ADMIN_TELEGRAM_IDS)}")
365
+ logger.info(f" Сейчас UTC: {datetime.now(timezone.utc)}")
366
+ logger.info("")
367
+
368
+ # Получаем активные диалоги
369
+ try:
370
+ logger.info("📊 Получение активных диалогов...")
371
+ conversations = await conversation_manager.get_active_conversations()
372
+ logger.info(f"✅ Получено {len(conversations)} диалогов")
373
+ except Exception as e:
374
+ logger.error(f"❌ Ошибка получения диалогов: {e}")
375
+ return False
376
+
377
+ if not conversations:
378
+ logger.info("💬 Нет активных диалогов")
379
+ logger.info("💡 Создайте диалог командой /чат USER_ID для тестирования")
380
+
381
+ # Показываем пример создания тестового диалога
382
+ logger.info("\n🧪 Пример создания тестового диалога:")
383
+ logger.info(f"1. Запустите бота: python {bot_name}.py")
384
+ logger.info("2. Как админ выполните: /чат 123456789")
385
+ logger.info("3. Затем проверьте: /чаты")
386
+ return True # Нет диалогов = нет проблем
387
+
388
+ logger.info(f"📊 Найдено {len(conversations)} активных диалогов:")
389
+ logger.info("")
390
+
391
+ problems_found = 0
392
+
393
+ for i, conv in enumerate(conversations, 1):
394
+ logger.info(f"{i}. Диалог ID: {conv['id']}")
395
+ logger.info(f" 👤 Пользователь: {conv['user_id']}")
396
+ logger.info(f" 👑 Админ: {conv['admin_id']}")
397
+
398
+ # Анализируем времена
399
+ started_at_str = conv["started_at"]
400
+ auto_end_str = conv["auto_end_at"]
401
+
402
+ logger.info(f" 🕐 started_at (сырое): {started_at_str}")
403
+ logger.info(f" ⏰ auto_end_at (сырое): {auto_end_str}")
404
+
405
+ try:
406
+ # Парсим время начала с правильной обработкой timezone
407
+ if started_at_str.endswith("Z"):
408
+ start_time = datetime.fromisoformat(
409
+ started_at_str.replace("Z", "+00:00")
410
+ )
411
+ elif "+" in started_at_str or started_at_str.count(":") >= 3:
412
+ start_time = datetime.fromisoformat(started_at_str)
413
+ else:
414
+ naive_time = datetime.fromisoformat(started_at_str)
415
+ start_time = naive_time.replace(tzinfo=timezone.utc)
416
+
417
+ # Парсим время автозавершения с правильной обработкой timezone
418
+ if auto_end_str.endswith("Z"):
419
+ auto_end = datetime.fromisoformat(auto_end_str.replace("Z", "+00:00"))
420
+ elif "+" in auto_end_str or auto_end_str.count(":") >= 3:
421
+ auto_end = datetime.fromisoformat(auto_end_str)
422
+ else:
423
+ naive_time = datetime.fromisoformat(auto_end_str)
424
+ auto_end = naive_time.replace(tzinfo=timezone.utc)
425
+
426
+ logger.info(f" 📅 start_time (parsed): {start_time}")
427
+ logger.info(f" ⏰ auto_end (parsed): {auto_end}")
428
+
429
+ # Планируемая длительность
430
+ planned_duration = auto_end - start_time
431
+ planned_minutes = int(planned_duration.total_seconds() / 60)
432
+ logger.info(f" 📏 Планируемая длительность: {planned_minutes} минут")
433
+
434
+ # Текущее время в UTC
435
+ now_utc = datetime.now(timezone.utc)
436
+
437
+ # Приводим все к UTC для корректных расчетов
438
+ if start_time.tzinfo != timezone.utc:
439
+ start_time_utc = start_time.astimezone(timezone.utc)
440
+ else:
441
+ start_time_utc = start_time
442
+
443
+ if auto_end.tzinfo != timezone.utc:
444
+ auto_end_utc = auto_end.astimezone(timezone.utc)
445
+ else:
446
+ auto_end_utc = auto_end
447
+
448
+ # Прошло времени
449
+ elapsed = now_utc - start_time_utc
450
+ elapsed_minutes = max(0, int(elapsed.total_seconds() / 60))
451
+ logger.info(f" ⏱️ Прошло времени: {elapsed_minutes} минут")
452
+
453
+ # Оставшееся время
454
+ remaining = auto_end_utc - now_utc
455
+ remaining_minutes = max(0, int(remaining.total_seconds() / 60))
456
+ logger.info(f" ⏰ Осталось времени: {remaining_minutes} минут")
457
+
458
+ # Проверяем корректность конфигурации
459
+ expected_timeout = config.ADMIN_SESSION_TIMEOUT_MINUTES
460
+ if (
461
+ abs(planned_minutes - expected_timeout) <= 2
462
+ ): # допускаем погрешность 2 минуты
463
+ logger.info(
464
+ f" ✅ Таймаут корректный (ожидался {expected_timeout} мин)"
465
+ )
466
+ else:
467
+ logger.error(
468
+ f" ❌ ОШИБКА: ожидался {expected_timeout} мин, получили {planned_minutes} мин"
469
+ )
470
+ problems_found += 1
471
+
472
+ # Проверяем математику
473
+ total_check = elapsed_minutes + remaining_minutes
474
+ logger.info(
475
+ f" 🔢 Проверка: {elapsed_minutes} + {remaining_minutes} = {total_check} мин"
476
+ )
477
+
478
+ if abs(total_check - planned_minutes) > 2:
479
+ logger.warning(
480
+ " ⚠️ ПРОБЛЕМА: сумма не сходится! Возможна проблема с timezone"
481
+ )
482
+ problems_found += 1
483
+ else:
484
+ logger.info(" ✅ Математика сходится")
485
+
486
+ except Exception as e:
487
+ logger.error(f" ❌ Ошибка парсинга: {e}")
488
+ problems_found += 1
489
+ logger.exception(" Стек ошибки:")
490
+
491
+ logger.info("")
492
+
493
+ # Тестируем функцию форматирования
494
+ logger.info("🧪 Тестирование format_active_conversations:")
495
+ try:
496
+ formatted_text = conversation_manager.format_active_conversations(conversations)
497
+ logger.info(formatted_text)
498
+ except Exception as e:
499
+ logger.error(f"❌ Ошибка форматирования: {e}")
500
+ problems_found += 1
501
+
502
+ # Итоговый результат
503
+ logger.info(f"\n{'='*50}")
504
+ logger.info("📊 ИТОГОВЫЙ РЕЗУЛЬТАТ:")
505
+ if problems_found == 0:
506
+ logger.info("✅ Все таймауты корректны!")
507
+ else:
508
+ logger.error(f"❌ Найдено {problems_found} проблем")
509
+ logger.info("💡 Запустите fix_existing_timeouts.py для исправления")
510
+ logger.info(f"{'='*50}")
511
+
512
+ return problems_found == 0
513
+
514
+
515
+ def main():
516
+ """Точка входа для запуска из командной строки"""
517
+ # Убираем лишние логи для чистого вывода
518
+ logging.basicConfig(level=logging.INFO, format="%(message)s")
519
+
520
+ logger.info("🔍 Утилита проверки таймаутов диалогов")
521
+ logger.info("Использование:")
522
+ logger.info(" python -m smart_bot_factory.timeout_checker [bot_name]")
523
+ logger.info(" python -m smart_bot_factory.timeout_checker growthmed-october-24")
524
+ logger.info("")
525
+
526
+ if len(sys.argv) > 1 and sys.argv[1] in ["-h", "--help", "help"]:
527
+ return
528
+
529
+ # Определяем какого бота проверять
530
+ bot_name = "growthmed-october-24" # по умолчанию
531
+ if len(sys.argv) > 1:
532
+ bot_name = sys.argv[1]
533
+
534
+ try:
535
+ success = asyncio.run(check_timeouts(bot_name))
536
+ if not success:
537
+ sys.exit(1)
538
+ except KeyboardInterrupt:
539
+ logger.info("\n⏹️ Прервано пользователем")
540
+ except Exception as e:
541
+ logger.error(f"\n💥 Критическая ошибка: {e}")
542
+ logger.exception("Стек ошибки:")
543
+ sys.exit(1)
544
+
545
+
546
+ if __name__ == "__main__":
547
+ main()
@@ -0,0 +1,14 @@
1
+ from .common import get_user_locale
2
+ from .dialog_calendar import DialogCalendar
3
+ from .schemas import (CalendarLabels, DialogCalendarCallback,
4
+ SimpleCalendarCallback)
5
+ from .simple_calendar import SimpleCalendar
6
+
7
+ __all__ = [
8
+ "SimpleCalendar",
9
+ "DialogCalendar",
10
+ "SimpleCalendarCallback",
11
+ "DialogCalendarCallback",
12
+ "CalendarLabels",
13
+ "get_user_locale",
14
+ ]
@@ -0,0 +1,64 @@
1
+ import calendar
2
+ import locale
3
+ from datetime import datetime
4
+
5
+ from aiogram.types import User
6
+
7
+ from .schemas import CalendarLabels
8
+
9
+
10
+ async def get_user_locale(from_user: User) -> str:
11
+ "Always returns ru_RU locale for consistent Russian language support"
12
+ return "ru_RU"
13
+
14
+
15
+ class GenericCalendar:
16
+
17
+ def __init__(
18
+ self,
19
+ locale: str = None, # оставляем для обратной совместимости
20
+ cancel_btn: str = None,
21
+ today_btn: str = None,
22
+ show_alerts: bool = False,
23
+ ) -> None:
24
+ """Initialize calendar with Russian language by default
25
+
26
+ Parameters:
27
+ locale (str): Ignored, always uses Russian
28
+ cancel_btn (str): label for button Cancel to cancel date input
29
+ today_btn (str): label for button Today to set calendar back to todays date
30
+ show_alerts (bool): defines how the date range error would shown (defaults to False)
31
+ """
32
+ self._labels = CalendarLabels()
33
+
34
+ if cancel_btn:
35
+ self._labels.cancel_caption = cancel_btn
36
+ if today_btn:
37
+ self._labels.today_caption = today_btn
38
+
39
+ self.min_date = None
40
+ self.max_date = None
41
+ self.show_alerts = show_alerts
42
+
43
+ def set_dates_range(self, min_date: datetime, max_date: datetime):
44
+ """Sets range of minimum & maximum dates"""
45
+ self.min_date = min_date
46
+ self.max_date = max_date
47
+
48
+ async def process_day_select(self, data, query):
49
+ """Checks selected date is in allowed range of dates"""
50
+ date = datetime(int(data.year), int(data.month), int(data.day))
51
+ if self.min_date and self.min_date > date:
52
+ await query.answer(
53
+ f'Дата должна быть позже {self.min_date.strftime("%d/%m/%Y")}',
54
+ show_alert=self.show_alerts,
55
+ )
56
+ return False, None
57
+ elif self.max_date and self.max_date < date:
58
+ await query.answer(
59
+ f'Дата должна быть раньше {self.max_date.strftime("%d/%m/%Y")}',
60
+ show_alert=self.show_alerts,
61
+ )
62
+ return False, None
63
+ await query.message.delete_reply_markup() # removing inline keyboard
64
+ return True, date