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([