From a1767fd69c90a630a4f41a403c6b3eb5173e59ec Mon Sep 17 00:00:00 2001 From: Daniel Sateler Date: Mon, 2 Mar 2026 14:13:35 -0300 Subject: [PATCH] feat(whatsapp): consolidate tool progress into single editable message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of sending a separate WhatsApp message for each tool call during agent execution (N+1 messages), the first tool sends a new message and subsequent tools edit it to append their line. Result: 1 growing progress message + 1 final response = 2 messages instead of N+1. Changes: - bridge.js: Add POST /edit endpoint using Baileys message editing - base.py: Add optional edit_message() to BasePlatformAdapter (no-op default, so platforms without editing support work unchanged) - whatsapp.py: Implement edit_message() calling bridge /edit - run.py: Rewrite send_progress_messages() to accumulate tool lines and edit the progress message. Falls back to sending a new message if edit fails (graceful degradation). Before (5 tools = 6 messages): āš• Hermes Agent ─── šŸ” web_search... "query" āš• Hermes Agent ─── šŸ“„ web_extract... "url" āš• Hermes Agent ─── šŸ’» terminal... "pip install" āš• Hermes Agent ─── āœļø write_file... "app.py" āš• Hermes Agent ─── šŸ’» terminal... "python app.py" āš• Hermes Agent ─── Done! The server is running... After (5 tools = 2 messages): āš• Hermes Agent ─── šŸ” web_search... "query" šŸ“„ web_extract... "url" šŸ’» terminal... "pip install" āœļø write_file... "app.py" šŸ’» terminal... "python app.py" āš• Hermes Agent ─── Done! The server is running... Co-Authored-By: Claude Opus 4.6 --- gateway/platforms/base.py | 15 ++++++++- gateway/platforms/whatsapp.py | 31 ++++++++++++++++++- gateway/run.py | 51 ++++++++++++++++++++++++++----- scripts/whatsapp-bridge/bridge.js | 22 +++++++++++++ 4 files changed, 109 insertions(+), 10 deletions(-) diff --git a/gateway/platforms/base.py b/gateway/platforms/base.py index 18613fe54..702e737d7 100644 --- a/gateway/platforms/base.py +++ b/gateway/platforms/base.py @@ -398,7 +398,20 @@ class BasePlatformAdapter(ABC): SendResult with success status and message ID """ pass - + + async def edit_message( + self, + chat_id: str, + message_id: str, + content: str, + ) -> SendResult: + """ + Edit a previously sent message. Optional — platforms that don't + support editing return success=False and callers fall back to + sending a new message. + """ + return SendResult(success=False, error="Not supported") + async def send_typing(self, chat_id: str) -> None: """ Send a typing indicator. diff --git a/gateway/platforms/whatsapp.py b/gateway/platforms/whatsapp.py index b3916aef0..e3c96137f 100644 --- a/gateway/platforms/whatsapp.py +++ b/gateway/platforms/whatsapp.py @@ -351,7 +351,36 @@ class WhatsAppAdapter(BasePlatformAdapter): ) except Exception as e: return SendResult(success=False, error=str(e)) - + + async def edit_message( + self, + chat_id: str, + message_id: str, + content: str, + ) -> SendResult: + """Edit a previously sent message via the WhatsApp bridge.""" + if not self._running: + return SendResult(success=False, error="Not connected") + try: + import aiohttp + async with aiohttp.ClientSession() as session: + async with session.post( + f"http://localhost:{self._bridge_port}/edit", + json={ + "chatId": chat_id, + "messageId": message_id, + "message": content, + }, + timeout=aiohttp.ClientTimeout(total=15) + ) as resp: + if resp.status == 200: + return SendResult(success=True, message_id=message_id) + else: + error = await resp.text() + return SendResult(success=False, error=error) + except Exception as e: + return SendResult(success=False, error=str(e)) + async def send_typing(self, chat_id: str) -> None: """Send typing indicator via bridge.""" if not self._running: diff --git a/gateway/run.py b/gateway/run.py index fa6ada0fc..38d831f41 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -1939,32 +1939,67 @@ class GatewayRunner: progress_queue.put(msg) # Background task to send progress messages + # Accumulates tool lines into a single message that gets edited async def send_progress_messages(): if not progress_queue: return - + adapter = self.adapters.get(source.platform) if not adapter: return - + + progress_lines = [] # Accumulated tool lines + progress_msg_id = None # ID of the progress message to edit + while True: try: - # Non-blocking check with small timeout msg = progress_queue.get_nowait() - await adapter.send(chat_id=source.chat_id, content=msg) - # Restore typing indicator after sending progress message + progress_lines.append(msg) + full_text = "\n".join(progress_lines) + + if progress_msg_id is None: + # First tool: send as new message + result = await adapter.send(chat_id=source.chat_id, content=full_text) + if result.success and result.message_id: + progress_msg_id = result.message_id + else: + # Subsequent tools: try to edit, fall back to new message + result = await adapter.edit_message( + chat_id=source.chat_id, + message_id=progress_msg_id, + content=full_text, + ) + if not result.success: + # Edit failed — send as new message and track it + result = await adapter.send(chat_id=source.chat_id, content=full_text) + if result.success and result.message_id: + progress_msg_id = result.message_id + + # Restore typing indicator await asyncio.sleep(0.3) await adapter.send_typing(source.chat_id) + except queue.Empty: - await asyncio.sleep(0.3) # Check again soon + await asyncio.sleep(0.3) except asyncio.CancelledError: - # Drain remaining messages + # Drain remaining queued messages while not progress_queue.empty(): try: msg = progress_queue.get_nowait() - await adapter.send(chat_id=source.chat_id, content=msg) + progress_lines.append(msg) except Exception: break + # Final edit with all remaining tools + if progress_lines and progress_msg_id: + full_text = "\n".join(progress_lines) + try: + await adapter.edit_message( + chat_id=source.chat_id, + message_id=progress_msg_id, + content=full_text, + ) + except Exception: + pass return except Exception as e: logger.error("Progress message error: %s", e) diff --git a/scripts/whatsapp-bridge/bridge.js b/scripts/whatsapp-bridge/bridge.js index 951a62154..7404f5aed 100644 --- a/scripts/whatsapp-bridge/bridge.js +++ b/scripts/whatsapp-bridge/bridge.js @@ -8,6 +8,7 @@ * Endpoints (matches gateway/platforms/whatsapp.py expectations): * GET /messages - Long-poll for new incoming messages * POST /send - Send a message { chatId, message, replyTo? } + * POST /edit - Edit a sent message { chatId, messageId, message } * POST /typing - Send typing indicator { chatId } * GET /chat/:id - Get chat info * GET /health - Health check @@ -216,6 +217,27 @@ app.post('/send', async (req, res) => { } }); +// Edit a previously sent message +app.post('/edit', async (req, res) => { + if (!sock || connectionState !== 'connected') { + return res.status(503).json({ error: 'Not connected to WhatsApp' }); + } + + const { chatId, messageId, message } = req.body; + if (!chatId || !messageId || !message) { + return res.status(400).json({ error: 'chatId, messageId, and message are required' }); + } + + try { + const prefixed = `āš• *Hermes Agent*\n────────────\n${message}`; + const key = { id: messageId, fromMe: true, remoteJid: chatId }; + await sock.sendMessage(chatId, { text: prefixed, edit: key }); + res.json({ success: true }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + // Typing indicator app.post('/typing', async (req, res) => { if (!sock || connectionState !== 'connected') {