адресация: группы без chat_type по chat_id<0; облако decision — таймаут 30 с, кулдаун 60 с
- старые записи инбокса без chat_type (до t-261013113629) считались личкой и обходили фильтр адресации — группа теперь и по chat_id < 0; - утренний холодный старт clef:27b в облаке (> 15 с) взводил предохранитель на 5 минут → ответы от CPU-nimble по 10 с: облако ждём до 30 с, его кулдаун 60 с (локальной — по-прежнему 5 минут). Реплей реальной истории «Правления» (62 сообщения, бот отвечает только на адресованное, облако ~0.66 с): приветствия и переписка людей между собой отфильтрованы; разговор Alex с ботом без упоминания распознан по контексту; спорные — 2–3 в каждую сторону, «Коди»/@/reply — всегда. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
9d13e59742
commit
3ff504143a
5 changed files with 24 additions and 7 deletions
|
|
@ -356,7 +356,7 @@ nimble — арбитр там, где механика слепа; недост
|
|||
off — тесты герметичны), точечно `SIMBIOT_DECISION_<TASKS|TOPIC|GATE>`,
|
||||
`SIMBIOT_DECISION_API_KEY` (есть → основной = облако clef:27b, нет →
|
||||
локальная nimble без запасного), `SIMBIOT_DECISION_URL`,
|
||||
`SIMBIOT_DECISION_MODEL`, `SIMBIOT_DECISION_TIMEOUT` (облако 15 с /
|
||||
`SIMBIOT_DECISION_MODEL`, `SIMBIOT_DECISION_TIMEOUT` (облако 30 с, предохранитель 60 с /
|
||||
локально 90 с), `SIMBIOT_DECISION_FALLBACK=on|off`,
|
||||
`SIMBIOT_DECISION_FALLBACK_URL|MODEL|TIMEOUT` (локальная nimble, 90 с), `SIMBIOT_DECISION_GATE_MIN_P` (0.35),
|
||||
`SIMBIOT_DECISION_KEEP_ALIVE` (2h). Глобальный shadow — потолок для всех.
|
||||
|
|
|
|||
|
|
@ -31,6 +31,15 @@ GROUP_TYPES = ("group", "supergroup")
|
|||
_LOCK = threading.Lock()
|
||||
|
||||
|
||||
def is_group(m: dict) -> bool:
|
||||
"""Группа: chat_type, а у старых записей без него — отрицательный chat_id TG."""
|
||||
ct = m.get("chat_type")
|
||||
if ct:
|
||||
return ct in GROUP_TYPES
|
||||
cid = m.get("chat_id")
|
||||
return isinstance(cid, int) and cid < 0
|
||||
|
||||
|
||||
def _names_re():
|
||||
names = os.environ.get("SIMBIOT_BOT_NAMES", "коди,cody,juno,джуно")
|
||||
alts = "|".join(re.escape(n.strip()) for n in names.split(",") if n.strip())
|
||||
|
|
@ -46,13 +55,13 @@ def _min_p() -> float:
|
|||
|
||||
def needs_triage(m: dict) -> bool:
|
||||
"""Групповое входящее без вердикта (и не отвеченное/не проигнорированное)."""
|
||||
return (m.get("chat_type") in GROUP_TYPES and "addressed" not in m
|
||||
return (is_group(m) and "addressed" not in m
|
||||
and not m.get("replied") and not m.get("ignored"))
|
||||
|
||||
|
||||
def rule_verdict(m: dict, bot_id=None):
|
||||
"""Детерминированная часть: (True|False, правило) или (None, None) — серая зона."""
|
||||
if m.get("chat_type") not in GROUP_TYPES:
|
||||
if not is_group(m):
|
||||
return True, "private"
|
||||
if m.get("mentions_bot"):
|
||||
return True, "mention"
|
||||
|
|
|
|||
|
|
@ -50,6 +50,9 @@ MAX_OPTIONS = 9 # шортлист stage2 строго ≤ 9 (бенч:
|
|||
CACHE_TTL = 3600.0 # ответы детерминированы — час держим
|
||||
CACHE_MAX = 256
|
||||
BREAKER_COOLDOWN = 300.0 # после сбоя молчим 5 минут, не тормозим тики таймаутами
|
||||
# облако: холодный старт clef после простоя бывает > 15 с (утро 2026-10-17:
|
||||
# один таймаут → 5 минут ответов от CPU-nimble по 10 с) — короткий кулдаун
|
||||
CLOUD_BREAKER_COOLDOWN = 60.0
|
||||
|
||||
_LOCK = threading.Lock()
|
||||
_CACHE = {} # sha256(вопросы+состояние) → (ts, answers, model)
|
||||
|
|
@ -97,7 +100,7 @@ def backends() -> list:
|
|||
primary = {"name": "primary", "key": key,
|
||||
"url": _cfg("SIMBIOT_DECISION_URL", CLOUD_URL),
|
||||
"model": _cfg("SIMBIOT_DECISION_MODEL", "clef:27b"),
|
||||
"timeout": _float("SIMBIOT_DECISION_TIMEOUT", 15.0)}
|
||||
"timeout": _float("SIMBIOT_DECISION_TIMEOUT", 30.0)}
|
||||
else:
|
||||
primary = {"name": "primary", "key": "",
|
||||
"url": _cfg("SIMBIOT_DECISION_URL", LOCAL_URL),
|
||||
|
|
@ -192,8 +195,8 @@ def ask(questions: dict, state: str):
|
|||
except (urllib.error.URLError, OSError, ValueError, TimeoutError) as e:
|
||||
err = f"{e.__class__.__name__}: {str(e)[:160]}"
|
||||
with _LOCK:
|
||||
_BREAKERS[be["name"]] = {"until": time.time() + BREAKER_COOLDOWN,
|
||||
"error": err}
|
||||
cool = CLOUD_BREAKER_COOLDOWN if be["key"] else BREAKER_COOLDOWN
|
||||
_BREAKERS[be["name"]] = {"until": time.time() + cool, "error": err}
|
||||
errors.append(f"{be['name']} {be['model']}: {err} "
|
||||
f"({int((time.time() - t1) * 1000)} мс)")
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -254,7 +254,8 @@ def _is_pending(m: dict) -> bool:
|
|||
return False
|
||||
# группа: только адресованное боту (harness/addressing.py, аудит 2026-10-17);
|
||||
# без вердикта триажа — ещё не входящее для LLM
|
||||
if m.get("chat_type") in ("group", "supergroup") and not m.get("addressed"):
|
||||
from .addressing import is_group
|
||||
if is_group(m) and not m.get("addressed"):
|
||||
return False
|
||||
if m.get("text"):
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -53,6 +53,10 @@ check("слово-подстрока не ловится («кодировка»
|
|||
check("reply человеку → нет", rv(g("согласен", reply_to_user_id=156156021))
|
||||
== (False, "reply_to_human"))
|
||||
check("просто реплика → серая зона", rv(g("Доброе утро!")) == (None, None))
|
||||
check("старая запись без chat_type, chat_id<0 → группа",
|
||||
addressing.is_group({"chat_id": G}) and rv({"chat_id": G, "text": "эй"}) == (None, None))
|
||||
check("без chat_type, chat_id>0 → личка", not addressing.is_group({"chat_id": 7569223131}))
|
||||
check("портал-чат (строка) → не группа", not addressing.is_group({"chat_id": "portal:alex"}))
|
||||
|
||||
# ── триаж в сторе ──
|
||||
CALLS = []
|
||||
|
|
|
|||
Loading…
Reference in a new issue