@
feat(sudya): спектр продажи по нашей методике + накопитель слабых мест Судья умел говорить, чего она НЕ должна говорить (красные флажки), но не умел судить, КАК она продаёт. Добавлены признаки по методике из базы наставника: крючок, лесенка подачи, финал по теплоте, ведёт вопросами, цену вперёд не тащит. 🔴 Живой замер на сервере опроверг мою догадку. Я ждал, что она ВЫВАЛИВАЕТ доводы пачкой — а она за шесть реплик назвала ОДИН блок из тринадцати. Говорит 71% времени и почти ничего не доносит: много сочувствия, мало содержания. По доле речи это не видно, поэтому добавлен признак 42 на пустословие. Детектор блоков проверен на эталонном тексте базы — узнаёт 12 из 13, то есть находка настоящая, не слепота. Накопитель: один разговор ничего не говорит, смысл появляется на пачке. Раскладывает находки на системное / частое / шум — чтобы не гоняться за случайностью и не портить то, что работает. Признак считается один раз на разговор: десять срабатываний в одном не делают дефект системным. 🔴 Судья ДОКЛАДЫВАЕТ, а не правит: где промахнулась, как часто, дословные примеры. Что чинить — решает владелец. Советов «крути здесь» в накопителе нет намеренно. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> @
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Brain Status (auto-generated)
|
||||
|
||||
Last updated: 2026-07-18T15:59:33.296Z
|
||||
Last updated: 2026-07-18T16:33:42.017Z
|
||||
|
||||
| Контролёр | Состояние | Детали |
|
||||
|---|---|---|
|
||||
@@ -112,8 +112,8 @@ Episodes since last run: 542 / threshold: 10
|
||||
|
||||
| PID | Имя | CPU-время | Возраст |
|
||||
|---|---|---|---|
|
||||
| 3540 | MsMpEng | 3.24ч | 0.0ч |
|
||||
| 10836 | Code | 1.36ч | NaNч |
|
||||
| 3540 | MsMpEng | 3.30ч | 0.0ч |
|
||||
| 10836 | Code | 1.46ч | 0.0ч |
|
||||
|
||||
⚠️ Проверь, не «осиротевшие» ли это процессы от завершённых Claude-сессий.
|
||||
|
||||
|
||||
@@ -164,8 +164,75 @@ NE_IZMERYAETSYA = {
|
||||
3: "звук: съеденные окончания — там же",
|
||||
4: "звук: сверка с эталоном — там же",
|
||||
5: "звук: бренд через распознавание — там же",
|
||||
37: "смысл: понял ли клиент крючок — судит роль «продавец»",
|
||||
39: "смысл: соответствие финала теплоте клиента — судит роль «продавец»",
|
||||
9: "из журнала не видно: мост пишет, что ЖДАЛ думающего, но не пишет случаи, когда "
|
||||
"оборвал его. Нужна отметка моста о срабатывании умного сторожа на КАЖДОЙ паузе",
|
||||
35: "проверяется оркестратором (sudya.py), а не прибором",
|
||||
36: "проверяется надзором (promahi.py), а не прибором",
|
||||
}
|
||||
|
||||
|
||||
# ══════════ НАША методика продажи (база наставника), а не общая теория ══════════
|
||||
# 13 блоков выгод из продающей базы. Методика прямо велит подавать их ЛЕСЕНКОЙ —
|
||||
# по одному за раз. «Вываливает пачкой» — самый частый порок: он и даёт раздутую
|
||||
# долю речи, но по доле речи не видно ПРИЧИНЫ. Здесь видно.
|
||||
BLOKI = {
|
||||
"платишь за факт": re.compile(r"платит\w*\s+(только\s+)?за\s+факт|нет\s+абонплат|без\s+абонплат", re.I),
|
||||
"подарок и проверка": re.compile(r"тысяч\w*\s+рубл|подар\w*|проверьте\s+на\s+сво[её]м", re.I),
|
||||
"управляемость": re.compile(r"лимит\w*|управля\w*|прозрачн\w*|сами\s+реша\w*", re.I),
|
||||
"цена от объёма": re.compile(r"ступен\w*|чем\s+больше\s+(бер[её]те|объ[её]м)|дешевле\s+выход", re.I),
|
||||
"своя CRM": re.compile(r"\bсрм\b|crm|вс[её]\s+в\s+одном|отдельн\w*\s+сервис", re.I),
|
||||
"конкурентное поле": re.compile(r"конкурент\w*\s+пол|найд[её]м\s+источник|подберём\s+источник", re.I),
|
||||
"гибкость": re.compile(r"пауз\w*\s+(проект|в любой)|смен\w*\s+источник", re.I),
|
||||
"быстрый старт": re.compile(r"восемнадцат\w*|18:00|завтра\s+ж?е?\s*начн", re.I),
|
||||
"законность": re.compile(r"законн\w*|не\s+спам|согласи\w*\s+на\s+обработ", re.I),
|
||||
"показатели": re.compile(r"половин\w*\s+(из|от)|в\s+среднем\s+прихо", re.I),
|
||||
"оплата и документы": re.compile(r"сч[её]т\w*\s+(на\s+оплату|выстав)|закрывающ\w*|документ\w*", re.I),
|
||||
"доверие": re.compile(r"данные\s+в\s+росси|поддержк\w*\s+(круглосуточ|24)", re.I),
|
||||
"развитие": re.compile(r"в\s+планах|развива\w*", re.I),
|
||||
}
|
||||
CENA_UPOMYANUTA = re.compile(r"рубл|₽|цена|стоит|поч[её]м|дешевл|дорог", re.I)
|
||||
CENA_SPROSHENA = re.compile(r"скольк|поч[её]м|цена|стоит|дорог|деш|прайс|бюджет", re.I)
|
||||
DOLYA_VOPROSOV_MIN = 0.5 # v9: каждая реплика заканчивается вопросом и замолкает
|
||||
|
||||
|
||||
def proverit_prodazhu(r):
|
||||
"""Признаки 38, 40, 41 — методика подачи. Смотрим, КАК она продаёт,
|
||||
а не только чего не должна говорить."""
|
||||
nahodki = []
|
||||
ee = r.ee_repliki()
|
||||
for i, rep in enumerate(r.repliki):
|
||||
if rep["kto"] != "александра":
|
||||
continue
|
||||
t = rep["tekst"]
|
||||
nazvany = [name for name, rx in BLOKI.items() if rx.search(t)]
|
||||
if len(nazvany) > 1:
|
||||
nahodki.append(_nahodka(38, i, ", ".join(nazvany),
|
||||
"методика: лесенка подачи — один довод за раз"))
|
||||
# цену вперёд не тащим: её спросили? смотрим ВСЕ реплики клиента до этой
|
||||
if CENA_UPOMYANUTA.search(t):
|
||||
sprosili = any(CENA_SPROSHENA.search(p["tekst"])
|
||||
for p in r.repliki[:i] if p["kto"] == "клиент")
|
||||
if not sprosili:
|
||||
nahodki.append(_nahodka(41, i, CENA_UPOMYANUTA.search(t).group(0),
|
||||
"методика: цену сама вперёд не тащи"))
|
||||
# 🔴 Найдено живым замером на сервере 18.07: я ждал, что она ВЫВАЛИВАЕТ доводы пачкой,
|
||||
# а она за шесть реплик назвала ОДИН блок из тринадцати — говорила 71% времени и почти
|
||||
# ничего не донесла. Пустословие незаметно по доле речи: слов много, содержания нет.
|
||||
if len(ee) >= 4:
|
||||
vse_bloki = set()
|
||||
for rep in ee:
|
||||
vse_bloki |= {n for n, rx in BLOKI.items() if rx.search(rep["tekst"])}
|
||||
if len(vse_bloki) < 2:
|
||||
nahodki.append(_nahodka(42, -1,
|
||||
f"за {len(ee)} реплик названо доводов: {len(vse_bloki)}",
|
||||
"методика: 13 блоков выгод, подавать лесенкой — но подавать"))
|
||||
if ee:
|
||||
s_voprosom = sum(1 for x in ee if "?" in x["tekst"])
|
||||
dolya = s_voprosom / len(ee)
|
||||
if dolya < DOLYA_VOPROSOV_MIN:
|
||||
nahodki.append(_nahodka(40, -1,
|
||||
f"вопросом заканчивается {s_voprosom} из {len(ee)} реплик",
|
||||
f"методика: ведём вопросами, норма от {DOLYA_VOPROSOV_MIN:.0%}"))
|
||||
return nahodki
|
||||
|
||||
@@ -57,6 +57,13 @@ PRIZNAKI = [
|
||||
Priznak(32, "V", "продавец", "ТРЕВОГА", "предложен следующий шаг"),
|
||||
Priznak(33, "V", "продавец", "ТРЕВОГА", "не отшила к человеку, зная ответ"),
|
||||
Priznak(34, "V", "продавец", "СИГНАЛ", "чем кончилось: тепло / шатается / холодно / потерян"),
|
||||
# ── V (продолжение): НАША методика продажи из базы наставника, а не общая теория ──
|
||||
Priznak(37, "V", "продавец", "ТРЕВОГА", "начала с главного крючка, а не с прайса"),
|
||||
Priznak(38, "V", "прибор", "ТРЕВОГА", "лесенка подачи: не больше одного блока выгод за реплику"),
|
||||
Priznak(39, "V", "продавец", "ТРЕВОГА", "закончила конкретным шагом по теплоте клиента"),
|
||||
Priznak(40, "V", "прибор", "СИГНАЛ", "ведёт вопросами, а не рассказом"),
|
||||
Priznak(41, "V", "прибор", "ТРЕВОГА", "цену вперёд сама не тащит — называет, когда спросили"),
|
||||
Priznak(42, "V", "прибор", "ТРЕВОГА", "за разговор донесла доводы, а не одно сочувствие"),
|
||||
|
||||
# ── VI. Проверка самого судьи ──
|
||||
Priznak(35, "VI", "прибор", "СТОП", "живой якорь получил НОРМА — иначе партия недействительна"),
|
||||
|
||||
@@ -15,6 +15,7 @@ def sudit(r, yakor_validen=True, polnaya_kollegia=None):
|
||||
nahodki += pribory.proverit_chisla(r)
|
||||
nahodki += pribory.proverit_tehniku(r)
|
||||
nahodki += pribory.proverit_harakter(r)
|
||||
nahodki += pribory.proverit_prodazhu(r)
|
||||
nahodki += pribory.proverit_perebivanie(r)
|
||||
|
||||
# Полная коллегия — на приёмке; в бою только по запросу (экономим деньги)
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""НАКОПИТЕЛЬ — карта слабых мест по пачке разговоров.
|
||||
|
||||
Один разговор не говорит ничего: модель шумит, и находка может быть случайностью.
|
||||
Смысл появляется на десятке — что ПОВТОРЯЕТСЯ, то системное.
|
||||
|
||||
🔴 Судья ДОКЛАДЫВАЕТ, а не правит. Здесь нет и не должно быть советов «что крутить»:
|
||||
только где промахнулась, как часто и дословные примеры. Что чинить — решает владелец.
|
||||
|
||||
Три полки:
|
||||
СИСТЕМНОЕ — в большинстве разговоров. Это про саму Александру, а не про случай.
|
||||
ЧАСТОЕ — заметная доля, но не всегда. Смотреть глазами.
|
||||
ШУМ — один-два раза на пачку. Гоняться за этим вредно: испортишь то, что работает.
|
||||
"""
|
||||
import rubrika
|
||||
|
||||
PORAG_SISTEMNOE = 0.6 # доля разговоров, с которой находка считается системной
|
||||
PORAG_CHASTOE = 0.25
|
||||
PRIMEROV = 2 # сколько дословных цитат показывать на признак
|
||||
|
||||
|
||||
def svesti(kartochki):
|
||||
"""kartochki — список карточек от sudya.sudit(). → карта слабых мест."""
|
||||
vsego = len(kartochki)
|
||||
if not vsego:
|
||||
return {"razgovorov": 0, "sistemnoe": [], "chastoe": [], "shum": [], "chisto": True}
|
||||
|
||||
po_priznaku = {}
|
||||
for k in kartochki:
|
||||
# признак считаем ОДИН раз на разговор: десять срабатываний в одном разговоре
|
||||
# не делают дефект системным — системным его делает повтор в РАЗНЫХ разговорах
|
||||
vidennye = set()
|
||||
for n in k.get("nahodki", []):
|
||||
nom = n["priznak"]
|
||||
zapis = po_priznaku.setdefault(nom, {"razgovorov": 0, "vsego_srabotok": 0,
|
||||
"primery": [], "tyazhesti": set()})
|
||||
zapis["vsego_srabotok"] += 1
|
||||
zapis["tyazhesti"].add(n["tyazhest"])
|
||||
if nom not in vidennye:
|
||||
vidennye.add(nom)
|
||||
zapis["razgovorov"] += 1
|
||||
if len(zapis["primery"]) < PRIMEROV and n.get("citata"):
|
||||
zapis["primery"].append({"razgovor": k.get("id"), "citata": n["citata"]})
|
||||
|
||||
polki = {"sistemnoe": [], "chastoe": [], "shum": []}
|
||||
for nom, z in po_priznaku.items():
|
||||
p = rubrika.po_nomeru(nom)
|
||||
dolya = z["razgovorov"] / vsego
|
||||
stroka = {"priznak": nom, "spektr": p.spektr, "formulirovka": p.formulirovka,
|
||||
"tyazhest": "СТОП" if "СТОП" in z["tyazhesti"] else
|
||||
("ТРЕВОГА" if "ТРЕВОГА" in z["tyazhesti"] else "СИГНАЛ"),
|
||||
"razgovorov": z["razgovorov"], "iz": vsego, "dolya": round(dolya, 2),
|
||||
"vsego_srabotok": z["vsego_srabotok"], "primery": z["primery"]}
|
||||
polka = ("sistemnoe" if dolya >= PORAG_SISTEMNOE else
|
||||
"chastoe" if dolya >= PORAG_CHASTOE else "shum")
|
||||
polki[polka].append(stroka)
|
||||
|
||||
poryadok = {"СТОП": 0, "ТРЕВОГА": 1, "СИГНАЛ": 2}
|
||||
for polka in polki.values():
|
||||
polka.sort(key=lambda s: (poryadok[s["tyazhest"]], -s["dolya"]))
|
||||
return {"razgovorov": vsego, "chisto": not po_priznaku, **polki}
|
||||
|
||||
|
||||
def kratko(karta):
|
||||
"""Человеческий вид — для владельца."""
|
||||
if not karta["razgovorov"]:
|
||||
return "разговоров не было"
|
||||
stroki = [f"КАРТА СЛАБЫХ МЕСТ — {karta['razgovorov']} разговоров"]
|
||||
if karta["chisto"]:
|
||||
stroki.append(" замечаний нет")
|
||||
return "\n".join(stroki)
|
||||
zagolovki = [("sistemnoe", "🔴 СИСТЕМНОЕ — повторяется в большинстве разговоров"),
|
||||
("chastoe", "⚠️ ЧАСТОЕ — заметная доля, смотреть глазами"),
|
||||
("shum", "· ШУМ — один-два раза, это случайность")]
|
||||
for klyuch, zagolovok in zagolovki:
|
||||
if not karta[klyuch]:
|
||||
continue
|
||||
stroki.append("")
|
||||
stroki.append(zagolovok)
|
||||
for s in karta[klyuch]:
|
||||
stroki.append(f" [{s['tyazhest']}] признак {s['priznak']} ({s['spektr']}): "
|
||||
f"{s['formulirovka']}")
|
||||
stroki.append(f" {s['razgovorov']} из {s['iz']} разговоров"
|
||||
f" ({s['dolya']:.0%}), срабатываний {s['vsego_srabotok']}")
|
||||
for pr in s["primery"]:
|
||||
stroki.append(f" пример: «{pr['citata'][:90]}»")
|
||||
return "\n".join(stroki)
|
||||
@@ -1,13 +1,14 @@
|
||||
import rubrika
|
||||
|
||||
|
||||
def test_rubrika_soderzhit_36_priznakov():
|
||||
assert len(rubrika.PRIZNAKI) == 36
|
||||
def test_rubrika_soderzhit_42_priznaka():
|
||||
assert len(rubrika.PRIZNAKI) == 42
|
||||
|
||||
|
||||
def test_nomera_unikalny_i_bez_dyr():
|
||||
nomera = [p.nomer for p in rubrika.PRIZNAKI]
|
||||
assert nomera == list(range(1, 37))
|
||||
nomera = sorted(p.nomer for p in rubrika.PRIZNAKI)
|
||||
assert nomera == list(range(1, 43))
|
||||
assert len(nomera) == len(set(nomera))
|
||||
|
||||
|
||||
def test_polya_dopustimy():
|
||||
@@ -23,7 +24,12 @@ def test_najti_po_nomeru():
|
||||
assert rubrika.po_nomeru(28).tyazhest == "СИГНАЛ"
|
||||
|
||||
|
||||
def test_spektr_V_sudit_prodavec():
|
||||
# спектр V — новое, ради чего всё затевалось: продала ли она
|
||||
for p in rubrika.spektra("V"):
|
||||
assert p.kto == "продавец", p
|
||||
def test_spektr_V_sudyat_prodavec_i_pribory():
|
||||
"""Спектр V — ради чего всё затевалось: продала ли она.
|
||||
|
||||
Часть по НАШЕЙ методике меряется прибором (лесенка подачи, вопросы, цена вперёд),
|
||||
часть требует понимания смысла — её судит роль «продавец».
|
||||
"""
|
||||
kto = {p.kto for p in rubrika.spektra("V")}
|
||||
assert kto == {"продавец", "прибор"}, kto
|
||||
assert len(rubrika.spektra("V")) == 11
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Накопитель: отделяет системное от случайного — иначе будем чинить шум."""
|
||||
import svodka
|
||||
|
||||
|
||||
def _k(id, *pary):
|
||||
"""пары: (номер признака, тяжесть, цитата)"""
|
||||
return {"id": id, "nahodki": [{"priznak": n, "tyazhest": t, "citata": c} for n, t, c in pary]}
|
||||
|
||||
|
||||
DESYAT = (
|
||||
[_k(f"з-{i}", (12, "СИГНАЛ", "доля речи 70%"), (42, "ТРЕВОГА", "доводов: 1")) for i in range(8)]
|
||||
+ [_k("з-8", (12, "СИГНАЛ", "доля речи 68%"), (24, "ТРЕВОГА", "директолог"))]
|
||||
+ [_k("з-9", (27, "ТРЕВОГА", "Александра, это не про рекламу"))]
|
||||
)
|
||||
|
||||
|
||||
def test_povtoryayushcheesya_eto_sistemnoe():
|
||||
k = svodka.svesti(DESYAT)
|
||||
nomera = [s["priznak"] for s in k["sistemnoe"]]
|
||||
assert 12 in nomera and 42 in nomera
|
||||
|
||||
|
||||
def test_odinochnoe_eto_shum():
|
||||
"""Оба признака встретились по разу из десяти — это случайность, а не система."""
|
||||
k = svodka.svesti(DESYAT)
|
||||
assert sorted(s["priznak"] for s in k["shum"]) == [24, 27]
|
||||
|
||||
|
||||
def test_srednee_eto_chastoe():
|
||||
k = svodka.svesti([_k("а", (24, "ТРЕВОГА", "х")), _k("б", (24, "ТРЕВОГА", "х")),
|
||||
_k("в"), _k("г"), _k("д")])
|
||||
assert [s["priznak"] for s in k["chastoe"]] == [24]
|
||||
|
||||
|
||||
def test_povtor_v_odnom_razgovore_ne_delaet_sistemnym():
|
||||
"""Десять срабатываний в ОДНОМ разговоре — это один разговор, а не система."""
|
||||
k = svodka.svesti([_k("а", *[(6, "ТРЕВОГА", "пауза")] * 10),
|
||||
_k("б"), _k("в"), _k("г"), _k("д")])
|
||||
s = k["shum"][0]
|
||||
assert s["razgovorov"] == 1 and s["vsego_srabotok"] == 10
|
||||
|
||||
|
||||
def test_primery_dословny_i_ogranicheny():
|
||||
k = svodka.svesti(DESYAT)
|
||||
for polka in ("sistemnoe", "chastoe", "shum"):
|
||||
for s in k[polka]:
|
||||
assert len(s["primery"]) <= svodka.PRIMEROV
|
||||
for p in s["primery"]:
|
||||
assert p["citata"]
|
||||
|
||||
|
||||
def test_chistaya_pachka():
|
||||
k = svodka.svesti([_k("а"), _k("б")])
|
||||
assert k["chisto"] is True
|
||||
assert "замечаний нет" in svodka.kratko(k)
|
||||
|
||||
|
||||
def test_pustoy_spisok_ne_padaet():
|
||||
assert svodka.svesti([])["razgovorov"] == 0
|
||||
Reference in New Issue
Block a user