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).
|
"""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
|
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
|
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
|
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,
|
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.
|
or the draft lands on top of the message and lingers for its 30 s.
|
||||||
"""
|
"""
|
||||||
if self._task is None:
|
if self._task is not None:
|
||||||
return
|
self._task.cancel()
|
||||||
self._task.cancel()
|
await asyncio.gather(self._task, return_exceptions=True)
|
||||||
with contextlib.suppress(asyncio.CancelledError):
|
self._task = None
|
||||||
await self._task
|
|
||||||
self._task = None
|
|
||||||
if self._inflight is not None:
|
if self._inflight is not None:
|
||||||
with contextlib.suppress(Exception):
|
with contextlib.suppress(Exception):
|
||||||
await self._inflight
|
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:
|
async def _run(self) -> None:
|
||||||
while not self._broken:
|
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.drafts import Draft
|
||||||
from beaver_gateway.frontends.telegram.inbox import Inbox
|
from beaver_gateway.frontends.telegram.inbox import Inbox
|
||||||
from beaver_gateway.frontends.telegram.outbox import Outbox
|
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:
|
if TYPE_CHECKING:
|
||||||
from beaver_gateway.core.bus import Event, EventBus
|
from beaver_gateway.core.bus import Event, EventBus
|
||||||
@@ -659,7 +659,9 @@ class TelegramFrontend(Frontend):
|
|||||||
f"⏳ {status_label(str(event['name']), event.get('input'))}"
|
f"⏳ {status_label(str(event['name']), event.get('input'))}"
|
||||||
)
|
)
|
||||||
case "turn.end":
|
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":
|
if event.get("stop") == "error" and event.get("origin") == "user":
|
||||||
await self._deliver(
|
await self._deliver(
|
||||||
conv,
|
conv,
|
||||||
@@ -691,7 +693,11 @@ class TelegramFrontend(Frontend):
|
|||||||
async def _on_reply(self, conv: Conversation, event: Event) -> None:
|
async def _on_reply(self, conv: Conversation, event: Event) -> None:
|
||||||
turn_id = str(event.get("turn_id") or "")
|
turn_id = str(event.get("turn_id") or "")
|
||||||
origin = str(event.get("item_origin") or "")
|
origin = str(event.get("item_origin") or "")
|
||||||
|
text = str(event.get("text") or "")
|
||||||
await self._unreact(event.get("item"))
|
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("сид"):
|
if origin != FRONTEND and not origin.startswith("сид"):
|
||||||
user_text = str(event.get("user_text") or "")
|
user_text = str(event.get("user_text") or "")
|
||||||
if user_text:
|
if user_text:
|
||||||
@@ -701,9 +707,7 @@ class TelegramFrontend(Frontend):
|
|||||||
turn_id=turn_id,
|
turn_id=turn_id,
|
||||||
key=f"{turn_id}:mirror",
|
key=f"{turn_id}:mirror",
|
||||||
)
|
)
|
||||||
await self._deliver(
|
await self._deliver(conv, text, turn_id=turn_id, key=f"{turn_id}:reply")
|
||||||
conv, str(event.get("text") or ""), turn_id=turn_id, key=f"{turn_id}:reply"
|
|
||||||
)
|
|
||||||
|
|
||||||
# ---- drafts ------------------------------------------------------------
|
# ---- drafts ------------------------------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -251,6 +251,9 @@ async def test_general_is_master_and_reply_has_no_thread(stack: Stack) -> None:
|
|||||||
await asyncio.sleep(0.2)
|
await asyncio.sleep(0.2)
|
||||||
final = max(i for i, o in enumerate(stack.bot.order) if o[0] == "message")
|
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:])
|
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)
|
rows = await stack.world.conversations.queue.recent(master.id)
|
||||||
assert [r.origin for r in rows] == ["telegram"]
|
assert [r.origin for r in rows] == ["telegram"]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user