smart-bot-factory 0.1.2__py3-none-any.whl → 0.1.4__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 (61) hide show
  1. smart_bot_factory/__init__.py +33 -0
  2. smart_bot_factory/admin/__init__.py +16 -0
  3. smart_bot_factory/admin/admin_logic.py +430 -0
  4. smart_bot_factory/admin/admin_manager.py +141 -0
  5. smart_bot_factory/admin/admin_migration.sql +136 -0
  6. smart_bot_factory/admin/admin_tester.py +151 -0
  7. smart_bot_factory/admin/timeout_checker.py +499 -0
  8. smart_bot_factory/analytics/__init__.py +7 -0
  9. smart_bot_factory/analytics/analytics_manager.py +355 -0
  10. smart_bot_factory/cli.py +768 -0
  11. smart_bot_factory/config.py +235 -0
  12. smart_bot_factory/configs/growthmed-helper/env_example.txt +1 -0
  13. smart_bot_factory/configs/growthmed-helper/prompts/1sales_context.txt +9 -0
  14. smart_bot_factory/configs/growthmed-helper/prompts/2product_info.txt +582 -0
  15. smart_bot_factory/configs/growthmed-helper/prompts/3objection_handling.txt +66 -0
  16. smart_bot_factory/configs/growthmed-helper/prompts/final_instructions.txt +232 -0
  17. smart_bot_factory/configs/growthmed-helper/prompts/help_message.txt +28 -0
  18. smart_bot_factory/configs/growthmed-helper/prompts/welcome_message.txt +7 -0
  19. smart_bot_factory/configs/growthmed-helper/welcome_file/welcome_file_msg.txt +16 -0
  20. smart_bot_factory/configs/growthmed-helper/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
  21. smart_bot_factory/configs/growthmed-october-24/prompts/1sales_context.txt +16 -0
  22. smart_bot_factory/configs/growthmed-october-24/prompts/2product_info.txt +582 -0
  23. smart_bot_factory/configs/growthmed-october-24/prompts/3objection_handling.txt +66 -0
  24. smart_bot_factory/configs/growthmed-october-24/prompts/final_instructions.txt +212 -0
  25. smart_bot_factory/configs/growthmed-october-24/prompts/help_message.txt +28 -0
  26. smart_bot_factory/configs/growthmed-october-24/prompts/welcome_message.txt +8 -0
  27. smart_bot_factory/configs/growthmed-october-24/reports/test_20250924_064229.txt +818 -0
  28. smart_bot_factory/configs/growthmed-october-24/reports/test_20250924_064335.txt +32 -0
  29. smart_bot_factory/configs/growthmed-october-24/reports/test_20250924_064638.txt +35 -0
  30. smart_bot_factory/configs/growthmed-october-24/tests/quick_scenarios.yaml +66 -0
  31. smart_bot_factory/configs/growthmed-october-24/tests/realistic_scenarios.yaml +108 -0
  32. smart_bot_factory/configs/growthmed-october-24/tests/scenario_examples.yaml +46 -0
  33. smart_bot_factory/configs/growthmed-october-24/welcome_file/welcome_file_msg.txt +16 -0
  34. 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
  35. smart_bot_factory/core/__init__.py +22 -0
  36. smart_bot_factory/core/bot_utils.py +703 -0
  37. smart_bot_factory/core/conversation_manager.py +536 -0
  38. smart_bot_factory/core/decorators.py +230 -0
  39. smart_bot_factory/core/message_sender.py +249 -0
  40. smart_bot_factory/core/states.py +14 -0
  41. smart_bot_factory/creation/__init__.py +8 -0
  42. smart_bot_factory/creation/bot_builder.py +329 -0
  43. smart_bot_factory/creation/bot_testing.py +986 -0
  44. smart_bot_factory/database/database_structure.sql +57 -0
  45. smart_bot_factory/database/schema.sql +1094 -0
  46. smart_bot_factory/handlers/handlers.py +583 -0
  47. smart_bot_factory/integrations/__init__.py +9 -0
  48. smart_bot_factory/integrations/openai_client.py +435 -0
  49. smart_bot_factory/integrations/supabase_client.py +592 -0
  50. smart_bot_factory/setup_checker.py +476 -0
  51. smart_bot_factory/utils/__init__.py +9 -0
  52. smart_bot_factory/utils/debug_routing.py +103 -0
  53. smart_bot_factory/utils/prompt_loader.py +427 -0
  54. smart_bot_factory/utm_link_generator.py +106 -0
  55. smart_bot_factory-0.1.4.dist-info/METADATA +126 -0
  56. smart_bot_factory-0.1.4.dist-info/RECORD +59 -0
  57. smart_bot_factory-0.1.4.dist-info/licenses/LICENSE +24 -0
  58. smart_bot_factory-0.1.2.dist-info/METADATA +0 -31
  59. smart_bot_factory-0.1.2.dist-info/RECORD +0 -4
  60. {smart_bot_factory-0.1.2.dist-info → smart_bot_factory-0.1.4.dist-info}/WHEEL +0 -0
  61. {smart_bot_factory-0.1.2.dist-info → smart_bot_factory-0.1.4.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,986 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Система тестирования ботов
4
+
5
+ ИСПОЛЬЗОВАНИЕ:
6
+ python bot_testing.py valera # Тестировать бота valera (все сценарии)
7
+ python bot_testing.py valera final_scenarios # Тестировать только файл final_scenarios.yaml
8
+ python bot_testing.py valera -v # Подробный вывод
9
+ python bot_testing.py valera --max-concurrent 10 # Увеличить количество потоков до 10 (По умолчанию 5)
10
+
11
+ ФАЙЛЫ СЦЕНАРИЕВ:
12
+ bots/BOT_ID/tests/*.yaml - тестовые сценарии
13
+
14
+ ОТЧЕТЫ:
15
+ bots/BOT_ID/reports/test_YYYYMMDD_HHMMSS.txt - подробные отчеты
16
+ """
17
+
18
+ import asyncio
19
+ import yaml
20
+ import os
21
+ import sys
22
+ import argparse
23
+ import logging
24
+ import glob
25
+ import time
26
+ import uuid
27
+ from datetime import datetime
28
+ from pathlib import Path
29
+ from typing import List
30
+ from dotenv import load_dotenv
31
+
32
+
33
+ from project_root_finder import root
34
+ from pathlib import Path
35
+ # Глобальная переменная для корневой директории проекта
36
+ PROJECT_ROOT = root # smart_bot_factory/creation/ -> project_root
37
+
38
+ def set_project_root(new_root: Path):
39
+ """Устанавливает новую корневую директорию проекта"""
40
+ global PROJECT_ROOT
41
+ PROJECT_ROOT = Path(new_root).resolve()
42
+ logging.info(f"Корневая директория проекта изменена на: {PROJECT_ROOT}")
43
+
44
+ def get_project_root() -> Path:
45
+ """Возвращает текущую корневую директорию проекта"""
46
+ # Проверяем переменную окружения PROJECT_ROOT
47
+ env_project_root = os.getenv('PROJECT_ROOT')
48
+ if env_project_root:
49
+ return Path(env_project_root).resolve()
50
+ return PROJECT_ROOT
51
+
52
+ # Импорты для работы с ботом
53
+ try:
54
+ # Попробуем относительные импорты (если запускается как модуль)
55
+ from ..integrations.openai_client import OpenAIClient
56
+ from ..utils.prompt_loader import PromptLoader
57
+ from ..integrations.supabase_client import SupabaseClient
58
+ from ..core.bot_utils import parse_ai_response
59
+ except ImportError:
60
+ # Если не работает, используем абсолютные импорты (если запускается как скрипт)
61
+ from smart_bot_factory.integrations.openai_client import OpenAIClient
62
+ from smart_bot_factory.utils.prompt_loader import PromptLoader
63
+ from smart_bot_factory.integrations.supabase_client import SupabaseClient
64
+ from smart_bot_factory.core.bot_utils import parse_ai_response
65
+
66
+
67
+ logger = logging.getLogger(__name__)
68
+
69
+
70
+ class TestStep:
71
+ """Класс для представления одного шага в сценарии"""
72
+
73
+ def __init__(self, user_input: str, expected_keywords: List[str],
74
+ forbidden_keywords: List[str] = None):
75
+ self.user_input = user_input
76
+ self.expected_keywords = [kw.lower() for kw in expected_keywords]
77
+ self.forbidden_keywords = [kw.lower() for kw in (forbidden_keywords or [])]
78
+
79
+
80
+ class TestScenario:
81
+ """Класс для представления тестового сценария с последовательными шагами"""
82
+
83
+ def __init__(self, name: str, steps: List[TestStep]):
84
+ self.name = name
85
+ self.steps = steps
86
+
87
+
88
+ class StepResult:
89
+ """Класс для представления результата одного шага"""
90
+
91
+ def __init__(self, step: TestStep, bot_response: str,
92
+ passed: bool, missing_keywords: List[str] = None,
93
+ found_forbidden: List[str] = None):
94
+ self.step = step
95
+ self.bot_response = bot_response
96
+ self.passed = passed
97
+ self.missing_keywords = missing_keywords or []
98
+ self.found_forbidden = found_forbidden or []
99
+
100
+
101
+ class TestResult:
102
+ """Класс для представления результата тестирования сценария"""
103
+
104
+ def __init__(self, scenario: TestScenario, step_results: List[StepResult]):
105
+ self.scenario = scenario
106
+ self.step_results = step_results
107
+ self.passed = all(step.passed for step in step_results)
108
+ self.total_steps = len(step_results)
109
+ self.passed_steps = sum(1 for step in step_results if step.passed)
110
+ self.failed_steps = self.total_steps - self.passed_steps
111
+
112
+
113
+ class ScenarioLoader:
114
+ """Загрузчик тестовых сценариев из YAML файлов"""
115
+
116
+ @staticmethod
117
+ def load_scenarios_from_file(file_path: str) -> List[TestScenario]:
118
+ """Загружает сценарии из YAML файла"""
119
+ try:
120
+ with open(file_path, 'r', encoding='utf-8') as file:
121
+ data = yaml.safe_load(file)
122
+
123
+ scenarios = []
124
+ file_name = Path(file_path).stem
125
+
126
+ for i, scenario_data in enumerate(data.get('scenarios', [])):
127
+ # Проверяем новый формат со steps
128
+ if 'steps' in scenario_data:
129
+ # Новый формат: сценарий с шагами
130
+ name = scenario_data.get('name', f"[{file_name}-{i+1}]")
131
+ steps = []
132
+
133
+ for step_data in scenario_data['steps']:
134
+ step = TestStep(
135
+ user_input=step_data.get('user_input', ''),
136
+ expected_keywords=step_data.get('expected_keywords', []),
137
+ forbidden_keywords=step_data.get('forbidden_keywords', [])
138
+ )
139
+ steps.append(step)
140
+
141
+ scenario = TestScenario(name=name, steps=steps)
142
+
143
+ else:
144
+ # Старый формат: одиночный вопрос -> превращаем в сценарий с одним шагом
145
+ name = scenario_data.get('name', f"[{file_name}-{i+1}]")
146
+ step = TestStep(
147
+ user_input=scenario_data.get('user_input', ''),
148
+ expected_keywords=scenario_data.get('expected_keywords', []),
149
+ forbidden_keywords=scenario_data.get('forbidden_keywords', [])
150
+ )
151
+ scenario = TestScenario(name=name, steps=[step])
152
+
153
+ # Добавляем информацию о файле для отчетности
154
+ scenario.source_file = file_name
155
+ scenario.scenario_number = i + 1
156
+
157
+ scenarios.append(scenario)
158
+
159
+ return scenarios
160
+
161
+ except Exception as e:
162
+ logging.error(f"Ошибка загрузки сценариев из {file_path}: {e}")
163
+ return []
164
+
165
+ @staticmethod
166
+ def load_all_scenarios_for_bot(bot_id: str, project_root: Path = None) -> List[TestScenario]:
167
+ """Загружает все сценарии для указанного бота"""
168
+ # Используем переданную корневую директорию или глобальную
169
+ user_root_dir = project_root or get_project_root()
170
+
171
+ # Проверяем наличие папки bots/{bot_id}
172
+ bots_dir = user_root_dir / "bots" / bot_id
173
+ if not bots_dir.exists():
174
+ logging.warning(f"Папка bots/{bot_id} не найдена в проекте: {bots_dir}")
175
+ return []
176
+
177
+ # Путь к папке tests в корневой директории пользователя
178
+ tests_dir = user_root_dir / "bots" / bot_id / "tests"
179
+
180
+ if not tests_dir.exists():
181
+ logging.warning(f"Каталог тестов не найден: {tests_dir}")
182
+ return []
183
+
184
+ all_scenarios = []
185
+ for yaml_file in tests_dir.glob("*.yaml"):
186
+ scenarios = ScenarioLoader.load_scenarios_from_file(str(yaml_file))
187
+ all_scenarios.extend(scenarios)
188
+
189
+ return all_scenarios
190
+
191
+
192
+ class BotTester:
193
+ """Основной класс для тестирования ботов"""
194
+
195
+ def __init__(self, bot_id: str, project_root: Path = None):
196
+ self.bot_id = bot_id
197
+ self.project_root = project_root or get_project_root()
198
+ self.openai_client = None
199
+ self.prompt_loader = None
200
+ self.supabase_client = None
201
+ self.config_dir = None
202
+ self._initialize_bot()
203
+
204
+ def _initialize_bot(self):
205
+ """Инициализация компонентов бота, используя готовую логику из запускалки"""
206
+ try:
207
+ logging.info(f"")
208
+ logging.info(f"══════════════════════════════════════════════════════════")
209
+ logging.info(f"ИНИЦИАЛИЗАЦИЯ ТЕСТЕРА БОТА: {self.bot_id}")
210
+ logging.info(f"══════════════════════════════════════════════════════════")
211
+
212
+ # Используем корневую директорию проекта
213
+ user_root_dir = self.project_root
214
+ logging.info(f"Корневая директория проекта: {user_root_dir}")
215
+
216
+ # Добавляем корневую директорию в sys.path для импорта библиотеки
217
+ sys.path.insert(0, str(user_root_dir))
218
+ logging.info(f"Добавлен путь в sys.path: {user_root_dir}")
219
+
220
+ # Путь к конфигурации бота в корневой директории проекта
221
+ self.config_dir = user_root_dir / 'bots' / self.bot_id
222
+ logging.info(f"Каталог конфигурации: {self.config_dir}")
223
+
224
+ if not self.config_dir.exists():
225
+ raise ValueError(f"Папка конфигурации не найдена: {self.config_dir}")
226
+ logging.info(f"Каталог конфигурации найден")
227
+
228
+ # Устанавливаем BOT_ID как в оригинальном файле
229
+ os.environ['BOT_ID'] = self.bot_id
230
+ logging.info(f"BOT_ID установлен: {self.bot_id}")
231
+
232
+ # Загружаем .env из папки бота
233
+ env_file = self.config_dir / '.env'
234
+ logging.info(f"Загрузка .env файла: {env_file}")
235
+ if env_file.exists():
236
+ load_dotenv(env_file)
237
+ logging.info(f".env файл загружен")
238
+ else:
239
+ raise ValueError(f"Файл .env не найден: {env_file}")
240
+
241
+ # Путь к папке промптов (абсолютный путь)
242
+ prompts_dir = self.config_dir / 'prompts'
243
+ if not prompts_dir.exists():
244
+ raise ValueError(f"Папка с промптами не найдена: {prompts_dir}")
245
+
246
+ logging.info(f"Каталог промптов: {prompts_dir}")
247
+
248
+ # Читаем переменные напрямую из .env файла
249
+ logging.info(f"⚙️ Чтение конфигурации из .env...")
250
+
251
+ # OpenAI настройки
252
+ openai_api_key = os.getenv('OPENAI_API_KEY')
253
+ openai_model = os.getenv('OPENAI_MODEL', 'gpt-4o-mini')
254
+ openai_max_tokens = int(os.getenv('OPENAI_MAX_TOKENS', '4000'))
255
+ openai_temperature = float(os.getenv('OPENAI_TEMPERATURE', '0.7'))
256
+
257
+ # Supabase настройки
258
+ supabase_url = os.getenv('SUPABASE_URL')
259
+ supabase_key = os.getenv('SUPABASE_KEY')
260
+
261
+ # Проверяем обязательные переменные
262
+ if not openai_api_key:
263
+ raise ValueError("OPENAI_API_KEY не найден в .env файле")
264
+ if not supabase_url:
265
+ raise ValueError("SUPABASE_URL не найден в .env файле")
266
+ if not supabase_key:
267
+ raise ValueError("SUPABASE_KEY не найден в .env файле")
268
+
269
+ logging.info(f"Конфигурация загружена (модель: {openai_model})")
270
+
271
+ logging.info(f"Инициализация OpenAI клиента...")
272
+ self.openai_client = OpenAIClient(
273
+ api_key=openai_api_key,
274
+ model=openai_model,
275
+ max_tokens=openai_max_tokens,
276
+ temperature=openai_temperature
277
+ )
278
+ logging.info(f"OpenAI клиент создан (модель: {openai_model})")
279
+
280
+ logging.info(f"Инициализация загрузчика промптов...")
281
+ # PromptLoader автоматически найдет все .txt файлы в папке промптов
282
+ self.prompt_loader = PromptLoader(prompts_dir=prompts_dir)
283
+ logging.info(f"Загрузчик промптов создан")
284
+
285
+ # Инициализируем Supabase клиент
286
+ logging.info(f"Инициализация Supabase клиента...")
287
+ self.supabase_client = SupabaseClient(
288
+ url=supabase_url,
289
+ key=supabase_key,
290
+ bot_id=self.bot_id
291
+ )
292
+ logging.info(f"Supabase клиент создан")
293
+
294
+ logging.info(f"Бот {self.bot_id} инициализирован успешно!")
295
+ logging.info(f"══════════════════════════════════════════════════════════")
296
+
297
+ except Exception as e:
298
+ logging.error(f"══════════════════════════════════════════════════════════")
299
+ logging.error(f"ОШИБКА ИНИЦИАЛИЗАЦИИ БОТА: {self.bot_id}")
300
+ logging.error(f"Описание: {e}")
301
+ logging.error(f"══════════════════════════════════════════════════════════")
302
+ raise
303
+
304
+ async def test_scenario(self, scenario: TestScenario) -> TestResult:
305
+ """Тестирует сценарий с последовательными шагами"""
306
+ try:
307
+ # Инициализируем Supabase клиент перед тестом
308
+ if not self.supabase_client.client:
309
+ await self.supabase_client.initialize()
310
+ logging.info(f"🔌 Supabase клиент инициализирован для бота {self.bot_id}")
311
+
312
+ step_results = []
313
+
314
+ # Генерируем уникальный тестовый telegram_id (только цифры)
315
+ # Формат: 999 + timestamp + случайные цифры -> гарантированно уникальный
316
+ timestamp_part = str(int(time.time()))[-6:] # Последние 6 цифр timestamp
317
+ random_part = str(uuid.uuid4().int)[:3] # Первые 3 цифры из UUID
318
+ unique_test_telegram_id = int(f"999{timestamp_part}{random_part}")
319
+
320
+ user_data = {
321
+ 'telegram_id': unique_test_telegram_id,
322
+ 'username': 'test_user',
323
+ 'first_name': 'Test',
324
+ 'last_name': 'User',
325
+ 'language_code': 'ru'
326
+ }
327
+
328
+ logging.info(f"")
329
+ logging.info(f"🧪 ═══════════════════════════════════════════════════════════")
330
+ logging.info(f"🎯 НАЧИНАЕМ ТЕСТ СЦЕНАРИЯ: {scenario.name}")
331
+ logging.info(f"🤖 Бот: {self.bot_id}")
332
+ logging.info(f"👤 Тестовый пользователь: {unique_test_telegram_id}")
333
+ logging.info(f"📝 Количество шагов: {len(scenario.steps)}")
334
+ logging.info(f"═══════════════════════════════════════════════════════════")
335
+
336
+ session_id, system_prompt = await self.create_test_session(user_data)
337
+ logging.info(f"🆔 Создана тестовая сессия: {session_id}")
338
+
339
+ for i, step in enumerate(scenario.steps):
340
+ step_num = i + 1
341
+ logging.info(f"")
342
+ logging.info(f"🔄 ─────────────── ШАГ {step_num}/{len(scenario.steps)} ───────────────")
343
+ logging.info(f"💬 Ввод пользователя: '{step.user_input}'")
344
+
345
+ if step.expected_keywords:
346
+ logging.info(f"🎯 Ожидаемые слова: {step.expected_keywords}")
347
+ if step.forbidden_keywords:
348
+ logging.info(f"🚫 Запрещенные слова: {step.forbidden_keywords}")
349
+
350
+ # Анализируем ответ на этом шаге
351
+ start_time = time.time()
352
+ clean_response = await self.process_user_message_test(step.user_input, session_id, system_prompt)
353
+ step_duration = int((time.time() - start_time) * 1000)
354
+
355
+ # Показываем ответ бота (обрезанный)
356
+ response_preview = clean_response[:150] + "..." if len(clean_response) > 150 else clean_response
357
+ response_preview = response_preview.replace('\n', ' ')
358
+ logging.info(f"🤖 Ответ бота: '{response_preview}'")
359
+ logging.info(f"⏱️ Время обработки: {step_duration}мс")
360
+
361
+ # Проверяем ожидаемые ключевые слова
362
+ missing_keywords = []
363
+ found_expected = []
364
+ for keyword in step.expected_keywords:
365
+ if keyword.lower() in clean_response.lower():
366
+ found_expected.append(keyword)
367
+ else:
368
+ missing_keywords.append(keyword)
369
+
370
+ # Проверяем запрещенные ключевые слова
371
+ found_forbidden = []
372
+ for keyword in step.forbidden_keywords:
373
+ if keyword.lower() in clean_response.lower():
374
+ found_forbidden.append(keyword)
375
+
376
+ # Выводим результаты проверки
377
+ if found_expected:
378
+ logging.info(f"✅ Найденные ожидаемые: {found_expected}")
379
+ if missing_keywords:
380
+ logging.info(f"❌ НЕ найденные ожидаемые: {missing_keywords}")
381
+ if found_forbidden:
382
+ logging.info(f"🚫 Найденные запрещенные: {found_forbidden}")
383
+
384
+ # Определяем результат шага
385
+ passed = len(missing_keywords) == 0 and len(found_forbidden) == 0
386
+ status_icon = "✅" if passed else "❌"
387
+ status_text = "ПРОЙДЕН" if passed else "ПРОВАЛЕН"
388
+ logging.info(f"🎯 Результат шага {step_num}: {status_icon} {status_text}")
389
+
390
+ step_result = StepResult(
391
+ step=step,
392
+ bot_response=clean_response,
393
+ passed=passed,
394
+ missing_keywords=missing_keywords,
395
+ found_forbidden=found_forbidden
396
+ )
397
+
398
+ step_results.append(step_result)
399
+
400
+ # Короткая пауза между шагами
401
+ await asyncio.sleep(0.1)
402
+
403
+ # Финальная статистика сценария
404
+ passed_steps = sum(1 for step in step_results if step.passed)
405
+ total_steps = len(step_results)
406
+ success_rate = (passed_steps / total_steps) * 100 if total_steps > 0 else 0
407
+
408
+ logging.info(f"")
409
+ logging.info(f"🏁 ─────────────── ИТОГ СЦЕНАРИЯ ───────────────")
410
+ logging.info(f"📊 Пройдено шагов: {passed_steps}/{total_steps} ({success_rate:.1f}%)")
411
+ overall_status = "✅ СЦЕНАРИЙ ПРОЙДЕН" if passed_steps == total_steps else "❌ СЦЕНАРИЙ ПРОВАЛЕН"
412
+ logging.info(f"🎯 {overall_status}")
413
+ logging.info(f"═══════════════════════════════════════════════════════════")
414
+
415
+ return TestResult(scenario=scenario, step_results=step_results)
416
+
417
+ except Exception as e:
418
+ logging.error(f"")
419
+ logging.error(f"💥 ═══════════════════════════════════════════════════════════")
420
+ logging.error(f"❌ КРИТИЧЕСКАЯ ОШИБКА В СЦЕНАРИИ: {scenario.name}")
421
+ logging.error(f"🐛 Ошибка: {str(e)}")
422
+ logging.error(f"═══════════════════════════════════════════════════════════")
423
+ logging.exception("📋 Полный стек ошибки:")
424
+
425
+ # Создаем результат с ошибкой для всех шагов
426
+ step_results = []
427
+ for step in scenario.steps:
428
+ step_result = StepResult(
429
+ step=step,
430
+ bot_response=f"ОШИБКА: {str(e)}",
431
+ passed=False,
432
+ missing_keywords=step.expected_keywords,
433
+ found_forbidden=[]
434
+ )
435
+ step_results.append(step_result)
436
+
437
+ return TestResult(scenario=scenario, step_results=step_results)
438
+
439
+ async def get_welcome_file_caption_test(self) -> str:
440
+ """
441
+ Тестовая версия получения подписи к приветственному файлу.
442
+ Возвращает только текст из файла welcome_file_msg.txt без взаимодействия с ботом.
443
+
444
+ Returns:
445
+ str: текст подписи из файла или пустая строка
446
+ """
447
+ try:
448
+ # Путь к папке welcome_files (абсолютный путь)
449
+ folder = self.config_dir / 'welcome_files'
450
+ if not folder.exists() or not folder.is_dir():
451
+ logger.info(f"Директория приветственных файлов не найдена: {folder}")
452
+ return ""
453
+
454
+ # Ищем файл welcome_file_msg.txt в директории
455
+ msg_path = folder / 'welcome_file_msg.txt'
456
+ if not msg_path.is_file():
457
+ logger.info(f"Файл подписи не найден: {msg_path}")
458
+ return ""
459
+
460
+ # Читаем содержимое файла
461
+ try:
462
+ with open(msg_path, 'r', encoding='utf-8') as f:
463
+ caption = f.read().strip()
464
+ logger.info(f"Подпись загружена из файла: {msg_path}")
465
+ return caption
466
+ except Exception as e:
467
+ logger.error(f"Ошибка при чтении файла подписи {msg_path}: {e}")
468
+ return ""
469
+
470
+ except Exception as e:
471
+ logger.error(f"Ошибка при получении подписи к приветственному файлу: {e}")
472
+ return ""
473
+
474
+ async def create_test_session(self, user_data: dict) -> tuple[str, str]:
475
+ """
476
+ Создает тестовую сессию без взаимодействия с ботом.
477
+
478
+ Args:
479
+ user_data: словарь с данными пользователя (telegram_id, username, first_name, last_name, language_code)
480
+
481
+ Returns:
482
+ tuple[str, str, str]: (session_id, system_prompt, welcome_message)
483
+ - session_id: ID созданной сессии
484
+ - system_prompt: системный промпт
485
+ - welcome_message: приветственное сообщение (включая подпись к файлу, если есть)
486
+ """
487
+
488
+ try:
489
+ logging.info(f"🔄 Создание тестовой сессии...")
490
+
491
+ # 1. ЗАГРУЖАЕМ ПРОМПТЫ
492
+ logging.info(f"📋 Загрузка промптов...")
493
+ system_prompt = await self.prompt_loader.load_system_prompt()
494
+ welcome_message = await self.prompt_loader.load_welcome_message()
495
+ logging.info(f"✅ Промпты загружены: система ({len(system_prompt)} симв.), приветствие ({len(welcome_message)} симв.)")
496
+
497
+ # 2. СОЗДАЕМ НОВУЮ СЕССИЮ
498
+ logging.info(f"🗄️ Создание сессии в Supabase...")
499
+ session_id = await self.supabase_client.create_chat_session(user_data, system_prompt)
500
+ logging.info(f"✅ Сессия создана с ID: {session_id}")
501
+
502
+ # 3. ПРОВЕРЯЕМ НАЛИЧИЕ ПРИВЕТСТВЕННОГО ФАЙЛА И ЕГО ПОДПИСИ
503
+ logging.info(f"📎 Проверка приветственного файла...")
504
+ caption = await self.get_welcome_file_caption_test()
505
+
506
+ # 4. ОБЪЕДИНЯЕМ ПРИВЕТСТВЕННОЕ СООБЩЕНИЕ С ПОДПИСЬЮ К ФАЙЛУ
507
+ if caption:
508
+ welcome_message = f"{welcome_message}\n\nПодпись к файлу:\n\n{caption}"
509
+ logging.info(f"📎 Добавлена подпись к файлу ({len(caption)} симв.)")
510
+ else:
511
+ logging.info(f"📎 Приветственный файл не найден или пуст")
512
+
513
+ # 5. СОХРАНЯЕМ ПРИВЕТСТВЕННОЕ СООБЩЕНИЕ В БД
514
+ logging.info(f"💾 Сохранение приветственного сообщения в БД...")
515
+ await self.supabase_client.add_message(
516
+ session_id=session_id,
517
+ role='assistant',
518
+ content=welcome_message,
519
+ message_type='text'
520
+ )
521
+ logging.info(f"✅ Приветственное сообщение сохранено")
522
+
523
+ return session_id, system_prompt
524
+
525
+ except Exception as e:
526
+ logging.error(f"💥 Ошибка в create_test_session: {e}")
527
+ logging.exception("📋 Полный стек ошибки:")
528
+ raise
529
+
530
+ async def process_user_message_test(self, user_message: str, session_id: str, system_prompt: str):
531
+ """
532
+ Тестовая версия обработки сообщений пользователя без взаимодействия с ботом.
533
+ Возвращает только текст ответа от нейросети и сохраняет данные в БД.
534
+ """
535
+
536
+ from datetime import datetime
537
+ import pytz
538
+ import time
539
+
540
+ # Читаем настройки напрямую из .env
541
+ max_context_messages = int(os.getenv('MAX_CONTEXT_MESSAGES', '20'))
542
+
543
+ try:
544
+ logging.info(f"📨 Обработка сообщения пользователя...")
545
+
546
+ # Сохраняем сообщение пользователя
547
+ logging.info(f"💾 Сохранение сообщения пользователя в БД...")
548
+ await self.supabase_client.add_message(
549
+ session_id=session_id,
550
+ role='user',
551
+ content=user_message,
552
+ message_type='text'
553
+ )
554
+ logging.info(f"✅ Сообщение пользователя сохранено")
555
+
556
+ # Получаем историю сообщений
557
+ logging.info(f"📚 Получение истории сообщений (лимит: {max_context_messages})...")
558
+ chat_history = await self.supabase_client.get_chat_history(session_id, limit=max_context_messages)
559
+ logging.info(f"✅ Получено {len(chat_history)} сообщений из истории")
560
+
561
+ # Добавляем время
562
+ moscow_tz = pytz.timezone('Europe/Moscow')
563
+ current_time = datetime.now(moscow_tz)
564
+ time_info = current_time.strftime('%H:%M, %d.%m.%Y, %A')
565
+ logging.info(f"🕐 Текущее время: {time_info}")
566
+
567
+ # Модифицируем системный промпт с временем
568
+ system_prompt_with_time = f"""
569
+ {system_prompt}
570
+
571
+ ТЕКУЩЕЕ ВРЕМЯ: {time_info} (московское время)
572
+ """
573
+
574
+ # Формируем контекст для OpenAI
575
+ messages = [{"role": "system", "content": system_prompt_with_time}]
576
+
577
+ for msg in chat_history[-max_context_messages:]:
578
+ messages.append({
579
+ "role": msg['role'],
580
+ "content": msg['content']
581
+ })
582
+
583
+ # Добавляем финальные инструкции
584
+ logging.info(f"📋 Загрузка финальных инструкций...")
585
+ final_instructions = await self.prompt_loader.load_final_instructions()
586
+ if final_instructions:
587
+ messages.append({"role": "system", "content": final_instructions})
588
+ logging.info(f"✅ Финальные инструкции добавлены ({len(final_instructions)} симв.)")
589
+ else:
590
+ logging.info(f"📋 Финальные инструкции отсутствуют")
591
+
592
+ logging.info(f"🧠 Отправка запроса к OpenAI (сообщений в контексте: {len(messages)})...")
593
+
594
+ # Получаем ответ от OpenAI
595
+ start_time = time.time()
596
+ ai_response = await self.openai_client.get_completion(messages)
597
+ processing_time = int((time.time() - start_time) * 1000)
598
+
599
+ logging.info(f"✅ Ответ получен от OpenAI за {processing_time}мс")
600
+
601
+ # Инициализируем переменные
602
+ tokens_used = 0
603
+ ai_metadata = {}
604
+ response_text = ""
605
+
606
+ # Проверяем ответ
607
+ if not ai_response or not ai_response.strip():
608
+ response_text = "Извините, произошла техническая ошибка. Попробуйте переформулировать вопрос."
609
+ tokens_used = 0
610
+ ai_metadata = {}
611
+ logging.warning(f"⚠️ Пустой ответ от OpenAI")
612
+ else:
613
+ tokens_used = self.openai_client.estimate_tokens(ai_response)
614
+ logging.info(f"🔢 Оценка токенов: {tokens_used}")
615
+
616
+ logging.info(f"🔍 Парсинг AI-ответа на метаданные...")
617
+ response_text, ai_metadata = parse_ai_response(ai_response)
618
+
619
+ if not ai_metadata:
620
+ response_text = ai_response
621
+ ai_metadata = {}
622
+ logging.info(f"📝 Метаданные не найдены, используем исходный ответ")
623
+ elif not response_text.strip():
624
+ response_text = ai_response
625
+ logging.info(f"📝 Пустой текст после парсинга, используем исходный ответ")
626
+ else:
627
+ logging.info(f"📝 Найдены метаданные: {list(ai_metadata.keys())}")
628
+
629
+ # Обновляем этап сессии и качество лида если есть метаданные
630
+ if ai_metadata:
631
+ stage = ai_metadata.get('этап')
632
+ quality = ai_metadata.get('качество')
633
+ if stage or quality is not None:
634
+ logging.info(f"🎯 Обновление этапа сессии: этап={stage}, качество={quality}")
635
+ await self.supabase_client.update_session_stage(session_id, stage, quality)
636
+
637
+ # Сохраняем ответ ассистента
638
+ logging.info(f"💾 Сохранение ответа ассистента в БД...")
639
+ await self.supabase_client.add_message(
640
+ session_id=session_id,
641
+ role='assistant',
642
+ content=response_text,
643
+ message_type='text',
644
+ tokens_used=tokens_used,
645
+ processing_time_ms=processing_time,
646
+ ai_metadata=ai_metadata
647
+ )
648
+ logging.info(f"✅ Ответ ассистента сохранен")
649
+
650
+ return response_text
651
+
652
+ except Exception as e:
653
+ logging.error(f"💥 Ошибка в process_user_message_test: {e}")
654
+ logging.exception("📋 Полный стек ошибки:")
655
+ return "Произошла ошибка при обработке сообщения."
656
+
657
+
658
+ class TestRunner:
659
+ """Запускатель тестов с асинхронным выполнением"""
660
+
661
+ def __init__(self, bot_id: str, max_concurrent: int = 5, project_root: Path = None):
662
+ self.bot_id = bot_id
663
+ self.max_concurrent = max_concurrent
664
+ self.project_root = project_root or get_project_root()
665
+ self.bot_tester = BotTester(bot_id, self.project_root)
666
+
667
+ async def run_tests(self, scenarios: List[TestScenario]) -> List[TestResult]:
668
+ """Запускает тесты асинхронно"""
669
+ semaphore = asyncio.Semaphore(self.max_concurrent)
670
+
671
+ async def run_single_test(scenario: TestScenario) -> TestResult:
672
+ async with semaphore:
673
+ logging.info(f"🧪 Выполнение теста: {scenario.name}")
674
+ result = await self.bot_tester.test_scenario(scenario)
675
+ status = "✅" if result.passed else "❌"
676
+ logging.info(f" {status} {scenario.name}")
677
+ return result
678
+
679
+ # Запускаем все тесты параллельно
680
+ tasks = [run_single_test(scenario) for scenario in scenarios]
681
+ results = await asyncio.gather(*tasks, return_exceptions=True)
682
+
683
+ # Обрабатываем исключения
684
+ processed_results = []
685
+ for i, result in enumerate(results):
686
+ if isinstance(result, Exception):
687
+ logging.error(f"Исключение в тесте {scenarios[i].name}: {result}")
688
+ # Создаем результат с ошибкой для всех шагов
689
+ step_results = []
690
+ for step in scenarios[i].steps:
691
+ step_result = StepResult(
692
+ step=step,
693
+ bot_response=f"ИСКЛЮЧЕНИЕ: {str(result)}",
694
+ passed=False,
695
+ missing_keywords=step.expected_keywords,
696
+ found_forbidden=[]
697
+ )
698
+ step_results.append(step_result)
699
+
700
+ processed_results.append(TestResult(
701
+ scenario=scenarios[i],
702
+ step_results=step_results
703
+ ))
704
+ else:
705
+ processed_results.append(result)
706
+
707
+ return processed_results
708
+
709
+
710
+ class ReportGenerator:
711
+ """Генератор отчетов о тестировании"""
712
+
713
+ @staticmethod
714
+ def cleanup_old_reports(reports_dir: str, max_reports: int = 10):
715
+ """Удаляет старые отчеты, оставляя только max_reports самых новых"""
716
+ if not os.path.exists(reports_dir):
717
+ return
718
+
719
+ # Находим все файлы отчетов
720
+ report_pattern = os.path.join(reports_dir, "test_*.txt")
721
+ report_files = glob.glob(report_pattern)
722
+
723
+ if len(report_files) <= max_reports:
724
+ return # Количество файлов в пределах лимита
725
+
726
+ # Сортируем по времени модификации (старые первыми)
727
+ report_files.sort(key=lambda x: os.path.getmtime(x))
728
+
729
+ # Удаляем старые файлы, оставляя только max_reports-1 (место для нового)
730
+ files_to_delete = report_files[:-(max_reports-1)]
731
+
732
+ for file_path in files_to_delete:
733
+ try:
734
+ os.remove(file_path)
735
+ filename = os.path.basename(file_path)
736
+ logging.info(f"🗑️ Удален старый отчет: {filename}")
737
+ except Exception as e:
738
+ logging.warning(f"Не удалось удалить отчет {file_path}: {e}")
739
+
740
+ @staticmethod
741
+ def generate_console_report(bot_id: str, results: List[TestResult]):
742
+ """Генерирует отчет в консоль"""
743
+ passed_count = sum(1 for r in results if r.passed)
744
+ failed_count = len(results) - passed_count
745
+ total_steps = sum(r.total_steps for r in results)
746
+ passed_steps = sum(r.passed_steps for r in results)
747
+ success_rate = (passed_count / len(results)) * 100 if results else 0
748
+ step_success_rate = (passed_steps / total_steps) * 100 if total_steps else 0
749
+
750
+ print(f"\n📊 РЕЗУЛЬТАТЫ: {bot_id.upper()}")
751
+ print(f"✅ Сценариев пройдено: {passed_count}/{len(results)} ({success_rate:.1f}%)")
752
+ print(f"📝 Шагов пройдено: {passed_steps}/{total_steps} ({step_success_rate:.1f}%)")
753
+
754
+ # Определяем уровень качества по сценариям
755
+ if success_rate >= 90:
756
+ print("🎉 ОТЛИЧНО — бот готов к продакшену")
757
+ elif success_rate >= 80:
758
+ print("✅ ХОРОШО — небольшие улучшения")
759
+ elif success_rate >= 60:
760
+ print("⚠️ УДОВЛЕТВОРИТЕЛЬНО — требуются улучшения")
761
+ else:
762
+ print("🚨 ПЛОХО — критические проблемы с промптами")
763
+
764
+ @staticmethod
765
+ def generate_detailed_report(bot_id: str, results: List[TestResult]) -> str:
766
+ """Генерирует подробный отчет"""
767
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
768
+
769
+ report_lines = [
770
+ f"ОТЧЕТ ТЕСТИРОВАНИЯ: {bot_id.upper()}",
771
+ f"Время: {timestamp}",
772
+ f"Сценариев: {len(results)}",
773
+ ""
774
+ ]
775
+
776
+ # Статистика
777
+ passed_count = sum(1 for r in results if r.passed)
778
+ total_steps = sum(r.total_steps for r in results)
779
+ passed_steps = sum(r.passed_steps for r in results)
780
+ success_rate = (passed_count / len(results)) * 100 if results else 0
781
+ step_success_rate = (passed_steps / total_steps) * 100 if total_steps else 0
782
+
783
+ report_lines.extend([
784
+ f"УСПЕШНОСТЬ СЦЕНАРИЕВ: {success_rate:.1f}% ({passed_count}/{len(results)})",
785
+ f"УСПЕШНОСТЬ ШАГОВ: {step_success_rate:.1f}% ({passed_steps}/{total_steps})",
786
+ ""
787
+ ])
788
+
789
+ # Список ошибок в требуемом формате
790
+ failed_tests = [r for r in results if not r.passed]
791
+ if failed_tests:
792
+ report_lines.extend([
793
+ "═══════════════════════════════════════════════════════════════",
794
+ "СПИСОК ОШИБОК:",
795
+ "═══════════════════════════════════════════════════════════════"
796
+ ])
797
+
798
+ for result in failed_tests:
799
+ scenario = result.scenario
800
+ source_file = getattr(scenario, 'source_file', 'unknown')
801
+ scenario_number = getattr(scenario, 'scenario_number', '?')
802
+
803
+ # Правильное отображение имени сценария
804
+ display_name = scenario.name if not scenario.name.startswith('[') else f"[{source_file}-{scenario_number}]"
805
+ report_lines.extend([
806
+ f"ФАЙЛ: {source_file}.yaml | СЦЕНАРИЙ: {display_name}",
807
+ f"СТАТУС: {result.passed_steps}/{result.total_steps} шагов пройдено",
808
+ ""
809
+ ])
810
+
811
+ # Детальная информация о каждом шаге
812
+ for i, step_result in enumerate(result.step_results):
813
+ step_num = i + 1
814
+ status = "✅" if step_result.passed else "❌"
815
+
816
+ report_lines.extend([
817
+ f"ШАГ {step_num} {status}:",
818
+ f" Ввод: \"{step_result.step.user_input}\"",
819
+ f" Ожидаемые: {step_result.step.expected_keywords}",
820
+ f" Запрещенные: {step_result.step.forbidden_keywords}",
821
+ ""
822
+ ])
823
+
824
+ # Полный ответ бота для всех шагов (и успешных, и провальных)
825
+ bot_response = step_result.bot_response.strip()
826
+ # Заменяем переносы строк на символы для лучшей читаемости в отчете
827
+ bot_response_formatted = bot_response.replace('\n', '\\n')
828
+
829
+ report_lines.extend([
830
+ f" Полный ответ бота:",
831
+ f" \"{bot_response_formatted}\"",
832
+ ""
833
+ ])
834
+
835
+ # Конкретные ошибки для неуспешных шагов
836
+ if not step_result.passed:
837
+ if step_result.missing_keywords:
838
+ report_lines.append(f" ❌ НЕ НАЙДЕНЫ: {', '.join(step_result.missing_keywords)}")
839
+
840
+ if step_result.found_forbidden:
841
+ report_lines.append(f" ❌ НАЙДЕНЫ ЗАПРЕЩЕННЫЕ: {', '.join(step_result.found_forbidden)}")
842
+
843
+ report_lines.append("")
844
+
845
+ report_lines.extend([
846
+ "-" * 67,
847
+ ""
848
+ ])
849
+
850
+ # Краткая сводка по пройденным тестам
851
+ passed_tests = [r for r in results if r.passed]
852
+ if passed_tests:
853
+ report_lines.extend([
854
+ "ПРОЙДЕННЫЕ СЦЕНАРИИ:",
855
+ ""
856
+ ])
857
+ for result in passed_tests:
858
+ scenario = result.scenario
859
+ source_file = getattr(scenario, 'source_file', 'unknown')
860
+ scenario_number = getattr(scenario, 'scenario_number', '?')
861
+
862
+ # Правильное отображение имени сценария для пройденных тестов (сокращенный формат)
863
+ display_name = scenario.name if not scenario.name.startswith('[') else f"[{source_file}-{scenario_number}]"
864
+ report_lines.append(f"✅ {source_file}.yaml | {display_name}")
865
+
866
+ return "\n".join(report_lines)
867
+
868
+ @staticmethod
869
+ def save_report(bot_id: str, results: List[TestResult], project_root: Path = None) -> str:
870
+ """Сохраняет отчет в файл"""
871
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
872
+
873
+ # Используем переданную корневую директорию или глобальную
874
+ user_root_dir = project_root or get_project_root()
875
+
876
+ # Проверяем наличие папки bots/{bot_id}
877
+ bots_dir = user_root_dir / "bots" / bot_id
878
+ if not bots_dir.exists():
879
+ logging.warning(f"Папка bots/{bot_id} не найдена в проекте: {bots_dir}")
880
+ return ""
881
+
882
+ # Создаем папку отчетов в папке бота
883
+ reports_dir = user_root_dir / "bots" / bot_id / "reports"
884
+ os.makedirs(reports_dir, exist_ok=True)
885
+
886
+ # Очищаем старые отчеты перед созданием нового
887
+ ReportGenerator.cleanup_old_reports(str(reports_dir), max_reports=10)
888
+
889
+ # Лаконичное название файла
890
+ report_filename = reports_dir / f"test_{timestamp}.txt"
891
+
892
+ report_content = ReportGenerator.generate_detailed_report(bot_id, results)
893
+
894
+ with open(report_filename, 'w', encoding='utf-8') as f:
895
+ f.write(report_content)
896
+
897
+ return str(report_filename)
898
+
899
+
900
+ async def main():
901
+ """Главная функция CLI"""
902
+ parser = argparse.ArgumentParser(description="Система тестирования ботов")
903
+ parser.add_argument("bot_id", nargs="?", default="growthmed-october-24",
904
+ help="ID бота для тестирования")
905
+ parser.add_argument("scenario_file", nargs="?", default=None,
906
+ help="Название файла сценариев (без расширения или с .yaml)")
907
+ parser.add_argument("--scenario-file", dest="scenario_file_legacy",
908
+ help="Конкретный файл сценариев (устаревший параметр)")
909
+ parser.add_argument("--max-concurrent", type=int, default=5,
910
+ help="Максимальное количество параллельных тестов")
911
+ parser.add_argument("--verbose", "-v", action="store_true",
912
+ help="Подробный вывод")
913
+
914
+ args = parser.parse_args()
915
+
916
+ # Настройка логирования
917
+ log_level = logging.INFO if args.verbose else logging.INFO # Всегда показываем INFO
918
+ logging.basicConfig(level=log_level, format='%(message)s')
919
+
920
+ try:
921
+ # Определяем какой файл сценариев использовать
922
+ scenario_file = args.scenario_file or args.scenario_file_legacy
923
+
924
+ # Получаем корневую директорию проекта
925
+ project_root = get_project_root()
926
+
927
+ # Загружаем сценарии
928
+ if scenario_file:
929
+ # Обрабатываем название файла
930
+ if not scenario_file.endswith('.yaml'):
931
+ scenario_file += '.yaml'
932
+
933
+ # Проверяем наличие папки bots/{bot_id}
934
+ bots_dir = project_root / "bots" / args.bot_id
935
+ if not bots_dir.exists():
936
+ print(f"Папка bots/{args.bot_id} не найдена в проекте: {bots_dir}")
937
+ return 1
938
+
939
+ # Путь к файлу сценариев в корневой директории проекта
940
+ scenario_path = project_root / "bots" / args.bot_id / "tests" / scenario_file
941
+ scenarios = ScenarioLoader.load_scenarios_from_file(str(scenario_path))
942
+
943
+ if not scenarios:
944
+ print(f"Файл сценариев '{scenario_file}' не найден или пуст")
945
+ return 1
946
+ else:
947
+ scenarios = ScenarioLoader.load_all_scenarios_for_bot(args.bot_id, project_root)
948
+
949
+ if not scenarios:
950
+ print(f"Сценарии для бота '{args.bot_id}' не найдены")
951
+ return 1
952
+
953
+ print(f"🚀 Запуск тестирования бота: {args.bot_id}")
954
+ if scenario_file:
955
+ print(f"📋 Тестируется файл: {scenario_file}")
956
+ else:
957
+ print(f"📋 Тестируются все файлы сценариев")
958
+ print(f"📋 Найдено сценариев: {len(scenarios)}")
959
+
960
+ # Запускаем тесты
961
+ test_runner = TestRunner(args.bot_id, args.max_concurrent, project_root)
962
+ results = await test_runner.run_tests(scenarios)
963
+
964
+ # Генерируем отчеты
965
+ ReportGenerator.generate_console_report(args.bot_id, results)
966
+ report_file = ReportGenerator.save_report(args.bot_id, results, project_root)
967
+
968
+ print(f"\n📄 Подробный отчет сохранен: {report_file}")
969
+
970
+ # Возвращаем код выхода
971
+ failed_count = sum(1 for r in results if not r.passed)
972
+ return 0 if failed_count == 0 else 1
973
+
974
+ except Exception as e:
975
+ logging.error(f"Критическая ошибка: {e}")
976
+ return 1
977
+
978
+
979
+ if __name__ == "__main__":
980
+ try:
981
+ exit_code = asyncio.run(main())
982
+ sys.exit(exit_code)
983
+ except KeyboardInterrupt:
984
+ print("\n⏹️ Тестирование прервано пользователем")
985
+ sys.exit(130)
986
+