From 0daf10135cf00d7c84be2458a1ab68b4452a76e5 Mon Sep 17 00:00:00 2001 From: Simbiot Harness Date: Thu, 15 Oct 2026 23:02:41 +0000 Subject: [PATCH] =?UTF-8?q?e-261015222515:=20=D0=BF=D0=B0=D0=BC=D1=8F?= =?UTF-8?q?=D1=82=D1=8C=20=D1=80=D0=B0=D0=B7=D0=B3=D0=BE=D0=B2=D0=BE=D1=80?= =?UTF-8?q?=D0=B0=20=D0=B2=20=D0=BA=D0=BE=D0=BD=D1=82=D0=B5=D0=BA=D1=81?= =?UTF-8?q?=D1=82=D0=B5=20=D1=82=D0=B8=D0=BA=D0=B0=20=E2=80=94=20=D1=81?= =?UTF-8?q?=D0=B5=D0=BA=D1=86=D0=B8=D1=8F=20=D0=9D=D0=95=D0=94=D0=90=D0=92?= =?UTF-8?q?=D0=9D=D0=98=D0=99=20=D0=A0=D0=90=D0=97=D0=93=D0=9E=D0=92=D0=9E?= =?UTF-8?q?=D0=A0=20(=D0=BE=D1=82=D0=B2=D0=B5=D1=87=D0=B5=D0=BD=D0=BD?= =?UTF-8?q?=D1=8B=D0=B5=20=D1=80=D0=B5=D0=BF=D0=BB=D0=B8=D0=BA=D0=B8=20+?= =?UTF-8?q?=20=D0=BD=D0=B0=D1=88=20=D0=BE=D1=82=D0=B2=D0=B5=D1=82=20=D1=82?= =?UTF-8?q?=D0=B5=D1=85=20=D0=B6=D0=B5=20=D1=87=D0=B0=D1=82=D0=BE=D0=B2,?= =?UTF-8?q?=20cap=2012)=20=D1=80=D1=8F=D0=B4=D0=BE=D0=BC=20=D1=81=20=D0=B1?= =?UTF-8?q?=D0=B0=D1=82=D1=87=D0=B5=D0=BC=20=D0=BD=D0=B5=D0=BE=D1=82=D0=B2?= =?UTF-8?q?=D0=B5=D1=87=D0=B5=D0=BD=D0=BD=D1=8B=D1=85;=20=D0=BC=D0=B8?= =?UTF-8?q?=D0=BD-=D1=83=D1=80=D0=BE=D0=B2=D0=B5=D0=BD=D1=8C=20=D0=B1?= =?UTF-8?q?=D0=BE=D0=BB=D1=8C=D1=88=D0=B5=20=D0=BD=D0=B5=20=D1=82=D0=B5?= =?UTF-8?q?=D1=80=D1=8F=D0=B5=D1=82=20=D0=BD=D0=B8=D1=82=D1=8C=20=D0=BF?= =?UTF-8?q?=D0=BE=D1=81=D0=BB=D0=B5=20ReplySent;=20=D1=82=D0=B5=D1=81?= =?UTF-8?q?=D1=82=20+6=20PASS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- harness/scheduler.py | 24 ++++++++++++++++++++++++ tests/test_inbox_batch.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/harness/scheduler.py b/harness/scheduler.py index 16f0484..0f00e9a 100644 --- a/harness/scheduler.py +++ b/harness/scheduler.py @@ -380,6 +380,7 @@ def build_context(level_cfg, model: dict, batch=None): if m.get("reply_to_message_id") is not None else "")) + ": " + m.get('text','')[:1200]) batch_ids = ",".join(ids) + chat_ids = {m.get("chat_id") for m in inbox if m.get("chat_id") is not None} # t-261013131228: если в батче есть reply на сообщение без текста — # добавляем последние документы этого чата с caption, чтобы триаж # видел, о каком документе речь, даже если «голова» старше капа инбокса @@ -395,6 +396,29 @@ def build_context(level_cfg, model: dict, batch=None): + "; ".join(docs)) if docs_ctx: parts.append("КОНТЕКСТ ДОКУМЕНТОВ ЧАТА:\n" + "\n".join(docs_ctx.values())) + # t-261015222515: память разговора — рядом с батчем неотвеченных лежит + # нить недавнего диалога (отвеченные реплики + наши ответы) тех же + # чатов. Раньше после ReplySent входящее выпадало из контекста тика — + # минутный уровень отвечал без памяти о разговоре (Alex про SPA-договор: + # «только что же обсуждали»). Источник — та же проекция инбокса + # (reply_text из ReplySent), отдельный стор не нужен. + replied_hist = [m for m in (model.get("inbox") or {}).values() + if m.get("replied") and not m.get("ignored") + and m.get("chat_id") in chat_ids + and (m.get("text") or "").strip()] + if replied_hist: + replied_hist = sorted(replied_hist, key=lambda x: x.get("ts") or "")[-12:] + hist_lines = [] + for m in replied_hist: + m_id = "m-" + str(m.get("update_id")) + who = m.get("username") or m.get("first_name") or m.get("user_id") + line = f"- [{m_id}] {who}: {(m.get('text') or '')[:600]}" + rt = (m.get("reply_text") or "").strip() + if rt: + line += f"\n └ наш ответ: {rt[:600]}" + hist_lines.append(line) + parts.append("НЕДАВНИЙ РАЗГОВОР (уже отвечено; это память тика — НЕ отвечай " + "на них снова, используй как контекст):\n" + "\n".join(hist_lines)) # t-260930064924: батч-триаж — ВСЕ неотвеченные обрабатываются ОДНИМ тиком. # Раньше мин-уровень отвечал по одному за тик (m-516173222..228 — 7 тиков # подряд на один разговор). Теперь в контексте лежит ГОТОВАЯ команда diff --git a/tests/test_inbox_batch.py b/tests/test_inbox_batch.py index b6925e5..d9ca1bc 100644 --- a/tests/test_inbox_batch.py +++ b/tests/test_inbox_batch.py @@ -67,6 +67,35 @@ check("build_context: ГОТОВАЯ команда со ВСЕМИ id", f"cli.py tg-reply {ids_line} -c" in sec, sec[:400]) check("build_context: инструкция «за ЭТОТ тик»", "именно в ЭТОМ тике" in sec) +# ── 1b. t-261015222515: память разговора — отвеченные реплики тех же чатов ── +# Сценарий эскалации: обсуждали SPA-договор, мин-тик ответил (ReplySent) — +# на следующем тике контекст ДОЛЖЕН помнить нить (текст + наш ответ), +# иначе триаж отвечает без памяти о разговоре (Alex: «только что же обсуждали»). +fake["inbox"]["m-70100"] = { + "update_id": 70100, "chat_id": 42, "text": "вот этот SPA-договор мы только что обсуждали", + "kind": "text", "replied": True, "ignored": False, + "reply_text": "да, это вторая версия без ЗП-обязательств", + "ts": "2026-09-30T06:55:00+00:00", +} +fake["inbox"]["m-70101"] = { + "update_id": 70101, "chat_id": 7, "text": "сообщение ДРУГОГО чата — не должно попасть", + "kind": "text", "replied": True, "ignored": False, + "ts": "2026-09-30T06:56:00+00:00", +} +ctx = build_context(LEVELS["min"], fake) +hist = re.search(r"НЕДАВНИЙ РАЗГОВОР.*?(?:\n\n|$)", ctx, re.S) +hsec = hist.group(0) if hist else "" +check("память: секция НЕДАВНИЙ РАЗГОВОР есть", bool(hist), ctx[:200]) +check("память: отвеченное сообщение в контексте", "SPA-договор" in hsec, hsec[:200]) +check("память: наш ответ виден", "без ЗП-обязательств" in hsec, hsec[:300]) +check("память: чужой чат НЕ в памяти", "ДРУГОГО чата" not in hsec and "m-70101" not in hsec) +check("память: помечена «НЕ отвечай снова»", "НЕ отвечай" in hsec) +# без неотвеченных (нет батча) — секции нет: нечего контекстуализировать +only_replied = {"inbox": {"m-70100": dict(fake["inbox"]["m-70100"])}, + "tasks": {}, "escalations": {}, "policy": {}} +check("память: без неотвеченных секции нет", + "НЕДАВНИЙ РАЗГОВОР" not in build_context(LEVELS["min"], only_replied)) + # ── 2. send_batch_reply: одна отправка → ReplySent на каждое входящее ── # сценарий из постановки: 7 сообщений одного разговора об экономии тиков for i, text in enumerate([