first commit
This commit is contained in:
commit
9b78a5a1bb
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
60
README.md
Normal file
60
README.md
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
# SEE/SMC Autotests
|
||||||
|
|
||||||
|
Набор тестов для проверки моделей **SEE** (Semantic Entity Extraction) и **SMC** (Short Message Classification) после обновления.
|
||||||
|
|
||||||
|
## Файлы
|
||||||
|
|
||||||
|
| Файл | Назначение |
|
||||||
|
|------|-----------|
|
||||||
|
| `test_see_models.py` | Тест всех моделей SEE (entity + classifier) |
|
||||||
|
| `test_smc_models.py` | Тест всех моделей SMC (classifier) |
|
||||||
|
| `see_asserts.json` | Ожидаемые проверки для SEE (опционально) |
|
||||||
|
| `smc_asserts.json` | Ожидаемые проверки для SMC (опционально) |
|
||||||
|
|
||||||
|
## Использование
|
||||||
|
|
||||||
|
### Без проверок (только непустой ответ)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 test_see_models.py
|
||||||
|
python3 test_smc_models.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### С проверками по asserts
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 test_see_models.py --asserts see_asserts.json
|
||||||
|
python3 test_smc_models.py --asserts smc_asserts.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Другой сервер
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 test_see_models.py --host 10.101.60.27 --port 6184
|
||||||
|
python3 test_smc_models.py --host 10.101.60.27 --port 6181
|
||||||
|
```
|
||||||
|
|
||||||
|
## Формат asserts
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"asserts": [
|
||||||
|
{
|
||||||
|
"model": "fio",
|
||||||
|
"text": "Иванов Иван Иванович",
|
||||||
|
"checks": {
|
||||||
|
"min_found": 1,
|
||||||
|
"text_contains": "иван",
|
||||||
|
"confidence_min": 0.3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `model` — имя модели (подстрока для поиска)
|
||||||
|
- `text` — текст для запроса (опционально)
|
||||||
|
- `min_found` — минимальное количество найденных сущностей/классов
|
||||||
|
- `text_contains` — подстрока в тексте/классе первой сущности
|
||||||
|
- `calculated_contains` — подстрока в `calculated` (только entity)
|
||||||
|
- `confidence_min` — минимальный confidence
|
||||||
2009
see_asserts.json
Normal file
2009
see_asserts.json
Normal file
File diff suppressed because it is too large
Load Diff
237
smc_asserts.json
Normal file
237
smc_asserts.json
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
{
|
||||||
|
"_format": "Массив asserts для SMC. Все модели типа classifier.",
|
||||||
|
"asserts": [
|
||||||
|
{
|
||||||
|
"model": "МФЦ НСК~bf505da3280184027363ec7349c0e6fc",
|
||||||
|
"text": "хочу записаться в мфц",
|
||||||
|
"checks": {
|
||||||
|
"confidence_min": 0.1,
|
||||||
|
"min_found": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "МФЦ НСК~ffb403fff9f5802d53aac8a5c844af26",
|
||||||
|
"text": "хочу записаться в мфц",
|
||||||
|
"checks": {
|
||||||
|
"confidence_min": 0.1,
|
||||||
|
"min_found": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "филиалы 54_МФЦ-backup",
|
||||||
|
"text": "хочу записаться в мфц",
|
||||||
|
"checks": {
|
||||||
|
"confidence_min": 0.1,
|
||||||
|
"min_found": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "mfc_test_old-backup",
|
||||||
|
"text": "хочу записаться в мфц",
|
||||||
|
"checks": {
|
||||||
|
"confidence_min": 0.1,
|
||||||
|
"min_found": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "тест мфц нск-new",
|
||||||
|
"text": "хочу записаться в мфц",
|
||||||
|
"checks": {
|
||||||
|
"confidence_min": 0.1,
|
||||||
|
"min_found": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "МФЦ НСК-backup~4f2d37c9a020797ba3151b7814303264",
|
||||||
|
"text": "хочу записаться в мфц",
|
||||||
|
"checks": {
|
||||||
|
"confidence_min": 0.1,
|
||||||
|
"min_found": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "МФЦ НСК-backup",
|
||||||
|
"text": "хочу записаться в мфц",
|
||||||
|
"checks": {
|
||||||
|
"confidence_min": 0.1,
|
||||||
|
"min_found": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "mfc_test_old",
|
||||||
|
"text": "хочу записаться в мфц",
|
||||||
|
"checks": {
|
||||||
|
"confidence_min": 0.1,
|
||||||
|
"min_found": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "ekc_smc",
|
||||||
|
"text": "записаться в екц",
|
||||||
|
"checks": {
|
||||||
|
"confidence_min": 0.1,
|
||||||
|
"min_found": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "ekc_smc-backup",
|
||||||
|
"text": "записаться в екц",
|
||||||
|
"checks": {
|
||||||
|
"confidence_min": 0.1,
|
||||||
|
"min_found": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "minsoc_20_july",
|
||||||
|
"text": "какие выплаты от минсоца",
|
||||||
|
"checks": {
|
||||||
|
"confidence_min": 0.1,
|
||||||
|
"min_found": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "МФЦ НСК-backup~081406f0904c707cf068a6fbb8f249bb",
|
||||||
|
"text": "хочу записаться в мфц",
|
||||||
|
"checks": {
|
||||||
|
"confidence_min": 0.1,
|
||||||
|
"min_found": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "филиалы 54_МФЦ",
|
||||||
|
"text": "хочу записаться в мфц",
|
||||||
|
"checks": {
|
||||||
|
"confidence_min": 0.1,
|
||||||
|
"min_found": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "МФЦ НСК-backup~ffb403fff9f5802d53aac8a5c844af26",
|
||||||
|
"text": "хочу записаться в мфц",
|
||||||
|
"checks": {
|
||||||
|
"confidence_min": 0.1,
|
||||||
|
"min_found": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "ЕКЦ НСК~ffb403fff9f5802d53aac8a5c844af26",
|
||||||
|
"text": "записаться в екц",
|
||||||
|
"checks": {
|
||||||
|
"confidence_min": 0.1,
|
||||||
|
"min_found": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "offices_mfc",
|
||||||
|
"text": "хочу записаться в мфц",
|
||||||
|
"checks": {
|
||||||
|
"confidence_min": 0.1,
|
||||||
|
"min_found": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "minsoc_20_july-backup",
|
||||||
|
"text": "какие выплаты от минсоца",
|
||||||
|
"checks": {
|
||||||
|
"confidence_min": 0.1,
|
||||||
|
"min_found": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "Техническая поддержка",
|
||||||
|
"text": "не работает компьютер",
|
||||||
|
"checks": {
|
||||||
|
"confidence_min": 0.1,
|
||||||
|
"min_found": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "Тест~ffb403fff9f5802d53aac8a5c844af26",
|
||||||
|
"text": "тестовое сообщение",
|
||||||
|
"checks": {
|
||||||
|
"confidence_min": 0.1,
|
||||||
|
"min_found": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "филиалы 54_МФЦ~bf505da3280184027363ec7349c0e6fc",
|
||||||
|
"text": "хочу записаться в мфц",
|
||||||
|
"checks": {
|
||||||
|
"confidence_min": 0.1,
|
||||||
|
"min_found": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "МФЦ НСК",
|
||||||
|
"text": "хочу записаться в мфц",
|
||||||
|
"checks": {
|
||||||
|
"confidence_min": 0.1,
|
||||||
|
"min_found": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "МФЦ НСК~4f2d37c9a020797ba3151b7814303264",
|
||||||
|
"text": "хочу записаться в мфц",
|
||||||
|
"checks": {
|
||||||
|
"confidence_min": 0.1,
|
||||||
|
"min_found": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "МФЦ НСК~081406f0904c707cf068a6fbb8f249bb",
|
||||||
|
"text": "хочу записаться в мфц",
|
||||||
|
"checks": {
|
||||||
|
"confidence_min": 0.1,
|
||||||
|
"min_found": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "филиалы 54_МФЦ~4f2d37c9a020797ba3151b7814303264",
|
||||||
|
"text": "хочу записаться в мфц",
|
||||||
|
"checks": {
|
||||||
|
"confidence_min": 0.1,
|
||||||
|
"min_found": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "ЕКЦ НСК-backup~ffb403fff9f5802d53aac8a5c844af26",
|
||||||
|
"text": "записаться в екц",
|
||||||
|
"checks": {
|
||||||
|
"confidence_min": 0.1,
|
||||||
|
"min_found": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "МФЦ2-new",
|
||||||
|
"text": "хочу записаться в мфц",
|
||||||
|
"checks": {
|
||||||
|
"confidence_min": 0.1,
|
||||||
|
"min_found": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "Тест-backup~ffb403fff9f5802d53aac8a5c844af26",
|
||||||
|
"text": "тестовое сообщение",
|
||||||
|
"checks": {
|
||||||
|
"confidence_min": 0.1,
|
||||||
|
"min_found": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "МФЦ1",
|
||||||
|
"text": "хочу записаться в мфц",
|
||||||
|
"checks": {
|
||||||
|
"confidence_min": 0.1,
|
||||||
|
"min_found": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "филиалы 54_МФЦ-backup~bf505da3280184027363ec7349c0e6fc",
|
||||||
|
"text": "хочу записаться в мфц",
|
||||||
|
"checks": {
|
||||||
|
"confidence_min": 0.1,
|
||||||
|
"min_found": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
374
test_see_models.py
Normal file
374
test_see_models.py
Normal file
@ -0,0 +1,374 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""SEE model autotest — проверяет все загруженные модели после обновления.
|
||||||
|
|
||||||
|
Использование:
|
||||||
|
python3 test_see_models.py
|
||||||
|
python3 test_see_models.py --host 10.101.60.26
|
||||||
|
python3 test_see_models.py --host 10.101.60.25 --port 6184
|
||||||
|
python3 test_see_models.py --asserts see_asserts.json
|
||||||
|
|
||||||
|
Файл ассертов (JSON):
|
||||||
|
{
|
||||||
|
"asserts": [
|
||||||
|
{
|
||||||
|
"model": "fio", // подстрока для поиска модели
|
||||||
|
"type": "entity|classifier",
|
||||||
|
"text": "Иванов Иван Иванович", // текст для запроса
|
||||||
|
"checks": {
|
||||||
|
// для entity:
|
||||||
|
"min_found": 1, // мин. количество найденных сущностей
|
||||||
|
"text_contains": "иванов", // подстрока в text первой сущности
|
||||||
|
"calculated_contains": "Ф:", // подстрока в calculated
|
||||||
|
"confidence_min": 0.5, // мин. confidence
|
||||||
|
|
||||||
|
// для classifier:
|
||||||
|
"min_classes": 1, // мин. количество классов
|
||||||
|
"top_class_contains": "Yes", // подстрока в top классе
|
||||||
|
"confidence_min": 0.5 // мин. confidence топ-класса
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Возвращает exit code 0 если все модели прошли, иначе 1.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
try:
|
||||||
|
import urllib.request as urlreq
|
||||||
|
import urllib.error as urlerr
|
||||||
|
except ImportError:
|
||||||
|
urlreq = None
|
||||||
|
|
||||||
|
DEFAULT_HOST = "10.101.60.25"
|
||||||
|
DEFAULT_PORT = 6184
|
||||||
|
BASE_PATH = "/see"
|
||||||
|
|
||||||
|
PRETRAINED_TESTS = {
|
||||||
|
"fio": {"text": "Иванов Иван Иванович", "hint": "ФИО"},
|
||||||
|
"phone": {"text": "8 913 456 78 90", "hint": "телефон"},
|
||||||
|
"number": {"text": "пятьсот двадцать три", "hint": "число"},
|
||||||
|
"date": {"text": "на завтра", "hint": "дата"},
|
||||||
|
"uuid": {"text": "550e8400-e29b-41d4-a716-446655440000", "hint": "UUID"},
|
||||||
|
"time": {"text": "в пять часов вечера", "hint": "время"},
|
||||||
|
}
|
||||||
|
|
||||||
|
HANDLER_TESTS = {
|
||||||
|
"fio2": {"text": "Иванов Иван Иванович", "type": "entity"},
|
||||||
|
"phone2": {"text": "8 913 456 78 90", "type": "entity"},
|
||||||
|
"record_date": {"text": "на завтра", "type": "entity"},
|
||||||
|
"record_time": {"text": "в пять часов вечера", "type": "entity"},
|
||||||
|
"yesno": {"text": "да", "type": "classifier"},
|
||||||
|
"yesno_obzv": {"text": "да", "type": "classifier"},
|
||||||
|
"weekday": {"text": "в пятницу", "type": "classifier"},
|
||||||
|
"end_repeat": {"text": "все, до свидания", "type": "classifier"},
|
||||||
|
"operatorcheck": {"text": "соедините с оператором", "type": "classifier"},
|
||||||
|
"see_grade": {"text": "пять", "type": "classifier"},
|
||||||
|
"other_payment": {"text": "мобилизационные выплаты", "type": "classifier"},
|
||||||
|
"mobilizac": {"text": "мобилизационные", "type": "classifier"},
|
||||||
|
"matcap_direct": {"text": "на улучшение жилищных условий", "type": "classifier"},
|
||||||
|
"new_minsoc_subsidii": {"text": "субсидия на жкх", "type": "classifier"},
|
||||||
|
"new_trud": {"text": "потерял работу", "type": "classifier"},
|
||||||
|
"rating_med": {"text": "отлично", "type": "classifier"},
|
||||||
|
"zags": {"text": "загс центрального района", "type": "classifier"},
|
||||||
|
"zags_handler": {"text": "загс на нижегородской", "type": "classifier"},
|
||||||
|
"filial": {"text": "в мошковский филиал", "type": "classifier"},
|
||||||
|
"offices_mfc_see": {"text": "родники", "type": "classifier"},
|
||||||
|
"service_mfc_see": {"text": "хочу получить водительское удостоверение", "type": "classifier"},
|
||||||
|
"nsk_fillials": {"text": "в филиал на родниках", "type": "classifier"},
|
||||||
|
}
|
||||||
|
|
||||||
|
CUSTOM_MODEL_MATCHES = [
|
||||||
|
("record_date", "на завтра"),
|
||||||
|
("record_time", "в пять часов вечера"),
|
||||||
|
("fio", "Иванов Иван Иванович"),
|
||||||
|
("phone", "8 913 456 78 90"),
|
||||||
|
("date", "на завтра"),
|
||||||
|
("time", "в пять часов вечера"),
|
||||||
|
("uuid", "550e8400-e29b-41d4-a716-446655440000"),
|
||||||
|
("number", "пятьсот двадцать три"),
|
||||||
|
("filial", "в филиал на родниках"),
|
||||||
|
("zags", "загс центрального района"),
|
||||||
|
("yesno", "да"),
|
||||||
|
("weekday", "в пятницу"),
|
||||||
|
("grade", "пять"),
|
||||||
|
("obzv", "да"),
|
||||||
|
("operator", "соедините с оператором"),
|
||||||
|
("repeat", "все, до свидания"),
|
||||||
|
("matcap", "на улучшение жилищных условий"),
|
||||||
|
("mobilizac", "мобилизационные"),
|
||||||
|
("trud", "потерял работу"),
|
||||||
|
("subsidii", "субсидия на жкх"),
|
||||||
|
("rating", "отлично"),
|
||||||
|
("payment", "мобилизационные выплаты"),
|
||||||
|
("service_mfc", "хочу получить паспорт"),
|
||||||
|
("offices_mfc", "родники"),
|
||||||
|
("паспорт", "хочу получить загранпаспорт"),
|
||||||
|
("ЕПГУ", "проверить статус заявления на госуслугах"),
|
||||||
|
("лаврентьев", "как получить лаврентьевскую карту"),
|
||||||
|
("возраст", "мне 25 лет"),
|
||||||
|
("загран", "хочу сделать загранпаспорт"),
|
||||||
|
("вернуться", "вернуться в главное меню"),
|
||||||
|
("не знаю код", "не знаю код подразделения"),
|
||||||
|
("оценк", "оцениваю обслуживание на пять"),
|
||||||
|
("несогласи", "я не согласен с этим"),
|
||||||
|
("начало диалог", "здравствуйте"),
|
||||||
|
("пособие", "какое пособие по рождению"),
|
||||||
|
("УК", "проблема с управляющей компанией"),
|
||||||
|
]
|
||||||
|
|
||||||
|
FALLBACK_TEXT = "Здравствуйте, хочу записаться на приём в МФЦ для получения паспорта, запись на завтра"
|
||||||
|
|
||||||
|
|
||||||
|
def load_asserts(path):
|
||||||
|
if not path:
|
||||||
|
return []
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
return data.get("asserts", [])
|
||||||
|
|
||||||
|
|
||||||
|
class SeeAPI:
|
||||||
|
def __init__(self, host=DEFAULT_HOST, port=DEFAULT_PORT):
|
||||||
|
self.base = f"http://{host}:{port}{BASE_PATH}"
|
||||||
|
|
||||||
|
def get_models(self):
|
||||||
|
return json.loads(self._request(f"{self.base}/models"))
|
||||||
|
|
||||||
|
def test_entity(self, model_id, text, similarity=70):
|
||||||
|
encoded = quote(model_id, safe="")
|
||||||
|
url = f"{self.base}/entities/{encoded}?text={quote(text)}&similarity={similarity}"
|
||||||
|
return json.loads(self._request(url))
|
||||||
|
|
||||||
|
def _request(self, url, timeout=15):
|
||||||
|
try:
|
||||||
|
with urlreq.urlopen(url, timeout=timeout) as r:
|
||||||
|
return r.read().decode("utf-8")
|
||||||
|
except urlerr.HTTPError as e:
|
||||||
|
raise RuntimeError(f"HTTP {e.code}: {e.read().decode('utf-8')[:200]}")
|
||||||
|
except urlerr.URLError as e:
|
||||||
|
raise RuntimeError(f"Connection failed: {e.reason}")
|
||||||
|
|
||||||
|
|
||||||
|
class TestSeeModels(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.api = SeeAPI(cls.args.host, cls.args.port)
|
||||||
|
cls.asserts = load_asserts(cls.args.asserts)
|
||||||
|
data = cls.api.get_models()
|
||||||
|
TestSeeModels.models = data
|
||||||
|
total = len(data["models"]) + len(data["handlers"]) + len(data["pretrained"])
|
||||||
|
print(f"\n models: {len(data['models'])}, "
|
||||||
|
f"handlers: {len(data['handlers'])}, "
|
||||||
|
f"pretrained: {len(data['pretrained'])}")
|
||||||
|
if cls.asserts:
|
||||||
|
print(f" asserts loaded: {len(cls.asserts)}")
|
||||||
|
if total == 0:
|
||||||
|
raise unittest.SkipTest("Нет ни одной модели")
|
||||||
|
|
||||||
|
def _find_assert(self, model_id):
|
||||||
|
for a in self.asserts:
|
||||||
|
if a["model"] in model_id:
|
||||||
|
return a
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _check_assert(self, model_id, result):
|
||||||
|
a = self._find_assert(model_id)
|
||||||
|
if not a:
|
||||||
|
return
|
||||||
|
checks = a.get("checks", {})
|
||||||
|
has_classes = isinstance(result, dict) and "classes" in result
|
||||||
|
if has_classes:
|
||||||
|
classes = result.get("classes", [])
|
||||||
|
if "min_found" in checks:
|
||||||
|
self.assertGreaterEqual(len(classes), checks["min_found"],
|
||||||
|
f"{model_id}: expected >= {checks['min_found']} items, got {len(classes)}")
|
||||||
|
if classes and "text_contains" in checks:
|
||||||
|
self.assertIn(checks["text_contains"], classes[0].get("class", ""),
|
||||||
|
f"{model_id}: missing '{checks['text_contains']}'")
|
||||||
|
if classes and "confidence_min" in checks:
|
||||||
|
self.assertGreaterEqual(classes[0].get("confidence", 0), checks["confidence_min"],
|
||||||
|
f"{model_id}: confidence < {checks['confidence_min']}")
|
||||||
|
else:
|
||||||
|
entities = []
|
||||||
|
for v in result.values():
|
||||||
|
if isinstance(v, list):
|
||||||
|
entities.extend(v)
|
||||||
|
count = len(entities)
|
||||||
|
if "min_found" in checks:
|
||||||
|
self.assertGreaterEqual(count, checks["min_found"],
|
||||||
|
f"{model_id}: expected >= {checks['min_found']} entities, got {count}")
|
||||||
|
if count > 0:
|
||||||
|
first = entities[0]
|
||||||
|
if "text_contains" in checks:
|
||||||
|
self.assertIn(checks["text_contains"], first.get("text", ""),
|
||||||
|
f"{model_id}: text missing '{checks['text_contains']}'")
|
||||||
|
if "calculated_contains" in checks:
|
||||||
|
self.assertIn(checks["calculated_contains"], first.get("calculated", ""),
|
||||||
|
f"{model_id}: calculated missing '{checks['calculated_contains']}'")
|
||||||
|
if "confidence_min" in checks:
|
||||||
|
self.assertGreaterEqual(first.get("confidence", 0), checks["confidence_min"],
|
||||||
|
f"{model_id}: confidence < {checks['confidence_min']}")
|
||||||
|
|
||||||
|
def _text_for_model(self, model_id):
|
||||||
|
a = self._find_assert(model_id)
|
||||||
|
if a and "text" in a:
|
||||||
|
return a["text"]
|
||||||
|
for substr, text in CUSTOM_MODEL_MATCHES:
|
||||||
|
if substr in model_id:
|
||||||
|
return text
|
||||||
|
return FALLBACK_TEXT
|
||||||
|
|
||||||
|
def _is_empty(self, result):
|
||||||
|
if not result or result == {}:
|
||||||
|
return True
|
||||||
|
return all(
|
||||||
|
isinstance(v, list) and len(v) == 0 for v in result.values()
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_01_list_models(self):
|
||||||
|
"""Получение списка моделей"""
|
||||||
|
data = self.models
|
||||||
|
self.assertIn("models", data)
|
||||||
|
self.assertIn("handlers", data)
|
||||||
|
self.assertIn("pretrained", data)
|
||||||
|
|
||||||
|
def _fmt_result(self, model_id, result):
|
||||||
|
print(f" >> {model_id}")
|
||||||
|
if isinstance(result, dict) and "classes" in result:
|
||||||
|
for c in result["classes"][:5]:
|
||||||
|
print(f" class={c.get('class','?')} conf={c.get('confidence',0):.3f}")
|
||||||
|
if len(result["classes"]) > 5:
|
||||||
|
print(f" ... ({len(result['classes'])} classes)")
|
||||||
|
else:
|
||||||
|
for key, entities in result.items():
|
||||||
|
if isinstance(entities, list):
|
||||||
|
for e in entities[:3]:
|
||||||
|
text = e.get("text", "?")
|
||||||
|
calc = e.get("calculated", "?")
|
||||||
|
conf = e.get("confidence", 0)
|
||||||
|
print(f" {key}: text='{text}' calc='{calc}' conf={conf:.3f}")
|
||||||
|
if len(entities) > 3:
|
||||||
|
print(f" ... ({len(entities)} entities)")
|
||||||
|
|
||||||
|
def test_02_pretrained_models(self):
|
||||||
|
"""Проверка pretrained-моделей"""
|
||||||
|
errors = []
|
||||||
|
for model_id, td in PRETRAINED_TESTS.items():
|
||||||
|
if model_id not in self.models.get("pretrained", []):
|
||||||
|
errors.append(f" {model_id}: нет в списке")
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
result = self.api.test_entity(model_id, td["text"])
|
||||||
|
if self._is_empty(result):
|
||||||
|
errors.append(f" {model_id}: пустой результат")
|
||||||
|
else:
|
||||||
|
self._check_assert(model_id, result)
|
||||||
|
self._fmt_result(model_id, result)
|
||||||
|
except AssertionError:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(f" {model_id}: {e}")
|
||||||
|
if errors:
|
||||||
|
self.fail("Ошибки pretrained:\n" + "\n".join(errors))
|
||||||
|
|
||||||
|
def test_03_handler_models(self):
|
||||||
|
"""Проверка handler-моделей"""
|
||||||
|
errors = []
|
||||||
|
handlers = self.models.get("handlers", [])
|
||||||
|
if not handlers:
|
||||||
|
self.skipTest("Нет handler моделей")
|
||||||
|
for model_id in handlers:
|
||||||
|
text = FALLBACK_TEXT
|
||||||
|
for key, td in HANDLER_TESTS.items():
|
||||||
|
if key in model_id:
|
||||||
|
text = td["text"]
|
||||||
|
break
|
||||||
|
a = self._find_assert(model_id)
|
||||||
|
if a and "text" in a:
|
||||||
|
text = a["text"]
|
||||||
|
try:
|
||||||
|
result = self.api.test_entity(model_id, text)
|
||||||
|
if self._is_empty(result):
|
||||||
|
print(f" [!] {model_id}: пустой результат")
|
||||||
|
else:
|
||||||
|
self._check_assert(model_id, result)
|
||||||
|
self._fmt_result(model_id, result)
|
||||||
|
except AssertionError:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(f" {model_id}: {e}")
|
||||||
|
if errors:
|
||||||
|
self.fail("Ошибки handler:\n" + "\n".join(errors))
|
||||||
|
|
||||||
|
def test_04_custom_models(self):
|
||||||
|
"""Проверка кастомных моделей"""
|
||||||
|
errors = []
|
||||||
|
models = self.models.get("models", [])
|
||||||
|
if not models:
|
||||||
|
self.skipTest("Нет кастомных моделей")
|
||||||
|
for model_id in models:
|
||||||
|
text = self._text_for_model(model_id)
|
||||||
|
try:
|
||||||
|
result = self.api.test_entity(model_id, text)
|
||||||
|
if self._is_empty(result):
|
||||||
|
print(f" [!] {model_id}")
|
||||||
|
else:
|
||||||
|
self._check_assert(model_id, result)
|
||||||
|
self._fmt_result(model_id, result)
|
||||||
|
except AssertionError:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(f" {model_id}: {e}")
|
||||||
|
if errors:
|
||||||
|
self.fail("Ошибки кастомных моделей:\n" + "\n".join(errors))
|
||||||
|
|
||||||
|
def test_05_handler_pretrained_coverage(self):
|
||||||
|
"""Проверка handler моделей phone/uuid/number/fio"""
|
||||||
|
errors = []
|
||||||
|
handlers = self.models.get("handlers", [])
|
||||||
|
for htype, td in [("phone", PRETRAINED_TESTS["phone"]),
|
||||||
|
("uuid", PRETRAINED_TESTS["uuid"]),
|
||||||
|
("number", PRETRAINED_TESTS["number"]),
|
||||||
|
("fio", PRETRAINED_TESTS["fio"])]:
|
||||||
|
matching = [h for h in handlers if htype in h.lower()]
|
||||||
|
if not matching:
|
||||||
|
continue
|
||||||
|
for mid in matching[:2]:
|
||||||
|
try:
|
||||||
|
result = self.api.test_entity(mid, td["text"])
|
||||||
|
if self._is_empty(result):
|
||||||
|
print(f" [!] {mid} ({td['hint']})")
|
||||||
|
else:
|
||||||
|
self._check_assert(mid, result)
|
||||||
|
self._fmt_result(mid, result)
|
||||||
|
except AssertionError:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(f" {mid}: {e}")
|
||||||
|
if errors:
|
||||||
|
self.fail("Ошибки handler coverage:\n" + "\n".join(errors))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="SEE model autotest")
|
||||||
|
parser.add_argument("--host", default=DEFAULT_HOST, help=f"Хост (default: {DEFAULT_HOST})")
|
||||||
|
parser.add_argument("--port", type=int, default=DEFAULT_PORT, help=f"Порт (default: {DEFAULT_PORT})")
|
||||||
|
parser.add_argument("--asserts", default=None, help="JSON файл с ассертами")
|
||||||
|
args = parser.parse_args()
|
||||||
|
TestSeeModels.args = args
|
||||||
|
|
||||||
|
suite = unittest.TestLoader().loadTestsFromTestCase(TestSeeModels)
|
||||||
|
runner = unittest.TextTestRunner(verbosity=2)
|
||||||
|
result = runner.run(suite)
|
||||||
|
sys.exit(0 if result.wasSuccessful() else 1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
202
test_smc_models.py
Normal file
202
test_smc_models.py
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""SMC model autotest — проверяет все загруженные модели классификации после обновления.
|
||||||
|
|
||||||
|
Использование:
|
||||||
|
python3 test_smc_models.py
|
||||||
|
python3 test_smc_models.py --host 10.101.60.27
|
||||||
|
python3 test_smc_models.py --host 10.101.60.27 --port 6181
|
||||||
|
python3 test_smc_models.py --asserts smc_asserts.json
|
||||||
|
|
||||||
|
Файл ассертов (JSON):
|
||||||
|
{
|
||||||
|
"asserts": [
|
||||||
|
{
|
||||||
|
"model": "МФЦ НСК", // подстрока для поиска модели
|
||||||
|
"type": "classifier",
|
||||||
|
"text": "записаться на приём", // опционально, свой текст
|
||||||
|
"checks": {
|
||||||
|
"min_classes": 1, // мин. количество классов
|
||||||
|
"top_class_contains": "МФЦ", // подстрока в top классе
|
||||||
|
"confidence_min": 0.5 // мин. confidence топ-класса
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Возвращает exit code 0 если все модели прошли, иначе 1.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
try:
|
||||||
|
import urllib.request as urlreq
|
||||||
|
import urllib.error as urlerr
|
||||||
|
except ImportError:
|
||||||
|
urlreq = None
|
||||||
|
|
||||||
|
DEFAULT_HOST = "10.101.60.27"
|
||||||
|
DEFAULT_PORT = 6181
|
||||||
|
BASE_PATH = "/smc"
|
||||||
|
|
||||||
|
DEFAULT_TEXT = "Здравствуйте, хочу записаться на приём в МФЦ для получения паспорта"
|
||||||
|
|
||||||
|
|
||||||
|
def load_asserts(path):
|
||||||
|
if not path:
|
||||||
|
return []
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
return data.get("asserts", [])
|
||||||
|
|
||||||
|
|
||||||
|
class SmcAPI:
|
||||||
|
def __init__(self, host=DEFAULT_HOST, port=DEFAULT_PORT):
|
||||||
|
self.base = f"http://{host}:{port}{BASE_PATH}"
|
||||||
|
|
||||||
|
def get_models(self):
|
||||||
|
return json.loads(self._request(f"{self.base}/models"))
|
||||||
|
|
||||||
|
def classify(self, model_id, text):
|
||||||
|
encoded = quote(model_id, safe="")
|
||||||
|
url = f"{self.base}/classify/{encoded}?text={quote(text)}"
|
||||||
|
return json.loads(self._request(url))
|
||||||
|
|
||||||
|
def _request(self, url, timeout=30):
|
||||||
|
try:
|
||||||
|
with urlreq.urlopen(url, timeout=timeout) as r:
|
||||||
|
return r.read().decode("utf-8")
|
||||||
|
except urlerr.HTTPError as e:
|
||||||
|
raise RuntimeError(f"HTTP {e.code}: {e.read().decode('utf-8')[:200]}")
|
||||||
|
except urlerr.URLError as e:
|
||||||
|
raise RuntimeError(f"Connection failed: {e.reason}")
|
||||||
|
|
||||||
|
|
||||||
|
class TestSmcModels(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.api = SmcAPI(cls.args.host, cls.args.port)
|
||||||
|
cls.asserts = load_asserts(cls.args.asserts)
|
||||||
|
data = cls.api.get_models()
|
||||||
|
TestSmcModels.models = data
|
||||||
|
models = data.get("models", [])
|
||||||
|
handlers = data.get("handlers", [])
|
||||||
|
print(f"\n models: {len(models)}, handlers: {len(handlers)}")
|
||||||
|
if cls.asserts:
|
||||||
|
print(f" asserts loaded: {len(cls.asserts)}")
|
||||||
|
if len(models) + len(handlers) == 0:
|
||||||
|
raise unittest.SkipTest("Нет ни одной модели")
|
||||||
|
|
||||||
|
def _find_assert(self, model_id):
|
||||||
|
for a in self.asserts:
|
||||||
|
if a["model"] in model_id:
|
||||||
|
return a
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _check_assert(self, model_id, result):
|
||||||
|
a = self._find_assert(model_id)
|
||||||
|
if not a:
|
||||||
|
return
|
||||||
|
checks = a.get("checks", {})
|
||||||
|
classes = result.get("classes", [])
|
||||||
|
if "min_found" in checks:
|
||||||
|
self.assertGreaterEqual(len(classes), checks["min_found"],
|
||||||
|
f"{model_id}: expected >= {checks['min_found']} classes, got {len(classes)}")
|
||||||
|
if classes and "confidence_min" in checks:
|
||||||
|
self.assertGreaterEqual(classes[0].get("confidence", 0), checks["confidence_min"],
|
||||||
|
f"{model_id}: top confidence < {checks['confidence_min']}")
|
||||||
|
|
||||||
|
def test_01_list_models(self):
|
||||||
|
"""Получение списка моделей"""
|
||||||
|
data = self.models
|
||||||
|
self.assertIn("models", data)
|
||||||
|
self.assertIn("handlers", data)
|
||||||
|
print(f"\n models: {len(data['models'])}")
|
||||||
|
print(f" handlers: {len(data['handlers'])}")
|
||||||
|
|
||||||
|
def test_02_models(self):
|
||||||
|
"""Классификация через модели"""
|
||||||
|
errors = []
|
||||||
|
models = self.models.get("models", [])
|
||||||
|
for mid in models:
|
||||||
|
if not mid:
|
||||||
|
continue
|
||||||
|
text = DEFAULT_TEXT
|
||||||
|
a = self._find_assert(mid)
|
||||||
|
if a and "text" in a:
|
||||||
|
text = a["text"]
|
||||||
|
try:
|
||||||
|
result = self.api.classify(mid, text)
|
||||||
|
classes = result.get("classes", [])
|
||||||
|
if not classes:
|
||||||
|
print(f" [!] {mid}: пустой результат")
|
||||||
|
else:
|
||||||
|
self._check_assert(mid, result)
|
||||||
|
print(f" >> {mid}")
|
||||||
|
for c in classes[:5]:
|
||||||
|
cl = c.get("class", "?")
|
||||||
|
conf = c.get("confidence", 0)
|
||||||
|
print(f" class='{cl}' conf={conf:.3f}")
|
||||||
|
if len(classes) > 5:
|
||||||
|
print(f" ... ({len(classes)} classes)")
|
||||||
|
except AssertionError:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(f" {mid}: {e}")
|
||||||
|
if errors:
|
||||||
|
self.fail("Ошибки моделей:\n" + "\n".join(errors))
|
||||||
|
|
||||||
|
def test_03_handlers(self):
|
||||||
|
"""Классификация через handler модели"""
|
||||||
|
errors = []
|
||||||
|
handlers = self.models.get("handlers", [])
|
||||||
|
if not handlers:
|
||||||
|
self.skipTest("Нет handler моделей")
|
||||||
|
for mid in handlers:
|
||||||
|
if not mid:
|
||||||
|
continue
|
||||||
|
text = DEFAULT_TEXT
|
||||||
|
a = self._find_assert(mid)
|
||||||
|
if a and "text" in a:
|
||||||
|
text = a["text"]
|
||||||
|
try:
|
||||||
|
result = self.api.classify(mid, text)
|
||||||
|
classes = result.get("classes", [])
|
||||||
|
if not classes:
|
||||||
|
print(f" [!] {mid}: пустой результат")
|
||||||
|
else:
|
||||||
|
self._check_assert(mid, result)
|
||||||
|
print(f" >> {mid}")
|
||||||
|
for c in classes[:5]:
|
||||||
|
cl = c.get("class", "?")
|
||||||
|
conf = c.get("confidence", 0)
|
||||||
|
print(f" class='{cl}' conf={conf:.3f}")
|
||||||
|
if len(classes) > 5:
|
||||||
|
print(f" ... ({len(classes)} classes)")
|
||||||
|
except AssertionError:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(f" {mid}: {e}")
|
||||||
|
if errors:
|
||||||
|
self.fail("Ошибки internal:\n" + "\n".join(errors))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="SMC model autotest")
|
||||||
|
parser.add_argument("--host", default=DEFAULT_HOST, help=f"Хост (default: {DEFAULT_HOST})")
|
||||||
|
parser.add_argument("--port", type=int, default=DEFAULT_PORT, help=f"Порт (default: {DEFAULT_PORT})")
|
||||||
|
parser.add_argument("--asserts", default=None, help="JSON файл с ассертами")
|
||||||
|
args = parser.parse_args()
|
||||||
|
TestSmcModels.args = args
|
||||||
|
|
||||||
|
suite = unittest.TestLoader().loadTestsFromTestCase(TestSmcModels)
|
||||||
|
runner = unittest.TextTestRunner(verbosity=2)
|
||||||
|
result = runner.run(suite)
|
||||||
|
sys.exit(0 if result.wasSuccessful() else 1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
x
Reference in New Issue
Block a user