smart-bot-factory 0.3.7__py3-none-any.whl → 0.3.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of smart-bot-factory might be problematic. Click here for more details.

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