#!/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, 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_all_models(self, models, category): errors = [] for model_id in models: 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): 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}") 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 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()