fix(whatsapp): pass quoted UME alarm to agent when @ bot

Extract quotedText from reply context correctly and enrich inbound only after mention detection, keeping strict @-required group policy.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-06-23 14:36:14 +08:00
parent 538945fb66
commit 5336f8835f
4 changed files with 72 additions and 4 deletions

View file

@ -687,10 +687,14 @@ def process_inbound_payload(payload: dict[str, Any]) -> dict[str, Any]:
account = store.find_user_by_channel_account(channel=inbound.channel, account_id=account_id) or {}
from runtime.orchestration.group_ingest import (
build_group_sender_context,
enrich_alert_group_question,
extract_quoted_ume_alert_text,
mentions_include_bot,
metadata_mentions_bot,
resolve_group_policy,
session_user_key,
should_process_group_inbound,
text_mentions_bot,
)
group_policy = resolve_group_policy(account=account)
@ -844,8 +848,21 @@ def process_inbound_payload(payload: dict[str, Any]) -> dict[str, Any]:
elif not reply:
user_text = (inbound.text or "").strip()
if inbound.is_group:
meta_for_group = inbound.metadata if isinstance(inbound.metadata, dict) else {}
bot_reached = metadata_mentions_bot(meta_for_group) or mentions_include_bot(
mentions=list(inbound.mentions or []),
bot_jid=bot_jid,
metadata=meta_for_group,
) or text_mentions_bot(text=user_text, bot_jid=bot_jid)
if bot_reached:
quoted_alert = extract_quoted_ume_alert_text(metadata=meta_for_group)
if quoted_alert:
user_text = enrich_alert_group_question(
user_text=user_text,
quoted_alert=quoted_alert,
)
sender_ctx = build_group_sender_context(
metadata=inbound.metadata if isinstance(inbound.metadata, dict) else {},
metadata=meta_for_group,
external_user_id=inbound.external_user_id,
)
user_text = f"{sender_ctx}\n{user_text}" if user_text else sender_ctx

View file

@ -76,11 +76,14 @@ function unwrapMessage(m: proto.IMessage | null | undefined): proto.IMessage | n
function messageContextInfo(m: proto.IMessage | null | undefined): proto.IContextInfo | null | undefined {
const u = unwrapMessage(m);
if (!u) return null;
const extCtx = u.extendedTextMessage?.contextInfo;
// Reply+@ messages usually carry quote + mentions on extendedTextMessage.contextInfo.
if (extCtx && (extCtx.quotedMessage || extCtx.stanzaId)) return extCtx;
const top = (u as proto.IMessage & { messageContextInfo?: proto.IMessageContextInfo }).messageContextInfo;
const fromTop = top?.mentionedJid?.length ? top : null;
return (
(fromTop as unknown as proto.IContextInfo) ||
u.extendedTextMessage?.contextInfo ||
extCtx ||
u.imageMessage?.contextInfo ||
u.videoMessage?.contextInfo ||
u.documentMessage?.contextInfo ||
@ -129,11 +132,23 @@ function extractMentions(m: proto.IMessage | null | undefined): string[] {
return raw.map((j) => String(j || "").trim()).filter(Boolean);
}
function extractQuoteContext(m: proto.IMessage | null | undefined): { participant: string; stanzaId: string } {
function pickQuotedText(m: proto.IMessage | null | undefined): string {
const ctx = messageContextInfo(m);
const qm = ctx?.quotedMessage;
if (!qm) return "";
return pickText(qm);
}
function extractQuoteContext(m: proto.IMessage | null | undefined): {
participant: string;
stanzaId: string;
quotedText: string;
} {
const ctx = messageContextInfo(m);
return {
participant: String(ctx?.participant || "").trim(),
stanzaId: String(ctx?.stanzaId || "").trim(),
quotedText: pickQuotedText(m),
};
}
@ -937,7 +952,6 @@ async function main(): Promise<void> {
const isReplyToBot = Boolean(
botJidRaw &&
quote.participant &&
!mentions.length &&
botIdentityJids.some((botId) => mentionMatchesBotIdentity(quote.participant, botId)),
);
@ -952,6 +966,7 @@ async function main(): Promise<void> {
messageTimestamp: (msg as any).messageTimestamp || null,
quotedParticipant: quote.participant || null,
quotedStanzaId: quote.stanzaId || null,
quotedText: quote.quotedText || null,
isReplyToBot,
mentionsBot,
mentionedJids: mentions,

View file

@ -146,6 +146,24 @@ def is_reply_to_bot(*, metadata: dict[str, Any] | None, bot_jid: str | None) ->
return False
def extract_quoted_ume_alert_text(*, metadata: dict[str, Any] | None) -> str:
raw = _metadata_raw(metadata)
quoted_text = str(raw.get("quotedText") or raw.get("quoted_text") or "").strip()
if quoted_text and (quoted_text.startswith("[UME") or "[UME Alarm" in quoted_text[:120]):
return quoted_text
return ""
def enrich_alert_group_question(*, user_text: str, quoted_alert: str) -> str:
body = str(user_text or "").strip()
quote = str(quoted_alert or "").strip()
if not quote:
return body
if not body:
return f"[Quoted UME alarm]\n{quote}"
return f"[Quoted UME alarm]\n{quote}\n\n[User question]\n{body}"
def normalize_jids(jids: list[str]) -> set[str]:
out: set[str] = set()
for raw in jids or []:
@ -318,6 +336,8 @@ __all__ = [
"GroupPolicyConfig",
"build_group_sender_context",
"build_whatsapp_group_reply_metadata",
"enrich_alert_group_question",
"extract_quoted_ume_alert_text",
"mentions_include_bot",
"metadata_mentions_bot",
"normalize_jid",

View file

@ -221,6 +221,22 @@ def test_should_accept_group_trigger_without_mention() -> None:
)
def test_enrich_quoted_ume_alarm_when_mentioned() -> None:
from runtime.orchestration.group_ingest import (
enrich_alert_group_question,
extract_quoted_ume_alert_text,
)
meta = {"raw": {"quotedText": "[UME Alarm Raised]\nDevice: NE1", "mentionsBot": True}}
assert extract_quoted_ume_alert_text(metadata=meta).startswith("[UME")
enriched = enrich_alert_group_question(
user_text="what happened?",
quoted_alert="[UME Alarm Raised]\nDevice: NE1",
)
assert "[Quoted UME alarm]" in enriched
assert "what happened?" in enriched
def test_session_user_key_group_sentinel() -> None:
assert session_user_key(is_group=True, external_user_id="111@s.whatsapp.net") == GROUP_SESSION_USER_SENTINEL
assert session_user_key(is_group=False, external_user_id="111@s.whatsapp.net") == "111@s.whatsapp.net"