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