feat(whatsapp): consolidate tool progress into single editable message
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 <noreply@anthropic.com>
This commit is contained in:
parent
b4b426c69d
commit
a1767fd69c
4 changed files with 109 additions and 10 deletions
|
|
@ -399,6 +399,19 @@ class BasePlatformAdapter(ABC):
|
||||||
"""
|
"""
|
||||||
pass
|
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:
|
async def send_typing(self, chat_id: str) -> None:
|
||||||
"""
|
"""
|
||||||
Send a typing indicator.
|
Send a typing indicator.
|
||||||
|
|
|
||||||
|
|
@ -352,6 +352,35 @@ class WhatsAppAdapter(BasePlatformAdapter):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return SendResult(success=False, error=str(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:
|
async def send_typing(self, chat_id: str) -> None:
|
||||||
"""Send typing indicator via bridge."""
|
"""Send typing indicator via bridge."""
|
||||||
if not self._running:
|
if not self._running:
|
||||||
|
|
|
||||||
|
|
@ -1939,6 +1939,7 @@ class GatewayRunner:
|
||||||
progress_queue.put(msg)
|
progress_queue.put(msg)
|
||||||
|
|
||||||
# Background task to send progress messages
|
# Background task to send progress messages
|
||||||
|
# Accumulates tool lines into a single message that gets edited
|
||||||
async def send_progress_messages():
|
async def send_progress_messages():
|
||||||
if not progress_queue:
|
if not progress_queue:
|
||||||
return
|
return
|
||||||
|
|
@ -1947,24 +1948,58 @@ class GatewayRunner:
|
||||||
if not adapter:
|
if not adapter:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
progress_lines = [] # Accumulated tool lines
|
||||||
|
progress_msg_id = None # ID of the progress message to edit
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
# Non-blocking check with small timeout
|
|
||||||
msg = progress_queue.get_nowait()
|
msg = progress_queue.get_nowait()
|
||||||
await adapter.send(chat_id=source.chat_id, content=msg)
|
progress_lines.append(msg)
|
||||||
# Restore typing indicator after sending progress message
|
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 asyncio.sleep(0.3)
|
||||||
await adapter.send_typing(source.chat_id)
|
await adapter.send_typing(source.chat_id)
|
||||||
|
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
await asyncio.sleep(0.3) # Check again soon
|
await asyncio.sleep(0.3)
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
# Drain remaining messages
|
# Drain remaining queued messages
|
||||||
while not progress_queue.empty():
|
while not progress_queue.empty():
|
||||||
try:
|
try:
|
||||||
msg = progress_queue.get_nowait()
|
msg = progress_queue.get_nowait()
|
||||||
await adapter.send(chat_id=source.chat_id, content=msg)
|
progress_lines.append(msg)
|
||||||
except Exception:
|
except Exception:
|
||||||
break
|
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
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Progress message error: %s", e)
|
logger.error("Progress message error: %s", e)
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
* Endpoints (matches gateway/platforms/whatsapp.py expectations):
|
* Endpoints (matches gateway/platforms/whatsapp.py expectations):
|
||||||
* GET /messages - Long-poll for new incoming messages
|
* GET /messages - Long-poll for new incoming messages
|
||||||
* POST /send - Send a message { chatId, message, replyTo? }
|
* POST /send - Send a message { chatId, message, replyTo? }
|
||||||
|
* POST /edit - Edit a sent message { chatId, messageId, message }
|
||||||
* POST /typing - Send typing indicator { chatId }
|
* POST /typing - Send typing indicator { chatId }
|
||||||
* GET /chat/:id - Get chat info
|
* GET /chat/:id - Get chat info
|
||||||
* GET /health - Health check
|
* 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
|
// Typing indicator
|
||||||
app.post('/typing', async (req, res) => {
|
app.post('/typing', async (req, res) => {
|
||||||
if (!sock || connectionState !== 'connected') {
|
if (!sock || connectionState !== 'connected') {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue