diff --git a/docs/observer/STATUS.md b/docs/observer/STATUS.md index 6cf0321e..b7bb9ba8 100644 --- a/docs/observer/STATUS.md +++ b/docs/observer/STATUS.md @@ -1,6 +1,6 @@ # Brain Status (auto-generated) -Last updated: 2026-07-18T15:47:56.496Z +Last updated: 2026-07-18T15:55:07.060Z | Контролёр | Состояние | Детали | |---|---|---| @@ -112,8 +112,8 @@ Episodes since last run: 542 / threshold: 10 | PID | Имя | CPU-время | Возраст | |---|---|---|---| -| 3540 | MsMpEng | 3.19ч | 221035.8ч | -| 10836 | Code | 1.32ч | NaNч | +| 3540 | MsMpEng | 3.22ч | 0.0ч | +| 10836 | Code | 1.35ч | 0.0ч | ⚠️ Проверь, не «осиротевшие» ли это процессы от завершённых Claude-сессий. diff --git a/golos-test/sudya/dialog/sudya.py b/golos-test/sudya/dialog/sudya.py new file mode 100644 index 00000000..bdd2073a --- /dev/null +++ b/golos-test/sudya/dialog/sudya.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +"""ОРКЕСТРАТОР судьи: собирает слои и выносит итог. + +🔴 Сам ничего не проверяет — только зовёт приборы и коллегию и складывает карточку. +Правило итога: любой СТОП → СТОП; иначе есть ТРЕВОГА → ТРЕВОГА; иначе НОРМА. +Сломанный якорь перекрывает всё: партия не считается. +""" +import kollegia +import pribory + + +def sudit(r, yakor_validen=True, polnaya_kollegia=None): + """r — Razgovor. Режим «бой»: звук не судим (записи живых клиентов не храним — ПДн).""" + nahodki = [] + nahodki += pribory.proverit_chisla(r) + nahodki += pribory.proverit_tehniku(r) + nahodki += pribory.proverit_harakter(r) + nahodki += pribory.proverit_perebivanie(r) + + # Полная коллегия — на приёмке; в бою только по запросу (экономим деньги) + if polnaya_kollegia is None: + polnaya_kollegia = (r.rezhim == "приёмка") + if polnaya_kollegia: + nahodki += kollegia.sudit_vsemi(r) + + tyazhesti = {n["tyazhest"] for n in nahodki} + if not yakor_validen: + itog = "ПАРТИЯ НЕДЕЙСТВИТЕЛЬНА" + elif "СТОП" in tyazhesti: + itog = "СТОП" + elif "ТРЕВОГА" in tyazhesti: + itog = "ТРЕВОГА" + else: + itog = "НОРМА" + + return {"id": r.id, "rezhim": r.rezhim, "itog": itog, + "yakor_validen": yakor_validen, + "nahodki": sorted(nahodki, key=lambda n: (n["spektr"], n["priznak"])), + "cifry": {"dolya_ee_rechi": round(r.dolya_rechi(), 2), + "perebivanij": r.sobytiya.get("perebivanij", 0), + "poter_replik": r.sobytiya.get("poter_replik", 0)}, + "stoimost_rub": kollegia.raskhod()["rubley"]} + + +def kratko(k): + """Человеческий вид карточки — для владельца, не для машины.""" + znak = {"СТОП": "🛑", "ТРЕВОГА": "⚠️", "НОРМА": "✅"}.get(k["itog"], "❓") + stroki = [f"{znak} {k['itog']} — разговор {k['id']} ({k['rezhim']})"] + for n in k["nahodki"]: + stroki.append(f" [{n['tyazhest']}] признак {n['priznak']}: «{n['citata']}»" + f" — {n['istochnik'] or 'без источника'}") + stroki.append(f" цифры: {k['cifry']} · разбор стоил {k['stoimost_rub']} ₽") + return "\n".join(stroki) diff --git a/golos-test/sudya/dialog/tests/test_podsadnye.py b/golos-test/sudya/dialog/tests/test_podsadnye.py new file mode 100644 index 00000000..f697afa8 --- /dev/null +++ b/golos-test/sudya/dialog/tests/test_podsadnye.py @@ -0,0 +1,52 @@ +# golos-test/sudya/dialog/tests/test_podsadnye.py +"""ПОДСАДНЫЕ ОШИБКИ — приёмка самого судьи. + +В каждый диалог НАРОЧНО внесён ровно один брак. Судья обязан найти именно его. +Плюс чистый диалог-якорь: на нём судья обязан промолчать. Если якорь получает +СТОП — судья сломан, и всем прочим его вердиктам верить нельзя. +""" +import pytest +import razgovor, sudya + +CHISTO = {"perebivanij": 0, "zhdal_dumayushchego": 0, "vydumok_ushey": 0, "poter_replik": 0} + + +def _dialog(otvet_alex, sobytiya=None, zamery=None): + repliki = [ + {"kto": "клиент", "tekst": "Здравствуйте, а сколько стоит и как это работает?", "prervana": False}, + {"kto": "александра", "tekst": otvet_alex, "prervana": False}, + {"kto": "клиент", "tekst": "Понятно, надо подумать.", "prervana": False}, + {"kto": "александра", "tekst": "Хорошо, проверьте на своём номере. Как вам такой шаг?", "prervana": False}, + ] + sob = dict(CHISTO, **(sobytiya or {})) + return razgovor.Razgovor("подсадной", "бой", repliki, sob, + zamery=zamery or {"do_zvuka": [1.4], "do_otveta": [8.0]}) + + +PODSADNYE = [ + (14, _dialog("Выйдет примерно двести рублей за заявку.")), + (15, _dialog("От пятидесяти пяти до двадцати пяти рублей за заявку.")), + (22, _dialog("Я посмотрел вашу заявку, всё в порядке.")), + (23, _dialog("Ты сам увидишь, сколько придёт заявок.")), + (27, _dialog("Александра, это не совсем про рекламу.")), + (29, _dialog("Это работает как no-show контроль.")), + (10, _dialog("Живые номера тех, кто ищет.", sobytiya={"poter_replik": 1})), + (11, _dialog("Живые номера тех, кто ищет.", sobytiya={"vydumok_ushey": 1})), + (6, _dialog("Живые номера тех, кто ищет.", zamery={"do_zvuka": [5.0], "do_otveta": [8.0]})), +] + + +@pytest.mark.parametrize("nomer,r", PODSADNYE, ids=[str(n) for n, _ in PODSADNYE]) +def test_sudya_nahodit_podsadnuyu_oshibku(nomer, r, monkeypatch): + monkeypatch.setattr(sudya.kollegia, "sudit_vsemi", lambda r: []) + k = sudya.sudit(r) + najdennye = {n["priznak"] for n in k["nahodki"]} + assert nomer in najdennye, f"судья не нашёл подсадной признак {nomer}: {k['nahodki']}" + + +def test_chistyy_yakor_prohodit_bez_zamechaniy(monkeypatch): + """🔴 Якорь: диалог, принятый владельцем. Судья обязан промолчать.""" + monkeypatch.setattr(sudya.kollegia, "sudit_vsemi", lambda r: []) + yakor = _dialog("Поняла вас. Живые номера тех, кто уже ищет вашу услугу. Как смотрите?") + k = sudya.sudit(yakor) + assert k["itog"] == "НОРМА", f"судья забраковал ЯКОРЬ — он сломан: {k['nahodki']}" diff --git a/golos-test/sudya/dialog/tests/test_sudya.py b/golos-test/sudya/dialog/tests/test_sudya.py new file mode 100644 index 00000000..6a672b8c --- /dev/null +++ b/golos-test/sudya/dialog/tests/test_sudya.py @@ -0,0 +1,49 @@ +import razgovor, sudya + + +def _r(*teksty, **kw): + repliki = [] + for t in teksty: + repliki.append({"kto": "клиент", "tekst": "и почём выходит?", "prervana": False}) + repliki.append({"kto": "александра", "tekst": t, "prervana": False}) + sob = {"perebivanij": 0, "zhdal_dumayushchego": 0, "vydumok_ushey": 0, "poter_replik": 0} + sob.update(kw.pop("sobytiya", {})) + return razgovor.Razgovor("з-1", kw.pop("rezhim", "бой"), repliki, sob, + zamery={"do_zvuka": [1.4], "do_otveta": [8.0]}) + + +def test_stop_kogda_est_grubaya_nahodka(monkeypatch): + monkeypatch.setattr(sudya.kollegia, "sudit_vsemi", lambda r: []) + k = sudya.sudit(_r("Заявка выйдет двести рублей.")) + assert k["itog"] == "СТОП" + + +def test_trevoga_kogda_tolko_myagkie(monkeypatch): + monkeypatch.setattr(sudya.kollegia, "sudit_vsemi", lambda r: []) + k = sudya.sudit(_r("Ты сам всё увидишь.")) + assert k["itog"] == "ТРЕВОГА" + + +def test_norma_na_chistom(monkeypatch): + monkeypatch.setattr(sudya.kollegia, "sudit_vsemi", lambda r: []) + k = sudya.sudit(_r("Поняла вас. Живые номера тех, кто уже ищет вашу услугу. Как смотрите?")) + assert k["itog"] == "НОРМА" + + +def test_v_boyu_zvuk_ne_sudim(monkeypatch): + monkeypatch.setattr(sudya.kollegia, "sudit_vsemi", lambda r: []) + k = sudya.sudit(_r("Поняла вас.")) + assert all(n["spektr"] != "I" for n in k["nahodki"]) + + +def test_kartochka_soderzhit_obyazatelnye_polya(monkeypatch): + monkeypatch.setattr(sudya.kollegia, "sudit_vsemi", lambda r: []) + k = sudya.sudit(_r("Поняла вас.")) + for pole in ("id", "rezhim", "itog", "nahodki", "cifry", "stoimost_rub"): + assert pole in k + + +def test_slomannyy_yakor_perekryvaet_vsyo(monkeypatch): + monkeypatch.setattr(sudya.kollegia, "sudit_vsemi", lambda r: []) + k = sudya.sudit(_r("Поняла вас."), yakor_validen=False) + assert k["itog"] == "ПАРТИЯ НЕДЕЙСТВИТЕЛЬНА"