mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-01 12:02:05 +00:00
fix(telegram): notify user when clarify button tap arrives after expiry
This commit is contained in:
parent
90d25adc9e
commit
3f543229f2
2 changed files with 112 additions and 11 deletions
|
|
@ -4292,6 +4292,31 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
# Catch-all (e.g. page counter button "mx:noop")
|
||||
await query.answer()
|
||||
|
||||
async def _notify_clarify_expired(self, query, user_display: str) -> None:
|
||||
"""Tell the user a clarify tap arrived too late to be delivered.
|
||||
|
||||
Fires when the clarify entry was evicted by ``clarify_timeout`` or the
|
||||
gateway restarted between asking and the tap. In both cases the agent
|
||||
thread is no longer waiting, so the tap would otherwise leave a
|
||||
misleading ✓ (or an "awaiting typed response" prompt) on a button the
|
||||
agent never receives.
|
||||
"""
|
||||
try:
|
||||
await query.answer(text="⚠️ This prompt expired — please /retry.")
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
await query.edit_message_text(
|
||||
text=(
|
||||
f"❓ {_html.escape(query.message.text or '')}\n\n"
|
||||
"<i>⚠️ This question expired or the session reset — please /retry.</i>"
|
||||
),
|
||||
parse_mode=ParseMode.HTML,
|
||||
reply_markup=None,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def _handle_callback_query(
|
||||
self, update: "Update", context: "ContextTypes.DEFAULT_TYPE"
|
||||
) -> None:
|
||||
|
|
@ -4529,12 +4554,20 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
# clarify. Do NOT pop _clarify_state yet — we still
|
||||
# need it if the user is slow to respond and the entry
|
||||
# is cleared by something else.
|
||||
flipped = False
|
||||
try:
|
||||
from tools.clarify_gateway import mark_awaiting_text
|
||||
mark_awaiting_text(clarify_id)
|
||||
flipped = mark_awaiting_text(clarify_id)
|
||||
except Exception as exc:
|
||||
logger.warning("[%s] mark_awaiting_text failed: %s", self.name, exc)
|
||||
|
||||
if not flipped:
|
||||
# Entry evicted (clarify_timeout) or gateway restarted
|
||||
# between ask and tap — a typed answer would go nowhere.
|
||||
self._clarify_state.pop(clarify_id, None)
|
||||
await self._notify_clarify_expired(query, user_display)
|
||||
return
|
||||
|
||||
await query.answer(text="✏️ Type your answer in the chat.")
|
||||
try:
|
||||
await query.edit_message_text(
|
||||
|
|
@ -4580,22 +4613,25 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
logger.error("[%s] resolve_gateway_clarify failed: %s", self.name, exc)
|
||||
resolved = False
|
||||
|
||||
await query.answer(text=f"✓ {resolved_text[:60]}")
|
||||
try:
|
||||
await query.edit_message_text(
|
||||
text=f"❓ {_html.escape(query.message.text or '')}\n\n<b>{_html.escape(user_display)}:</b> {_html.escape(resolved_text)}",
|
||||
parse_mode=ParseMode.HTML,
|
||||
reply_markup=None,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if resolved:
|
||||
await query.answer(text=f"✓ {resolved_text[:60]}")
|
||||
try:
|
||||
await query.edit_message_text(
|
||||
text=f"❓ {_html.escape(query.message.text or '')}\n\n<b>{_html.escape(user_display)}:</b> {_html.escape(resolved_text)}",
|
||||
parse_mode=ParseMode.HTML,
|
||||
reply_markup=None,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
logger.info(
|
||||
"Telegram clarify button resolved (id=%s, choice=%r, user=%s)",
|
||||
clarify_id, resolved_text, user_display,
|
||||
)
|
||||
else:
|
||||
# Entry evicted (clarify_timeout) or gateway restarted
|
||||
# between ask and tap — surface this instead of leaving a
|
||||
# misleading ✓ on a button the agent will never receive.
|
||||
await self._notify_clarify_expired(query, user_display)
|
||||
logger.warning(
|
||||
"Telegram clarify button: resolve_gateway_clarify returned False (id=%s)",
|
||||
clarify_id,
|
||||
|
|
|
|||
|
|
@ -351,6 +351,71 @@ class TestTelegramClarifyCallback:
|
|||
# State preserved
|
||||
assert adapter._clarify_state["cidC"] == "sk-auth"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_numeric_choice_expired_notifies_user(self):
|
||||
"""Late tap after the entry was evicted (timeout) or the gateway
|
||||
restarted must surface an expiry notice, not a misleading ✓."""
|
||||
adapter = _make_adapter()
|
||||
# _clarify_state still maps the id (timeout eviction does not pop it),
|
||||
# but the clarify primitive entry is gone → resolve returns False.
|
||||
adapter._clarify_state["cidExpired"] = "sk-expired"
|
||||
|
||||
query = AsyncMock()
|
||||
query.data = "cl:cidExpired:0"
|
||||
query.message = MagicMock()
|
||||
query.message.chat_id = 12345
|
||||
query.message.text = "Pick"
|
||||
query.from_user = MagicMock()
|
||||
query.from_user.id = "777"
|
||||
query.from_user.first_name = "Tester"
|
||||
query.answer = AsyncMock()
|
||||
query.edit_message_text = AsyncMock()
|
||||
|
||||
update = MagicMock()
|
||||
update.callback_query = query
|
||||
context = MagicMock()
|
||||
|
||||
with patch.dict(os.environ, {"TELEGRAM_ALLOWED_USERS": "*"}, clear=False):
|
||||
await adapter._handle_callback_query(update, context)
|
||||
|
||||
# User is told the prompt expired — not a misleading checkmark.
|
||||
answer_text = query.answer.call_args[1]["text"].lower()
|
||||
assert "expired" in answer_text
|
||||
edit_text = query.edit_message_text.call_args[1]["text"].lower()
|
||||
assert "expired" in edit_text or "session reset" in edit_text
|
||||
assert "/retry" in edit_text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_other_button_expired_notifies_user(self):
|
||||
"""Tapping 'Other' after the entry was evicted must tell the user the
|
||||
prompt expired instead of silently entering text-capture mode."""
|
||||
adapter = _make_adapter()
|
||||
# No clarify primitive entry → mark_awaiting_text returns False.
|
||||
adapter._clarify_state["cidOtherExpired"] = "sk-other-expired"
|
||||
|
||||
query = AsyncMock()
|
||||
query.data = "cl:cidOtherExpired:other"
|
||||
query.message = MagicMock()
|
||||
query.message.chat_id = 12345
|
||||
query.message.text = "Pick"
|
||||
query.from_user = MagicMock()
|
||||
query.from_user.id = "777"
|
||||
query.from_user.first_name = "Tester"
|
||||
query.answer = AsyncMock()
|
||||
query.edit_message_text = AsyncMock()
|
||||
|
||||
update = MagicMock()
|
||||
update.callback_query = query
|
||||
context = MagicMock()
|
||||
|
||||
with patch.dict(os.environ, {"TELEGRAM_ALLOWED_USERS": "*"}, clear=False):
|
||||
await adapter._handle_callback_query(update, context)
|
||||
|
||||
answer_text = query.answer.call_args[1]["text"].lower()
|
||||
assert "expired" in answer_text
|
||||
# State popped so a subsequent typed message is not mis-captured.
|
||||
assert "cidOtherExpired" not in adapter._clarify_state
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invalid_choice_token(self):
|
||||
from tools import clarify_gateway as cm
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue