From 3ff504143af3cdc2de90b9f3293e1f26ceb19a56 Mon Sep 17 00:00:00 2001 From: Simbiot Harness Date: Sat, 17 Oct 2026 07:13:35 +0000 Subject: [PATCH] =?UTF-8?q?=D0=B0=D0=B4=D1=80=D0=B5=D1=81=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8F:=20=D0=B3=D1=80=D1=83=D0=BF=D0=BF=D1=8B=20=D0=B1?= =?UTF-8?q?=D0=B5=D0=B7=20chat=5Ftype=20=D0=BF=D0=BE=20chat=5Fid<0;=20?= =?UTF-8?q?=D0=BE=D0=B1=D0=BB=D0=B0=D0=BA=D0=BE=20decision=20=E2=80=94=20?= =?UTF-8?q?=D1=82=D0=B0=D0=B9=D0=BC=D0=B0=D1=83=D1=82=2030=20=D1=81,=20?= =?UTF-8?q?=D0=BA=D1=83=D0=BB=D0=B4=D0=B0=D1=83=D0=BD=2060=20=D1=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - старые записи инбокса без 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 --- README.md | 2 +- harness/addressing.py | 13 +++++++++++-- harness/decision.py | 9 ++++++--- harness/scheduler.py | 3 ++- tests/test_addressing.py | 4 ++++ 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 733d12d..f3150f3 100644 --- a/README.md +++ b/README.md @@ -356,7 +356,7 @@ nimble — арбитр там, где механика слепа; недост off — тесты герметичны), точечно `SIMBIOT_DECISION_`, `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 — потолок для всех. diff --git a/harness/addressing.py b/harness/addressing.py index 51cc042..57fa055 100644 --- a/harness/addressing.py +++ b/harness/addressing.py @@ -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" diff --git a/harness/decision.py b/harness/decision.py index a89d096..7c71a3a 100644 --- a/harness/decision.py +++ b/harness/decision.py @@ -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 diff --git a/harness/scheduler.py b/harness/scheduler.py index 0c76cfd..8061a31 100644 --- a/harness/scheduler.py +++ b/harness/scheduler.py @@ -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 diff --git a/tests/test_addressing.py b/tests/test_addressing.py index d00ddd6..d66ac73 100644 --- a/tests/test_addressing.py +++ b/tests/test_addressing.py @@ -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 = []