From 6e48b5da68369b9abfeb708162dac5d1394a3de5 Mon Sep 17 00:00:00 2001 From: Maksim Totmin Date: Tue, 23 Jun 2026 10:47:32 +0700 Subject: [PATCH] =?UTF-8?q?=D1=83=D0=BD=D0=B8=D0=B2=D0=B5=D1=80=D1=81?= =?UTF-8?q?=D0=B0=D0=BB=D1=8C=D0=BD=D0=B0=D1=8F=20=D0=B2=D0=B5=D1=80=D1=81?= =?UTF-8?q?=D0=B8=D1=8F:=20=D0=B1=D0=B5=D0=B7=20=D1=85=D0=B0=D1=80=D0=B4?= =?UTF-8?q?=D0=BA=D0=BE=D0=B4=D0=B0=20=D0=B8=D0=BC=D1=91=D0=BD=20=D0=BC?= =?UTF-8?q?=D0=BE=D0=B4=D0=B5=D0=BB=D0=B5=D0=B9,=20pick=5Ftext=20=D0=BF?= =?UTF-8?q?=D0=BE=20=D0=BA=D0=BB=D1=8E=D1=87=D0=B5=D0=B2=D1=8B=D0=BC=20?= =?UTF-8?q?=D1=81=D0=BB=D0=BE=D0=B2=D0=B0=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test_see_models.py | 282 +++++++++++++++++---------------------------- test_smc_models.py | 38 ++++-- 2 files changed, 136 insertions(+), 184 deletions(-) diff --git a/test_see_models.py b/test_see_models.py index f12032b..6de29eb 100644 --- a/test_see_models.py +++ b/test_see_models.py @@ -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") diff --git a/test_smc_models.py b/test_smc_models.py index 3488d51..de2cd26 100644 --- a/test_smc_models.py +++ b/test_smc_models.py @@ -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():