mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-04-28 03:30:23 +00:00
feat: reply to specific messages in group chats with quote
This commit is contained in:
parent
6bf63eb9c6
commit
71a6eb7557
7 changed files with 47 additions and 11 deletions
|
|
@ -4,7 +4,7 @@ import asyncio
|
|||
from helpers.extension import Extension
|
||||
from helpers.print_style import PrintStyle
|
||||
from agent import AgentContext, LoopData, UserMessage
|
||||
from plugins._whatsapp_integration.helpers.handler import CTX_WA_CHAT_ID, CTX_WA_ATTACHMENTS
|
||||
from plugins._whatsapp_integration.helpers.handler import CTX_WA_CHAT_ID, CTX_WA_ATTACHMENTS, CTX_WA_REPLY_TO
|
||||
|
||||
MAX_SEND_RETRIES: int = 2
|
||||
CTX_SEND_FAILURES: str = "_wa_send_failures"
|
||||
|
|
@ -25,15 +25,17 @@ class WhatsAppAutoReply(Extension):
|
|||
return
|
||||
|
||||
attachments = context.data.pop(CTX_WA_ATTACHMENTS, [])
|
||||
reply_to = context.data.pop(CTX_WA_REPLY_TO, "")
|
||||
if attachments:
|
||||
PrintStyle.info(f"WhatsApp: sending reply with {len(attachments)} attachment(s)")
|
||||
asyncio.create_task(self._send_reply(context, response_text, attachments))
|
||||
asyncio.create_task(self._send_reply(context, response_text, attachments, reply_to))
|
||||
|
||||
async def _send_reply(
|
||||
self, context: AgentContext, response_text: str, attachments: list[str],
|
||||
reply_to: str = "",
|
||||
):
|
||||
from plugins._whatsapp_integration.helpers.handler import send_wa_reply
|
||||
error = await send_wa_reply(context, response_text, attachments)
|
||||
error = await send_wa_reply(context, response_text, attachments, reply_to=reply_to)
|
||||
if not error:
|
||||
context.data[CTX_SEND_FAILURES] = 0
|
||||
return
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
from helpers.extension import Extension
|
||||
from helpers.print_style import PrintStyle
|
||||
from helpers.tool import Response
|
||||
from plugins._whatsapp_integration.helpers.handler import CTX_WA_CHAT_ID, CTX_WA_ATTACHMENTS
|
||||
from plugins._whatsapp_integration.helpers.handler import CTX_WA_CHAT_ID, CTX_WA_ATTACHMENTS, CTX_WA_REPLY_TO
|
||||
|
||||
|
||||
class WhatsAppResponseIntercept(Extension):
|
||||
|
|
@ -23,10 +23,13 @@ class WhatsAppResponseIntercept(Extension):
|
|||
if not tool:
|
||||
return
|
||||
|
||||
# Capture attachments for later (process_chain_end) or inline send
|
||||
# Capture attachments and reply_to for later (process_chain_end) or inline send
|
||||
attachments = tool.args.get("attachments", [])
|
||||
if attachments:
|
||||
context.data[CTX_WA_ATTACHMENTS] = attachments
|
||||
reply_to = tool.args.get("reply_to", "")
|
||||
if reply_to:
|
||||
context.data[CTX_WA_REPLY_TO] = reply_to
|
||||
|
||||
# Check break_loop arg from agent
|
||||
agent_break = tool.args.get("break_loop", True)
|
||||
|
|
@ -43,11 +46,12 @@ class WhatsAppResponseIntercept(Extension):
|
|||
|
||||
text = tool.args.get("text", tool.args.get("message", ""))
|
||||
attachments = context.data.pop(CTX_WA_ATTACHMENTS, [])
|
||||
reply_to = context.data.pop(CTX_WA_REPLY_TO, "")
|
||||
|
||||
if attachments:
|
||||
PrintStyle.info(f"WhatsApp: sending update with {len(attachments)} attachment(s)")
|
||||
|
||||
error = await send_wa_reply(context, text, attachments or None)
|
||||
error = await send_wa_reply(context, text, attachments or None, reply_to=reply_to)
|
||||
|
||||
if error:
|
||||
result = agent.read_prompt("fw.wa.update_error.md", error=error)
|
||||
|
|
|
|||
|
|
@ -28,8 +28,10 @@ CTX_WA_SENDER_NAME = "wa_sender_name"
|
|||
CTX_WA_SENDER_NUMBER = "wa_sender_number"
|
||||
CTX_WA_IS_GROUP = "wa_is_group"
|
||||
CTX_WA_LAST_BODY = "wa_last_body"
|
||||
CTX_WA_LAST_MSG_ID = "wa_last_msg_id"
|
||||
# Transient — consumed per-reply, not persisted
|
||||
CTX_WA_ATTACHMENTS = "_wa_response_attachments"
|
||||
CTX_WA_REPLY_TO = "_wa_reply_to"
|
||||
|
||||
# Poll task — lives here (not in extension module) because
|
||||
# extension modules are re-executed on each job_loop tick,
|
||||
|
|
@ -112,6 +114,7 @@ async def _start_new_chat(config: dict, msg: dict) -> None:
|
|||
context.data[CTX_WA_SENDER_NUMBER] = sender_number
|
||||
context.data[CTX_WA_IS_GROUP] = is_group
|
||||
context.data[CTX_WA_LAST_BODY] = msg.get("body", "")
|
||||
context.data[CTX_WA_LAST_MSG_ID] = msg.get("messageId", "")
|
||||
|
||||
project = config.get("project", "")
|
||||
if project:
|
||||
|
|
@ -147,6 +150,7 @@ async def _route_to_chat(
|
|||
return
|
||||
|
||||
context.data[CTX_WA_LAST_BODY] = msg.get("body", "")
|
||||
context.data[CTX_WA_LAST_MSG_ID] = msg.get("messageId", "")
|
||||
|
||||
user_msg = _build_user_message(context.agent0, msg, config)
|
||||
msg_id = str(uuid.uuid4())
|
||||
|
|
@ -196,6 +200,7 @@ def _build_user_message(agent: Agent, msg: dict, config: dict) -> str:
|
|||
sender_name=sender_name,
|
||||
sender_number=sender_number,
|
||||
group_name=msg.get("chatName", ""),
|
||||
message_id=msg.get("messageId", ""),
|
||||
body=msg.get("body", ""),
|
||||
)
|
||||
instructions = config.get("agent_instructions", "")
|
||||
|
|
@ -214,6 +219,7 @@ async def send_wa_reply(
|
|||
context: AgentContext,
|
||||
response_text: str,
|
||||
attachments: list[str] | None = None,
|
||||
reply_to: str = "",
|
||||
) -> str | None:
|
||||
chat_id = context.data.get(CTX_WA_CHAT_ID)
|
||||
if not chat_id:
|
||||
|
|
@ -223,12 +229,16 @@ async def send_wa_reply(
|
|||
port = int(config.get("bridge_port", 3100))
|
||||
base_url = bridge_manager.get_bridge_url(port)
|
||||
|
||||
# For group chats, auto-reply to last received message if no explicit reply_to
|
||||
if not reply_to and context.data.get(CTX_WA_IS_GROUP):
|
||||
reply_to = context.data.get(CTX_WA_LAST_MSG_ID, "")
|
||||
|
||||
# Typing indicator
|
||||
await wa_client.send_typing(base_url, chat_id)
|
||||
|
||||
# Send text
|
||||
try:
|
||||
result = await wa_client.send_message(base_url, chat_id, response_text)
|
||||
result = await wa_client.send_message(base_url, chat_id, response_text, reply_to=reply_to)
|
||||
if result.get("error"):
|
||||
return result["error"]
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -18,12 +18,15 @@ async def get_messages(base_url: str) -> list[dict]:
|
|||
|
||||
|
||||
async def send_message(
|
||||
base_url: str, chat_id: str, message: str,
|
||||
base_url: str, chat_id: str, message: str, reply_to: str = "",
|
||||
) -> dict:
|
||||
payload: dict = {"chatId": chat_id, "message": message}
|
||||
if reply_to:
|
||||
payload["replyTo"] = reply_to
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(
|
||||
f"{base_url}/send",
|
||||
json={"chatId": chat_id, "message": message},
|
||||
json=payload,
|
||||
timeout=aiohttp.ClientTimeout(total=30),
|
||||
) as resp:
|
||||
return await resp.json()
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ dont use code to send message
|
|||
break_loop true > stop working and wait for user reply
|
||||
break_loop false > only for mid-task progress updates then keep working
|
||||
include file paths in attachments array for sending files
|
||||
in group chats use reply_to with msg id to quote a specific message
|
||||
group replies auto-quote last received message if reply_to omitted
|
||||
usage:
|
||||
|
||||
~~~json
|
||||
|
|
@ -27,6 +29,7 @@ usage:
|
|||
"attachments": [
|
||||
"/path/file.png"
|
||||
],
|
||||
"reply_to": "msg_id",
|
||||
"break_loop": true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
[WhatsApp group "{{group_name}}" from {{sender_name}}]
|
||||
[WhatsApp group "{{group_name}}" from {{sender_name}} msg:{{message_id}}]
|
||||
|
||||
{{body}}
|
||||
|
|
|
|||
|
|
@ -91,6 +91,10 @@ const MAX_QUEUE_SIZE = 100;
|
|||
const recentlySentIds = new Set();
|
||||
const MAX_RECENT_IDS = 50;
|
||||
|
||||
// Store received messages for reply quoting
|
||||
const messageStore = new Map();
|
||||
const MAX_STORED_MESSAGES = 200;
|
||||
|
||||
let sock = null;
|
||||
let connectionState = 'disconnected';
|
||||
let latestQrDataUrl = null;
|
||||
|
|
@ -348,6 +352,12 @@ async function startSocket() {
|
|||
timestamp: msg.messageTimestamp,
|
||||
};
|
||||
|
||||
// Store raw message for reply quoting
|
||||
messageStore.set(msg.key.id, msg);
|
||||
if (messageStore.size > MAX_STORED_MESSAGES) {
|
||||
messageStore.delete(messageStore.keys().next().value);
|
||||
}
|
||||
|
||||
messageQueue.push(event);
|
||||
if (messageQueue.length > MAX_QUEUE_SIZE) {
|
||||
messageQueue.shift();
|
||||
|
|
@ -378,7 +388,11 @@ app.post('/send', async (req, res) => {
|
|||
}
|
||||
|
||||
try {
|
||||
const sent = await sock.sendMessage(chatId, { text: message });
|
||||
const opts = {};
|
||||
if (replyTo && messageStore.has(replyTo)) {
|
||||
opts.quoted = messageStore.get(replyTo);
|
||||
}
|
||||
const sent = await sock.sendMessage(chatId, { text: message }, opts);
|
||||
|
||||
if (sent?.key?.id) {
|
||||
recentlySentIds.add(sent.key.id);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue