smart-bot-factory 0.3.6__py3-none-any.whl → 0.3.8__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.
- smart_bot_factory/admin/__init__.py +7 -7
- smart_bot_factory/admin/admin_events.py +483 -383
- smart_bot_factory/admin/admin_logic.py +234 -158
- smart_bot_factory/admin/admin_manager.py +68 -53
- smart_bot_factory/admin/admin_tester.py +46 -40
- smart_bot_factory/admin/timeout_checker.py +201 -153
- smart_bot_factory/aiogram_calendar/__init__.py +11 -3
- smart_bot_factory/aiogram_calendar/common.py +12 -18
- smart_bot_factory/aiogram_calendar/dialog_calendar.py +126 -64
- smart_bot_factory/aiogram_calendar/schemas.py +49 -28
- smart_bot_factory/aiogram_calendar/simple_calendar.py +94 -50
- smart_bot_factory/analytics/analytics_manager.py +414 -392
- smart_bot_factory/cli.py +204 -148
- smart_bot_factory/config.py +123 -102
- smart_bot_factory/core/bot_utils.py +480 -324
- smart_bot_factory/core/conversation_manager.py +287 -200
- smart_bot_factory/core/decorators.py +1145 -739
- smart_bot_factory/core/message_sender.py +287 -266
- smart_bot_factory/core/router.py +170 -100
- smart_bot_factory/core/router_manager.py +121 -83
- smart_bot_factory/core/states.py +4 -3
- smart_bot_factory/creation/__init__.py +1 -1
- smart_bot_factory/creation/bot_builder.py +320 -242
- smart_bot_factory/creation/bot_testing.py +440 -365
- smart_bot_factory/dashboard/__init__.py +1 -3
- smart_bot_factory/event/__init__.py +2 -7
- smart_bot_factory/handlers/handlers.py +682 -466
- smart_bot_factory/integrations/openai_client.py +218 -168
- smart_bot_factory/integrations/supabase_client.py +928 -637
- smart_bot_factory/message/__init__.py +18 -22
- smart_bot_factory/router/__init__.py +2 -2
- smart_bot_factory/setup_checker.py +162 -126
- smart_bot_factory/supabase/__init__.py +1 -1
- smart_bot_factory/supabase/client.py +631 -515
- smart_bot_factory/utils/__init__.py +2 -3
- smart_bot_factory/utils/debug_routing.py +38 -27
- smart_bot_factory/utils/prompt_loader.py +153 -120
- smart_bot_factory/utils/user_prompt_loader.py +55 -56
- smart_bot_factory/utm_link_generator.py +123 -116
- {smart_bot_factory-0.3.6.dist-info → smart_bot_factory-0.3.8.dist-info}/METADATA +3 -1
- smart_bot_factory-0.3.8.dist-info/RECORD +59 -0
- smart_bot_factory-0.3.6.dist-info/RECORD +0 -59
- {smart_bot_factory-0.3.6.dist-info → smart_bot_factory-0.3.8.dist-info}/WHEEL +0 -0
- {smart_bot_factory-0.3.6.dist-info → smart_bot_factory-0.3.8.dist-info}/entry_points.txt +0 -0
- {smart_bot_factory-0.3.6.dist-info → smart_bot_factory-0.3.8.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
|
|
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
|
-
|
|
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(
|
|
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 ..
|
|
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.
|
|
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__(
|
|
80
|
-
|
|
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__(
|
|
115
|
-
|
|
116
|
-
|
|
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,
|
|
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(
|
|
159
|
+
|
|
160
|
+
for i, scenario_data in enumerate(data.get("scenarios", [])):
|
|
150
161
|
# Проверяем новый формат со steps
|
|
151
|
-
if
|
|
162
|
+
if "steps" in scenario_data:
|
|
152
163
|
# Новый формат: сценарий с шагами
|
|
153
|
-
name = scenario_data.get(
|
|
164
|
+
name = scenario_data.get("name", f"[{file_name}-{i+1}]")
|
|
154
165
|
steps = []
|
|
155
|
-
|
|
156
|
-
for step_data in scenario_data[
|
|
166
|
+
|
|
167
|
+
for step_data in scenario_data["steps"]:
|
|
157
168
|
step = TestStep(
|
|
158
|
-
user_input=step_data.get(
|
|
159
|
-
expected_keywords=step_data.get(
|
|
160
|
-
forbidden_keywords=step_data.get(
|
|
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(
|
|
179
|
+
name = scenario_data.get("name", f"[{file_name}-{i+1}]")
|
|
169
180
|
step = TestStep(
|
|
170
|
-
user_input=scenario_data.get(
|
|
171
|
-
expected_keywords=scenario_data.get(
|
|
172
|
-
forbidden_keywords=scenario_data.get(
|
|
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(
|
|
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(
|
|
231
|
-
logging.info(
|
|
243
|
+
logging.info("")
|
|
244
|
+
logging.info("══════════════════════════════════════════════════════════")
|
|
232
245
|
logging.info(f"ИНИЦИАЛИЗАЦИЯ ТЕСТЕРА БОТА: {self.bot_id}")
|
|
233
|
-
logging.info(
|
|
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 /
|
|
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(
|
|
250
|
-
|
|
262
|
+
logging.info("Каталог конфигурации найден")
|
|
263
|
+
|
|
251
264
|
# Устанавливаем BOT_ID как в оригинальном файле
|
|
252
|
-
os.environ[
|
|
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 /
|
|
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(
|
|
261
|
-
|
|
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(
|
|
276
|
-
openai_model = os.getenv(
|
|
277
|
-
openai_max_tokens = int(os.getenv(
|
|
278
|
-
openai_temperature = float(os.getenv(
|
|
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(
|
|
282
|
-
supabase_key = os.getenv(
|
|
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(
|
|
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(
|
|
303
|
+
|
|
304
|
+
logging.info("Инициализация загрузчика промптов...")
|
|
304
305
|
# PromptLoader автоматически найдет все .txt файлы в папке промптов
|
|
305
|
-
self.prompt_loader = PromptLoader(prompts_dir=
|
|
306
|
-
logging.info(
|
|
307
|
-
|
|
306
|
+
self.prompt_loader = PromptLoader(prompts_dir=self.config_dir / "prompts")
|
|
307
|
+
logging.info("Загрузчик промптов создан")
|
|
308
|
+
|
|
308
309
|
# Инициализируем Supabase клиент
|
|
309
|
-
logging.info(
|
|
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(
|
|
316
|
-
|
|
314
|
+
logging.info("Supabase клиент создан")
|
|
315
|
+
|
|
317
316
|
logging.info(f"Бот {self.bot_id} инициализирован успешно!")
|
|
318
|
-
logging.info(
|
|
319
|
-
|
|
317
|
+
logging.info("══════════════════════════════════════════════════════════")
|
|
318
|
+
|
|
320
319
|
except Exception as e:
|
|
321
|
-
logging.error(
|
|
320
|
+
logging.error("══════════════════════════════════════════════════════════")
|
|
322
321
|
logging.error(f"ОШИБКА ИНИЦИАЛИЗАЦИИ БОТА: {self.bot_id}")
|
|
323
322
|
logging.error(f"Описание: {e}")
|
|
324
|
-
logging.error(
|
|
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(
|
|
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]
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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(
|
|
352
|
-
logging.info(
|
|
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(
|
|
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(
|
|
365
|
-
logging.info(
|
|
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(
|
|
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 =
|
|
387
|
-
|
|
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(
|
|
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(
|
|
462
|
-
logging.info(
|
|
463
|
-
logging.info(
|
|
464
|
-
|
|
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(
|
|
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(
|
|
472
|
-
logging.error(
|
|
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(
|
|
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 /
|
|
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 /
|
|
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,
|
|
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(
|
|
543
|
-
|
|
565
|
+
logging.info("🔄 Создание тестовой сессии...")
|
|
566
|
+
|
|
544
567
|
# 1. ЗАГРУЖАЕМ ПРОМПТЫ
|
|
545
|
-
logging.info(
|
|
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(
|
|
549
|
-
|
|
571
|
+
logging.info(
|
|
572
|
+
f"✅ Промпты загружены: система ({len(system_prompt)} симв.), приветствие ({len(welcome_message)} симв.)"
|
|
573
|
+
)
|
|
574
|
+
|
|
550
575
|
# 2. СОЗДАЕМ НОВУЮ СЕССИЮ
|
|
551
|
-
logging.info(
|
|
552
|
-
session_id = await self.supabase_client.create_chat_session(
|
|
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(
|
|
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(
|
|
565
|
-
|
|
591
|
+
logging.info("📎 Приветственный файл не найден или пуст")
|
|
592
|
+
|
|
566
593
|
# 5. СОХРАНЯЕМ ПРИВЕТСТВЕННОЕ СООБЩЕНИЕ В БД
|
|
567
|
-
logging.info(
|
|
594
|
+
logging.info("💾 Сохранение приветственного сообщения в БД...")
|
|
568
595
|
await self.supabase_client.add_message(
|
|
569
596
|
session_id=session_id,
|
|
570
|
-
role=
|
|
597
|
+
role="assistant",
|
|
571
598
|
content=welcome_message,
|
|
572
|
-
message_type=
|
|
599
|
+
message_type="text",
|
|
573
600
|
)
|
|
574
|
-
logging.info(
|
|
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(
|
|
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
|
-
|
|
592
|
-
|
|
622
|
+
|
|
593
623
|
# Читаем настройки напрямую из .env
|
|
594
|
-
max_context_messages = int(os.getenv(
|
|
624
|
+
max_context_messages = int(os.getenv("MAX_CONTEXT_MESSAGES", "20"))
|
|
595
625
|
|
|
596
626
|
try:
|
|
597
|
-
logging.info(
|
|
598
|
-
|
|
627
|
+
logging.info("📨 Обработка сообщения пользователя...")
|
|
628
|
+
|
|
599
629
|
# Сохраняем сообщение пользователя
|
|
600
|
-
logging.info(
|
|
630
|
+
logging.info("💾 Сохранение сообщения пользователя в БД...")
|
|
601
631
|
await self.supabase_client.add_message(
|
|
602
632
|
session_id=session_id,
|
|
603
|
-
role=
|
|
633
|
+
role="user",
|
|
604
634
|
content=user_message,
|
|
605
|
-
message_type=
|
|
635
|
+
message_type="text",
|
|
606
636
|
)
|
|
607
|
-
logging.info(
|
|
608
|
-
|
|
637
|
+
logging.info("✅ Сообщение пользователя сохранено")
|
|
638
|
+
|
|
609
639
|
# Получаем историю сообщений
|
|
610
|
-
logging.info(
|
|
611
|
-
|
|
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(
|
|
649
|
+
moscow_tz = pytz.timezone("Europe/Moscow")
|
|
616
650
|
current_time = datetime.now(moscow_tz)
|
|
617
|
-
time_info = current_time.strftime(
|
|
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
|
-
|
|
633
|
-
"content": msg['content']
|
|
634
|
-
})
|
|
635
|
-
|
|
665
|
+
messages.append({"role": msg["role"], "content": msg["content"]})
|
|
666
|
+
|
|
636
667
|
# Добавляем финальные инструкции
|
|
637
|
-
logging.info(
|
|
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(
|
|
672
|
+
logging.info(
|
|
673
|
+
f"✅ Финальные инструкции добавлены ({len(final_instructions)} симв.)"
|
|
674
|
+
)
|
|
642
675
|
else:
|
|
643
|
-
logging.info(
|
|
644
|
-
|
|
645
|
-
logging.info(
|
|
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(
|
|
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(
|
|
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(
|
|
710
|
+
logging.info("📝 Метаданные не найдены, используем исходный ответ")
|
|
676
711
|
elif not response_text.strip():
|
|
677
712
|
response_text = ai_response
|
|
678
|
-
logging.info(
|
|
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(
|
|
688
|
-
|
|
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(
|
|
732
|
+
logging.info("💾 Сохранение ответа ассистента в БД...")
|
|
692
733
|
await self.supabase_client.add_message(
|
|
693
734
|
session_id=session_id,
|
|
694
|
-
role=
|
|
735
|
+
role="assistant",
|
|
695
736
|
content=response_text,
|
|
696
|
-
message_type=
|
|
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(
|
|
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(
|
|
754
|
-
scenario=scenarios[i],
|
|
755
|
-
|
|
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[
|
|
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(
|
|
805
|
-
|
|
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
|
-
|
|
838
|
-
|
|
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,
|
|
854
|
-
scenario_number = getattr(scenario,
|
|
855
|
-
|
|
901
|
+
source_file = getattr(scenario, "source_file", "unknown")
|
|
902
|
+
scenario_number = getattr(scenario, "scenario_number", "?")
|
|
903
|
+
|
|
856
904
|
# Правильное отображение имени сценария
|
|
857
|
-
display_name =
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
f"
|
|
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
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
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(
|
|
889
|
-
|
|
890
|
-
report_lines.extend(
|
|
891
|
-
|
|
892
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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,
|
|
921
|
-
scenario_number = getattr(scenario,
|
|
922
|
-
|
|
972
|
+
source_file = getattr(scenario, "source_file", "unknown")
|
|
973
|
+
scenario_number = getattr(scenario, "scenario_number", "?")
|
|
974
|
+
|
|
923
975
|
# Правильное отображение имени сценария для пройденных тестов (сокращенный формат)
|
|
924
|
-
display_name =
|
|
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(
|
|
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,
|
|
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(
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
parser.add_argument(
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
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=
|
|
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(
|
|
992
|
-
scenario_file +=
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
-
|