see-smc-autotest/test_see_models.py

336 lines
14 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 — проверяет все загруженные модели после обновления.
Универсальный: не содержит хардкода имён моделей, подходит для любой SEE-инсталляции.
Текст запроса подбирается динамически по имени модели.
Для точных проверок можно подключить asserts-файл.
Использование:
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", // подстрока для поиска модели
"text": "Иванов Иван Иванович", // текст запроса (опционально)
"checks": {
"min_found": 1, // мин. количество сущностей/классов
"text_contains": "иван", // подстрока в text или class
"calculated_contains": "Ф:", // подстрока в calculated
"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.25"
DEFAULT_PORT = 6184
BASE_PATH = "/see"
TEXT_RULES = [
# (keyword, text) — первое совпадение побеждает
("fio", "Иванов Иван Иванович"),
("phone", "8 913 456 78 90"),
("number", "пятьсот двадцать три"),
("date", "на завтра"),
("record_date", "на завтра"),
("time", "в пять часов вечера"),
("record_time", "в пять часов вечера"),
("uuid", "550e8400-e29b-41d4-a716-446655440000"),
("zags", "загс центрального района"),
("nsk_fillials", "родники"),
("filial", "в филиал на родниках"),
("offices_mfc", "родники"),
("без уточнения возраста", "вписать ребенка в загранпаспорт"),
("тип загранпаспорта", "новый"),
("возраст", "мне 25 лет"),
("загран", "вписать ребенка в загранпаспорт"),
("yesno", "да"),
("weekday", "в пятницу"),
("дни недели", "в пятницу"),
("operator", "соедините с оператором"),
("end_repeat", "все, до свидания"),
("повтор", "повторите ещё раз"),
("other_payment", "мобилизационные выплаты"),
("payment", "мобилизационные выплаты"),
("mobilizac", "оператор"),
("trud", "зарегистрироваться в цзн"),
("subsidii", "субсидия на жкх"),
("minsoc", "субсидия на жкх"),
("matcap", "на улучшение жилищных условий"),
("маткап", "на улучшение жилищных условий"),
("паспорт", "загран"),
("пособие", "пособие по рождению и воспитанию детей"),
("начало диалог", "старт"),
("граждан", "я гражданин россии"),
("выплат", "пособие по рождению и воспитанию детей"),
("статус", "заявление зарегистрировано"),
("суперсервис", "рождение ребенка"),
("рождение", "рождение ребенка"),
("лаврентьев", "лаврентьевская карта"),
("несогласи", "это не то время"),
("отказ", "отказываюсь"),
("оценк", "пять"),
("grade", "пять"),
("rating", "пять"),
("исключение", "вопрос по жкх"),
("режим", "режим работы мфц"),
("жкх", "вопрос по жкх"),
("вернуться", "вернуться в главное меню"),
("не знаю", "не знаю код подразделения"),
("service_mfc", "хочу получить услугу"),
("service mfc", "хочу получить услугу"),
("мои", "мои талоны"),
("количество", "2"),
("mfc", "записаться в мфц"),
("мфц", "записаться в мфц"),
("тест", "тестовое сообщение"),
("test", "test message"),
("заглушк", "тестовый запрос"),
("stub", "test request"),
("конкретик", "конкретный вопрос"),
("да-нет", "да"),
]
def pick_text(model_id):
name_lower = model_id.lower()
for keyword, text in TEXT_RULES:
if keyword in name_lower:
return text
return "Здравствуйте, хочу записаться на приём в МФЦ"
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=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 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.get("models", [])) + len(data.get("handlers", [])) + len(data.get("pretrained", []))
print(f"\n models: {len(data.get('models', []))}, "
f"handlers: {len(data.get('handlers', []))}, "
f"pretrained: {len(data.get('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 _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 _fmt_result(self, result):
if isinstance(result, dict) and "classes" in result:
cs = result.get("classes", [])
if cs:
top = cs[0]
return f"{top.get('class','?')} ({top.get('confidence',0):.3f})"
return ""
for entities in result.values():
if isinstance(entities, list) and entities:
e = entities[0]
calc = e.get("calculated", "")
if calc:
return calc[:60]
return e.get("text", "?")[:40]
return ""
def _run_section(self, section_name, model_ids):
"""Тестирует список моделей, выводит компактно."""
results = []
for model_id in model_ids:
if not model_id:
continue
text = pick_text(model_id)
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):
results.append(("empty", model_id, None, None))
else:
self._check_assert(model_id, result)
results.append(("ok", model_id, result, text))
except AssertionError:
results.append(("assert_fail", model_id, None, text))
raise
except Exception as e:
results.append(("error", model_id, None, f"{e}"))
passed = sum(1 for r in results if r[0] == "ok")
empty = sum(1 for r in results if r[0] == "empty")
errors = [r for r in results if r[0] == "error"]
assert_fails = [r for r in results if r[0] == "assert_fail"]
print(f"\n── {section_name} ──────────────────────────────────────────────")
if results:
for status, mid, result, extra in results:
if status == "ok" and self.args.verbose:
print(f"{mid}{self._fmt_result(result)}")
elif status == "ok":
print(f"{mid}")
elif status == "empty":
print(f"{mid} → empty")
elif status == "assert_fail":
print(f"{mid} → assert fail")
elif status == "error":
print(f"{mid}{extra}")
print(f" ─── {passed} passed, {empty} empty", end="")
if errors:
print(f", {len(errors)} errors", end="")
if assert_fails:
print(f", {len(assert_fails)} assert fails", end="")
print()
error_msgs = []
for r in errors:
error_msgs.append(f" {r[1]}: {r[3]}")
if error_msgs:
self.fail(f"Ошибки {section_name}:\n" + "\n".join(error_msgs))
def test_01_list_models(self):
"""Получение списка моделей"""
data = self.models
self.assertIn("models", data)
self.assertIn("handlers", data)
self.assertIn("pretrained", data)
def test_02_pretrained_models(self):
"""Проверка pretrained-моделей"""
pretrained = self.models.get("pretrained", [])
if not pretrained:
self.skipTest("Нет pretrained моделей")
self._run_section("PRETRAINED", pretrained)
def test_03_handler_models(self):
"""Проверка handler-моделей"""
handlers = self.models.get("handlers", [])
if not handlers:
self.skipTest("Нет handler моделей")
self._run_section("HANDLERS", handlers)
def test_04_custom_models(self):
"""Проверка кастомных моделей"""
models = self.models.get("models", [])
if not models:
self.skipTest("Нет кастомных моделей")
self._run_section("CUSTOM", models)
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 файл с ассертами")
parser.add_argument("--verbose", "-v", action="store_true", help="Показать результат каждой модели")
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()