see-smc-autotest/test_see_models.py
2026-06-23 10:41:45 +07:00

375 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()