diff --git a/harness/addressing.py b/harness/addressing.py index cb0f07c..52ce517 100644 --- a/harness/addressing.py +++ b/harness/addressing.py @@ -53,6 +53,47 @@ def _names_re(): re.IGNORECASE) +ACTIVE_MIN = 10 # бот писал в чат не позже N минут назад — с ним идёт разговор + + +def bot_idle_minutes(m: dict): + """Сколько минут бот молчал в этом чате до сообщения (None — не писал вовсе). + + Кейс 2026-10-17 18:40: «Ща обновлю тебе до премиум» (Alex — Vlad'у) → + учитель дал P(бот)=0.68 при пороге 0.5, хотя бот молчал в группе 2 часа: + «тебе» без разговора с ботом — почти всегда другому человеку.""" + import sqlite3 + from datetime import datetime + chat, ts = m.get("chat_id"), m.get("ts") or "" + try: + db = sqlite3.connect(f"file:{es.DB_PATH}?mode=ro", uri=True, timeout=30) + try: + row = db.execute( + "SELECT MAX(timestamp) FROM events WHERE timestamp < ? AND (" + "(event_type='OutboundMessage' AND json_extract(data,'$.chat_id')=?) OR " + "(event_type IN ('ReplySent','FollowupSent') AND json_extract(data,'$.to_chat')=?))", + (ts or "9999", chat, chat)).fetchone() + finally: + db.close() + if not row or not row[0]: + return None + last = datetime.fromisoformat(row[0]) + now = datetime.fromisoformat(ts) if ts else datetime.now(last.tzinfo) + return max(0.0, (now - last).total_seconds() / 60) + except Exception: + return None + + +def _min_p_for(idle) -> float: + """Порог P(бот): в разговоре с ботом — базовый, бот давно молчит — строже.""" + if idle is not None and idle <= ACTIVE_MIN: + return _min_p() + try: + return float(os.environ.get("SIMBIOT_ADDRESSING_MIN_P_IDLE", "0.6")) + except ValueError: + return 0.6 + + def _min_p() -> float: try: return float(os.environ.get("SIMBIOT_ADDRESSING_MIN_P", "0.5")) @@ -111,11 +152,19 @@ def model_verdict(m: dict, inbox: dict): "sharing links, thinking aloud)") rt = (f" [in reply to: {(m.get('reply_to_text') or m.get('reply_to_desc') or '')[:150]}]" if m.get("reply_to_message_id") else "") + idle = bot_idle_minutes(m) + # подсказка — только когда бот давно молчит: в активном диалоге «люди, возможно, + # говорят с Коди» завышало P (реплей 17.10: вставленный текст от Claude → «боту») + spoke = ("" if idle is not None and idle <= ACTIVE_MIN else + ("Cody has not written in this chat yet; " if idle is None else + f"Cody last wrote in this chat {int(idle)} minutes ago; ") + + "Cody is NOT part of the current conversation, so «ты/тебе» without " + "Cody's name most likely means another human.") state = (f"Group chat '{m.get('chat_title') or ''}': humans talk to each other; Cody " "(@juno42bot, called «Коди» in Russian) is an AI assistant bot that must " "speak ONLY when addressed. Talking ABOUT Cody in the third person " "(«Коди должен…», «он Коди», «Коди стал…») is people discussing the bot " - "with each other — NOT addressing Cody.\n" + "with each other — NOT addressing Cody. " + spoke + "\n" "Recent messages:\n" + "\n".join(hist) + f"\nLAST message — {who}{rt}: " f"{(m.get('text') or '[' + (m.get('kind') or '?') + ']')[:600]}") @@ -172,8 +221,8 @@ def triage(model: dict, log=None) -> int: ok, rule = p >= _min_p(), "core" else: p = model_verdict(m, inbox) - ok, rule = ((p >= _min_p(), "decision") if p is not None - else (False, "no_decision")) + ok, rule = ((p >= _min_p_for(bot_idle_minutes(m)), "decision") + if p is not None else (False, "no_decision")) try: from . import kodi_core if kodi_core.mode() != "off": diff --git a/tests/test_addressing.py b/tests/test_addressing.py index 6edbe96..9c6a58a 100644 --- a/tests/test_addressing.py +++ b/tests/test_addressing.py @@ -154,5 +154,18 @@ check("приём: reply на бота", ib[mid2].get("reply_to_is_bot") is True and ib[mid2].get("reply_to_user_id") == BOT_ID) check("приём: обычный текст без упоминания", tg_inbox._mentions_bot({"text": "привет"}) is False) +# ── бот молчит давно → строже (кейс 17.10: «Ща обновлю тебе до премиум» Vlad'у) ── +from datetime import datetime, timedelta, timezone +os.environ.pop("SIMBIOT_ADDRESSING_MIN_P_IDLE", None) +check("порог: в разговоре с ботом (≤10 мин) — базовый 0.5", addressing._min_p_for(3) == 0.5) +check("порог: бот молчит давно — 0.6", addressing._min_p_for(120) == 0.6 + and addressing._min_p_for(None) == 0.6) +t0 = datetime.now(timezone.utc) +es.append_event("outbound", f"{G}:900", "OutboundMessage", {"chat_id": G, "message_id": 900, "text": "ответ"}) +mm = {"chat_id": G, "ts": (t0 + timedelta(minutes=30)).isoformat()} +idle = addressing.bot_idle_minutes(mm) +check("молчание бота считается по журналу исходящих", idle is not None and 29 <= idle <= 31, idle) +check("в другом чате бот не писал → None", addressing.bot_idle_minutes({"chat_id": -5, "ts": mm["ts"]}) is None) + print(f"\n{'OK' if not fails else 'FAILED'}: {len(fails)} fail(s)") sys.exit(1 if fails else 0)