simbiot-harness/tests/test_kodi_core.py
Simbiot Harness c1d5a11bfb ядро Коди: обучаемый ученик адресации (цикл как в systemone-bench), теневой режим
harness/kodi_core.py — цикл engine42/router42 бенча на решениях Коди,
первая голова addressing (отвечать ли в группе):
- опыт — подписанные события core: CoreLabel (учитель по истории;
  человек — кнопки «Это было мне?» в личку, ≤5/сут), CoreFeedback
  (неявно из чата: ответил, а следом «не тебе/продолжает» → 0; промолчал,
  а автор тут же «@juno42bot»/«Коди?» → 1), живые вердикты учителя;
  веса: учитель 1, обратная связь 3, человек 5;
- признаки: nomic-embed-text сообщения (локально, кеш) + 8 флагов;
  контекстный эмбеддинг убран — шумел (5 фолдов 0.822 → 0.845 без него);
- ученик: логрегрессия (numpy, Adam, L2) — эмбеддинги без поразмерной
  стандартизации (раздувала шум на сотнях примеров); kNN-память новизны,
  пороги новизны калибруются на холдауте (у nomic она 0.03–0.09);
- роутер: знакомо и |p−0.5| ≥ 0.4 → ученик; незнакомо и учитель
  сомневается → вопрос Alex; иначе учитель. SIMBIOT_CORE_ADDRESSING=
  shadow (дефолт: решает учитель, ученик пишет CorePrediction) | on | off;
- тренировка в дневной тик: временной холдаут, претендент против
  чемпиона по log-loss, CoreModelPromoted с sha256; подмена файла →
  ученика нет; cli.py core status|train|backfill|feedback|rollback.
Репетиция на истории правления (259 примеров): холдаут — совпадение с
учителем 73%, уверенная доля 4% при 100% совпадения; 5 фолдов при
CONF 0.4 — покрытие 25%, совпадение 95%. До включения нужны метки.
Поллер: callback_query (allowed_updates явно). numpy — в requirements.
Тест: test_kodi_core 28 PASS; полный набор — без регрессий.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 08:20:02 +00:00

215 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""Ядро Коди (harness/kodi_core.py), голова addressing — hermetic.
Запуск: ./venv/bin/python tests/test_kodi_core.py
Эмбеддинги подменены детерминированными (тема «боту» vs «людям» + шум),
Ollama/Telegram не трогаются. Проверяет: датасет (серая зона, приоритет
меток human > feedback > teacher), обучение и промоут чемпиона с
подписанным событием, отказ от худшего претендента, подмена файла модели
→ ученика нет, откат; теневой прогноз пишется один раз; роутер;
вопрос Alex с лимитом в сутки и кнопка → метка человека (чужой — нет);
неявная обратная связь (встревание → 0, «дёрнул бота» → 1); триаж: в
shadow решает учитель, в on уверенный знакомый ученик решает сам.
"""
import hashlib
import json
import os
import sys
import tempfile
sys.path.insert(0, "/root/simbiot-harness")
_tmp = tempfile.mkdtemp(prefix="simbiot_core_")
os.environ["SIMBIOT_DB"] = os.path.join(_tmp, "harness.db")
os.environ["SIMBIOT_CORE_DIR"] = os.path.join(_tmp, "core")
for _v in ("SIMBIOT_CORE_ADDRESSING", "SIMBIOT_CORE_ASK_PER_DAY", "SIMBIOT_DECISION"):
os.environ.pop(_v, None)
import numpy as np
from harness import addressing, decision, es, kodi_core, tg_inbox
es.init_db()
fails = []
def check(name, cond, extra=""):
print(("PASS" if cond else "FAIL"), name, ("| " + str(extra)) if not cond else "")
if not cond:
fails.append(name)
A = np.zeros(768, np.float32); A[0] = 1
B = np.zeros(768, np.float32); B[1] = 1
def fake_embed(texts):
out = []
for t in texts:
h = int(hashlib.sha1(t.encode()).hexdigest(), 16)
rng = np.random.default_rng(h % (2 ** 32))
base = A if "#bot" in t else B
v = base + 0.02 * rng.standard_normal(768).astype(np.float32)
out.append(v / np.linalg.norm(v))
return out
kodi_core.embed = fake_embed
API = []
tg_inbox._api = lambda method, params, timeout=35: API.append((method, dict(params))) or {"ok": True, "result": True}
tg_inbox._load_env = lambda: None
tg_inbox._default_chat_id = lambda: 7569223131
tg_inbox.mark_seen = lambda *a, **k: True
G = -1004402779881
UID = [1000]
def recv(text, who="Vlad", ts=None):
UID[0] += 1
mid = f"m-{UID[0]}"
es.append_event("inbox", mid, "MessageReceived", {
"update_id": UID[0], "chat_id": G, "chat_type": "supergroup", "chat_title": "Правление",
"first_name": who, "text": text, "kind": "text", "message_id": UID[0]})
return mid
# ── 1. опыт: 60 размеченных учителем сообщений серой зоны + правило + метки ──
hm = recv("#bot спорное, учитель ошибся")
es.append_event("core", f"label:addressing:{hm}:teacher", "CoreLabel",
{"head": "addressing", "msg": hm, "label": 0.2, "source": "teacher_backfill"})
es.append_event("core", f"label:addressing:{hm}:human", "CoreLabel",
{"head": "addressing", "msg": hm, "label": 1, "source": "human"})
for i in range(60):
bot = i % 2 == 0
mid = recv(f"{'#bot сделай отчёт' if bot else 'обсуждаем между собой'} {i}")
es.append_event("core", f"label:addressing:{mid}:teacher", "CoreLabel", {
"head": "addressing", "msg": mid, "label": 0.9 if bot else 0.1, "source": "teacher_backfill"})
rule_mid = recv("Коди, привет") # правило — не в датасете
es.append_event("inbox", rule_mid, "MessageTriaged", {"addressed": True, "rule": "name"})
rows = kodi_core.dataset()
ids = {r[0]["id"] for r in rows}
check("датасет: серая зона с метками (61)", len(rows) == 61, len(rows))
check("датасет: решённое правилом не входит", rule_mid not in ids)
hrow = next(r for r in rows if r[0]["id"] == hm)
check("метка человека сильнее учителя (y=1, вес 5)", hrow[2] == 1 and hrow[3] == 5.0 and hrow[4] == "human")
# ── 2. обучение и промоут ──
rep = kodi_core.train(log=lambda *_: None)
check("первое обучение → промоут", rep["promoted"] and rep["holdout"]["acc"] >= 0.9, rep)
st = kodi_core.status()
check("чемпион записан", st["champion"] and st["champion_n"] == 61, st)
prom = [json.loads(e["data"]) for e in es.get_events(0, 10**6) if e["event_type"] == "CoreModelPromoted"]
check("CoreModelPromoted с sha256", prom and len(prom[-1]["sha256"]) == 64)
model = kodi_core.load_champion()
_ctx = [{"first_name": "Vlad", "text": "обсуждаем между собой 1"},
{"first_name": "Vlad", "text": "#bot сделай отчёт 2"}]
X, em = kodi_core.featurize([({"first_name": "Vlad", "text": "#bot глянь отчёт"}, _ctx),
({"first_name": "Vlad", "text": "обсуждаем между собой"}, _ctx)])
p = kodi_core.predict(model, X)
check("ученик различает «боту» и «людям»", p[0] > 0.7 and p[1] < 0.4, p)
nov = kodi_core.novelty(model, em)
check("знакомое — ниже порога «незнакомо» модели", nov.max() < model["nov_q"][1], (nov, model["nov_q"]))
check("пороги новизны откалиброваны и упорядочены", 0 <= model["nov_q"][0] <= model["nov_q"][1] < 1, model["nov_q"])
rep2 = kodi_core.train(log=lambda *_: None)
check("тот же опыт — чемпион остаётся (не лучше)", rep2["promoted"] is False, rep2)
# подмена файла модели
meta = kodi_core._champion_meta()
path = kodi_core._model_path(meta["file"])
blob = open(path, "rb").read()
open(path, "wb").write(blob[:-1] + bytes([blob[-1] ^ 1]))
kodi_core._STATE.update(model=None, loaded_sha=None)
check("файл модели подменён → ученика нет", kodi_core.load_champion() is None)
open(path, "wb").write(blob)
kodi_core._STATE.update(model=None, loaded_sha=None)
# ── 3. роутер ──
check("роутер: знакомо+уверенно → ученик", kodi_core.route(0.95, 0.1, 0.9) == "student")
check("роутер: незнакомо+учитель сомневается → человек", kodi_core.route(0.6, 0.8, 0.5) == "human")
check("роутер: иначе → учитель", kodi_core.route(0.55, 0.1, 0.9) == "teacher")
# ── 4. триаж в shadow: решает учитель, ученик пишет прогноз ──
CALLS = []
decision.choose = lambda purpose, opts, state, instr: CALLS.append(1) or {
"choice": "group", "applied": True, "confidence": .5, "probabilities": {"bot": 0.2, "group": 0.8}}
mid = recv("#bot сделай отчёт за неделю")
addressing.triage(es.run_projections())
ib = es.run_projections()["inbox"][mid]
pred = [json.loads(e["data"]) for e in es.get_events(0, 10**6) if e["event_type"] == "CorePrediction"]
check("shadow: решение учителя (не боту, p=0.2)", ib.get("ignored") and CALLS,
(ib.get("ignored"), ib.get("ignore_reason"), ib.get("addressed"), CALLS))
check("shadow: прогноз ученика записан, расходится с учителем",
pred and pred[-1]["msg"] == mid and pred[-1]["p_student"] > .7 and pred[-1]["agree"] is False, pred[-1:])
kodi_core.shadow({"update_id": int(mid[2:]), "first_name": "Vlad", "text": "x"}, [], 0.2, True)
check("прогноз на сообщение пишется один раз",
sum(1 for e in es.get_events(0, 10**6) if e["event_type"] == "CorePrediction"
and json.loads(e["data"])["msg"] == mid) == 1)
# ── 5. on: уверенный знакомый ученик решает сам, учитель не зовётся ──
os.environ["SIMBIOT_CORE_ADDRESSING"] = "on"
CALLS.clear()
_route = kodi_core.route # пороги роутера проверены в §3; здесь — связка с триажем
kodi_core.route = lambda p, nov, t, q=None: "student" if p is not None and abs(p - .5) >= .3 else "teacher"
mid2 = recv("#bot собери сводку")
addressing.triage(es.run_projections())
ib2 = es.run_projections()["inbox"][mid2]
check("on: ученик решил (rule=core), учитель не вызван",
ib2.get("addressed") is True and ib2["triage"]["rule"] == "core" and not CALLS, (ib2.get("triage"), CALLS))
os.environ.pop("SIMBIOT_CORE_ADDRESSING")
kodi_core.route = _route
# ── 6. вопрос Alex и кнопки ──
os.environ["SIMBIOT_CORE_ASK_PER_DAY"] = "2"
for t in ("странное 1", "странное 2", "странное 3"):
mm = {"update_id": int(recv(t)[2:]), "first_name": "Vlad", "text": t, "chat_title": "Правление"}
kodi_core.ask_human(mm, [])
asked = [p for m_, p in API if m_ == "sendMessage"]
check("вопрос Alex: лимит в сутки соблюдён (2)", len(asked) == 2, len(asked))
kb = json.loads(asked[0]["reply_markup"])["inline_keyboard"][0]
check("вопрос: кнопки «Да, Коди / Нет»", kb[0]["callback_data"].endswith(":1") and kb[1]["callback_data"].endswith(":0"))
target = kb[1]["callback_data"].split(":")[2]
check("кнопка от чужого — метки нет",
kodi_core.on_callback({"id": "q1", "data": kb[1]["callback_data"], "from": {"id": 666}},
{"7569223131"}) and not [e for e in es.get_events(0, 10**6)
if e["event_type"] == "CoreLabel" and
json.loads(e["data"]).get("source") == "human"
and json.loads(e["data"])["msg"] == target])
kodi_core.on_callback({"id": "q2", "data": kb[1]["callback_data"], "from": {"id": 7569223131},
"message": {"message_id": 5, "chat": {"id": 7569223131}}}, {"7569223131"})
hl = [json.loads(e["data"]) for e in es.get_events(0, 10**6) if e["event_type"] == "CoreLabel"
and json.loads(e["data"]).get("source") == "human" and json.loads(e["data"])["msg"] == target]
check("кнопка Alex → метка человека 0", hl and hl[-1]["label"] == 0, hl)
check("кнопки убраны после ответа", any(m_ == "editMessageReplyMarkup" for m_, _ in API))
check("чужие callback не наши", kodi_core.on_callback({"data": "other:x"}, set()) is False)
# ── 7. неявная обратная связь ──
fp = recv("ну что, как дела у всех")
es.append_event("inbox", fp, "MessageTriaged", {"addressed": True, "rule": "decision", "p": 0.6})
es.append_event("inbox", fp, "ReplySent", {"text": "Всё отлично 🙂"})
recv("Коди, не тебе", who="Alex")
fn = recv("подготовь сводку по встрече", who="Alex")
es.append_event("inbox", fn, "MessageIgnored", {"reason": "not_addressed", "rule": "decision", "p": 0.3})
poke = recv("@juno42bot", who="Alex")
es.append_event("inbox", poke, "MessageTriaged", {"addressed": True, "rule": "mention"})
n = kodi_core.harvest_feedback()
fb = {json.loads(e["data"])["msg"]: json.loads(e["data"]) for e in es.get_events(0, 10**6)
if e["event_type"] == "CoreFeedback"}
check("встревание + «не тебе» → метка 0", fb.get(fp, {}).get("label") == 0, fb)
check("промолчал + «@juno42bot» тем же автором → метка 1", fb.get(fn, {}).get("label") == 1, fb)
check("обратная связь пишется один раз", kodi_core.harvest_feedback() == 0)
# ── 8. откат ──
for i in range(30): # новый опыт → новый чемпион, чтобы было куда откатываться
mid = recv(f"{'#bot' if i % 2 else ''} новое {i}")
es.append_event("core", f"label:addressing:{mid}:human", "CoreLabel",
{"head": "addressing", "msg": mid, "label": 1 if i % 2 else 0, "source": "human"})
first = kodi_core._champion_meta()["file"]
r3 = kodi_core.train(log=lambda *_: None)
if r3["promoted"]:
rb = kodi_core.rollback()
check("откат к предыдущему чемпиону", rb["ok"] and kodi_core._champion_meta()["file"] == first, rb)
else:
check("откат: история пуста — честный отказ", kodi_core.rollback()["ok"] is False)
print(f"\n{'OK' if not fails else 'FAILED'}: {len(fails)} fail(s)")
sys.exit(1 if fails else 0)