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

Files changed (45) hide show
  1. smart_bot_factory/admin/__init__.py +7 -7
  2. smart_bot_factory/admin/admin_events.py +483 -383
  3. smart_bot_factory/admin/admin_logic.py +234 -158
  4. smart_bot_factory/admin/admin_manager.py +68 -53
  5. smart_bot_factory/admin/admin_tester.py +46 -40
  6. smart_bot_factory/admin/timeout_checker.py +201 -153
  7. smart_bot_factory/aiogram_calendar/__init__.py +11 -3
  8. smart_bot_factory/aiogram_calendar/common.py +12 -18
  9. smart_bot_factory/aiogram_calendar/dialog_calendar.py +126 -64
  10. smart_bot_factory/aiogram_calendar/schemas.py +49 -28
  11. smart_bot_factory/aiogram_calendar/simple_calendar.py +94 -50
  12. smart_bot_factory/analytics/analytics_manager.py +414 -392
  13. smart_bot_factory/cli.py +204 -148
  14. smart_bot_factory/config.py +123 -102
  15. smart_bot_factory/core/bot_utils.py +474 -332
  16. smart_bot_factory/core/conversation_manager.py +287 -200
  17. smart_bot_factory/core/decorators.py +1200 -755
  18. smart_bot_factory/core/message_sender.py +287 -266
  19. smart_bot_factory/core/router.py +170 -100
  20. smart_bot_factory/core/router_manager.py +121 -83
  21. smart_bot_factory/core/states.py +4 -3
  22. smart_bot_factory/creation/__init__.py +1 -1
  23. smart_bot_factory/creation/bot_builder.py +320 -242
  24. smart_bot_factory/creation/bot_testing.py +440 -365
  25. smart_bot_factory/dashboard/__init__.py +1 -3
  26. smart_bot_factory/event/__init__.py +2 -7
  27. smart_bot_factory/handlers/handlers.py +676 -472
  28. smart_bot_factory/integrations/openai_client.py +218 -168
  29. smart_bot_factory/integrations/supabase_client.py +948 -637
  30. smart_bot_factory/message/__init__.py +18 -22
  31. smart_bot_factory/router/__init__.py +2 -2
  32. smart_bot_factory/setup_checker.py +162 -126
  33. smart_bot_factory/supabase/__init__.py +1 -1
  34. smart_bot_factory/supabase/client.py +631 -515
  35. smart_bot_factory/utils/__init__.py +2 -3
  36. smart_bot_factory/utils/debug_routing.py +38 -27
  37. smart_bot_factory/utils/prompt_loader.py +153 -120
  38. smart_bot_factory/utils/user_prompt_loader.py +55 -56
  39. smart_bot_factory/utm_link_generator.py +123 -116
  40. {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.9.dist-info}/METADATA +3 -1
  41. smart_bot_factory-0.3.9.dist-info/RECORD +59 -0
  42. smart_bot_factory-0.3.7.dist-info/RECORD +0 -59
  43. {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.9.dist-info}/WHEEL +0 -0
  44. {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.9.dist-info}/entry_points.txt +0 -0
  45. {smart_bot_factory-0.3.7.dist-info → smart_bot_factory-0.3.9.dist-info}/licenses/LICENSE +0 -0
@@ -5,39 +5,39 @@
5
5
  import logging
6
6
  import time
7
7
  from datetime import datetime
8
- from typing import Dict, Any, Optional
8
+ from typing import Any, Dict, Optional
9
+
9
10
  import pytz
10
11
 
11
12
  logger = logging.getLogger(__name__)
12
13
 
14
+
13
15
  async def send_message_by_ai(
14
- user_id: int,
15
- message_text: str,
16
- session_id: str = None
16
+ user_id: int, message_text: str, session_id: str = None
17
17
  ) -> Dict[str, Any]:
18
18
  """
19
19
  Отправляет сообщение пользователю через ИИ (копирует логику process_user_message)
20
-
20
+
21
21
  Args:
22
22
  user_id: ID пользователя в Telegram
23
23
  message_text: Текст сообщения для обработки ИИ
24
24
  session_id: ID сессии чата (если не указан, будет использована активная сессия)
25
-
25
+
26
26
  Returns:
27
27
  Результат отправки
28
28
  """
29
29
  try:
30
30
  # Импортируем необходимые компоненты
31
- from .bot_utils import parse_ai_response, process_events
32
-
33
31
  # Получаем компоненты из глобального контекста
34
32
  from ..handlers.handlers import get_global_var
35
- bot = get_global_var('bot')
36
- supabase_client = get_global_var('supabase_client')
37
- openai_client = get_global_var('openai_client')
38
- config = get_global_var('config')
39
- prompt_loader = get_global_var('prompt_loader')
40
-
33
+ from .bot_utils import parse_ai_response, process_events
34
+
35
+ bot = get_global_var("bot")
36
+ supabase_client = get_global_var("supabase_client")
37
+ openai_client = get_global_var("openai_client")
38
+ config = get_global_var("config")
39
+ prompt_loader = get_global_var("prompt_loader")
40
+
41
41
  # Если session_id не указан, получаем активную сессию пользователя
42
42
  if not session_id:
43
43
  session_info = await supabase_client.get_active_session(user_id)
@@ -45,10 +45,10 @@ async def send_message_by_ai(
45
45
  return {
46
46
  "status": "error",
47
47
  "error": "Активная сессия не найдена",
48
- "user_id": user_id
48
+ "user_id": user_id,
49
49
  }
50
- session_id = session_info['id']
51
-
50
+ session_id = session_info["id"]
51
+
52
52
  # Загружаем системный промпт
53
53
  try:
54
54
  system_prompt = await prompt_loader.load_system_prompt()
@@ -58,246 +58,237 @@ async def send_message_by_ai(
58
58
  return {
59
59
  "status": "error",
60
60
  "error": "Не удалось загрузить системный промпт",
61
- "user_id": user_id
61
+ "user_id": user_id,
62
62
  }
63
-
63
+
64
64
  # Сохраняем сообщение пользователя в БД
65
65
  await supabase_client.add_message(
66
66
  session_id=session_id,
67
- role='user',
67
+ role="user",
68
68
  content=message_text,
69
- message_type='text'
69
+ message_type="text",
70
70
  )
71
- logger.info(f"✅ Сообщение пользователя сохранено в БД")
72
-
71
+ logger.info("✅ Сообщение пользователя сохранено в БД")
72
+
73
73
  # Получаем историю сообщений
74
- chat_history = await supabase_client.get_chat_history(session_id, limit=config.MAX_CONTEXT_MESSAGES)
74
+ chat_history = await supabase_client.get_chat_history(
75
+ session_id, limit=config.MAX_CONTEXT_MESSAGES
76
+ )
75
77
  logger.info(f"📚 Загружена история: {len(chat_history)} сообщений")
76
-
78
+
77
79
  # Добавляем текущее время
78
- moscow_tz = pytz.timezone('Europe/Moscow')
80
+ moscow_tz = pytz.timezone("Europe/Moscow")
79
81
  current_time = datetime.now(moscow_tz)
80
- time_info = current_time.strftime('%H:%M, %d.%m.%Y, %A')
81
-
82
+ time_info = current_time.strftime("%H:%M, %d.%m.%Y, %A")
83
+
82
84
  # Модифицируем системный промпт, добавляя время
83
85
  system_prompt_with_time = f"""
84
86
  {system_prompt}
85
87
 
86
88
  ТЕКУЩЕЕ ВРЕМЯ: {time_info} (московское время)
87
89
  """
88
-
90
+
89
91
  # Формируем контекст для OpenAI
90
92
  messages = [{"role": "system", "content": system_prompt_with_time}]
91
-
92
- for msg in chat_history[-config.MAX_CONTEXT_MESSAGES:]:
93
- messages.append({
94
- "role": msg['role'],
95
- "content": msg['content']
96
- })
97
-
93
+
94
+ for msg in chat_history[-config.MAX_CONTEXT_MESSAGES :]:
95
+ messages.append({"role": msg["role"], "content": msg["content"]})
96
+
98
97
  # Добавляем финальные инструкции
99
98
  final_instructions = await prompt_loader.load_final_instructions()
100
99
  if final_instructions:
101
100
  messages.append({"role": "system", "content": final_instructions})
102
- logger.info(f"🎯 Добавлены финальные инструкции")
103
-
101
+ logger.info("🎯 Добавлены финальные инструкции")
102
+
104
103
  logger.info(f"📝 Контекст сформирован: {len(messages)} сообщений")
105
-
104
+
106
105
  # Отправляем действие "печатает"
107
106
  await bot.send_chat_action(user_id, "typing")
108
-
107
+
109
108
  # Получаем ответ от ИИ
110
109
  start_time = time.time()
111
110
  ai_response = await openai_client.get_completion(messages)
112
111
  processing_time = int((time.time() - start_time) * 1000)
113
-
112
+
114
113
  logger.info(f"🤖 OpenAI ответил за {processing_time}мс")
115
-
114
+
116
115
  # Обрабатываем ответ
117
116
  tokens_used = 0
118
117
  ai_metadata = {}
119
118
  response_text = ""
120
-
119
+
121
120
  if not ai_response or not ai_response.strip():
122
- logger.warning(f"❌ OpenAI вернул пустой ответ!")
121
+ logger.warning("❌ OpenAI вернул пустой ответ!")
123
122
  fallback_message = "Извините, произошла техническая ошибка. Попробуйте переформулировать вопрос."
124
123
  ai_response = fallback_message
125
124
  response_text = fallback_message
126
125
  else:
127
126
  tokens_used = openai_client.estimate_tokens(ai_response)
128
127
  response_text, ai_metadata = parse_ai_response(ai_response)
129
-
128
+
130
129
  if not ai_metadata:
131
130
  response_text = ai_response
132
131
  ai_metadata = {}
133
132
  elif not response_text.strip():
134
133
  response_text = ai_response
135
-
134
+
136
135
  # Обновляем этап сессии и качество лида
137
136
  if ai_metadata:
138
- stage = ai_metadata.get('этап')
139
- quality = ai_metadata.get('качество')
140
-
137
+ stage = ai_metadata.get("этап")
138
+ quality = ai_metadata.get("качество")
139
+
141
140
  if stage or quality is not None:
142
141
  await supabase_client.update_session_stage(session_id, stage, quality)
143
- logger.info(f"✅ Этап и качество обновлены в БД")
144
-
142
+ logger.info("✅ Этап и качество обновлены в БД")
143
+
145
144
  # Обрабатываем события
146
- events = ai_metadata.get('събития', [])
145
+ events = ai_metadata.get("събития", [])
147
146
  if events:
148
147
  logger.info(f"🔔 Обрабатываем {len(events)} событий")
149
148
  should_send_response = await process_events(session_id, events, user_id)
150
-
149
+
151
150
  # Сохраняем ответ ассистента
152
151
  await supabase_client.add_message(
153
152
  session_id=session_id,
154
- role='assistant',
153
+ role="assistant",
155
154
  content=response_text,
156
- message_type='text',
155
+ message_type="text",
157
156
  tokens_used=tokens_used,
158
157
  processing_time_ms=processing_time,
159
- ai_metadata=ai_metadata
158
+ ai_metadata=ai_metadata,
160
159
  )
161
-
160
+
162
161
  # Определяем финальный ответ
163
162
  if config.DEBUG_MODE:
164
163
  final_response = ai_response
165
164
  else:
166
165
  final_response = response_text
167
-
166
+
168
167
  # Проверяем, нужно ли отправлять сообщение от ИИ
169
- if 'should_send_response' in locals() and not should_send_response:
170
- logger.info("🔇 События запретили отправку сообщения от ИИ (message_sender), пропускаем отправку")
168
+ if "should_send_response" in locals() and not should_send_response:
169
+ logger.info(
170
+ "🔇 События запретили отправку сообщения от ИИ (message_sender), пропускаем отправку"
171
+ )
171
172
  return {
172
173
  "status": "skipped",
173
174
  "reason": "send_ai_response=False",
174
- "user_id": user_id
175
+ "user_id": user_id,
175
176
  }
176
-
177
+
177
178
  # Отправляем ответ пользователю напрямую через бота
178
- await bot.send_message(
179
- chat_id=user_id,
180
- text=final_response
181
- )
182
-
179
+ await bot.send_message(chat_id=user_id, text=final_response)
180
+
183
181
  return {
184
182
  "status": "success",
185
183
  "user_id": user_id,
186
184
  "response_text": response_text,
187
185
  "tokens_used": tokens_used,
188
186
  "processing_time_ms": processing_time,
189
- "events_processed": len(events) if events else 0
187
+ "events_processed": len(events) if events else 0,
190
188
  }
191
-
189
+
192
190
  except Exception as e:
193
191
  logger.error(f"❌ Ошибка в send_message_by_ai: {e}")
194
- return {
195
- "status": "error",
196
- "error": str(e),
197
- "user_id": user_id
198
- }
192
+ return {"status": "error", "error": str(e), "user_id": user_id}
193
+
199
194
 
200
195
  async def send_message_by_human(
201
- user_id: int,
202
- message_text: str,
203
- session_id: Optional[str] = None
196
+ user_id: int, message_text: str, session_id: Optional[str] = None
204
197
  ) -> Dict[str, Any]:
205
198
  """
206
199
  Отправляет сообщение пользователю от имени человека (готовый текст)
207
-
200
+
208
201
  Args:
209
202
  user_id: ID пользователя в Telegram
210
203
  message_text: Готовый текст сообщения
211
204
  session_id: ID сессии (опционально, для сохранения в БД)
212
-
205
+
213
206
  Returns:
214
207
  Результат отправки
215
208
  """
216
209
  try:
217
210
  # Импортируем необходимые компоненты
218
211
  from ..handlers.handlers import get_global_var
219
- bot = get_global_var('bot')
220
- supabase_client = get_global_var('supabase_client')
221
-
212
+
213
+ bot = get_global_var("bot")
214
+ supabase_client = get_global_var("supabase_client")
215
+
222
216
  # Отправляем сообщение пользователю
223
- message = await bot.send_message(
224
- chat_id=user_id,
225
- text=message_text
226
- )
227
-
217
+ message = await bot.send_message(chat_id=user_id, text=message_text)
218
+
228
219
  # Если указана сессия, сохраняем сообщение в БД
229
220
  if session_id:
230
221
  await supabase_client.add_message(
231
222
  session_id=session_id,
232
- role='assistant',
223
+ role="assistant",
233
224
  content=message_text,
234
- message_type='text',
235
- metadata={'sent_by_human': True}
225
+ message_type="text",
226
+ metadata={"sent_by_human": True},
236
227
  )
237
- logger.info(f"💾 Сообщение от человека сохранено в БД")
238
-
228
+ logger.info("💾 Сообщение от человека сохранено в БД")
229
+
239
230
  return {
240
231
  "status": "success",
241
232
  "user_id": user_id,
242
233
  "message_id": message.message_id,
243
234
  "message_text": message_text,
244
- "saved_to_db": bool(session_id)
235
+ "saved_to_db": bool(session_id),
245
236
  }
246
-
237
+
247
238
  except Exception as e:
248
239
  logger.error(f"❌ Ошибка в send_message_by_human: {e}")
249
- return {
250
- "status": "error",
251
- "error": str(e),
252
- "user_id": user_id
253
- }
240
+ return {"status": "error", "error": str(e), "user_id": user_id}
241
+
254
242
 
255
243
  async def send_message_to_users_by_stage(
256
- stage: str,
257
- message_text: str,
258
- bot_id: str
244
+ stage: str, message_text: str, bot_id: str
259
245
  ) -> Dict[str, Any]:
260
246
  """
261
247
  Отправляет сообщение всем пользователям, находящимся на определенной стадии
262
-
248
+
263
249
  Args:
264
250
  stage: Стадия диалога (например, 'introduction', 'qualification', 'closing')
265
251
  message_text: Текст сообщения для отправки
266
252
  bot_id: ID бота (если не указан, используется текущий бот)
267
-
253
+
268
254
  Returns:
269
255
  Результат отправки с количеством отправленных сообщений
270
256
  """
271
257
  try:
272
258
  # Импортируем необходимые компоненты
273
259
  from ..handlers.handlers import get_global_var
274
- bot = get_global_var('bot')
275
- supabase_client = get_global_var('supabase_client')
276
- current_bot_id = get_global_var('config').BOT_ID if get_global_var('config') else bot_id
277
-
260
+
261
+ bot = get_global_var("bot")
262
+ supabase_client = get_global_var("supabase_client")
263
+ current_bot_id = (
264
+ get_global_var("config").BOT_ID if get_global_var("config") else bot_id
265
+ )
266
+
278
267
  if not current_bot_id:
279
- return {
280
- "status": "error",
281
- "error": "Не удалось определить bot_id"
282
- }
283
-
284
- logger.info(f"🔍 Ищем пользователей на стадии '{stage}' для бота '{current_bot_id}'")
285
-
268
+ return {"status": "error", "error": "Не удалось определить bot_id"}
269
+
270
+ logger.info(
271
+ f"🔍 Ищем пользователей на стадии '{stage}' для бота '{current_bot_id}'"
272
+ )
273
+
286
274
  # Получаем последние сессии для каждого пользователя с нужной стадией
287
275
  # Сначала получаем все активные сессии с нужной стадией
288
- sessions_query = supabase_client.client.table('sales_chat_sessions').select(
289
- 'user_id, id, current_stage, created_at'
290
- ).eq('status', 'active').eq('current_stage', stage)
291
-
276
+ sessions_query = (
277
+ supabase_client.client.table("sales_chat_sessions")
278
+ .select("user_id, id, current_stage, created_at")
279
+ .eq("status", "active")
280
+ .eq("current_stage", stage)
281
+ )
282
+
292
283
  # Фильтруем по bot_id если указан
293
284
  if current_bot_id:
294
- sessions_query = sessions_query.eq('bot_id', current_bot_id)
295
-
285
+ sessions_query = sessions_query.eq("bot_id", current_bot_id)
286
+
296
287
  # Сортируем по дате создания (последние сначала)
297
- sessions_query = sessions_query.order('created_at', desc=True)
298
-
288
+ sessions_query = sessions_query.order("created_at", desc=True)
289
+
299
290
  sessions_data = sessions_query.execute()
300
-
291
+
301
292
  if not sessions_data.data:
302
293
  logger.info(f"📭 Пользователи на стадии '{stage}' не найдены")
303
294
  return {
@@ -305,159 +296,156 @@ async def send_message_to_users_by_stage(
305
296
  "stage": stage,
306
297
  "users_found": 0,
307
298
  "messages_sent": 0,
308
- "errors": []
299
+ "errors": [],
309
300
  }
310
-
301
+
311
302
  # Выбираем уникальные user_id (берем только последнюю сессию для каждого пользователя)
312
303
  unique_users = {}
313
304
  for session in sessions_data.data:
314
- user_id = session['user_id']
305
+ user_id = session["user_id"]
315
306
  # Если пользователь еще не добавлен, добавляем его (так как сессии отсортированы по дате, первая будет самой последней)
316
307
  if user_id not in unique_users:
317
308
  unique_users[user_id] = {
318
- 'session_id': session['id'],
319
- 'current_stage': session['current_stage']
309
+ "session_id": session["id"],
310
+ "current_stage": session["current_stage"],
320
311
  }
321
-
322
- logger.info(f"👥 Найдено {len(unique_users)} уникальных пользователей на стадии '{stage}'")
323
-
312
+
313
+ logger.info(
314
+ f"👥 Найдено {len(unique_users)} уникальных пользователей на стадии '{stage}'"
315
+ )
316
+
324
317
  # Отправляем сообщения
325
318
  messages_sent = 0
326
319
  errors = []
327
-
320
+
328
321
  for user_id, user_data in unique_users.items():
329
- session_id = user_data['session_id']
330
-
322
+ session_id = user_data["session_id"]
323
+
331
324
  try:
332
325
  # Отправляем сообщение пользователю
333
- await bot.send_message(
334
- chat_id=user_id,
335
- text=message_text
336
- )
337
-
326
+ await bot.send_message(chat_id=user_id, text=message_text)
327
+
338
328
  # Сохраняем сообщение в БД
339
329
  await supabase_client.add_message(
340
330
  session_id=session_id,
341
- role='assistant',
331
+ role="assistant",
342
332
  content=message_text,
343
- message_type='text',
333
+ message_type="text",
344
334
  metadata={
345
- 'sent_by_stage_broadcast': True,
346
- 'target_stage': stage,
347
- 'broadcast_timestamp': datetime.now().isoformat()
348
- }
335
+ "sent_by_stage_broadcast": True,
336
+ "target_stage": stage,
337
+ "broadcast_timestamp": datetime.now().isoformat(),
338
+ },
349
339
  )
350
-
340
+
351
341
  messages_sent += 1
352
- logger.info(f"✅ Сообщение отправлено пользователю {user_id} (стадия: {stage})")
353
-
342
+ logger.info(
343
+ f"✅ Сообщение отправлено пользователю {user_id} (стадия: {stage})"
344
+ )
345
+
354
346
  except Exception as e:
355
347
  error_msg = f"Ошибка отправки пользователю {user_id}: {str(e)}"
356
348
  errors.append(error_msg)
357
349
  logger.error(f"❌ {error_msg}")
358
-
350
+
359
351
  result = {
360
352
  "status": "success",
361
353
  "stage": stage,
362
354
  "users_found": len(unique_users),
363
355
  "messages_sent": messages_sent,
364
- "errors": errors
356
+ "errors": errors,
365
357
  }
366
-
367
- logger.info(f"📊 Результат рассылки по стадии '{stage}': {messages_sent}/{len(unique_users)} сообщений отправлено")
368
-
358
+
359
+ logger.info(
360
+ f"📊 Результат рассылки по стадии '{stage}': {messages_sent}/{len(unique_users)} сообщений отправлено"
361
+ )
362
+
369
363
  return result
370
-
364
+
371
365
  except Exception as e:
372
366
  logger.error(f"❌ Ошибка в send_message_to_users_by_stage: {e}")
373
- return {
374
- "status": "error",
375
- "error": str(e),
376
- "stage": stage
377
- }
367
+ return {"status": "error", "error": str(e), "stage": stage}
378
368
 
379
- async def get_users_by_stage_stats(
380
- bot_id: Optional[str] = None
381
- ) -> Dict[str, Any]:
369
+
370
+ async def get_users_by_stage_stats(bot_id: Optional[str] = None) -> Dict[str, Any]:
382
371
  """
383
372
  Получает статистику пользователей по стадиям
384
-
373
+
385
374
  Args:
386
375
  bot_id: ID бота (если не указан, используется текущий бот)
387
-
376
+
388
377
  Returns:
389
378
  Статистика по стадиям с количеством пользователей
390
379
  """
391
380
  try:
392
381
  # Импортируем необходимые компоненты
393
382
  from ..handlers.handlers import get_global_var
394
- supabase_client = get_global_var('supabase_client')
395
- current_bot_id = get_global_var('config').BOT_ID if get_global_var('config') else bot_id
396
-
383
+
384
+ supabase_client = get_global_var("supabase_client")
385
+ current_bot_id = (
386
+ get_global_var("config").BOT_ID if get_global_var("config") else bot_id
387
+ )
388
+
397
389
  if not current_bot_id:
398
- return {
399
- "status": "error",
400
- "error": "Не удалось определить bot_id"
401
- }
402
-
390
+ return {"status": "error", "error": "Не удалось определить bot_id"}
391
+
403
392
  logger.info(f"📊 Получаем статистику по стадиям для бота '{current_bot_id}'")
404
-
393
+
405
394
  # Получаем статистику по стадиям с user_id для подсчета уникальных пользователей
406
- stats_query = supabase_client.client.table('sales_chat_sessions').select(
407
- 'user_id, current_stage, created_at'
408
- ).eq('status', 'active')
409
-
395
+ stats_query = (
396
+ supabase_client.client.table("sales_chat_sessions")
397
+ .select("user_id, current_stage, created_at")
398
+ .eq("status", "active")
399
+ )
400
+
410
401
  # Фильтруем по bot_id если указан
411
402
  if current_bot_id:
412
- stats_query = stats_query.eq('bot_id', current_bot_id)
413
-
403
+ stats_query = stats_query.eq("bot_id", current_bot_id)
404
+
414
405
  # Сортируем по дате создания (последние сначала)
415
- stats_query = stats_query.order('created_at', desc=True)
416
-
406
+ stats_query = stats_query.order("created_at", desc=True)
407
+
417
408
  sessions_data = stats_query.execute()
418
-
409
+
419
410
  # Подсчитываем уникальных пользователей по стадиям (берем последнюю сессию каждого пользователя)
420
411
  user_stages = {} # {user_id: stage}
421
-
412
+
422
413
  for session in sessions_data.data:
423
- user_id = session['user_id']
424
- stage = session['current_stage'] or 'unknown'
425
-
414
+ user_id = session["user_id"]
415
+ stage = session["current_stage"] or "unknown"
416
+
426
417
  # Если пользователь еще не добавлен, добавляем его стадию (первая встреченная - самая последняя)
427
418
  if user_id not in user_stages:
428
419
  user_stages[user_id] = stage
429
-
420
+
430
421
  # Подсчитываем количество пользователей по стадиям
431
422
  stage_stats = {}
432
423
  for stage in user_stages.values():
433
424
  stage_stats[stage] = stage_stats.get(stage, 0) + 1
434
-
425
+
435
426
  total_users = len(user_stages)
436
-
427
+
437
428
  # Сортируем по количеству пользователей (по убыванию)
438
429
  sorted_stages = sorted(stage_stats.items(), key=lambda x: x[1], reverse=True)
439
-
430
+
440
431
  result = {
441
432
  "status": "success",
442
433
  "bot_id": current_bot_id,
443
434
  "total_active_users": total_users,
444
435
  "stages": dict(sorted_stages),
445
- "stages_list": sorted_stages
436
+ "stages_list": sorted_stages,
446
437
  }
447
-
438
+
448
439
  logger.info(f"📊 Статистика по стадиям: {total_users} активных пользователей")
449
440
  for stage, count in sorted_stages:
450
441
  logger.info(f" {stage}: {count} пользователей")
451
-
442
+
452
443
  return result
453
-
444
+
454
445
  except Exception as e:
455
446
  logger.error(f"❌ Ошибка в get_users_by_stage_stats: {e}")
456
- return {
457
- "status": "error",
458
- "error": str(e),
459
- "bot_id": bot_id
460
- }
447
+ return {"status": "error", "error": str(e), "bot_id": bot_id}
448
+
461
449
 
462
450
  async def send_message(
463
451
  message,
@@ -466,11 +454,11 @@ async def send_message(
466
454
  files_list: list = [],
467
455
  directories_list: list = [],
468
456
  parse_mode: str = "Markdown",
469
- **kwargs
457
+ **kwargs,
470
458
  ):
471
459
  """
472
460
  Пользовательская функция для отправки сообщений с файлами и кнопками
473
-
461
+
474
462
  Args:
475
463
  message: Message объект от aiogram
476
464
  text: Текст сообщения
@@ -479,18 +467,18 @@ async def send_message(
479
467
  directories_list: Список каталогов (отправятся все файлы)
480
468
  parse_mode: Режим парсинга ('Markdown', 'HTML' или None)
481
469
  **kwargs: Дополнительные параметры (reply_markup и т.д.)
482
-
470
+
483
471
  Returns:
484
472
  Message объект отправленного сообщения или None
485
-
473
+
486
474
  Example:
487
475
  from smart_bot_factory.message import send_message
488
476
  from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
489
-
477
+
490
478
  keyboard = InlineKeyboardMarkup(inline_keyboard=[
491
479
  [InlineKeyboardButton(text="Кнопка", callback_data="action")]
492
480
  ])
493
-
481
+
494
482
  await send_message(
495
483
  message=message,
496
484
  text="Привет!",
@@ -501,91 +489,100 @@ async def send_message(
501
489
  )
502
490
  """
503
491
  from pathlib import Path
492
+
504
493
  from aiogram.types import FSInputFile
505
494
  from aiogram.utils.media_group import MediaGroupBuilder
506
-
507
- logger.info(f"📤 send_message вызвана:")
495
+
496
+ logger.info("📤 send_message вызвана:")
508
497
  logger.info(f" 👤 Пользователь: {message.from_user.id}")
509
498
  logger.info(f" 📝 Длина текста: {len(text)} символов")
510
499
  logger.info(f" 🔧 Parse mode: {parse_mode}")
511
-
500
+
512
501
  try:
513
502
  user_id = message.from_user.id
514
-
503
+
515
504
  # Устанавливаем parse_mode (None если передана строка 'None')
516
- actual_parse_mode = None if parse_mode == 'None' else parse_mode
517
-
505
+ actual_parse_mode = None if parse_mode == "None" else parse_mode
506
+
518
507
  # Текст уже готов, используем как есть
519
508
  final_text = text
520
-
509
+
521
510
  # Работаем с переданными файлами и каталогами
522
511
  logger.info(f" 📦 Передано файлов: {files_list}")
523
512
  logger.info(f" 📂 Передано каталогов: {directories_list}")
524
-
513
+
525
514
  # Получаем список уже отправленных файлов и каталогов
526
515
  sent_files = await supabase_client.get_sent_files(user_id)
527
516
  sent_directories = await supabase_client.get_sent_directories(user_id)
528
-
517
+
529
518
  logger.info(f" 📋 Уже отправлено файлов: {sent_files}")
530
519
  logger.info(f" 📋 Уже отправлено каталогов: {sent_directories}")
531
-
520
+
532
521
  # Фильтруем файлы и каталоги, которые уже отправлялись
533
522
  actual_files_list = [f for f in files_list if f not in sent_files]
534
- actual_directories_list = [d for d in directories_list if str(d) not in sent_directories]
535
-
523
+ actual_directories_list = [
524
+ d for d in directories_list if str(d) not in sent_directories
525
+ ]
526
+
536
527
  logger.info(f" 🆕 После фильтрации файлов: {actual_files_list}")
537
528
  logger.info(f" 🆕 После фильтрации каталогов: {actual_directories_list}")
538
-
529
+
539
530
  # Проверяем, что есть что отправлять
540
531
  if not final_text or not final_text.strip():
541
- logger.error(f"❌ КРИТИЧЕСКАЯ ОШИБКА: final_text пуст после обработки!")
532
+ logger.error("❌ КРИТИЧЕСКАЯ ОШИБКА: final_text пуст после обработки!")
542
533
  logger.error(f" Исходный text: '{text[:200]}...'")
543
534
  final_text = "Ошибка формирования ответа. Попробуйте еще раз."
544
-
535
+
545
536
  logger.info(f"📱 Подготовка сообщения: {len(final_text)} символов")
546
537
  logger.info(f" 📦 Файлов для обработки: {actual_files_list}")
547
538
  logger.info(f" 📂 Каталогов для обработки: {actual_directories_list}")
548
-
539
+
549
540
  # Проверяем наличие файлов для отправки
550
541
  if actual_files_list or actual_directories_list:
551
542
  # Функция определения типа медиа по расширению
552
543
  def get_media_type(file_path: str) -> str:
553
544
  ext = Path(file_path).suffix.lower()
554
- if ext in {'.jpg', '.jpeg', '.png'}:
555
- return 'photo'
556
- elif ext in {'.mp4', '.mov'}:
557
- return 'video'
545
+ if ext in {".jpg", ".jpeg", ".png"}:
546
+ return "photo"
547
+ elif ext in {".mp4", ".mov"}:
548
+ return "video"
558
549
  else:
559
- return 'document'
560
-
550
+ return "document"
551
+
561
552
  # Создаем списки для разных типов файлов
562
553
  video_files = []
563
554
  photo_files = []
564
555
  document_files = []
565
-
556
+
566
557
  # Функция обработки файла
567
558
  def process_file(file_path: Path, source: str = ""):
568
559
  if file_path.is_file():
569
560
  media_type = get_media_type(str(file_path))
570
- if media_type == 'video':
561
+ if media_type == "video":
571
562
  video_files.append(file_path)
572
- logger.info(f" 🎥 Добавлено видео{f' из {source}' if source else ''}: {file_path.name}")
573
- elif media_type == 'photo':
563
+ logger.info(
564
+ f" 🎥 Добавлено видео{f' из {source}' if source else ''}: {file_path.name}"
565
+ )
566
+ elif media_type == "photo":
574
567
  photo_files.append(file_path)
575
- logger.info(f" 📸 Добавлено фото{f' из {source}' if source else ''}: {file_path.name}")
568
+ logger.info(
569
+ f" 📸 Добавлено фото{f' из {source}' if source else ''}: {file_path.name}"
570
+ )
576
571
  else:
577
572
  document_files.append(file_path)
578
- logger.info(f" 📄 Добавлен документ{f' из {source}' if source else ''}: {file_path.name}")
573
+ logger.info(
574
+ f" 📄 Добавлен документ{f' из {source}' if source else ''}: {file_path.name}"
575
+ )
579
576
  else:
580
577
  logger.warning(f" ⚠️ Файл не найден: {file_path}")
581
-
578
+
582
579
  # Обрабатываем прямые файлы
583
580
  for file_name in actual_files_list:
584
581
  try:
585
582
  process_file(Path(f"files/{file_name}"))
586
583
  except Exception as e:
587
584
  logger.error(f" ❌ Ошибка обработки файла {file_name}: {e}")
588
-
585
+
589
586
  # Обрабатываем файлы из каталогов
590
587
  for dir_name in actual_directories_list:
591
588
  dir_name = Path(dir_name)
@@ -595,82 +592,102 @@ async def send_message(
595
592
  try:
596
593
  process_file(file_path, dir_name)
597
594
  except Exception as e:
598
- logger.error(f" ❌ Ошибка обработки файла {file_path}: {e}")
595
+ logger.error(
596
+ f" ❌ Ошибка обработки файла {file_path}: {e}"
597
+ )
599
598
  else:
600
599
  logger.warning(f" ⚠️ Каталог не найден: {dir_name}")
601
600
  except Exception as e:
602
601
  logger.error(f" ❌ Ошибка обработки каталога {dir_name}: {e}")
603
-
602
+
604
603
  # Списки для отслеживания реально отправленных файлов
605
604
  sent_files_to_save = []
606
605
  sent_dirs_to_save = []
607
-
606
+
608
607
  # 1. Отправляем видео (если есть)
609
608
  if video_files:
610
609
  video_group = MediaGroupBuilder()
611
610
  for file_path in video_files:
612
611
  video_group.add_video(media=FSInputFile(str(file_path)))
613
-
612
+
614
613
  videos = video_group.build()
615
614
  if videos:
616
615
  await message.answer_media_group(media=videos)
617
616
  logger.info(f" ✅ Отправлено {len(videos)} видео")
618
-
617
+
619
618
  # 2. Отправляем фото (если есть)
620
619
  if photo_files:
621
620
  photo_group = MediaGroupBuilder()
622
621
  for file_path in photo_files:
623
622
  photo_group.add_photo(media=FSInputFile(str(file_path)))
624
-
623
+
625
624
  photos = photo_group.build()
626
625
  if photos:
627
626
  await message.answer_media_group(media=photos)
628
627
  logger.info(f" ✅ Отправлено {len(photos)} фото")
629
-
628
+
630
629
  # 3. Отправляем текст
631
- result = await message.answer(final_text, parse_mode=actual_parse_mode, **kwargs)
632
- logger.info(f" ✅ Отправлен текст сообщения")
633
-
630
+ result = await message.answer(
631
+ final_text, parse_mode=actual_parse_mode, **kwargs
632
+ )
633
+ logger.info(" ✅ Отправлен текст сообщения")
634
+
634
635
  # 4. Отправляем документы (если есть)
635
636
  if document_files:
636
637
  doc_group = MediaGroupBuilder()
637
638
  for file_path in document_files:
638
639
  doc_group.add_document(media=FSInputFile(str(file_path)))
639
-
640
+
640
641
  docs = doc_group.build()
641
642
  if docs:
642
643
  await message.answer_media_group(media=docs)
643
644
  logger.info(f" ✅ Отправлено {len(docs)} документов")
644
-
645
+
645
646
  # 5. Собираем список реально отправленных файлов и каталогов
646
647
  if video_files or photo_files or document_files:
647
648
  sent_files_to_save.extend(actual_files_list)
648
- logger.info(f" 📝 Добавляем в список для сохранения файлы: {actual_files_list}")
649
+ logger.info(
650
+ f" 📝 Добавляем в список для сохранения файлы: {actual_files_list}"
651
+ )
649
652
  sent_dirs_to_save.extend([str(d) for d in actual_directories_list])
650
- logger.info(f" 📝 Добавляем в список для сохранения каталоги: {actual_directories_list}")
651
-
653
+ logger.info(
654
+ f" 📝 Добавляем в список для сохранения каталоги: {actual_directories_list}"
655
+ )
656
+
652
657
  # 6. Обновляем информацию в БД
653
658
  if sent_files_to_save or sent_dirs_to_save:
654
659
  try:
655
660
  if sent_files_to_save:
656
661
  logger.info(f" 💾 Сохраняем файлы в БД: {sent_files_to_save}")
657
- await supabase_client.add_sent_files(user_id, sent_files_to_save)
662
+ await supabase_client.add_sent_files(
663
+ user_id, sent_files_to_save
664
+ )
658
665
  if sent_dirs_to_save:
659
- logger.info(f" 💾 Сохраняем каталоги в БД: {sent_dirs_to_save}")
660
- await supabase_client.add_sent_directories(user_id, sent_dirs_to_save)
661
- logger.info(f" ✅ Обновлена информация о отправленных файлах в БД")
666
+ logger.info(
667
+ f" 💾 Сохраняем каталоги в БД: {sent_dirs_to_save}"
668
+ )
669
+ await supabase_client.add_sent_directories(
670
+ user_id, sent_dirs_to_save
671
+ )
672
+ logger.info(
673
+ " ✅ Обновлена информация о отправленных файлах в БД"
674
+ )
662
675
  except Exception as e:
663
- logger.error(f" ❌ Ошибка обновления информации о файлах в БД: {e}")
676
+ logger.error(
677
+ f" ❌ Ошибка обновления информации о файлах в БД: {e}"
678
+ )
664
679
  else:
665
- logger.info(f" ℹ️ Нет новых файлов для сохранения в БД")
666
-
680
+ logger.info(" ℹ️ Нет новых файлов для сохранения в БД")
681
+
667
682
  return result
668
683
  else:
669
684
  # Если нет файлов, отправляем просто текст
670
685
  logger.info(" ⚠️ Нет файлов для отправки, отправляем как текст")
671
- result = await message.answer(final_text, parse_mode=actual_parse_mode, **kwargs)
686
+ result = await message.answer(
687
+ final_text, parse_mode=actual_parse_mode, **kwargs
688
+ )
672
689
  return result
673
-
690
+
674
691
  except Exception as e:
675
692
  # Проверяем, является ли ошибка блокировкой бота
676
693
  if "Forbidden: bot was blocked by the user" in str(e):
@@ -679,23 +696,27 @@ async def send_message(
679
696
  elif "TelegramForbiddenError" in str(type(e).__name__):
680
697
  logger.warning(f"🚫 Бот заблокирован пользователем {user_id}")
681
698
  return None
682
-
699
+
683
700
  logger.error(f"❌ ОШИБКА в send_message: {e}")
684
701
  logger.exception("Полный стек ошибки send_message:")
685
-
702
+
686
703
  # Пытаемся отправить простое сообщение без форматирования
687
704
  try:
688
705
  fallback_text = "Произошла ошибка при отправке ответа. Попробуйте еще раз."
689
706
  result = await message.answer(fallback_text)
690
- logger.info(f"✅ Запасное сообщение отправлено")
707
+ logger.info("✅ Запасное сообщение отправлено")
691
708
  return result
692
709
  except Exception as e2:
693
710
  if "Forbidden: bot was blocked by the user" in str(e2):
694
- logger.warning(f"🚫 Бот заблокирован пользователем {user_id} (fallback)")
711
+ logger.warning(
712
+ f"🚫 Бот заблокирован пользователем {user_id} (fallback)"
713
+ )
695
714
  return None
696
715
  elif "TelegramForbiddenError" in str(type(e2).__name__):
697
- logger.warning(f"🚫 Бот заблокирован пользователем {user_id} (fallback)")
716
+ logger.warning(
717
+ f"🚫 Бот заблокирован пользователем {user_id} (fallback)"
718
+ )
698
719
  return None
699
-
720
+
700
721
  logger.error(f"❌ Даже запасное сообщение не отправилось: {e2}")
701
722
  raise