From 763a1a2a169a778bdb49a203afeb33b174da3325 Mon Sep 17 00:00:00 2001 From: Maksim Totmin Date: Fri, 19 Jun 2026 20:36:34 +0700 Subject: [PATCH] cleanup: remove dead archive_auditor module archive_auditor.py not imported or used by main.py since archive metrics were removed from the monitoring loop. Remove the dead module along with its 10 tests and dead imports (ModelsMonitoringConfig, AppConfig, ArchiveEntry). Tests: 59 -> 48 (all pass) --- src/ses_monitor/__init__.py | 1 - src/ses_monitor/archive_auditor.py | 433 ----------------------------- tests/test_scenario_runner.py | 415 --------------------------- 3 files changed, 849 deletions(-) delete mode 100644 src/ses_monitor/archive_auditor.py diff --git a/src/ses_monitor/__init__.py b/src/ses_monitor/__init__.py index 8b16ff3..8daa914 100644 --- a/src/ses_monitor/__init__.py +++ b/src/ses_monitor/__init__.py @@ -7,7 +7,6 @@ Основные компоненты: - checker: быстрые liveness-проверки (доступность API, лицензия) - scenario_runner: выполнение сценариев диалогов из YAML-файлов - - archive_auditor: анализ архива диалогов на ошибки - zabbix: отправка метрик в Zabbix через zabbix_sender """ diff --git a/src/ses_monitor/archive_auditor.py b/src/ses_monitor/archive_auditor.py deleted file mode 100644 index 08a7cf6..0000000 --- a/src/ses_monitor/archive_auditor.py +++ /dev/null @@ -1,433 +0,0 @@ -""" -Анализ архива диалогов SES на наличие ошибок и качество работы моделей. - -Проверяет записи в /ses/archive/list за последние N минут: - - Количество диалогов с флагом unknown (не распознано). - - Количество диалогов с событием "not found" или "no data". - - Общее количество диалогов (чтобы заметить резкое падение). - - SEE: точность распознавания намерений/сущностей (confidence, detections). - - SMC: точность классификации (confidence, classifications). - - TTS: наличие голосовых ответов. - - QAS: наполненность вопросно-ответной системы. - - SPR: распознавание речи (Speech Recognition) — наличие текстовых расшифровок. - -Метрики агрегируются для отправки в Zabbix. -""" - -from __future__ import annotations - -import logging -from dataclasses import dataclass, field -from datetime import datetime, timedelta, timezone -from typing import Any, Dict, List, Optional, Tuple - -from ses_monitor.config import ModelsMonitoringConfig -from ses_monitor.session_client import SESClient, ArchiveEntry - -log = logging.getLogger("ses_monitor.archive") - -# Формат даты в archive.list (пример: "2026-06-19 10:30:00") -_ARCHIVE_DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S" - - -@dataclass -class ModelMetric: - """Агрегированная метрика по одной модели (SEE или SMC).""" - - name: str = "" # Имя модели (например, "fio", "phone2", "project_classifier") - total: int = 0 # Всего детекций/классификаций - low_confidence: int = 0 # Количество срабатываний ниже порога - sum_confidence: float = 0.0 # Сумма confidence для расчёта среднего - - @property - def avg_confidence(self) -> float: - """Средняя уверенность модели за период.""" - if self.total == 0: - return 0.0 - return self.sum_confidence / self.total - - -@dataclass -class ArchiveAuditResult: - """Результат анализа архива за период.""" - - period_start: str = "" - period_end: str = "" - - # === Базовые метрики === - - total_dialogs: int = 0 - unknown_count: int = 0 - not_found_count: int = 0 - no_data_count: int = 0 - closed_count: int = 0 - answered_count: int = 0 - - # === SEE (распознавание намерений/сущностей) === - - see_total: int = 0 # Всего детекций SEE - see_low_confidence: int = 0 # Детекции ниже порога - see_avg_confidence: float = 0.0 # Средняя уверенность по всем SEE - # Детализация по конкретным SEE-моделям - see_by_model: Dict[str, ModelMetric] = field(default_factory=dict) - - # === SMC (классификация) === - - smc_total: int = 0 # Всего классификаций SMC - smc_low_confidence: int = 0 # Классификации ниже порога - smc_avg_confidence: float = 0.0 # Средняя уверенность по всем SMC - # Детализация по конкретным SMC-моделям - smc_by_model: Dict[str, ModelMetric] = field(default_factory=dict) - - # === TTS (синтез речи) === - - tts_voice_present: int = 0 # Ответы с голосом - tts_voice_missing: int = 0 # Ответы без голоса - - # === QAS (вопросно-ответная система) === - - qas_total: int = 0 # Диалоги с полем qas - qas_empty: int = 0 # Пустые qas - qas_filled: int = 0 # Заполненные qas - - # === SPR (распознавание речи: аудио→текст) === - - spr_total: int = 0 # Диалоги с полем reports - spr_filled: int = 0 # Заполненные reports - spr_empty: int = 0 # Пустые reports - - # === Время шага диалога === - # Парсится из raw["request"]["datetime"] / raw["reply"]["datetime"] - - avg_step_time_ms: float = 0.0 # Среднее время обработки шага (мс) - max_step_time_ms: float = 0.0 # Максимальное время (мс) - # Внутренние аккумуляторы для расчёта среднего - _total_step_time_ms: float = 0.0 - _step_time_count: int = 0 - - # === Ошибки === - - fetch_error: str = "" - - def to_zabbix_metrics(self, host: str) -> List[Tuple[str, str]]: - """Форматирует метрики для Zabbix. - - Returns: - Список кортежей (ключ_метрики, значение). - """ - metrics: List[Tuple[str, str]] = [ - # Базовые - ("ses.archive.total_5m", str(self.total_dialogs)), - ("ses.archive.unknown_5m", str(self.unknown_count)), - ("ses.archive.not_found_5m", str(self.not_found_count)), - ("ses.archive.no_data_5m", str(self.no_data_count)), - ("ses.archive.closed_5m", str(self.closed_count)), - ("ses.archive.answered_5m", str(self.answered_count)), - # SEE - ("ses.models.see.total", str(self.see_total)), - ("ses.models.see.low_confidence", str(self.see_low_confidence)), - ("ses.models.see.avg_confidence", str(round(self.see_avg_confidence, 3))), - # SMC - ("ses.models.smc.total", str(self.smc_total)), - ("ses.models.smc.low_confidence", str(self.smc_low_confidence)), - ("ses.models.smc.avg_confidence", str(round(self.smc_avg_confidence, 3))), - # TTS - ("ses.models.tts.voice_present", str(self.tts_voice_present)), - ("ses.models.tts.voice_missing", str(self.tts_voice_missing)), - # QAS - ("ses.models.qas.total", str(self.qas_total)), - ("ses.models.qas.empty", str(self.qas_empty)), - # SPR - ("ses.models.spr.total", str(self.spr_total)), - ("ses.models.spr.filled", str(self.spr_filled)), - # Время шага - ("ses.archive.avg_step_time_5m", str(round(self.avg_step_time_ms))), - ("ses.archive.max_step_time_5m", str(round(self.max_step_time_ms))), - ] - return metrics - - -class ArchiveAuditor: - """Анализирует архив диалогов SES. - - Запускается с заданным интервалом (например, каждые 10 минут) и проверяет - записи за последний период (например, 5 минут). - - Отслеживает: - - Базовые ошибки (unknown, not_found, no_data). - - Качество SEE (entity/intent recognition): confidence, detections. - - Качество SMC (classification): confidence, classifications. - - Доступность TTS (text-to-speech): наличие голосовых ответов. - - Наполненность QAS (question-answer): пустые qas. - - Наполненность SPR (распознавание речи): непустые reports. - """ - - def __init__( - self, - lookback_minutes: int = 5, - models_config: Optional[ModelsMonitoringConfig] = None, - step_time_enabled: bool = True, - ): - self._lookback = lookback_minutes - self._models_config = models_config or ModelsMonitoringConfig() - self._step_time_enabled = step_time_enabled - - async def audit(self, client: SESClient) -> ArchiveAuditResult: - """Выполняет анализ архива за последние N минут. - - Args: - client: HTTP-клиент для SES API. - - Returns: - Результат анализа со всеми метриками. - """ - now = datetime.now(timezone.utc) - period_start = now - timedelta(minutes=self._lookback) - - start_str = period_start.strftime("%Y-%m-%d %H:%M") - end_str = now.strftime("%Y-%m-%d %H:%M") - - result = ArchiveAuditResult( - period_start=start_str, - period_end=end_str, - ) - - try: - entries = await client.get_archive( - start_date=start_str, - end_date=end_str, - ) - - result.total_dialogs = len(entries) - - for entry in entries: - # Базовые проверки - self._audit_base(result, entry) - - # Мониторинг моделей - if self._models_config.enabled: - self._audit_models(result, entry) - - # Время шага диалога - if self._step_time_enabled: - self._calc_step_time(result, entry) - - # Расчёт средних значений - if result.see_total > 0: - total_conf = sum(m.sum_confidence for m in result.see_by_model.values()) - result.see_avg_confidence = total_conf / result.see_total - - if result.smc_total > 0: - total_conf = sum(m.sum_confidence for m in result.smc_by_model.values()) - result.smc_avg_confidence = total_conf / result.smc_total - - if result._step_time_count > 0: - result.avg_step_time_ms = result._total_step_time_ms / result._step_time_count - - log.info( - "Archive audit: %d диалогов за %d мин " - "(unknown=%d, nf=%d, nd=%d, closed=%d, answered=%d) | " - "SEE: %d det (low=%d, avg=%.2f) | " - "SMC: %d cls (low=%d, avg=%.2f) | " - "TTS: voice=%d no=%d | QAS: empty=%d/%d | SPR: filled=%d/%d | " - "step_time: avg=%dms max=%dms", - result.total_dialogs, self._lookback, - result.unknown_count, result.not_found_count, - result.no_data_count, result.closed_count, - result.answered_count, - result.see_total, result.see_low_confidence, - result.see_avg_confidence, - result.smc_total, result.smc_low_confidence, - result.smc_avg_confidence, - result.tts_voice_present, result.tts_voice_missing, - result.qas_empty, result.qas_total, - result.spr_filled, result.spr_total, - round(result.avg_step_time_ms), round(result.max_step_time_ms), - ) - - except Exception as e: - result.fetch_error = str(e) - log.error("Archive audit: ошибка получения данных: %s", e) - - return result - - # ------------------------------------------------------------------ - # Базовые проверки (без изменений) - # ------------------------------------------------------------------ - - def _audit_base(self, result: ArchiveAuditResult, entry: ArchiveEntry) -> None: - """Базовые проверки: unknown, not_found, no_data.""" - if entry.unknown: - result.unknown_count += 1 - - for event in entry.events: - if event == "not found": - result.not_found_count += 1 - elif event == "no data": - result.no_data_count += 1 - - if entry.closed and entry.closed is not False: - result.closed_count += 1 - - if entry.answered: - result.answered_count += 1 - - # ------------------------------------------------------------------ - # Мониторинг внутренних моделей SES - # ------------------------------------------------------------------ - - def _audit_models(self, result: ArchiveAuditResult, entry: ArchiveEntry) -> None: - """Анализирует модели SEE, SMC, TTS, QAS, SPR в одной записи архива.""" - raw = entry.raw - - # --- SEE и SMC: разбор request.data --- - request_data = raw.get("request", {}).get("data", []) - if isinstance(request_data, list): - for item in request_data: - item_type = item.get("type", "") - - if item_type == "see": - result.see_total += 1 - self._track_see(item, result) - - elif item_type == "smc": - result.smc_total += 1 - self._track_smc(item, result) - - # --- TTS: проверка голосовых ответов --- - if self._models_config.tts_enabled: - answers = raw.get("reply", {}).get("answers", []) - if isinstance(answers, list): - for ans in answers: - voice = ans.get("voice", "") - if voice: - result.tts_voice_present += 1 - else: - result.tts_voice_missing += 1 - - # --- QAS: проверка наполненности --- - if self._models_config.qas_enabled: - qas = raw.get("current", {}).get("qas", "") - result.qas_total += 1 - if not qas or qas == "": - result.qas_empty += 1 - else: - result.qas_filled += 1 - - # --- SPR: проверка reports --- - if self._models_config.spr_enabled: - reports = raw.get("reports", {}) - result.spr_total += 1 - if isinstance(reports, dict) and len(reports) > 0: - result.spr_filled += 1 - else: - result.spr_empty += 1 - - def _track_see(self, item: Dict[str, Any], result: ArchiveAuditResult) -> None: - """Учитывает одну SEE-детекцию в агрегированных метриках. - - SEE (Semantic Entity Extraction) — извлечение сущностей и намерений. - Каждая детекция имеет: model (имя модели), reply.confidence (уверенность). - """ - model_name = item.get("model", "unknown") - confidence = self._extract_confidence(item) - - # Глобальная агрегация - if confidence is not None and confidence < self._models_config.see_confidence_threshold: - result.see_low_confidence += 1 - - # Поимённая агрегация - metric = result.see_by_model.get(model_name) - if metric is None: - metric = ModelMetric(name=model_name) - result.see_by_model[model_name] = metric - - metric.total += 1 - if confidence is not None: - metric.sum_confidence += confidence - if confidence < self._models_config.see_confidence_threshold: - metric.low_confidence += 1 - - def _track_smc(self, item: Dict[str, Any], result: ArchiveAuditResult) -> None: - """Учитывает одну SMC-классификацию в агрегированных метриках. - - SMC (Semantic Meaning Classification) — классификация запроса. - Каждая классификация имеет: model (имя классификатора), reply.confidence. - """ - model_name = item.get("model", "unknown") - confidence = self._extract_confidence(item) - - if confidence is not None and confidence < self._models_config.smc_confidence_threshold: - result.smc_low_confidence += 1 - - metric = result.smc_by_model.get(model_name) - if metric is None: - metric = ModelMetric(name=model_name) - result.smc_by_model[model_name] = metric - - metric.total += 1 - if confidence is not None: - metric.sum_confidence += confidence - if confidence < self._models_config.smc_confidence_threshold: - metric.low_confidence += 1 - - @staticmethod - def _extract_confidence(item: Dict[str, Any]) -> Optional[float]: - """Извлекает confidence из reply-блока элемента запроса. - - Возможные структуры: - reply.confidence (число) - reply.position / reply.length (для NER, confidence обычно есть) - """ - reply = item.get("reply", {}) - if isinstance(reply, dict): - conf = reply.get("confidence") - if conf is not None: - try: - return float(conf) - except (TypeError, ValueError): - return None - return None - - def _calc_step_time(self, result: ArchiveAuditResult, entry: ArchiveEntry) -> None: - """Парсит request.datetime / reply.datetime из записи архива и считает разницу. - - Обновляет: - - result._total_step_time_ms — суммарное время - - result._step_time_count — количество учтённых записей - - result.max_step_time_ms — максимальное время - """ - raw = entry.raw - req_dt_str = raw.get("request", {}).get("datetime") - rep_dt_str = raw.get("reply", {}).get("datetime") - - if not req_dt_str or not rep_dt_str: - return - - try: - req_dt = datetime.strptime(str(req_dt_str), _ARCHIVE_DATETIME_FORMAT) - rep_dt = datetime.strptime(str(rep_dt_str), _ARCHIVE_DATETIME_FORMAT) - except (ValueError, TypeError): - return - - # Пропускаем если время ответа меньше времени запроса (рассинхрон часов) - if rep_dt < req_dt: - return - - ms = (rep_dt - req_dt).total_seconds() * 1000 - - result._total_step_time_ms += ms - result._step_time_count += 1 - if ms > result.max_step_time_ms: - result.max_step_time_ms = ms - - # Лог предупреждения для диалогов дольше 5 секунд - if ms > 5000: - robot_label = entry.robot[-24:] if len(entry.robot) > 24 else entry.robot - log.warning( - "Медленный диалог: session=%s time=%dms request=\"%s\" robot=%s", - entry.session, - round(ms), - entry.request_message[:80], - robot_label, - ) diff --git a/tests/test_scenario_runner.py b/tests/test_scenario_runner.py index fcc548f..e4f0de9 100644 --- a/tests/test_scenario_runner.py +++ b/tests/test_scenario_runner.py @@ -20,8 +20,6 @@ import pytest import yaml from ses_monitor.config import ( - AppConfig, - ModelsMonitoringConfig, ScenarioConfig, ScenarioStep, load_scenarios, @@ -562,419 +560,6 @@ class TestAggregateScenarioResults: assert agg["max_time"] == 11 # round(10.6) = 11 -# ============================================================================= -# Тесты: archive_auditor.py -# ============================================================================= - -class TestArchiveAuditor: - """Тесты анализа архива диалогов.""" - - @pytest.mark.asyncio - async def test_audit_normal_dialogs(self): - """Проверяет анализ нормальных диалогов без ошибок.""" - from ses_monitor.archive_auditor import ArchiveAuditor - - auditor = ArchiveAuditor(lookback_minutes=5) - mock_client = AsyncMock() - # Возвращаем пустой список — нет данных - mock_client.get_archive.return_value = [] - - result = await auditor.audit(mock_client) - assert result.total_dialogs == 0 - assert result.unknown_count == 0 - assert result.not_found_count == 0 - - @pytest.mark.asyncio - async def test_audit_with_errors(self): - """Проверяет анализ диалогов с ошибками.""" - from ses_monitor.archive_auditor import ArchiveAuditor - from ses_monitor.session_client import ArchiveEntry - - auditor = ArchiveAuditor(lookback_minutes=5) - - # Создаём тестовые записи архива - entries = [ - ArchiveEntry( - id="1", session="s1", robot="test-bot", hostname="test", - channel="telegram", unknown=True, closed=None, - endpoint=None, answered=False, conversion=False, - models=[], events=["not found"], - request_message="тест", last_reply="ответ", - raw={}, - ), - ArchiveEntry( - id="2", session="s2", robot="test-bot", hostname="test", - channel="telegram", unknown=False, closed="2026-01-01", - endpoint=None, answered=True, conversion=True, - models=[], events=["no data"], - request_message="тест2", last_reply="ответ2", - raw={}, - ), - ArchiveEntry( - id="3", session="s3", robot="test-bot", hostname="test", - channel="telegram", unknown=False, closed=None, - endpoint=None, answered=True, conversion=True, - models=[], events=[], - request_message="тест3", last_reply="ответ3", - raw={}, - ), - ] - - mock_client = AsyncMock() - mock_client.get_archive.return_value = entries - - result = await auditor.audit(mock_client) - assert result.total_dialogs == 3 - assert result.unknown_count == 1 - assert result.not_found_count == 1 - assert result.no_data_count == 1 - assert result.closed_count == 1 - assert result.answered_count == 2 - - @pytest.mark.asyncio - async def test_audit_see_model_confidence(self): - """Проверяет мониторинг SEE: уверенность распознавания.""" - from ses_monitor.archive_auditor import ArchiveAuditor - from ses_monitor.session_client import ArchiveEntry - - config = ModelsMonitoringConfig( - enabled=True, - see_confidence_threshold=0.5, - ) - auditor = ArchiveAuditor(lookback_minutes=5, models_config=config) - - # Запись с высокой уверенностью SEE - entry_high = ArchiveEntry( - id="1", session="s1", robot="test-bot", hostname="test", - channel="telegram", unknown=False, closed=None, - endpoint=None, answered=True, conversion=True, - models=["see:fio"], - events=[], request_message="тест", last_reply="ответ", - raw={ - "request": { - "data": [ - { - "type": "see", - "model": "fio", - "reply": {"confidence": 0.95}, - } - ] - } - }, - ) - - # Запись с низкой уверенностью SEE - entry_low = ArchiveEntry( - id="2", session="s2", robot="test-bot", hostname="test", - channel="telegram", unknown=False, closed=None, - endpoint=None, answered=True, conversion=True, - models=["see:fio"], - events=[], request_message="тест2", last_reply="ответ2", - raw={ - "request": { - "data": [ - { - "type": "see", - "model": "fio", - "reply": {"confidence": 0.3}, - } - ] - } - }, - ) - - mock_client = AsyncMock() - mock_client.get_archive.return_value = [entry_high, entry_low] - - result = await auditor.audit(mock_client) - assert result.see_total == 2 - assert result.see_low_confidence == 1 # Только вторая запись - assert result.see_avg_confidence == pytest.approx(0.625, abs=0.01) - - @pytest.mark.asyncio - async def test_audit_smc_model_confidence(self): - """Проверяет мониторинг SMC: уверенность классификации.""" - from ses_monitor.archive_auditor import ArchiveAuditor - from ses_monitor.session_client import ArchiveEntry - - config = ModelsMonitoringConfig( - enabled=True, - smc_confidence_threshold=0.7, - ) - auditor = ArchiveAuditor(lookback_minutes=5, models_config=config) - - entry = ArchiveEntry( - id="1", session="s1", robot="test-bot", hostname="test", - channel="telegram", unknown=False, closed=None, - endpoint=None, answered=True, conversion=True, - models=["smc:test_classifier"], - events=[], request_message="тест", last_reply="ответ", - raw={ - "request": { - "data": [ - { - "type": "smc", - "model": "test_classifier", - "reply": {"confidence": 0.65, "class": "test"}, - } - ] - } - }, - ) - - mock_client = AsyncMock() - mock_client.get_archive.return_value = [entry] - - result = await auditor.audit(mock_client) - assert result.smc_total == 1 - assert result.smc_low_confidence == 1 # 0.65 < 0.7 - assert result.smc_avg_confidence == 0.65 - - @pytest.mark.asyncio - async def test_audit_tts_voice_detection(self): - """Проверяет мониторинг TTS: наличие голосовых ответов.""" - from ses_monitor.archive_auditor import ArchiveAuditor - from ses_monitor.session_client import ArchiveEntry - - config = ModelsMonitoringConfig(enabled=True, tts_enabled=True) - auditor = ArchiveAuditor(lookback_minutes=5, models_config=config) - - entry = ArchiveEntry( - id="1", session="s1", robot="test-bot", hostname="test", - channel="telegram", unknown=False, closed=None, - endpoint=None, answered=True, conversion=True, - models=[], events=[], request_message="тест", last_reply="ответ", - raw={ - "reply": { - "answers": [ - {"messages": ["здравствуйте"], "voice": "Коля"}, - {"messages": ["до свидания"], "voice": ""}, - ] - } - }, - ) - - mock_client = AsyncMock() - mock_client.get_archive.return_value = [entry] - - result = await auditor.audit(mock_client) - assert result.tts_voice_present == 1 - assert result.tts_voice_missing == 1 - - @pytest.mark.asyncio - async def test_audit_qas_emptiness(self): - """Проверяет мониторинг QAS: пустые ответы.""" - from ses_monitor.archive_auditor import ArchiveAuditor - from ses_monitor.session_client import ArchiveEntry - - config = ModelsMonitoringConfig(enabled=True, qas_enabled=True) - auditor = ArchiveAuditor(lookback_minutes=5, models_config=config) - - entry_empty = ArchiveEntry( - id="1", session="s1", robot="test-bot", hostname="test", - channel="telegram", unknown=False, closed=None, - endpoint=None, answered=True, conversion=True, - models=[], events=[], request_message="тест", last_reply="ответ", - raw={"current": {"qas": ""}}, - ) - entry_filled = ArchiveEntry( - id="2", session="s2", robot="test-bot", hostname="test", - channel="telegram", unknown=False, closed=None, - endpoint=None, answered=True, conversion=True, - models=[], events=[], request_message="тест2", last_reply="ответ2", - raw={"current": {"qas": "какой-то ответ QAS"}}, - ) - - mock_client = AsyncMock() - mock_client.get_archive.return_value = [entry_empty, entry_filled] - - result = await auditor.audit(mock_client) - assert result.qas_total == 2 - assert result.qas_empty == 1 - assert result.qas_filled == 1 - - @pytest.mark.asyncio - async def test_audit_spr_reports(self): - """Проверяет мониторинг SPR: наполненность reports.""" - from ses_monitor.archive_auditor import ArchiveAuditor - from ses_monitor.session_client import ArchiveEntry - - config = ModelsMonitoringConfig(enabled=True, spr_enabled=True) - auditor = ArchiveAuditor(lookback_minutes=5, models_config=config) - - entry_filled = ArchiveEntry( - id="1", session="s1", robot="test-bot", hostname="test", - channel="telegram", unknown=False, closed=None, - endpoint=None, answered=True, conversion=True, - models=[], events=[], request_message="тест", last_reply="ответ", - raw={"reports": {"recognition": "текст расшифровки"}}, - ) - entry_empty = ArchiveEntry( - id="2", session="s2", robot="test-bot", hostname="test", - channel="telegram", unknown=False, closed=None, - endpoint=None, answered=True, conversion=True, - models=[], events=[], request_message="тест2", last_reply="ответ2", - raw={"reports": {}}, - ) - - mock_client = AsyncMock() - mock_client.get_archive.return_value = [entry_filled, entry_empty] - - result = await auditor.audit(mock_client) - assert result.spr_total == 2 - assert result.spr_filled == 1 - assert result.spr_empty == 1 - - @pytest.mark.asyncio - async def test_audit_models_disabled(self): - """Проверяет что при отключённом model_monitoring метрики нулевые.""" - from ses_monitor.archive_auditor import ArchiveAuditor - from ses_monitor.session_client import ArchiveEntry - - config = ModelsMonitoringConfig(enabled=False) - auditor = ArchiveAuditor(lookback_minutes=5, models_config=config) - - entry = ArchiveEntry( - id="1", session="s1", robot="test-bot", hostname="test", - channel="telegram", unknown=False, closed=None, - endpoint=None, answered=True, conversion=True, - models=["see:fio"], - events=[], request_message="тест", last_reply="ответ", - raw={ - "request": { - "data": [ - {"type": "see", "model": "fio", "reply": {"confidence": 0.3}} - ] - }, - "reply": {"answers": [{"voice": "Коля"}]}, - "current": {"qas": "ответ"}, - "reports": {"rec": "текст"}, - }, - ) - - mock_client = AsyncMock() - mock_client.get_archive.return_value = [entry] - - result = await auditor.audit(mock_client) - # Все model-метрики должны быть нулевыми - assert result.see_total == 0 - assert result.smc_total == 0 - assert result.tts_voice_present == 0 - assert result.qas_total == 0 - assert result.spr_total == 0 - - @pytest.mark.asyncio - async def test_audit_step_time_normal(self): - """Проверяет расчёт времени шага диалога из архива.""" - from ses_monitor.archive_auditor import ArchiveAuditor - from ses_monitor.session_client import ArchiveEntry - - auditor = ArchiveAuditor(lookback_minutes=5, step_time_enabled=True) - - entry1 = ArchiveEntry( - id="1", session="s-fast", robot="test-bot", hostname="test", - channel="telegram", unknown=False, closed=None, - endpoint=None, answered=True, conversion=True, - models=[], events=[], request_message="быстрый запрос", - last_reply="быстрый ответ", - raw={ - "request": {"datetime": "2026-06-19 10:00:00"}, - "reply": {"datetime": "2026-06-19 10:00:01"}, - }, - ) - entry2 = ArchiveEntry( - id="2", session="s-slow", robot="test-bot", hostname="test", - channel="telegram", unknown=False, closed=None, - endpoint=None, answered=True, conversion=True, - models=[], events=[], request_message="медленный запрос", - last_reply="медленный ответ", - raw={ - "request": {"datetime": "2026-06-19 10:00:00"}, - "reply": {"datetime": "2026-06-19 10:00:03"}, - }, - ) - - mock_client = AsyncMock() - mock_client.get_archive.return_value = [entry1, entry2] - - result = await auditor.audit(mock_client) - assert result._step_time_count == 2 - assert result.max_step_time_ms == 3000.0 # 3 секунды - assert result.avg_step_time_ms == 2000.0 # (1000 + 3000) / 2 - - @pytest.mark.asyncio - async def test_audit_step_time_missing_dates(self): - """Проверяет что записи без datetime не ломают расчёт.""" - from ses_monitor.archive_auditor import ArchiveAuditor - from ses_monitor.session_client import ArchiveEntry - - auditor = ArchiveAuditor(lookback_minutes=5, step_time_enabled=True) - - entry_no_dates = ArchiveEntry( - id="1", session="s-nodate", robot="test-bot", hostname="test", - channel="telegram", unknown=False, closed=None, - endpoint=None, answered=True, conversion=True, - models=[], events=[], request_message="тест", last_reply="ответ", - raw={}, - ) - entry_partial = ArchiveEntry( - id="2", session="s-partial", robot="test-bot", hostname="test", - channel="telegram", unknown=False, closed=None, - endpoint=None, answered=True, conversion=True, - models=[], events=[], request_message="тест2", last_reply="ответ2", - raw={ - "request": {"datetime": "2026-06-19 10:00:00"}, - # Нет reply.datetime - }, - ) - entry_bad_format = ArchiveEntry( - id="3", session="s-bad", robot="test-bot", hostname="test", - channel="telegram", unknown=False, closed=None, - endpoint=None, answered=True, conversion=True, - models=[], events=[], request_message="тест3", last_reply="ответ3", - raw={ - "request": {"datetime": "неправильный формат"}, - "reply": {"datetime": "2026-06-19 10:00:01"}, - }, - ) - - mock_client = AsyncMock() - mock_client.get_archive.return_value = [entry_no_dates, entry_partial, entry_bad_format] - - result = await auditor.audit(mock_client) - # Ни одна запись не дала времени — всё пропущено - assert result._step_time_count == 0 - assert result.max_step_time_ms == 0.0 - assert result.avg_step_time_ms == 0.0 - - @pytest.mark.asyncio - async def test_audit_step_time_disabled(self): - """Проверяет что при отключённом step_time метрики нулевые.""" - from ses_monitor.archive_auditor import ArchiveAuditor - from ses_monitor.session_client import ArchiveEntry - - auditor = ArchiveAuditor(lookback_minutes=5, step_time_enabled=False) - - entry = ArchiveEntry( - id="1", session="s1", robot="test-bot", hostname="test", - channel="telegram", unknown=False, closed=None, - endpoint=None, answered=True, conversion=True, - models=[], events=[], request_message="тест", last_reply="ответ", - raw={ - "request": {"datetime": "2026-06-19 10:00:00"}, - "reply": {"datetime": "2026-06-19 10:00:05"}, - }, - ) - - mock_client = AsyncMock() - mock_client.get_archive.return_value = [entry] - - result = await auditor.audit(mock_client) - assert result._step_time_count == 0 - assert result.max_step_time_ms == 0.0 - assert result.avg_step_time_ms == 0.0 - - @pytest.mark.asyncio class TestUserParameterWriter: """Тесты для UserParameterWriter."""