feat: thread_has_agent_participation + ACK reply linkage
1. thread_has_agent_participation (SOWA Priority 11):
- New function has_agent_chat_participation() in behavior_policy.py
- Checks if agent responded to ANY user in this chat within 30min
- When active + user asks question/imperative → agent responds
- Different from per-user conversation_context (Priority 12)
- Wired into both detect_explicit_request() and analyze_message()
2. ACK reply_to_message_id:
- When SOWA sends ACK ("NUTRA тут"), it now replies to the user's
message instead of sending a standalone message
- Better UX: visually linked to what the user wrote
- Uses allow_sending_without_reply=True for safety
Known issue (not fixed - too risky):
- Lines 1368-1639 in http_api.py are dead code (brand commands /бренд)
at incorrect indentation level (8 spaces, inside unreachable block)
- These commands never worked on NODE1, fixing 260 lines of indentation
carries regression risk — deferred to separate cleanup PR
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -118,6 +118,24 @@ def has_recent_interaction(agent_id: str, chat_id: str, user_id: str) -> bool:
|
|||||||
return is_recent
|
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:
|
def cleanup_old_contexts() -> None:
|
||||||
"""Remove old conversation contexts and ACK cooldowns to prevent memory leak."""
|
"""Remove old conversation contexts and ACK cooldowns to prevent memory leak."""
|
||||||
global _conversation_context, _ack_cooldown
|
global _conversation_context, _ack_cooldown
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ from behavior_policy import (
|
|||||||
record_ack,
|
record_ack,
|
||||||
get_ack_text,
|
get_ack_text,
|
||||||
is_prober_request,
|
is_prober_request,
|
||||||
|
has_agent_chat_participation,
|
||||||
NO_OUTPUT,
|
NO_OUTPUT,
|
||||||
BehaviorDecision,
|
BehaviorDecision,
|
||||||
AGENT_NAME_VARIANTS,
|
AGENT_NAME_VARIANTS,
|
||||||
@@ -2026,6 +2027,9 @@ async def handle_telegram_webhook(
|
|||||||
mentioned_agents.append(aid)
|
mentioned_agents.append(aid)
|
||||||
break
|
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)
|
# Gateway: compute has_explicit_request (single source of truth)
|
||||||
# CONTRACT: imperative OR (? AND (dm OR reply OR mention OR thread))
|
# CONTRACT: imperative OR (? AND (dm OR reply OR mention OR thread))
|
||||||
has_explicit_request = detect_explicit_request(
|
has_explicit_request = detect_explicit_request(
|
||||||
@@ -2033,7 +2037,7 @@ async def handle_telegram_webhook(
|
|||||||
is_dm=is_private_chat,
|
is_dm=is_private_chat,
|
||||||
is_reply_to_agent=is_reply_to_agent,
|
is_reply_to_agent=is_reply_to_agent,
|
||||||
mentioned_agents=mentioned_agents,
|
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)
|
# 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_explicit_request=has_explicit_request,
|
||||||
payload_has_link=has_link,
|
payload_has_link=has_link,
|
||||||
is_reply_to_agent=is_reply_to_agent,
|
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_decision = sowa_decision.should_respond
|
||||||
respond_reason = sowa_decision.reason
|
respond_reason = sowa_decision.reason
|
||||||
@@ -2088,10 +2092,16 @@ async def handle_telegram_webhook(
|
|||||||
try:
|
try:
|
||||||
url = f"https://api.telegram.org/bot{token}/sendMessage"
|
url = f"https://api.telegram.org/bot{token}/sendMessage"
|
||||||
async with httpx.AsyncClient(timeout=30) as client:
|
async with httpx.AsyncClient(timeout=30) as client:
|
||||||
resp = await client.post(url, json={
|
ack_payload = {
|
||||||
"chat_id": chat_id,
|
"chat_id": chat_id,
|
||||||
"text": ack_text,
|
"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:
|
if resp.status_code == 200:
|
||||||
logger.info(f"\U0001f44b ACK sent to chat {chat_id}: {ack_text}")
|
logger.info(f"\U0001f44b ACK sent to chat {chat_id}: {ack_text}")
|
||||||
else:
|
else:
|
||||||
|
|||||||
Reference in New Issue
Block a user