fix(telegram): last draft push is the final text so the client folds it into the message
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
"""One ``sendMessageDraft`` stream per running turn (§3.8).
|
||||
|
||||
The client folds a draft into the message that follows only when their
|
||||
texts are identical - so the last push is the final text itself, rendered
|
||||
exactly as the outbox will send it, with no status line.
|
||||
|
||||
A draft is ephemeral and lives 30 s, Telegram throttles edits to about one
|
||||
per second per chat, and thinking or a tool call would otherwise look like a
|
||||
hang - so the draft opens with a status line straight away, is refreshed on
|
||||
@@ -74,15 +78,29 @@ class Draft:
|
||||
The final ``sendMessage`` must reach Telegram after the last draft,
|
||||
or the draft lands on top of the message and lingers for its 30 s.
|
||||
"""
|
||||
if self._task is None:
|
||||
return
|
||||
if self._task is not None:
|
||||
self._task.cancel()
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
await self._task
|
||||
await asyncio.gather(self._task, return_exceptions=True)
|
||||
self._task = None
|
||||
if self._inflight is not None:
|
||||
with contextlib.suppress(Exception):
|
||||
await self._inflight
|
||||
self._inflight = None
|
||||
|
||||
async def finish(self, text: str) -> None:
|
||||
"""Last push: the final text verbatim, so the message replaces the draft."""
|
||||
await self.stop()
|
||||
if self._broken or not text.strip():
|
||||
return
|
||||
try:
|
||||
try:
|
||||
await self._send(to_html(text), "HTML")
|
||||
except TelegramBadRequest:
|
||||
await self._send(text, None)
|
||||
except TelegramAPIError as exc:
|
||||
_log.warning(
|
||||
"final draft to %s/%s failed: %s", self._chat_id, self._thread_id, exc
|
||||
)
|
||||
|
||||
async def _run(self) -> None:
|
||||
while not self._broken:
|
||||
|
||||
@@ -39,7 +39,7 @@ from beaver_gateway.frontends.base import Frontend
|
||||
from beaver_gateway.frontends.telegram.drafts import Draft
|
||||
from beaver_gateway.frontends.telegram.inbox import Inbox
|
||||
from beaver_gateway.frontends.telegram.outbox import Outbox
|
||||
from beaver_gateway.frontends.telegram.render import status_label
|
||||
from beaver_gateway.frontends.telegram.render import chunks, status_label
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from beaver_gateway.core.bus import Event, EventBus
|
||||
@@ -659,7 +659,9 @@ class TelegramFrontend(Frontend):
|
||||
f"⏳ {status_label(str(event['name']), event.get('input'))}"
|
||||
)
|
||||
case "turn.end":
|
||||
await self._close_draft(key)
|
||||
draft = self._drafts.get(key)
|
||||
if draft is not None:
|
||||
await draft.stop()
|
||||
if event.get("stop") == "error" and event.get("origin") == "user":
|
||||
await self._deliver(
|
||||
conv,
|
||||
@@ -691,7 +693,11 @@ class TelegramFrontend(Frontend):
|
||||
async def _on_reply(self, conv: Conversation, event: Event) -> None:
|
||||
turn_id = str(event.get("turn_id") or "")
|
||||
origin = str(event.get("item_origin") or "")
|
||||
text = str(event.get("text") or "")
|
||||
await self._unreact(event.get("item"))
|
||||
draft = self._drafts.pop(conv.external_id, None)
|
||||
if draft is not None:
|
||||
await draft.finish(chunks(text)[-1] if text.strip() else "")
|
||||
if origin != FRONTEND and not origin.startswith("сид"):
|
||||
user_text = str(event.get("user_text") or "")
|
||||
if user_text:
|
||||
@@ -701,9 +707,7 @@ class TelegramFrontend(Frontend):
|
||||
turn_id=turn_id,
|
||||
key=f"{turn_id}:mirror",
|
||||
)
|
||||
await self._deliver(
|
||||
conv, str(event.get("text") or ""), turn_id=turn_id, key=f"{turn_id}:reply"
|
||||
)
|
||||
await self._deliver(conv, text, turn_id=turn_id, key=f"{turn_id}:reply")
|
||||
|
||||
# ---- drafts ------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -251,6 +251,9 @@ async def test_general_is_master_and_reply_has_no_thread(stack: Stack) -> None:
|
||||
await asyncio.sleep(0.2)
|
||||
final = max(i for i, o in enumerate(stack.bot.order) if o[0] == "message")
|
||||
assert all(o[0] != "draft" for o in stack.bot.order[final:])
|
||||
last = stack.bot.drafts[-1]
|
||||
assert last["text"] == reply["text"] and last["parse_mode"] == "HTML"
|
||||
assert stack.bot.drafts[0]["text"] == "⏳ думаю"
|
||||
rows = await stack.world.conversations.queue.recent(master.id)
|
||||
assert [r.origin for r in rows] == ["telegram"]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user