адресация: бот давно молчит в чате — строже порог и подсказка модели («тебе» — другому человеку)
Кейсы «Правления» 17.10: «Ща обновлю тебе до премиум» (Alex — Vlad'у, 18:40, P=0.68) и «Так проверь пока опен роутер» (Alex — Vlad'у, 14:56, P=0.62) — бот молчал 2 и 6 часов, но «тебе/проверь» перевесило, и Коди встрял («О, спасибо! 🙏…», «Проверил OpenRouter ✅…»). - bot_idle_minutes: молчание бота в чате по журналу исходящих/ответам; - бот писал ≤ 10 мин назад — порог 0.5 и прежний промпт (подсказка «люди ещё говорят с Коди» завышала P — пробовал, откатил); - бот молчит дольше — порог 0.6 (SIMBIOT_ADDRESSING_MIN_P_IDLE) и в контексте модели: «Коди не участвует в текущем разговоре, «ты/тебе» без имени — скорее другому человеку». Реплей 111 спорных решений правления за 17.10: оба встревания ушли (P 0.68→0.18, 0.62→0.31), обращения к молчавшему боту сохранились («Можешь зайти на клоуд…» 0.77, «Чекни опен роутер генерацию…» 0.80). Тест: test_addressing 35 PASS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2da39af74d
commit
1745e6c1c8
2 changed files with 65 additions and 3 deletions
|
|
@ -53,6 +53,47 @@ def _names_re():
|
||||||
re.IGNORECASE)
|
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:
|
def _min_p() -> float:
|
||||||
try:
|
try:
|
||||||
return float(os.environ.get("SIMBIOT_ADDRESSING_MIN_P", "0.5"))
|
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)")
|
"sharing links, thinking aloud)")
|
||||||
rt = (f" [in reply to: {(m.get('reply_to_text') or m.get('reply_to_desc') or '')[:150]}]"
|
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 "")
|
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 "
|
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 "
|
"(@juno42bot, called «Коди» in Russian) is an AI assistant bot that must "
|
||||||
"speak ONLY when addressed. Talking ABOUT Cody in the third person "
|
"speak ONLY when addressed. Talking ABOUT Cody in the third person "
|
||||||
"(«Коди должен…», «он Коди», «Коди стал…») is people discussing the bot "
|
"(«Коди должен…», «он Коди», «Коди стал…») 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) +
|
"Recent messages:\n" + "\n".join(hist) +
|
||||||
f"\nLAST message — {who}{rt}: "
|
f"\nLAST message — {who}{rt}: "
|
||||||
f"{(m.get('text') or '[' + (m.get('kind') or '?') + ']')[:600]}")
|
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"
|
ok, rule = p >= _min_p(), "core"
|
||||||
else:
|
else:
|
||||||
p = model_verdict(m, inbox)
|
p = model_verdict(m, inbox)
|
||||||
ok, rule = ((p >= _min_p(), "decision") if p is not None
|
ok, rule = ((p >= _min_p_for(bot_idle_minutes(m)), "decision")
|
||||||
else (False, "no_decision"))
|
if p is not None else (False, "no_decision"))
|
||||||
try:
|
try:
|
||||||
from . import kodi_core
|
from . import kodi_core
|
||||||
if kodi_core.mode() != "off":
|
if kodi_core.mode() != "off":
|
||||||
|
|
|
||||||
|
|
@ -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)
|
and ib[mid2].get("reply_to_user_id") == BOT_ID)
|
||||||
check("приём: обычный текст без упоминания", tg_inbox._mentions_bot({"text": "привет"}) is False)
|
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)")
|
print(f"\n{'OK' if not fails else 'FAILED'}: {len(fails)} fail(s)")
|
||||||
sys.exit(1 if fails else 0)
|
sys.exit(1 if fails else 0)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue