адресация: группы без 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>`,
|
off — тесты герметичны), точечно `SIMBIOT_DECISION_<TASKS|TOPIC|GATE>`,
|
||||||
`SIMBIOT_DECISION_API_KEY` (есть → основной = облако clef:27b, нет →
|
`SIMBIOT_DECISION_API_KEY` (есть → основной = облако clef:27b, нет →
|
||||||
локальная nimble без запасного), `SIMBIOT_DECISION_URL`,
|
локальная 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`,
|
локально 90 с), `SIMBIOT_DECISION_FALLBACK=on|off`,
|
||||||
`SIMBIOT_DECISION_FALLBACK_URL|MODEL|TIMEOUT` (локальная nimble, 90 с), `SIMBIOT_DECISION_GATE_MIN_P` (0.35),
|
`SIMBIOT_DECISION_FALLBACK_URL|MODEL|TIMEOUT` (локальная nimble, 90 с), `SIMBIOT_DECISION_GATE_MIN_P` (0.35),
|
||||||
`SIMBIOT_DECISION_KEEP_ALIVE` (2h). Глобальный shadow — потолок для всех.
|
`SIMBIOT_DECISION_KEEP_ALIVE` (2h). Глобальный shadow — потолок для всех.
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,15 @@ GROUP_TYPES = ("group", "supergroup")
|
||||||
_LOCK = threading.Lock()
|
_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():
|
def _names_re():
|
||||||
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())
|
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:
|
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"))
|
and not m.get("replied") and not m.get("ignored"))
|
||||||
|
|
||||||
|
|
||||||
def rule_verdict(m: dict, bot_id=None):
|
def rule_verdict(m: dict, bot_id=None):
|
||||||
"""Детерминированная часть: (True|False, правило) или (None, None) — серая зона."""
|
"""Детерминированная часть: (True|False, правило) или (None, None) — серая зона."""
|
||||||
if m.get("chat_type") not in GROUP_TYPES:
|
if not is_group(m):
|
||||||
return True, "private"
|
return True, "private"
|
||||||
if m.get("mentions_bot"):
|
if m.get("mentions_bot"):
|
||||||
return True, "mention"
|
return True, "mention"
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,9 @@ MAX_OPTIONS = 9 # шортлист stage2 строго ≤ 9 (бенч:
|
||||||
CACHE_TTL = 3600.0 # ответы детерминированы — час держим
|
CACHE_TTL = 3600.0 # ответы детерминированы — час держим
|
||||||
CACHE_MAX = 256
|
CACHE_MAX = 256
|
||||||
BREAKER_COOLDOWN = 300.0 # после сбоя молчим 5 минут, не тормозим тики таймаутами
|
BREAKER_COOLDOWN = 300.0 # после сбоя молчим 5 минут, не тормозим тики таймаутами
|
||||||
|
# облако: холодный старт clef после простоя бывает > 15 с (утро 2026-10-17:
|
||||||
|
# один таймаут → 5 минут ответов от CPU-nimble по 10 с) — короткий кулдаун
|
||||||
|
CLOUD_BREAKER_COOLDOWN = 60.0
|
||||||
|
|
||||||
_LOCK = threading.Lock()
|
_LOCK = threading.Lock()
|
||||||
_CACHE = {} # sha256(вопросы+состояние) → (ts, answers, model)
|
_CACHE = {} # sha256(вопросы+состояние) → (ts, answers, model)
|
||||||
|
|
@ -97,7 +100,7 @@ def backends() -> list:
|
||||||
primary = {"name": "primary", "key": key,
|
primary = {"name": "primary", "key": key,
|
||||||
"url": _cfg("SIMBIOT_DECISION_URL", CLOUD_URL),
|
"url": _cfg("SIMBIOT_DECISION_URL", CLOUD_URL),
|
||||||
"model": _cfg("SIMBIOT_DECISION_MODEL", "clef:27b"),
|
"model": _cfg("SIMBIOT_DECISION_MODEL", "clef:27b"),
|
||||||
"timeout": _float("SIMBIOT_DECISION_TIMEOUT", 15.0)}
|
"timeout": _float("SIMBIOT_DECISION_TIMEOUT", 30.0)}
|
||||||
else:
|
else:
|
||||||
primary = {"name": "primary", "key": "",
|
primary = {"name": "primary", "key": "",
|
||||||
"url": _cfg("SIMBIOT_DECISION_URL", LOCAL_URL),
|
"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:
|
except (urllib.error.URLError, OSError, ValueError, TimeoutError) as e:
|
||||||
err = f"{e.__class__.__name__}: {str(e)[:160]}"
|
err = f"{e.__class__.__name__}: {str(e)[:160]}"
|
||||||
with _LOCK:
|
with _LOCK:
|
||||||
_BREAKERS[be["name"]] = {"until": time.time() + BREAKER_COOLDOWN,
|
cool = CLOUD_BREAKER_COOLDOWN if be["key"] else BREAKER_COOLDOWN
|
||||||
"error": err}
|
_BREAKERS[be["name"]] = {"until": time.time() + cool, "error": err}
|
||||||
errors.append(f"{be['name']} {be['model']}: {err} "
|
errors.append(f"{be['name']} {be['model']}: {err} "
|
||||||
f"({int((time.time() - t1) * 1000)} мс)")
|
f"({int((time.time() - t1) * 1000)} мс)")
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
|
|
@ -254,7 +254,8 @@ def _is_pending(m: dict) -> bool:
|
||||||
return False
|
return False
|
||||||
# группа: только адресованное боту (harness/addressing.py, аудит 2026-10-17);
|
# группа: только адресованное боту (harness/addressing.py, аудит 2026-10-17);
|
||||||
# без вердикта триажа — ещё не входящее для LLM
|
# без вердикта триажа — ещё не входящее для 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
|
return False
|
||||||
if m.get("text"):
|
if m.get("text"):
|
||||||
return True
|
return True
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,10 @@ check("слово-подстрока не ловится («кодировка»
|
||||||
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))
|
||||||
|
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 = []
|
CALLS = []
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue