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,653 @@
1
+ # Исправленный conversation_manager.py после обновления из GitHub - фикс расчета времени до автозавершения
2
+
3
+ import logging
4
+ from datetime import datetime, timezone
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ from aiogram.fsm.context import FSMContext
8
+ from aiogram.types import Message, User
9
+
10
+ from .bot_utils import get_global_var
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class ConversationManager:
16
+ """Управление диалогами между админами и пользователями"""
17
+
18
+ def __init__(
19
+ self, supabase_client, admin_manager, parse_mode, admin_session_timeout_minutes
20
+ ):
21
+ self.supabase = supabase_client
22
+ self.admin_manager = admin_manager
23
+ self.parse_mode = parse_mode
24
+ self.admin_session_timeout_minutes = admin_session_timeout_minutes
25
+
26
+ async def start_admin_conversation(self, admin_id: int, user_id: int) -> bool:
27
+ """Начинает диалог админа с пользователем"""
28
+ try:
29
+ from ..utils.debug_routing import debug_admin_conversation_creation
30
+
31
+ await debug_admin_conversation_creation(admin_id, user_id)
32
+
33
+ # Проверяем, что это действительно админ
34
+ if not self.admin_manager.is_admin(admin_id):
35
+ logger.warning(f"Попытка начать диалог не-админом {admin_id}")
36
+ return False
37
+
38
+ # Получаем активную сессию пользователя
39
+ session_info = await self.supabase.get_active_session(user_id)
40
+ if not session_info:
41
+ logger.warning(f"У пользователя {user_id} нет активной сессии")
42
+ return False
43
+
44
+ session_id = session_info["id"]
45
+ logger.info(f"✅ Найдена активная сессия: {session_id}")
46
+
47
+ # Создаем запись о диалоге в БД
48
+ logger.info("🔧 Создаем запись о диалоге в БД...")
49
+ conversation_id = await self.supabase.start_admin_conversation(
50
+ admin_id, user_id, session_id
51
+ )
52
+ logger.info(f"✅ Диалог создан с ID: {conversation_id}")
53
+
54
+ # Показываем последние 5 сообщений
55
+ await self._show_recent_messages(admin_id, user_id, session_id)
56
+
57
+ logger.info(
58
+ f"🎉 Диалог успешно начат: админ {admin_id} -> пользователь {user_id}"
59
+ )
60
+ return True
61
+
62
+ except Exception as e:
63
+ logger.error(
64
+ f"❌ Ошибка начала диалога админа {admin_id} с пользователем {user_id}: {e}"
65
+ )
66
+ logger.exception("Полный стек ошибки:")
67
+ return False
68
+
69
+ async def _show_recent_messages(self, admin_id: int, user_id: int, session_id: str):
70
+ """Показывает последние сообщения пользователя"""
71
+ from .message_sender import send_message_by_human
72
+
73
+ try:
74
+ # Получаем последние 5 сообщений (сортируем по убыванию и берем первые 5)
75
+ query = (
76
+ self.supabase.client.table("sales_messages")
77
+ .select("role", "content", "created_at")
78
+ .eq("session_id", session_id)
79
+ .order("created_at", desc=True)
80
+ .limit(5)
81
+ )
82
+
83
+ # Добавляем фильтр по bot_id если он указан
84
+ if self.supabase.bot_id:
85
+ query = query.eq("bot_id", self.supabase.bot_id)
86
+
87
+ response = query.execute()
88
+
89
+ recent_messages = response.data if response.data else []
90
+
91
+ if not recent_messages:
92
+ await send_message_by_human(
93
+ admin_id, "📭 Нет сообщений в текущей сессии"
94
+ )
95
+ return
96
+
97
+ # Получаем красивое имя пользователя
98
+ user_display = await self.get_user_display_name(user_id)
99
+
100
+ header = f"📜 Последние сообщения с {user_display}\n{'━' * 40}"
101
+ await send_message_by_human(admin_id, header)
102
+
103
+ # Разворачиваем чтобы показать в хронологическом порядке (старые -> новые)
104
+ for msg in reversed(recent_messages):
105
+ role_emoji = "👤" if msg["role"] == "user" else "🤖"
106
+ timestamp = datetime.fromisoformat(
107
+ msg["created_at"].replace("Z", "+00:00")
108
+ )
109
+ time_str = timestamp.strftime("%H:%M")
110
+
111
+ # Сокращаем длинные сообщения
112
+ content = self._truncate_message(msg["content"])
113
+
114
+ message_text = f"{role_emoji} {time_str} | {content}"
115
+
116
+ await send_message_by_human(admin_id, message_text)
117
+
118
+ # Разделитель
119
+ await send_message_by_human(
120
+ admin_id,
121
+ f"{'━' * 40}\n💬 Диалог начат. Ваши сообщения будут переданы пользователю.",
122
+ )
123
+
124
+ except Exception as e:
125
+ logger.error(f"Ошибка показа последних сообщений: {e}")
126
+
127
+ async def get_user_display_name(self, user_id: int) -> str:
128
+ """Получает красивое отображение пользователя с username"""
129
+ try:
130
+ query = (
131
+ self.supabase.client.table("sales_users")
132
+ .select("first_name", "last_name", "username")
133
+ .eq("telegram_id", user_id)
134
+ )
135
+
136
+ # Добавляем фильтр по bot_id если он указан
137
+ if self.supabase.bot_id:
138
+ query = query.eq("bot_id", self.supabase.bot_id)
139
+
140
+ response = query.execute()
141
+
142
+ if response.data:
143
+ user_info = response.data[0]
144
+ name_parts = []
145
+ if user_info.get("first_name"):
146
+ name_parts.append(user_info["first_name"])
147
+ if user_info.get("last_name"):
148
+ name_parts.append(user_info["last_name"])
149
+
150
+ name = " ".join(name_parts) if name_parts else ""
151
+
152
+ if user_info.get("username"):
153
+ if name:
154
+ return f"{name} (@{user_info['username']})"
155
+ else:
156
+ return f"@{user_info['username']}"
157
+ elif name:
158
+ return name
159
+ else:
160
+ return f"ID {user_id}"
161
+ else:
162
+ return f"ID {user_id}"
163
+
164
+ except Exception as e:
165
+ logger.error(f"Ошибка получения информации о пользователе {user_id}: {e}")
166
+ return f"ID {user_id}"
167
+
168
+ def _truncate_message(self, text: str, max_lines: int = 6) -> str:
169
+ """Сокращает длинные сообщения"""
170
+ if not text:
171
+ return ""
172
+
173
+ lines = text.split("\n")
174
+
175
+ if len(lines) <= max_lines:
176
+ return text
177
+
178
+ # Берем первые 3 и последние 3 строки
179
+ first_lines = lines[:3]
180
+ last_lines = lines[-3:]
181
+
182
+ truncated = "\n".join(first_lines) + "\n...\n" + "\n".join(last_lines)
183
+ return truncated
184
+
185
+ async def end_admin_conversation(self, admin_id: int) -> bool:
186
+ """Завершает текущий диалог админа"""
187
+ try:
188
+ await self.supabase.end_admin_conversations(admin_id)
189
+ logger.info(f"Завершен диалог админа {admin_id}")
190
+ return True
191
+
192
+ except Exception as e:
193
+ logger.error(f"Ошибка завершения диалога админа {admin_id}: {e}")
194
+ return False
195
+
196
+ async def is_user_in_admin_chat(self, user_id: int) -> Optional[Dict[str, Any]]:
197
+ """Проверяет, ведется ли диалог с пользователем"""
198
+ try:
199
+ logger.debug(f"🔍 Проверяем диалог с пользователем {user_id}")
200
+
201
+ conversation = await self.supabase.get_user_admin_conversation(user_id)
202
+
203
+ if conversation:
204
+ logger.debug(
205
+ f"✅ Найден активный диалог: админ {conversation['admin_id']}, ID: {conversation['id']}"
206
+ )
207
+ else:
208
+ logger.debug(f"❌ Активный диалог не найден для пользователя {user_id}")
209
+
210
+ return conversation
211
+
212
+ except Exception as e:
213
+ logger.error(f"❌ Ошибка проверки диалога пользователя {user_id}: {e}")
214
+ return None
215
+
216
+ async def get_admin_active_conversation(
217
+ self, admin_id: int
218
+ ) -> Optional[Dict[str, Any]]:
219
+ """Получает активный диалог админа"""
220
+ try:
221
+ return await self.supabase.get_admin_active_conversation(admin_id)
222
+
223
+ except Exception as e:
224
+ logger.error(f"Ошибка получения активного диалога админа {admin_id}: {e}")
225
+ return None
226
+
227
+ async def forward_message_to_admin(
228
+ self, message: Message, conversation: Dict[str, Any]
229
+ ):
230
+ """Пересылает сообщение пользователя админу"""
231
+ from .message_sender import send_message_by_human
232
+
233
+ admin_id = conversation["admin_id"]
234
+ user_id = message.from_user.id
235
+
236
+ logger.info(f"📤 Пересылаем сообщение от {user_id} админу {admin_id}")
237
+
238
+ # Форматируем сообщение для админа
239
+ user_info = self._format_user_info(message.from_user)
240
+
241
+ # Время с начала диалога
242
+ try:
243
+ start_time = datetime.fromisoformat(
244
+ conversation["started_at"].replace("Z", "+00:00")
245
+ )
246
+ duration = datetime.now(start_time.tzinfo) - start_time
247
+ minutes = int(duration.total_seconds() / 60)
248
+ except Exception as e:
249
+ logger.error(f"Ошибка расчета времени диалога: {e}")
250
+ minutes = 0
251
+
252
+ # ✅ ИСПРАВЛЕНИЕ: Убираем символы, которые могут вызвать ошибки парсинга
253
+ safe_user_info = self._escape_markdown(user_info)
254
+ safe_message_text = self._escape_markdown(message.text or "")
255
+
256
+ header = f"👤 {safe_user_info} | ⏱️ {minutes} мин"
257
+ separator = "━" * 20
258
+
259
+ full_message = f"{header}\n{separator}\n{safe_message_text}"
260
+
261
+ try:
262
+ logger.info(f"📨 Отправляем сообщение админу {admin_id}")
263
+
264
+ # ✅ ИСПРАВЛЕНИЕ: Убираем parse_mode='Markdown' чтобы избежать ошибок парсинга
265
+ await send_message_by_human(admin_id, full_message)
266
+
267
+ logger.info(f"✅ Сообщение успешно отправлено админу {admin_id}")
268
+
269
+ # Добавляем кнопки управления
270
+ await self._send_admin_controls(admin_id, user_id)
271
+
272
+ except Exception as e:
273
+ logger.error(f"❌ Ошибка пересылки сообщения админу {admin_id}: {e}")
274
+
275
+ # ✅ ДОБАВЛЯЕМ: Fallback отправка без форматирования
276
+ try:
277
+ simple_message = f"Сообщение от пользователя {user_id}:\n{message.text}"
278
+ await send_message_by_human(admin_id, simple_message)
279
+ logger.info(f"✅ Простое сообщение отправлено админу {admin_id}")
280
+ except Exception as e2:
281
+ logger.error(f"❌ Даже простое сообщение не отправилось: {e2}")
282
+ raise
283
+
284
+ def _escape_markdown(self, text: str) -> str:
285
+ """Экранирует специальные символы Markdown"""
286
+ if not text:
287
+ return ""
288
+
289
+ # Символы, которые нужно экранировать в Markdown
290
+ markdown_chars = [
291
+ "*",
292
+ "_",
293
+ "`",
294
+ "[",
295
+ "]",
296
+ "(",
297
+ ")",
298
+ "~",
299
+ ">",
300
+ "#",
301
+ "+",
302
+ "-",
303
+ "=",
304
+ "|",
305
+ "{",
306
+ "}",
307
+ ".",
308
+ "!",
309
+ ]
310
+
311
+ escaped_text = text
312
+ for char in markdown_chars:
313
+ escaped_text = escaped_text.replace(char, f"\\{char}")
314
+
315
+ return escaped_text
316
+
317
+ async def forward_message_to_user(
318
+ self, message: Message, conversation: Dict[str, Any]
319
+ ):
320
+ """Пересылает сообщение админа пользователю"""
321
+ from .message_sender import send_message_by_human
322
+
323
+ supabase_client = get_global_var("supabase_client")
324
+
325
+ user_id = conversation["user_id"]
326
+
327
+ try:
328
+ # Отправляем сообщение как от бота
329
+ await send_message_by_human(user_id, message.text)
330
+
331
+ # Сохраняем в БД как сообщение ассистента
332
+ session_info = await supabase_client.get_active_session(user_id)
333
+ if session_info:
334
+ await supabase_client.add_message(
335
+ session_id=session_info["id"],
336
+ role="assistant",
337
+ content=message.text,
338
+ message_type="text",
339
+ metadata={"from_admin": message.from_user.id},
340
+ )
341
+
342
+ except Exception as e:
343
+ logger.error(f"Ошибка пересылки сообщения пользователю {user_id}: {e}")
344
+
345
+ async def _send_admin_controls(self, admin_id: int, user_id: int):
346
+ """Отправляет кнопки управления диалогом"""
347
+ from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
348
+
349
+ from .message_sender import send_message_by_human
350
+
351
+ keyboard = InlineKeyboardMarkup(
352
+ inline_keyboard=[
353
+ [
354
+ InlineKeyboardButton(
355
+ text="📋 История", callback_data=f"admin_history_{user_id}"
356
+ ),
357
+ InlineKeyboardButton(
358
+ text="✅ Завершить", callback_data=f"admin_end_{user_id}"
359
+ ),
360
+ ]
361
+ ]
362
+ )
363
+
364
+ try:
365
+ await send_message_by_human(
366
+ admin_id, "🎛️ Управление диалогом:", reply_markup=keyboard
367
+ )
368
+ except Exception as e:
369
+ logger.error(f"Ошибка отправки кнопок управления: {e}")
370
+
371
+ def _format_user_info(self, user: User) -> str:
372
+ """Форматирует информацию о пользователе"""
373
+ name_parts = []
374
+ if user.first_name:
375
+ name_parts.append(user.first_name)
376
+ if user.last_name:
377
+ name_parts.append(user.last_name)
378
+
379
+ name = " ".join(name_parts) if name_parts else "Без имени"
380
+
381
+ if user.username:
382
+ return f"{name} (@{user.username})"
383
+ else:
384
+ return f"{name} (ID: {user.id})"
385
+
386
+ async def cleanup_expired_conversations(self):
387
+ """Очищает просроченные диалоги"""
388
+ try:
389
+ ended_count = await self.supabase.end_expired_conversations()
390
+ if ended_count > 0:
391
+ logger.info(f"Очищено {ended_count} просроченных диалогов")
392
+ return ended_count
393
+
394
+ except Exception as e:
395
+ logger.error(f"Ошибка очистки просроченных диалогов: {e}")
396
+ return 0
397
+
398
+ async def get_conversation_stats(self) -> Dict[str, int]:
399
+ """Возвращает статистику диалогов"""
400
+ try:
401
+ # Здесь можно добавить запросы к БД для получения статистики
402
+ # Пока возвращаем заглушку
403
+ return {
404
+ "active_conversations": 0,
405
+ "completed_today": 0,
406
+ "total_admin_messages": 0,
407
+ }
408
+ except Exception as e:
409
+ logger.error(f"Ошибка получения статистики диалогов: {e}")
410
+ return {}
411
+
412
+ async def get_active_conversations(self) -> List[Dict[str, Any]]:
413
+ """Получает все активные диалоги админов"""
414
+ try:
415
+ logger.info("🔍 Ищем активные диалоги админов...")
416
+
417
+ # Получаем все активные диалоги
418
+ query = (
419
+ self.supabase.client.table("admin_user_conversations")
420
+ .select("id", "admin_id", "user_id", "started_at", "auto_end_at")
421
+ .eq("status", "active")
422
+ )
423
+
424
+ # Добавляем фильтр по bot_id если он указан
425
+ if self.supabase.bot_id:
426
+ query = query.eq("bot_id", self.supabase.bot_id)
427
+
428
+ response = query.order("started_at", desc=True).execute()
429
+
430
+ logger.info(f"📊 Найдено {len(response.data)} активных диалогов в БД")
431
+
432
+ conversations = []
433
+ for conv in response.data:
434
+ # Получаем информацию о пользователе
435
+ try:
436
+ user_query = (
437
+ self.supabase.client.table("sales_users")
438
+ .select("first_name", "last_name", "username")
439
+ .eq("telegram_id", conv["user_id"])
440
+ )
441
+
442
+ # Добавляем фильтр по bot_id если он указан
443
+ if self.supabase.bot_id:
444
+ user_query = user_query.eq("bot_id", self.supabase.bot_id)
445
+
446
+ user_response = user_query.execute()
447
+
448
+ user_info = user_response.data[0] if user_response.data else {}
449
+ except Exception as e:
450
+ logger.error(
451
+ f"Ошибка получения данных пользователя {conv['user_id']}: {e}"
452
+ )
453
+ user_info = {}
454
+
455
+ # Получаем информацию об админе
456
+ try:
457
+ admin_query = (
458
+ self.supabase.client.table("sales_admins")
459
+ .select("first_name", "last_name", "username")
460
+ .eq("telegram_id", conv["admin_id"])
461
+ )
462
+
463
+ # Добавляем фильтр по bot_id если он указан
464
+ if self.supabase.bot_id:
465
+ admin_query = admin_query.eq("bot_id", self.supabase.bot_id)
466
+
467
+ admin_response = admin_query.execute()
468
+
469
+ admin_info = admin_response.data[0] if admin_response.data else {}
470
+ except Exception as e:
471
+ logger.error(
472
+ f"Ошибка получения данных админа {conv['admin_id']}: {e}"
473
+ )
474
+ admin_info = {}
475
+
476
+ conv["user_info"] = user_info
477
+ conv["admin_info"] = admin_info
478
+ conversations.append(conv)
479
+
480
+ logger.info(
481
+ f"✅ Получено {len(conversations)} активных диалогов с дополнительной информацией"
482
+ )
483
+ return conversations
484
+
485
+ except Exception as e:
486
+ logger.error(f"❌ Ошибка получения активных диалогов: {e}")
487
+ return []
488
+
489
+ def format_active_conversations(self, conversations: List[Dict[str, Any]]) -> str:
490
+ """Форматирует список активных диалогов - ИСПРАВЛЕН РАСЧЕТ ВРЕМЕНИ АВТОЗАВЕРШЕНИЯ"""
491
+ if not conversations:
492
+ return "💬 Нет активных диалогов"
493
+
494
+ lines = ["💬 АКТИВНЫЕ ДИАЛОГИ:", ""]
495
+
496
+ for i, conv in enumerate(conversations, 1):
497
+ # Информация о пользователе
498
+ user_info = conv.get("user_info", {})
499
+ user_name = []
500
+ if user_info.get("first_name"):
501
+ user_name.append(user_info["first_name"])
502
+ if user_info.get("last_name"):
503
+ user_name.append(user_info["last_name"])
504
+
505
+ user_display = " ".join(user_name) if user_name else f"ID {conv['user_id']}"
506
+ if user_info.get("username"):
507
+ user_display += f" (@{user_info['username']})"
508
+
509
+ # Информация об админе
510
+ admin_info = conv.get("admin_info", {})
511
+ admin_name = []
512
+ if admin_info.get("first_name"):
513
+ admin_name.append(admin_info["first_name"])
514
+ if admin_info.get("last_name"):
515
+ admin_name.append(admin_info["last_name"])
516
+
517
+ admin_display = (
518
+ " ".join(admin_name) if admin_name else f"ID {conv['admin_id']}"
519
+ )
520
+
521
+ # 🔧 ИСПРАВЛЕНИЕ: Правильный расчет времени с учетом timezone
522
+ try:
523
+ started_at_str = conv["started_at"]
524
+ logger.debug(f"🕐 Диалог {i}: started_at = '{started_at_str}'")
525
+
526
+ # Парсим время начала с правильной обработкой timezone
527
+ if started_at_str.endswith("Z"):
528
+ start_time = datetime.fromisoformat(
529
+ started_at_str.replace("Z", "+00:00")
530
+ )
531
+ elif "+" in started_at_str or started_at_str.count(":") >= 3:
532
+ # Уже есть timezone info
533
+ start_time = datetime.fromisoformat(started_at_str)
534
+ else:
535
+ # Если нет timezone info, считаем что это UTC
536
+ naive_time = datetime.fromisoformat(started_at_str)
537
+ start_time = naive_time.replace(tzinfo=timezone.utc)
538
+
539
+ logger.debug(f"✅ Парсед start_time: {start_time}")
540
+
541
+ # Получаем текущее время в UTC
542
+ now_utc = datetime.now(timezone.utc)
543
+ logger.debug(f"🕐 now_utc: {now_utc}")
544
+
545
+ # Приводим start_time к UTC если нужно
546
+ if start_time.tzinfo != timezone.utc:
547
+ start_time_utc = start_time.astimezone(timezone.utc)
548
+ else:
549
+ start_time_utc = start_time
550
+
551
+ # Длительность диалога
552
+ duration = now_utc - start_time_utc
553
+ minutes = max(0, int(duration.total_seconds() / 60))
554
+ logger.debug(f"⏱️ Длительность: {minutes} минут")
555
+
556
+ except Exception as e:
557
+ logger.error(f"❌ Ошибка расчета времени диалога {i}: {e}")
558
+ logger.error(f" started_at_str: '{started_at_str}'")
559
+ minutes = 0
560
+
561
+ # 🔧 ИСПРАВЛЕНИЕ: Время до автозавершения с правильной обработкой timezone
562
+ try:
563
+ auto_end_str = conv["auto_end_at"]
564
+ logger.debug(f"🕐 Диалог {i}: auto_end_at = '{auto_end_str}'")
565
+
566
+ # Парсим время автозавершения с правильной обработкой timezone
567
+ if auto_end_str.endswith("Z"):
568
+ auto_end = datetime.fromisoformat(
569
+ auto_end_str.replace("Z", "+00:00")
570
+ )
571
+ elif "+" in auto_end_str or auto_end_str.count(":") >= 3:
572
+ # Уже есть timezone info
573
+ auto_end = datetime.fromisoformat(auto_end_str)
574
+ else:
575
+ # Если нет timezone info, считаем что это UTC
576
+ naive_time = datetime.fromisoformat(auto_end_str)
577
+ auto_end = naive_time.replace(tzinfo=timezone.utc)
578
+
579
+ logger.debug(f"✅ Парсед auto_end: {auto_end}")
580
+
581
+ # Получаем текущее время в UTC
582
+ now_utc = datetime.now(timezone.utc)
583
+ logger.debug(f"🕐 now_utc для auto_end: {now_utc}")
584
+
585
+ # Приводим auto_end к UTC если нужно
586
+ if auto_end.tzinfo != timezone.utc:
587
+ auto_end_utc = auto_end.astimezone(timezone.utc)
588
+ else:
589
+ auto_end_utc = auto_end
590
+
591
+ # Оставшееся время
592
+ remaining = auto_end_utc - now_utc
593
+ remaining_minutes = max(0, int(remaining.total_seconds() / 60))
594
+ logger.debug(f"⏰ Remaining: {remaining_minutes} минут")
595
+
596
+ # 🔧 ДОПОЛНИТЕЛЬНАЯ ПРОВЕРКА: вычисляем планируемую длительность
597
+ if start_time.tzinfo != timezone.utc:
598
+ start_time_utc = start_time.astimezone(timezone.utc)
599
+ else:
600
+ start_time_utc = start_time
601
+
602
+ planned_duration = auto_end_utc - start_time_utc
603
+ planned_minutes = int(planned_duration.total_seconds() / 60)
604
+ logger.debug(f"📏 Планируемая длительность: {planned_minutes} минут")
605
+
606
+ # Проверяем корректность
607
+ expected_timeout = self.admin_session_timeout_minutes
608
+
609
+ logger.debug(f"🕐 expected_timeout: {expected_timeout}")
610
+
611
+ if (
612
+ abs(planned_minutes - expected_timeout) > 2
613
+ ): # допускаем погрешность в 2 минуты
614
+ logger.warning(
615
+ f"⚠️ Диалог {i}: планируемая длительность {planned_minutes} мин не соответствует конфигу {expected_timeout} мин"
616
+ )
617
+
618
+ except Exception as e:
619
+ logger.error(
620
+ f"❌ Ошибка расчета времени автозавершения диалога {i}: {e}"
621
+ )
622
+ logger.error(f" auto_end_str: '{auto_end_str}'")
623
+ remaining_minutes = 0
624
+
625
+ lines.append(f"{i}. 👤 {user_display}")
626
+ lines.append(f" 👑 Админ: {admin_display}")
627
+ lines.append(f" ⏱️ Длительность: {minutes} мин")
628
+ lines.append(f" ⏰ Автозавершение через: {remaining_minutes} мин")
629
+ lines.append(f" 🎛️ /чат {conv['user_id']}")
630
+ lines.append("")
631
+
632
+ return "\n".join(lines)
633
+
634
+ async def route_admin_message(self, message: Message, state: FSMContext) -> bool:
635
+ """
636
+ Маршрутизирует сообщение админа
637
+ Возвращает True если сообщение обработано как админское
638
+ """
639
+ admin_id = message.from_user.id
640
+
641
+ # Проверяем админские команды
642
+ if message.text and message.text.startswith("/"):
643
+ return False # Команды обрабатываются отдельно
644
+
645
+ # Проверяем, ведется ли диалог с пользователем
646
+ conversation = await self.get_admin_active_conversation(admin_id)
647
+
648
+ if conversation:
649
+ # Пересылаем сообщение пользователю
650
+ await self.forward_message_to_user(message, conversation)
651
+ return True
652
+
653
+ return False