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,114 @@
1
+ # debug_routing.py - Утилиты для отладки маршрутизации сообщений
2
+
3
+ import logging
4
+
5
+ from aiogram import Router
6
+ from aiogram.fsm.context import FSMContext
7
+ from aiogram.types import Message
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ # Создаем роутер для отладочных обработчиков
12
+ debug_router = Router()
13
+
14
+
15
+ def setup_debug_handlers(dp):
16
+ """Настройка отладочных обработчиков"""
17
+ dp.include_router(debug_router)
18
+
19
+
20
+ # Функция для получения глобальных переменных
21
+ def get_global_var(var_name):
22
+ """Получает глобальную переменную из модуля debug_routing"""
23
+ import sys
24
+
25
+ current_module = sys.modules[__name__]
26
+ return getattr(current_module, var_name, None)
27
+
28
+
29
+ async def debug_user_state(message: Message, state: FSMContext, context: str):
30
+ """Отладочная функция для логирования состояния пользователя"""
31
+ conversation_manager = get_global_var("conversation_manager")
32
+ supabase_client = get_global_var("supabase_client")
33
+
34
+ user_id = message.from_user.id
35
+ current_state = await state.get_state()
36
+ state_data = await state.get_data()
37
+
38
+ logger.info(f"🔍 DEBUG [{context}] User {user_id}:")
39
+ logger.info(f" 📊 FSM State: {current_state}")
40
+ logger.info(f" 📦 State Data: {list(state_data.keys())}")
41
+ logger.info(f" 💬 Message: '{message.text[:50]}...'")
42
+
43
+ # Проверяем диалог с админом в БД
44
+ conversation = await conversation_manager.is_user_in_admin_chat(user_id)
45
+ logger.info(f" 🗃️ Admin Chat in DB: {'✅' if conversation else '❌'}")
46
+
47
+ if conversation:
48
+ logger.info(f" 👑 Admin ID: {conversation['admin_id']}")
49
+ logger.info(f" 🆔 Conversation ID: {conversation['id']}")
50
+
51
+ # Проверяем активную сессию
52
+ session_info = await supabase_client.get_active_session(user_id)
53
+ logger.info(f" 🎯 Active Session: {'✅' if session_info else '❌'}")
54
+
55
+ if session_info:
56
+ logger.info(f" 📝 Session ID: {session_info['id']}")
57
+
58
+ logger.info(f" {'='*50}")
59
+
60
+
61
+ async def debug_admin_conversation_creation(admin_id: int, user_id: int):
62
+ """Отладка создания диалога админа с пользователем"""
63
+ supabase_client = get_global_var("supabase_client")
64
+
65
+ logger.info("🔍 DEBUG CONVERSATION CREATION:")
66
+ logger.info(f" 👑 Admin: {admin_id}")
67
+ logger.info(f" 👤 User: {user_id}")
68
+
69
+ # Проверяем активную сессию пользователя ДО создания диалога
70
+ session_info = await supabase_client.get_active_session(user_id)
71
+ logger.info(f" 🎯 User has active session: {'✅' if session_info else '❌'}")
72
+
73
+ if session_info:
74
+ logger.info(f" 📝 Session ID: {session_info['id']}")
75
+ logger.info(f" 📅 Session created: {session_info['created_at']}")
76
+
77
+ # Проверяем существующие диалоги пользователя
78
+ try:
79
+ existing = (
80
+ supabase_client.client.table("admin_user_conversations")
81
+ .select("*")
82
+ .eq("user_id", user_id)
83
+ .eq("status", "active")
84
+ .execute()
85
+ )
86
+
87
+ logger.info(f" 💬 Existing active conversations: {len(existing.data)}")
88
+ for conv in existing.data:
89
+ logger.info(f" - ID: {conv['id']}, Admin: {conv['admin_id']}")
90
+ except Exception as e:
91
+ logger.error(f" ❌ Error checking existing conversations: {e}")
92
+
93
+
94
+ async def test_message_routing(user_id: int, test_message: str):
95
+ """Тестирует маршрутизацию сообщения без отправки через Telegram"""
96
+ conversation_manager = get_global_var("conversation_manager")
97
+
98
+ logger.info("🧪 TESTING MESSAGE ROUTING:")
99
+ logger.info(f" 👤 User: {user_id}")
100
+ logger.info(f" 💬 Message: '{test_message}'")
101
+
102
+ # Проверяем есть ли диалог с админом
103
+ conversation = await conversation_manager.is_user_in_admin_chat(user_id)
104
+ logger.info(f" 🗃️ Admin conversation exists: {'✅' if conversation else '❌'}")
105
+
106
+ if conversation:
107
+ logger.info(f" 👑 Admin: {conversation['admin_id']}")
108
+ logger.info(f" 🆔 Conv ID: {conversation['id']}")
109
+ logger.info(f" 📅 Started: {conversation['started_at']}")
110
+
111
+ # Тестируем должен ли этот пользователь быть в admin_chat
112
+ return "admin_chat"
113
+ else:
114
+ return "bot_chat"
@@ -0,0 +1,529 @@
1
+ # Обновленный prompt_loader.py с поддержкой финальных инструкций
2
+
3
+ import logging
4
+ from pathlib import Path
5
+ from typing import Dict
6
+
7
+ import aiofiles
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class PromptLoader:
13
+ """Класс для загрузки промптов из локального каталога"""
14
+
15
+ def __init__(self, prompts_dir: str):
16
+ self.prompts_dir = Path(prompts_dir)
17
+ self.welcome_file = self.prompts_dir / "welcome_message.txt"
18
+ self.help_file = self.prompts_dir / "help_message.txt"
19
+ self.final_instructions_file = self.prompts_dir / "final_instructions.txt"
20
+
21
+ # Информация об инструментах для добавления в финальные инструкции
22
+ self._tools_description: str = ""
23
+
24
+ # Автоматически находим все .txt файлы промптов (кроме специальных)
25
+ all_txt_files = list(self.prompts_dir.glob("*.txt"))
26
+ special_files = {
27
+ "welcome_message.txt",
28
+ "help_message.txt",
29
+ "final_instructions.txt",
30
+ }
31
+ self.prompt_files = [
32
+ f.name for f in all_txt_files if f.name not in special_files
33
+ ]
34
+
35
+ logger.info(f"Инициализирован загрузчик промптов: {self.prompts_dir}")
36
+ logger.info(f"Найдено файлов промптов: {len(self.prompt_files)}")
37
+ logger.info(f"Файлы промптов: {self.prompt_files}")
38
+
39
+ async def load_system_prompt(self) -> str:
40
+ """
41
+ Загружает и объединяет все файлы промптов в один системный промпт
42
+
43
+ Returns:
44
+ Объединенный системный промпт с инструкциями по JSON
45
+ """
46
+ try:
47
+ prompt_parts = []
48
+
49
+ for filename in self.prompt_files:
50
+ logger.debug(f"Загружаем промпт из {filename}")
51
+ content = await self._load_file(filename)
52
+
53
+ if content:
54
+ # Добавляем заголовок секции
55
+ section_name = self._get_section_name(filename)
56
+ prompt_parts.append(f"\n### {section_name} ###\n")
57
+ prompt_parts.append(content.strip())
58
+ prompt_parts.append("\n")
59
+ else:
60
+ logger.warning(f"Файл {filename} пуст")
61
+
62
+ if not prompt_parts:
63
+ error_msg = "Не удалось загрузить ни одного промпт файла"
64
+ logger.error(error_msg)
65
+ raise ValueError(error_msg)
66
+
67
+ # Добавляем инструкции по JSON метаданным
68
+ json_instructions = self._get_json_instructions()
69
+ prompt_parts.append("\n")
70
+ prompt_parts.append(json_instructions)
71
+
72
+ # Объединяем все части
73
+ full_prompt = "".join(prompt_parts).strip()
74
+
75
+ logger.info(
76
+ f"Системный промпт загружен успешно ({len(full_prompt)} символов)"
77
+ )
78
+ return full_prompt
79
+
80
+ except Exception as e:
81
+ logger.error(f"Ошибка при загрузке системного промпта: {e}")
82
+ raise
83
+
84
+ def set_tools_description(self, tools_description: str):
85
+ """
86
+ Устанавливает описание инструментов для добавления в финальные инструкции
87
+
88
+ Args:
89
+ tools_description: Текст с описанием инструментов
90
+ """
91
+ self._tools_description = tools_description
92
+ logger.info(f"📝 Описание инструментов обновлено ({len(tools_description)} символов)")
93
+
94
+ # Логируем содержимое описания инструментов
95
+ if tools_description:
96
+ # Подсчитываем количество инструментов по паттерну "1. **название**"
97
+ import re
98
+ tool_pattern = r'\d+\.\s+\*\*[^*]+\*\*'
99
+ tool_matches = re.findall(tool_pattern, tools_description)
100
+ tool_count = len(tool_matches)
101
+ logger.info(f"📋 Описание инструментов для промпта содержит {tool_count} инструмент(ов)")
102
+
103
+ # Логируем первые строки для проверки
104
+ lines = tools_description.split('\n')
105
+ non_empty_lines = [line for line in lines if line.strip()]
106
+ preview_lines = non_empty_lines[:10] # Первые 10 непустых строк
107
+
108
+ logger.info("📋 Превью описания инструментов (первые строки):")
109
+ for line in preview_lines:
110
+ logger.info(f" {line}")
111
+
112
+ if len(non_empty_lines) > 10:
113
+ logger.info(f" ... (еще {len(non_empty_lines) - 10} строк)")
114
+
115
+ # Полное содержимое в debug режиме
116
+ logger.debug(f"📋 Полное содержимое описания инструментов:\n{tools_description}")
117
+
118
+ async def load_final_instructions(self) -> str:
119
+ """
120
+ Загружает финальные инструкции из final_instructions.txt
121
+ Автоматически добавляет информацию об инструментах если она установлена
122
+
123
+ Returns:
124
+ Финальные инструкции или пустая строка если файла нет
125
+ """
126
+ try:
127
+ logger.debug(
128
+ f"Загружаем финальные инструкции из {self.final_instructions_file.name}"
129
+ )
130
+
131
+ content = ""
132
+
133
+ if self.final_instructions_file.exists():
134
+ async with aiofiles.open(
135
+ self.final_instructions_file, "r", encoding="utf-8"
136
+ ) as f:
137
+ content = await f.read()
138
+
139
+ if not content.strip():
140
+ if not self.final_instructions_file.exists():
141
+ logger.debug(
142
+ f"Файл {self.final_instructions_file.name} не найден - пропускаем"
143
+ )
144
+ else:
145
+ logger.debug(
146
+ f"Файл {self.final_instructions_file.name} пуст - пропускаем"
147
+ )
148
+
149
+ # Если есть описание инструментов, возвращаем только его
150
+ if self._tools_description:
151
+ logger.info(f"Финальные инструкции: только описание инструментов ({len(self._tools_description)} символов)")
152
+ logger.debug(f"Содержимое описания инструментов (первые 500 символов):\n{self._tools_description[:500]}...")
153
+ return self._tools_description.strip()
154
+
155
+ return ""
156
+
157
+ # Добавляем описание инструментов в конец финальных инструкций
158
+ if self._tools_description:
159
+ # Проверяем, не добавлено ли уже описание инструментов
160
+ if "### ДОСТУПНЫЕ ИНСТРУМЕНТЫ ###" not in content:
161
+ original_length = len(content)
162
+ content = content.strip() + "\n\n" + self._tools_description
163
+ added_length = len(content) - original_length
164
+ logger.info(f"✅ Описание инструментов добавлено в финальные инструкции")
165
+ logger.info(f" Размер исходных инструкций: {original_length} символов")
166
+ logger.info(f" Добавлено символов: {added_length}")
167
+ logger.info(f" Итоговый размер: {len(content)} символов")
168
+ logger.debug(f" Добавленный текст (первые 500 символов):\n{self._tools_description[:500]}...")
169
+ else:
170
+ logger.debug("Описание инструментов уже присутствует в финальных инструкциях")
171
+
172
+ logger.info(f"Финальные инструкции загружены ({len(content)} символов)")
173
+ return content.strip()
174
+
175
+ except Exception as e:
176
+ logger.error(f"Ошибка при загрузке финальных инструкций: {e}")
177
+ # Не прерываем работу - финальные инструкции опциональны
178
+ # Но возвращаем описание инструментов если оно есть
179
+ if self._tools_description:
180
+ return self._tools_description.strip()
181
+ return ""
182
+
183
+ def _get_json_instructions(self) -> str:
184
+ """Возвращает инструкции по JSON метаданным для ИИ"""
185
+ return """
186
+ ### КРИТИЧЕСКИ ВАЖНЫЕ ИНСТРУКЦИИ ПО JSON МЕТАДАННЫМ ###
187
+
188
+ В КОНЦЕ КАЖДОГО своего ответа ОБЯЗАТЕЛЬНО добавляй служебную информацию в JSON формате:
189
+
190
+ {
191
+ "этап": "introduction|consult|offer|contacts",
192
+ "качество": 1-10,
193
+ "ссылка": 1,
194
+ "события": [
195
+ {
196
+ "тип": "телефон|консультация|покупка|отказ",
197
+ "инфо": "детали события"
198
+ }
199
+ ],
200
+ "файлы": ["file1.pdf", "file2.mp4"],
201
+ "каталоги": ["каталог1", "каталог2"]
202
+ }
203
+
204
+ ОПИСАНИЕ ЭТАПОВ:
205
+ - introduction: знакомство, сбор базовой информации о клиенте
206
+ - consult: консультирование, ответы на вопросы о конференции
207
+ - offer: предложение тарифов, обработка возражений
208
+ - contacts: получение контактов, завершение сделки
209
+
210
+ СИСТЕМА ОЦЕНКИ КАЧЕСТВА (1-10):
211
+ 1-3: низкий интерес, много возражений, скептически настроен
212
+ 4-6: средний интерес, есть вопросы, обдумывает
213
+ 7-8: высокий интерес, готов к покупке, активно интересуется
214
+ 9-10: горячий лид, предоставил контакты или готов к действию
215
+
216
+
217
+ СОБЫТИЯ - добавляй ТОЛЬКО когда происходит что-то из этого:
218
+ - "телефон": пользователь предоставил номер телефона
219
+ - "консультация": пользователь просит живую консультацию по телефону
220
+ - "покупка": пользователь готов купить/записаться на конференцию
221
+ - "отказ": пользователь явно отказывается участвовать
222
+
223
+ ВАЖНО:
224
+ - Добавляй файлы и катологи только в том случае, если они относятся к этому этапу. (Прописаны в самом этапе) Если не относятся - отсылай пустой массив []
225
+
226
+ ПРИМЕРЫ ПРАВИЛЬНОГО ИСПОЛЬЗОВАНИЯ:
227
+
228
+ Пример 1 - обычный диалог (без ссылки):
229
+ "Расскажу подробнее о конференции GrowthMED. Она пройдет 24-25 октября..."
230
+
231
+ {
232
+ "этап": "consult",
233
+ "качество": 6,
234
+ "события": [],
235
+ "файлы": [],
236
+ "каталоги": []
237
+ }
238
+
239
+ Пример 2 - получен телефон (без ссылки):
240
+ "Отлично! Записал ваш номер. Мы перезвоним в течение 10 минут!"
241
+
242
+ {
243
+ "этап": "contacts",
244
+ "качество": 9,
245
+ "события": [
246
+ {
247
+ "тип": "телефон",
248
+ "инфо": "Иван Петров +79219603144"
249
+ }
250
+ ],
251
+ "файлы": [],
252
+ "каталоги": []
253
+ }
254
+
255
+ Пример 3 - отправка презентации (без ссылки):
256
+ "Отправляю вам презентацию о нашей компании и прайс-лист с актуальными ценами."
257
+
258
+ {
259
+ "этап": "offer",
260
+ "качество": 7,
261
+ "события": [
262
+ {
263
+ "тип": "консультация",
264
+ "инфо": "Запросил материалы"
265
+ }
266
+ ],
267
+ "файлы": ["презентация.pdf", "прайс.pdf"],
268
+ "каталоги": []
269
+ }
270
+
271
+ Пример 4 - отправка файлов из каталога (без ссылки):
272
+ "В каталоге 'примеры_работ' вы можете посмотреть наши последние проекты."
273
+
274
+ {
275
+ "этап": "presentation",
276
+ "качество": 8,
277
+ "события": [],
278
+ "файлы": [],
279
+ "каталоги": ["примеры_работ"]
280
+ }
281
+
282
+ Пример 5 - комбинированная отправка (без ссылки):
283
+ "Отправляю вам коммерческое предложение и примеры похожих проектов из нашего портфолио."
284
+
285
+ {
286
+ "этап": "offer",
287
+ "качество": 9,
288
+ "события": [
289
+ {
290
+ "тип": "предложение",
291
+ "инфо": "Отправлено КП"
292
+ }
293
+ ],
294
+ "файлы": ["коммерческое_предложение.pdf"],
295
+ "каталоги": ["портфолио_2023"]
296
+ }
297
+
298
+
299
+ ТРЕБОВАНИЯ К JSON:
300
+ - JSON должен быть валидным и находиться в самом конце ответа
301
+ - Всегда используй кавычки для строк
302
+ - Массив "события" может быть пустым []
303
+ - Если событий нет - не добавляй их в массив
304
+ - Качество должно быть числом от 1 до 10
305
+
306
+
307
+ ПОМНИ: Этот JSON критически важен для работы системы администрирования и аналитики!
308
+ """
309
+
310
+ async def load_welcome_message(self) -> str:
311
+ """
312
+ Загружает приветственное сообщение из welcome_message.txt
313
+
314
+ Returns:
315
+ Текст приветственного сообщения
316
+ """
317
+ try:
318
+ logger.debug(
319
+ f"Загружаем приветственное сообщение из {self.welcome_file.name}"
320
+ )
321
+
322
+ if not self.welcome_file.exists():
323
+ error_msg = f"Файл приветствия не найден: {self.welcome_file}"
324
+ logger.error(error_msg)
325
+ raise FileNotFoundError(error_msg)
326
+
327
+ async with aiofiles.open(self.welcome_file, "r", encoding="utf-8") as f:
328
+ content = await f.read()
329
+
330
+ if not content.strip():
331
+ error_msg = f"Файл приветствия пуст: {self.welcome_file}"
332
+ logger.error(error_msg)
333
+ raise ValueError(error_msg)
334
+
335
+ logger.info(f"Приветственное сообщение загружено ({len(content)} символов)")
336
+ return content.strip()
337
+
338
+ except Exception as e:
339
+ logger.error(f"Ошибка при загрузке приветственного сообщения: {e}")
340
+ raise
341
+
342
+ async def load_help_message(self) -> str:
343
+ """
344
+ Загружает справочное сообщение из help_message.txt
345
+
346
+ Returns:
347
+ Текст справочного сообщения
348
+ """
349
+ try:
350
+ logger.debug(f"Загружаем справочное сообщение из {self.help_file.name}")
351
+
352
+ if self.help_file.exists():
353
+ async with aiofiles.open(self.help_file, "r", encoding="utf-8") as f:
354
+ content = await f.read()
355
+
356
+ if content.strip():
357
+ logger.info(
358
+ f"Справочное сообщение загружено ({len(content)} символов)"
359
+ )
360
+ return content.strip()
361
+
362
+ # Fallback если файл не найден или пуст
363
+ logger.warning(
364
+ "Файл help_message.txt не найден или пуст, используем дефолтную справку"
365
+ )
366
+ return "🤖 **Ваш помощник готов к работе!**\n\n**Команды:**\n/start - Начать диалог\n/help - Показать справку\n/status - Проверить статус"
367
+
368
+ except Exception as e:
369
+ logger.error(f"Ошибка при загрузке справочного сообщения: {e}")
370
+ # Возвращаем простую справку в случае ошибки
371
+ return "🤖 Ваш помощник готов к работе! Напишите /start для начала диалога."
372
+
373
+ async def _load_file(self, filename: str) -> str:
374
+ """Загружает содержимое файла из каталога промптов"""
375
+ file_path = self.prompts_dir / filename
376
+
377
+ try:
378
+ if not file_path.exists():
379
+ error_msg = f"Файл промпта не найден: {file_path}"
380
+ logger.error(error_msg)
381
+ raise FileNotFoundError(error_msg)
382
+
383
+ async with aiofiles.open(file_path, "r", encoding="utf-8") as f:
384
+ content = await f.read()
385
+
386
+ if not content.strip():
387
+ logger.warning(f"Файл {filename} пуст")
388
+ return ""
389
+
390
+ logger.debug(f"Загружен файл {filename} ({len(content)} символов)")
391
+ return content
392
+
393
+ except Exception as e:
394
+ logger.error(f"Ошибка чтения файла {file_path}: {e}")
395
+ raise
396
+
397
+ def _get_section_name(self, filename: str) -> str:
398
+ """Получает название секции по имени файла"""
399
+ name_mapping = {
400
+ "system_prompt.txt": "СИСТЕМНЫЙ ПРОМПТ",
401
+ "sales_context.txt": "КОНТЕКСТ ПРОДАЖ",
402
+ "product_info.txt": "ИНФОРМАЦИЯ О ПРОДУКТЕ",
403
+ "objection_handling.txt": "ОБРАБОТКА ВОЗРАЖЕНИЙ",
404
+ "1sales_context.txt": "КОНТЕКСТ ПРОДАЖ",
405
+ "2product_info.txt": "ИНФОРМАЦИЯ О ПРОДУКТЕ",
406
+ "3objection_handling.txt": "ОБРАБОТКА ВОЗРАЖЕНИЙ",
407
+ "final_instructions.txt": "ФИНАЛЬНЫЕ ИНСТРУКЦИИ", # 🆕
408
+ }
409
+
410
+ return name_mapping.get(filename, filename.replace(".txt", "").upper())
411
+
412
+ async def reload_prompts(self) -> str:
413
+ """Перезагружает промпты (для обновления без перезапуска бота)"""
414
+ logger.info("Перезагрузка промптов...")
415
+ return await self.load_system_prompt()
416
+
417
+ async def validate_prompts(self) -> Dict[str, bool]:
418
+ """Проверяет доступность всех файлов промптов и приветственного сообщения"""
419
+ results = {}
420
+
421
+ # Проверяем файлы промптов
422
+ for filename in self.prompt_files:
423
+ file_path = self.prompts_dir / filename
424
+ try:
425
+ if file_path.exists():
426
+ async with aiofiles.open(file_path, "r", encoding="utf-8") as f:
427
+ content = await f.read()
428
+ results[filename] = bool(
429
+ content.strip() and len(content.strip()) > 10
430
+ )
431
+ else:
432
+ results[filename] = False
433
+ except Exception:
434
+ results[filename] = False
435
+
436
+ # Проверяем файл приветственного сообщения
437
+ try:
438
+ if self.welcome_file.exists():
439
+ async with aiofiles.open(self.welcome_file, "r", encoding="utf-8") as f:
440
+ content = await f.read()
441
+ results["welcome_message.txt"] = bool(
442
+ content.strip() and len(content.strip()) > 5
443
+ )
444
+ else:
445
+ results["welcome_message.txt"] = False
446
+ except Exception:
447
+ results["welcome_message.txt"] = False
448
+
449
+ # Проверяем файл справки (опционально)
450
+ try:
451
+ if self.help_file.exists():
452
+ async with aiofiles.open(self.help_file, "r", encoding="utf-8") as f:
453
+ content = await f.read()
454
+ results["help_message.txt"] = bool(
455
+ content.strip() and len(content.strip()) > 5
456
+ )
457
+ else:
458
+ results["help_message.txt"] = False # Не критично
459
+ except Exception:
460
+ results["help_message.txt"] = False
461
+
462
+ # 🆕 Проверяем финальные инструкции (опционально)
463
+ try:
464
+ if self.final_instructions_file.exists():
465
+ async with aiofiles.open(
466
+ self.final_instructions_file, "r", encoding="utf-8"
467
+ ) as f:
468
+ content = await f.read()
469
+ results["final_instructions.txt"] = bool(
470
+ content.strip() and len(content.strip()) > 5
471
+ )
472
+ else:
473
+ results["final_instructions.txt"] = (
474
+ False # Не критично - опциональный файл
475
+ )
476
+ except Exception:
477
+ results["final_instructions.txt"] = False
478
+
479
+ return results
480
+
481
+ def get_prompt_info(self) -> Dict[str, any]:
482
+ """Возвращает информацию о конфигурации промптов"""
483
+ return {
484
+ "prompts_dir": str(self.prompts_dir),
485
+ "prompt_files": self.prompt_files,
486
+ "welcome_file": "welcome_message.txt",
487
+ "help_file": "help_message.txt",
488
+ "final_instructions_file": "final_instructions.txt", # 🆕
489
+ "total_files": len(self.prompt_files) + 1, # +1 для welcome message
490
+ "json_instructions_included": True,
491
+ }
492
+
493
+ async def test_json_parsing(self, test_response: str) -> Dict[str, any]:
494
+ """Тестирует парсинг JSON из ответа ИИ (для отладки)"""
495
+ import json
496
+ import re
497
+
498
+ try:
499
+ # Используем тот же алгоритм что и в main.py
500
+ json_pattern = r'\{[^{}]*"этап"[^{}]*\}$'
501
+ match = re.search(json_pattern, test_response.strip())
502
+
503
+ if match:
504
+ json_str = match.group(0)
505
+ response_text = test_response[: match.start()].strip()
506
+
507
+ try:
508
+ metadata = json.loads(json_str)
509
+ return {
510
+ "success": True,
511
+ "response_text": response_text,
512
+ "metadata": metadata,
513
+ "json_str": json_str,
514
+ }
515
+ except json.JSONDecodeError as e:
516
+ return {
517
+ "success": False,
518
+ "error": f"JSON decode error: {e}",
519
+ "json_str": json_str,
520
+ }
521
+ else:
522
+ return {
523
+ "success": False,
524
+ "error": "JSON pattern not found",
525
+ "response_text": test_response,
526
+ }
527
+
528
+ except Exception as e:
529
+ return {"success": False, "error": f"Parse error: {e}"}