feat(sudya): оркестратор, карточка звонка и приёмка на подсадных ошибках

Оркестратор сам ничего не проверяет — собирает слои и выносит итог: любой СТОП →
СТОП, иначе ТРЕВОГА, иначе НОРМА. Сломанный якорь перекрывает всё. В бою полная
коллегия не зовётся — только приборы, деньги владельца берегутся.

Приёмка судьи: девять диалогов с нарочно посаженным браком (выдуманное число,
перевёрнутая цена, мужской род о себе, «ты», своё имя вместо обращения, латиница,
потерянная реплика, выдумка ушей, долгая пауза) — судья обязан найти каждый.
Плюс чистый якорь, на котором он обязан промолчать: если судья бракует хорошее,
это хуже пропущенного брака. Все десять проверок прошли.

Проверено сквозняком на живом журнале, а не только на выдуманных примерах:
судья честно поймал, что она говорит 57% времени при норме 35–45%.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
This commit is contained in:
Дмитрий
2026-07-18 18:59:29 +03:00
parent e2e054365c
commit fbca7e13f9
4 changed files with 157 additions and 3 deletions
+3 -3
View File
@@ -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-сессий.
+53
View File
@@ -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)
@@ -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']}"
@@ -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"] == "ПАРТИЯ НЕДЕЙСТВИТЕЛЬНА"