diff --git a/backend/src/channels.py b/backend/src/channels.py index 8300e54..6420639 100644 --- a/backend/src/channels.py +++ b/backend/src/channels.py @@ -249,6 +249,7 @@ class Channels: }) self._trim(state["transcript"]) transcript = self._render(state["transcript"]) + await self.mark_as_read(agent_id, chat_id) await self._run_agent(agent_id, kind, chat_id, chat, sender_name, transcript) async def _run_agent(self, agent_id, kind, chat_id, chat, sender_name, transcript): @@ -274,6 +275,9 @@ class Channels: f"Latest message from: {sender_name}", ]) + # Signal presence for the lifetime of this run. + await self.set_online_status(agent_id, chat_id, online=True) + tool = ToolSpec( name="send_message", description="Send a text message back to this Telegram conversation.", @@ -338,6 +342,13 @@ class Channels: if len(text) > MAX_MESSAGE_LENGTH: text = text[:MAX_MESSAGE_LENGTH] + # Typing indicator immediately before the message so the user sees + # the three-dot animation right as the reply is ready. + try: + await self._api(token, "sendAction", {"chat_id": chat_id, "action": "typing"}) + except Exception as e: + print(f"[channel] sendAction typing failed for agent {agent_id}: {e}") + try: await self._api(token, "sendMessage", {"chat_id": chat_id, "text": text}) return "ok" @@ -345,6 +356,43 @@ class Channels: print(f"[channel] sendMessage failed for agent {agent_id}: {e}") return f"error: {e}" + async def send_typing(self, agent_id, chat_id, typing=True): + """Send a typing action indicator to the chat. + + Telegram exposes this via ``sendAction`` with ``typing`` (start) or + ``cancel`` (stop). Failures are swallowed: an indicator is cosmetic + and must never break a run. + """ + binding = await self.get_binding(agent_id) + if binding is None: + return + token = (binding.value or {}).get("token") + if not token: + return + action = "typing" if typing else "cancel" + try: + await self._api(token, "sendAction", {"chat_id": chat_id, "action": action}) + except Exception as e: + print(f"[channel] sendAction failed: {e}") + + async def mark_as_read(self, agent_id, chat_id): + """Mark an inbound message as read. + + Telegram bots have no native read receipt; acceptance is already + handled by :meth:`accept_update`. This stays an explicit hook on the + inbound path so a provider that does support receipts can be wired in + here later. + """ + pass + + async def set_online_status(self, agent_id, chat_id, online=True): + """Signal presence for the conversation. + + A single ``typing`` action marks the run as online; ``cancel`` clears + it. Delegates to :meth:`send_typing`. + """ + await self.send_typing(agent_id, chat_id, typing=online) + # --- In-memory transcript --- def _state(self, agent_id, chat_id):