адресация: имя бота — только звательное обращение; разговор О Коди — не повод отвечать

Кейс 2026-10-17 07:40: фильтр отсёк «Блять)»/«Продолжает)», но правило
«имя в тексте = обращение» ответило на «Так теперь Коди должен отвечать
когда его тегают…» и «Ещё и забыл что он Коди)» — люди обсуждали бота.
- детерминированно — только звательное: «Коди, …», «Коди!», «…, Коди»;
  имя посреди фразы — серая зона;
- промпт decision-модели: «Коди» = Cody, речь о нём в третьем лице —
  не обращение. Пробы: 8/8 (о боте ≤ 0.14, к боту ≥ 0.96); реплей
  истории правления — оба утренних ложных ответа ушли (0.40/0.13),
  разговор Alex с ботом без тега по-прежнему распознаётся;
- мандат мин-уровня: «Коди» в чате — это ты (бот писал о себе в
  третьем лице: «Коди … на вашей стороне»).
Тест: test_addressing 31 PASS; полный набор — без регрессий.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Simbiot Harness 2026-10-17 07:46:20 +00:00
parent 3ff504143a
commit 16d2b438bd
3 changed files with 27 additions and 7 deletions

View file

@ -41,9 +41,16 @@ def is_group(m: dict) -> bool:
def _names_re(): def _names_re():
"""Обращение по имени — только звательное, с пунктуацией: «Коди, …»,
«Коди!», «…, Коди», просто «Коди». Имя посреди фразы («Коди должен
отвечать…», «он Коди») — разговор О боте, его решает модель (кейс
2026-10-17: правило «имя где угодно» отвечало на обсуждение бота)."""
names = os.environ.get("SIMBIOT_BOT_NAMES", "коди,cody,juno,джуно") names = os.environ.get("SIMBIOT_BOT_NAMES", "коди,cody,juno,джуно")
alts = "|".join(re.escape(n.strip()) for n in names.split(",") if n.strip()) a = "|".join(re.escape(n.strip()) for n in names.split(",") if n.strip())
return re.compile(rf"(?<![\w@])({alts})(?!\w)", re.IGNORECASE) return re.compile(
rf"^\s*(?:{a})\s*(?:[,!:?]|$)" # «Коди, …» / «Коди!» / «Коди»
rf"|,\s*(?:{a})\s*[!?.)]*\s*$", # «…, Коди»
re.IGNORECASE)
def _min_p() -> float: def _min_p() -> float:
@ -96,8 +103,8 @@ def model_verdict(m: dict, inbox: dict):
hist = _history(m, inbox) hist = _history(m, inbox)
who = m.get("first_name") or m.get("username") or "?" who = m.get("first_name") or m.get("username") or "?"
people = sorted({x.split(":", 1)[0] for x in hist if not x.startswith("Cody (bot)")} - {who}) people = sorted({x.split(":", 1)[0] for x in hist if not x.startswith("Cody (bot)")} - {who})
crit = {"bot": "Cody, the AI assistant bot: the message asks Cody to answer, " crit = {"bot": "Cody (Коди), the AI assistant bot: the message speaks TO Cody — "
"explain or do something"} "asks Cody to answer, explain or do something"}
for nm in people[:6]: for nm in people[:6]:
crit["p_" + nm] = f"{nm} (a human in the chat)" crit["p_" + nm] = f"{nm} (a human in the chat)"
crit["group"] = ("the group in general / nobody specific (greetings, remarks, " crit["group"] = ("the group in general / nobody specific (greetings, remarks, "
@ -105,7 +112,10 @@ def model_verdict(m: dict, inbox: dict):
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 "")
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) is an AI assistant bot that must speak ONLY when addressed.\n" "(@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"
"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]}")

View file

@ -64,7 +64,11 @@ LEVELS = {
"min": CycleLevel( "min": CycleLevel(
name="min", label="Минута · пульс", every=MIN, uses_llm=True, name="min", label="Минута · пульс", every=MIN, uses_llm=True,
model="auto", effort="low", ctx_tokens=256_000, task_classes=("S",), model="auto", effort="low", ctx_tokens=256_000, task_classes=("S",),
mandate=("Ты — минутный пульс харнеса Simbiot. Триаж входящих TG-сообщений: " mandate=("Ты — минутный пульс харнеса Simbiot. В Telegram ты — Коди "
"(@juno42bot, Cody Command): «Коди» в чате — это ТЫ, говори о себе "
"в первом лице. Из групп тебе приходят только сообщения, адресованные "
"тебе (фильтр адресации) — на них отвечай. "
"Триаж входящих TG-сообщений: "
"все неотвеченные сообщения приходят тебе ОДНИМ батчем (секция " "все неотвеченные сообщения приходят тебе ОДНИМ батчем (секция "
"входящих в контексте) — обработай его ЦЕЛИКОМ за этот тик: " "входящих в контексте) — обработай его ЦЕЛИКОМ за этот тик: "
"сгруппируй в один сводный ответ по смыслу всего разговора " "сгруппируй в один сводный ответ по смыслу всего разговора "

View file

@ -48,8 +48,14 @@ check("личка → всегда", rv({"chat_type": "private", "text": "при
check("@juno42bot → да", rv(g("глянь", mentions_bot=True))[0] is True) check("@juno42bot → да", rv(g("глянь", mentions_bot=True))[0] is True)
check("reply на бота → да", rv(g("а это?", reply_to_user_id=BOT_ID, reply_to_is_bot=True))[0] is True) check("reply на бота → да", rv(g("а это?", reply_to_user_id=BOT_ID, reply_to_is_bot=True))[0] is True)
check("имя «Коди» → да", rv(g("Коди, сделай отчёт"))[1] == "name") check("имя «Коди» → да", rv(g("Коди, сделай отчёт"))[1] == "name")
check("«cody» латиницей → да", rv(g("cody pls"))[0] is True) check("«cody» латиницей → да", rv(g("cody, pls"))[0] is True)
check("слово-подстрока не ловится («кодировка»)", rv(g("кодировка сломалась"))[0] is None) check("слово-подстрока не ловится («кодировка»)", rv(g("кодировка сломалась"))[0] is None)
check("«сделай, Коди» → да", rv(g("сделай сводку, Коди"))[0] is True)
check("«Коди!» → да", rv(g("Коди!"))[0] is True)
check("разговор О боте → серая зона, не правило",
rv(g("Так теперь Коди должен отвечать когда его тегают")) == (None, None)
and rv(g("Ещё и забыл что он Коди)")) == (None, None)
and rv(g("Коди сейчас стал молчаливее")) == (None, None))
check("reply человеку → нет", rv(g("согласен", reply_to_user_id=156156021)) check("reply человеку → нет", rv(g("согласен", reply_to_user_id=156156021))
== (False, "reply_to_human")) == (False, "reply_to_human"))
check("просто реплика → серая зона", rv(g("Доброе утро!")) == (None, None)) check("просто реплика → серая зона", rv(g("Доброе утро!")) == (None, None))