diff --git a/gateway-bot/behavior_policy.py b/gateway-bot/behavior_policy.py index ddf52f83..3c4636a7 100644 --- a/gateway-bot/behavior_policy.py +++ b/gateway-bot/behavior_policy.py @@ -118,6 +118,24 @@ def has_recent_interaction(agent_id: str, chat_id: str, user_id: str) -> bool: return is_recent +def has_agent_chat_participation(agent_id: str, chat_id: str) -> bool: + """ + Check if agent has responded to ANY user in this chat recently. + Used for thread_has_agent_participation (SOWA Priority 11). + + Different from has_recent_interaction(): + - has_recent_interaction: THIS user talked to agent (per-user) + - has_agent_chat_participation: agent talked to ANYONE in this chat (per-chat) + """ + now = time.time() + str_chat_id = str(chat_id) + for (aid, cid, _uid), timestamp in _conversation_context.items(): + if aid == agent_id and cid == str_chat_id: + if now - timestamp < CONVERSATION_CONTEXT_TIMEOUT: + return True + return False + + def cleanup_old_contexts() -> None: """Remove old conversation contexts and ACK cooldowns to prevent memory leak.""" global _conversation_context, _ack_cooldown diff --git a/gateway-bot/http_api.py b/gateway-bot/http_api.py index d5e320da..4c20a92d 100644 --- a/gateway-bot/http_api.py +++ b/gateway-bot/http_api.py @@ -41,6 +41,7 @@ from behavior_policy import ( record_ack, get_ack_text, is_prober_request, + has_agent_chat_participation, NO_OUTPUT, BehaviorDecision, AGENT_NAME_VARIANTS, @@ -2026,6 +2027,9 @@ async def handle_telegram_webhook( mentioned_agents.append(aid) break + # Gateway: check if agent has been active in this chat recently (any user) + agent_active_in_chat = has_agent_chat_participation(agent_config.agent_id, chat_id) + # Gateway: compute has_explicit_request (single source of truth) # CONTRACT: imperative OR (? AND (dm OR reply OR mention OR thread)) has_explicit_request = detect_explicit_request( @@ -2033,7 +2037,7 @@ async def handle_telegram_webhook( is_dm=is_private_chat, is_reply_to_agent=is_reply_to_agent, mentioned_agents=mentioned_agents, - thread_has_agent_participation=False, # REQUIRED, fail-closed default + thread_has_agent_participation=agent_active_in_chat, ) # Check if this is a prober request (chat_id=0 or user_id=0) @@ -2051,7 +2055,7 @@ async def handle_telegram_webhook( payload_explicit_request=has_explicit_request, payload_has_link=has_link, is_reply_to_agent=is_reply_to_agent, - thread_has_agent_participation=False, # TODO: track per thread + thread_has_agent_participation=agent_active_in_chat, ) respond_decision = sowa_decision.should_respond respond_reason = sowa_decision.reason @@ -2088,10 +2092,16 @@ async def handle_telegram_webhook( try: url = f"https://api.telegram.org/bot{token}/sendMessage" async with httpx.AsyncClient(timeout=30) as client: - resp = await client.post(url, json={ + ack_payload = { "chat_id": chat_id, "text": ack_text, - }) + } + # Link ACK to the user's message for better UX + msg_id = update.message.get("message_id") + if msg_id: + ack_payload["reply_to_message_id"] = msg_id + ack_payload["allow_sending_without_reply"] = True + resp = await client.post(url, json=ack_payload) if resp.status_code == 200: logger.info(f"\U0001f44b ACK sent to chat {chat_id}: {ack_text}") else: