diff --git a/runtime/application/gateway/inbound_service.py b/runtime/application/gateway/inbound_service.py index 663a7f31..927a5f8e 100644 --- a/runtime/application/gateway/inbound_service.py +++ b/runtime/application/gateway/inbound_service.py @@ -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 diff --git a/runtime/operations/whatsapp_bridge/baileys_runner.ts b/runtime/operations/whatsapp_bridge/baileys_runner.ts index af8bd071..2058a7da 100644 --- a/runtime/operations/whatsapp_bridge/baileys_runner.ts +++ b/runtime/operations/whatsapp_bridge/baileys_runner.ts @@ -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 { const isReplyToBot = Boolean( botJidRaw && quote.participant && - !mentions.length && botIdentityJids.some((botId) => mentionMatchesBotIdentity(quote.participant, botId)), ); @@ -952,6 +966,7 @@ async function main(): Promise { messageTimestamp: (msg as any).messageTimestamp || null, quotedParticipant: quote.participant || null, quotedStanzaId: quote.stanzaId || null, + quotedText: quote.quotedText || null, isReplyToBot, mentionsBot, mentionedJids: mentions, diff --git a/runtime/orchestration/group_ingest.py b/runtime/orchestration/group_ingest.py index 423c9272..54788b6f 100644 --- a/runtime/orchestration/group_ingest.py +++ b/runtime/orchestration/group_ingest.py @@ -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", diff --git a/tests/test_group_ingest.py b/tests/test_group_ingest.py index 0a09812e..826a4ff9 100644 --- a/tests/test_group_ingest.py +++ b/tests/test_group_ingest.py @@ -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"