универсальная версия: без хардкода имён моделей, pick_text по ключевым словам
This commit is contained in:
parent
9b78a5a1bb
commit
6e48b5da68
@ -1,6 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
"""SEE model autotest — проверяет все загруженные модели после обновления.
|
||||
|
||||
Универсальный: не содержит хардкода имён моделей, подходит для любой SEE-инсталляции.
|
||||
Текст запроса подбирается динамически по имени модели.
|
||||
Для точных проверок можно подключить asserts-файл.
|
||||
|
||||
Использование:
|
||||
python3 test_see_models.py
|
||||
python3 test_see_models.py --host 10.101.60.26
|
||||
@ -11,20 +15,13 @@
|
||||
{
|
||||
"asserts": [
|
||||
{
|
||||
"model": "fio", // подстрока для поиска модели
|
||||
"type": "entity|classifier",
|
||||
"text": "Иванов Иван Иванович", // текст для запроса
|
||||
"model": "fio", // подстрока для поиска модели
|
||||
"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 топ-класса
|
||||
"min_found": 1, // мин. количество сущностей/классов
|
||||
"text_contains": "иван", // подстрока в text или class
|
||||
"calculated_contains": "Ф:", // подстрока в calculated
|
||||
"confidence_min": 0.5 // мин. confidence
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -35,7 +32,6 @@
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
from urllib.parse import quote
|
||||
@ -50,80 +46,74 @@ 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", "в пять часов вечера"),
|
||||
TEXT_RULES = [
|
||||
# (keyword, text) — первое совпадение побеждает
|
||||
("fio", "Иванов Иван Иванович"),
|
||||
("phone", "8 913 456 78 90"),
|
||||
("date", "на завтра"),
|
||||
("time", "в пять часов вечера"),
|
||||
("uuid", "550e8400-e29b-41d4-a716-446655440000"),
|
||||
("number", "пятьсот двадцать три"),
|
||||
("filial", "в филиал на родниках"),
|
||||
("date", "на завтра"),
|
||||
("record_date", "на завтра"),
|
||||
("time", "в пять часов вечера"),
|
||||
("record_time", "в пять часов вечера"),
|
||||
("uuid", "550e8400-e29b-41d4-a716-446655440000"),
|
||||
("zags", "загс центрального района"),
|
||||
("nsk_fillials", "в филиал на родниках"),
|
||||
("filial", "в филиал на родниках"),
|
||||
("offices_mfc", "родники"),
|
||||
("возраст", "мне 25 лет"),
|
||||
("загран", "хочу загранпаспорт"),
|
||||
("yesno", "да"),
|
||||
("weekday", "в пятницу"),
|
||||
("grade", "пять"),
|
||||
("obzv", "да"),
|
||||
("дни недели", "в пятницу"),
|
||||
("operator", "соедините с оператором"),
|
||||
("repeat", "все, до свидания"),
|
||||
("matcap", "на улучшение жилищных условий"),
|
||||
("end_repeat", "все, до свидания"),
|
||||
("повтор", "все, до свидания"),
|
||||
("other_payment", "мобилизационные выплаты"),
|
||||
("payment", "мобилизационные выплаты"),
|
||||
("mobilizac", "мобилизационные"),
|
||||
("trud", "потерял работу"),
|
||||
("subsidii", "субсидия на жкх"),
|
||||
("rating", "отлично"),
|
||||
("payment", "мобилизационные выплаты"),
|
||||
("service_mfc", "хочу получить паспорт"),
|
||||
("offices_mfc", "родники"),
|
||||
("паспорт", "хочу получить загранпаспорт"),
|
||||
("ЕПГУ", "проверить статус заявления на госуслугах"),
|
||||
("лаврентьев", "как получить лаврентьевскую карту"),
|
||||
("возраст", "мне 25 лет"),
|
||||
("загран", "хочу сделать загранпаспорт"),
|
||||
("вернуться", "вернуться в главное меню"),
|
||||
("не знаю код", "не знаю код подразделения"),
|
||||
("оценк", "оцениваю обслуживание на пять"),
|
||||
("несогласи", "я не согласен с этим"),
|
||||
("minsoc", "субсидия на жкх"),
|
||||
("matcap", "на улучшение жилищных условий"),
|
||||
("маткап", "на улучшение жилищных условий"),
|
||||
("паспорт", "хочу получить паспорт"),
|
||||
("пособие", "пособие по рождению"),
|
||||
("начало диалог", "здравствуйте"),
|
||||
("пособие", "какое пособие по рождению"),
|
||||
("УК", "проблема с управляющей компанией"),
|
||||
("граждан", "я гражданин россии"),
|
||||
("выплат", "ежемесячные выплаты"),
|
||||
("статус", "проверить статус заявления"),
|
||||
("суперсервис", "рождение ребенка"),
|
||||
("рождение", "рождение ребенка"),
|
||||
("лаврентьев", "лаврентьевская карта"),
|
||||
("несогласи", "я не согласен"),
|
||||
("отказ", "отказываюсь"),
|
||||
("оценк", "пять"),
|
||||
("grade", "пять"),
|
||||
("rating", "пять"),
|
||||
("исключение", "это не относится"),
|
||||
("режим", "режим работы мфц"),
|
||||
("жкх", "режим работы мфц"),
|
||||
("вернуться", "вернуться в главное меню"),
|
||||
("не знаю", "не знаю код подразделения"),
|
||||
("service_mfc", "хочу получить услугу"),
|
||||
("service mfc", "хочу получить услугу"),
|
||||
("mfc", "записаться в мфц"),
|
||||
("мфц", "записаться в мфц"),
|
||||
("тест", "тестовое сообщение"),
|
||||
("test", "test message"),
|
||||
("заглушк", "тестовый запрос"),
|
||||
("stub", "test request"),
|
||||
("конкретик", "конкретный вопрос"),
|
||||
("да-нет", "да"),
|
||||
]
|
||||
|
||||
FALLBACK_TEXT = "Здравствуйте, хочу записаться на приём в МФЦ для получения паспорта, запись на завтра"
|
||||
|
||||
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):
|
||||
@ -146,7 +136,7 @@ class SeeAPI:
|
||||
url = f"{self.base}/entities/{encoded}?text={quote(text)}&similarity={similarity}"
|
||||
return json.loads(self._request(url))
|
||||
|
||||
def _request(self, url, timeout=15):
|
||||
def _request(self, url, timeout=30):
|
||||
try:
|
||||
with urlreq.urlopen(url, timeout=timeout) as r:
|
||||
return r.read().decode("utf-8")
|
||||
@ -163,10 +153,10 @@ class TestSeeModels(unittest.TestCase):
|
||||
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'])}")
|
||||
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:
|
||||
@ -216,15 +206,6 @@ class TestSeeModels(unittest.TestCase):
|
||||
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
|
||||
@ -232,13 +213,6 @@ class TestSeeModels(unittest.TestCase):
|
||||
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:
|
||||
@ -257,64 +231,15 @@ class TestSeeModels(unittest.TestCase):
|
||||
if len(entities) > 3:
|
||||
print(f" ... ({len(entities)} entities)")
|
||||
|
||||
def test_02_pretrained_models(self):
|
||||
"""Проверка pretrained-моделей"""
|
||||
def _test_all_models(self, models, category):
|
||||
errors = []
|
||||
for model_id, td in PRETRAINED_TESTS.items():
|
||||
if model_id not in self.models.get("pretrained", []):
|
||||
errors.append(f" {model_id}: нет в списке")
|
||||
for model_id in models:
|
||||
if not 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
|
||||
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):
|
||||
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):
|
||||
@ -326,35 +251,42 @@ class TestSeeModels(unittest.TestCase):
|
||||
raise
|
||||
except Exception as e:
|
||||
errors.append(f" {model_id}: {e}")
|
||||
return errors
|
||||
|
||||
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 моделей")
|
||||
errors = self._test_all_models(pretrained, "pretrained")
|
||||
if errors:
|
||||
self.fail("Ошибки pretrained:\n" + "\n".join(errors))
|
||||
|
||||
def test_03_handler_models(self):
|
||||
"""Проверка handler-моделей"""
|
||||
handlers = self.models.get("handlers", [])
|
||||
if not handlers:
|
||||
self.skipTest("Нет handler моделей")
|
||||
errors = self._test_all_models(handlers, "handler")
|
||||
if errors:
|
||||
self.fail("Ошибки handler:\n" + "\n".join(errors))
|
||||
|
||||
def test_04_custom_models(self):
|
||||
"""Проверка кастомных моделей"""
|
||||
models = self.models.get("models", [])
|
||||
if not models:
|
||||
self.skipTest("Нет кастомных моделей")
|
||||
errors = self._test_all_models(models, "custom")
|
||||
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")
|
||||
|
||||
@ -11,13 +11,12 @@
|
||||
{
|
||||
"asserts": [
|
||||
{
|
||||
"model": "МФЦ НСК", // подстрока для поиска модели
|
||||
"type": "classifier",
|
||||
"model": "МФЦ НСК",
|
||||
"text": "записаться на приём", // опционально, свой текст
|
||||
"checks": {
|
||||
"min_classes": 1, // мин. количество классов
|
||||
"top_class_contains": "МФЦ", // подстрока в top классе
|
||||
"confidence_min": 0.5 // мин. confidence топ-класса
|
||||
"min_found": 1, // мин. количество классов
|
||||
"text_contains": "Запись", // подстрока в top классе
|
||||
"confidence_min": 0.5 // мин. confidence топ-класса
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -42,7 +41,28 @@ DEFAULT_HOST = "10.101.60.27"
|
||||
DEFAULT_PORT = 6181
|
||||
BASE_PATH = "/smc"
|
||||
|
||||
DEFAULT_TEXT = "Здравствуйте, хочу записаться на приём в МФЦ для получения паспорта"
|
||||
TEXT_RULES = [
|
||||
("мфц", "записаться в мфц"),
|
||||
("mfc", "записаться в мфц"),
|
||||
("филиал", "какой филиал работает"),
|
||||
("ekc", "записаться в екц"),
|
||||
("екц", "записаться в екц"),
|
||||
("minsoc", "какие выплаты"),
|
||||
("минсоц", "какие выплаты"),
|
||||
("техническая", "не работает сайт"),
|
||||
("поддержка", "не работает сайт"),
|
||||
("offices", "какие офисы работают"),
|
||||
("тест", "тестовый запрос"),
|
||||
("test", "test query"),
|
||||
]
|
||||
|
||||
|
||||
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):
|
||||
@ -124,7 +144,7 @@ class TestSmcModels(unittest.TestCase):
|
||||
for mid in models:
|
||||
if not mid:
|
||||
continue
|
||||
text = DEFAULT_TEXT
|
||||
text = pick_text(mid)
|
||||
a = self._find_assert(mid)
|
||||
if a and "text" in a:
|
||||
text = a["text"]
|
||||
@ -158,7 +178,7 @@ class TestSmcModels(unittest.TestCase):
|
||||
for mid in handlers:
|
||||
if not mid:
|
||||
continue
|
||||
text = DEFAULT_TEXT
|
||||
text = pick_text(mid)
|
||||
a = self._find_assert(mid)
|
||||
if a and "text" in a:
|
||||
text = a["text"]
|
||||
@ -181,7 +201,7 @@ class TestSmcModels(unittest.TestCase):
|
||||
except Exception as e:
|
||||
errors.append(f" {mid}: {e}")
|
||||
if errors:
|
||||
self.fail("Ошибки internal:\n" + "\n".join(errors))
|
||||
self.fail("Ошибки handler:\n" + "\n".join(errors))
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user