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