fix: keep rapid telegram follow-ups from getting cut off
This commit is contained in:
committed by
Teknium
parent
12b109b664
commit
9a9b8cd1e4
@@ -734,25 +734,56 @@ def merge_pending_message_event(
|
|||||||
pending_messages: Dict[str, MessageEvent],
|
pending_messages: Dict[str, MessageEvent],
|
||||||
session_key: str,
|
session_key: str,
|
||||||
event: MessageEvent,
|
event: MessageEvent,
|
||||||
|
*,
|
||||||
|
merge_text: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Store or merge a pending event for a session.
|
"""Store or merge a pending event for a session.
|
||||||
|
|
||||||
Photo bursts/albums often arrive as multiple near-simultaneous PHOTO
|
Photo bursts/albums often arrive as multiple near-simultaneous PHOTO
|
||||||
events. Merge those into the existing queued event so the next turn sees
|
events. Merge those into the existing queued event so the next turn sees
|
||||||
the whole burst, while non-photo follow-ups still replace the pending
|
the whole burst.
|
||||||
event normally.
|
|
||||||
|
When ``merge_text`` is enabled, rapid follow-up TEXT events are appended
|
||||||
|
instead of replacing the pending turn. This is used for Telegram bursty
|
||||||
|
follow-ups so a multi-part user thought is not silently truncated to only
|
||||||
|
the last queued fragment.
|
||||||
"""
|
"""
|
||||||
existing = pending_messages.get(session_key)
|
existing = pending_messages.get(session_key)
|
||||||
if (
|
if existing:
|
||||||
existing
|
existing_is_photo = getattr(existing, "message_type", None) == MessageType.PHOTO
|
||||||
and getattr(existing, "message_type", None) == MessageType.PHOTO
|
incoming_is_photo = event.message_type == MessageType.PHOTO
|
||||||
and event.message_type == MessageType.PHOTO
|
existing_has_media = bool(existing.media_urls)
|
||||||
):
|
incoming_has_media = bool(event.media_urls)
|
||||||
existing.media_urls.extend(event.media_urls)
|
|
||||||
existing.media_types.extend(event.media_types)
|
if existing_is_photo and incoming_is_photo:
|
||||||
if event.text:
|
existing.media_urls.extend(event.media_urls)
|
||||||
existing.text = BasePlatformAdapter._merge_caption(existing.text, event.text)
|
existing.media_types.extend(event.media_types)
|
||||||
return
|
if event.text:
|
||||||
|
existing.text = BasePlatformAdapter._merge_caption(existing.text, event.text)
|
||||||
|
return
|
||||||
|
|
||||||
|
if existing_has_media or incoming_has_media:
|
||||||
|
if incoming_has_media:
|
||||||
|
existing.media_urls.extend(event.media_urls)
|
||||||
|
existing.media_types.extend(event.media_types)
|
||||||
|
if event.text:
|
||||||
|
if existing.text:
|
||||||
|
existing.text = BasePlatformAdapter._merge_caption(existing.text, event.text)
|
||||||
|
else:
|
||||||
|
existing.text = event.text
|
||||||
|
if existing_is_photo or incoming_is_photo:
|
||||||
|
existing.message_type = MessageType.PHOTO
|
||||||
|
return
|
||||||
|
|
||||||
|
if (
|
||||||
|
merge_text
|
||||||
|
and getattr(existing, "message_type", None) == MessageType.TEXT
|
||||||
|
and event.message_type == MessageType.TEXT
|
||||||
|
):
|
||||||
|
if event.text:
|
||||||
|
existing.text = f"{existing.text}\n{event.text}" if existing.text else event.text
|
||||||
|
return
|
||||||
|
|
||||||
pending_messages[session_key] = event
|
pending_messages[session_key] = event
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2922,6 +2922,32 @@ class GatewayRunner:
|
|||||||
merge_pending_message_event(adapter._pending_messages, _quick_key, event)
|
merge_pending_message_event(adapter._pending_messages, _quick_key, event)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
_telegram_followup_grace = float(
|
||||||
|
os.getenv("HERMES_TELEGRAM_FOLLOWUP_GRACE_SECONDS", "3.0")
|
||||||
|
)
|
||||||
|
_started_at = self._running_agents_ts.get(_quick_key, 0)
|
||||||
|
if (
|
||||||
|
source.platform == Platform.TELEGRAM
|
||||||
|
and event.message_type == MessageType.TEXT
|
||||||
|
and _telegram_followup_grace > 0
|
||||||
|
and _started_at
|
||||||
|
and (time.time() - _started_at) <= _telegram_followup_grace
|
||||||
|
):
|
||||||
|
logger.debug(
|
||||||
|
"Telegram follow-up arrived %.2fs after run start for %s — queueing without interrupt",
|
||||||
|
time.time() - _started_at,
|
||||||
|
_quick_key[:20],
|
||||||
|
)
|
||||||
|
adapter = self.adapters.get(source.platform)
|
||||||
|
if adapter:
|
||||||
|
merge_pending_message_event(
|
||||||
|
adapter._pending_messages,
|
||||||
|
_quick_key,
|
||||||
|
event,
|
||||||
|
merge_text=True,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
running_agent = self._running_agents.get(_quick_key)
|
running_agent = self._running_agents.get(_quick_key)
|
||||||
if running_agent is _AGENT_PENDING_SENTINEL:
|
if running_agent is _AGENT_PENDING_SENTINEL:
|
||||||
# Agent is being set up but not ready yet.
|
# Agent is being set up but not ready yet.
|
||||||
@@ -2935,7 +2961,12 @@ class GatewayRunner:
|
|||||||
# agent starts.
|
# agent starts.
|
||||||
adapter = self.adapters.get(source.platform)
|
adapter = self.adapters.get(source.platform)
|
||||||
if adapter:
|
if adapter:
|
||||||
adapter._pending_messages[_quick_key] = event
|
merge_pending_message_event(
|
||||||
|
adapter._pending_messages,
|
||||||
|
_quick_key,
|
||||||
|
event,
|
||||||
|
merge_text=True,
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
if self._draining:
|
if self._draining:
|
||||||
if self._queue_during_drain_enabled():
|
if self._queue_during_drain_enabled():
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from gateway.config import GatewayConfig, Platform, PlatformConfig
|
from gateway.config import GatewayConfig, Platform, PlatformConfig
|
||||||
from gateway.platforms.base import MessageEvent, MessageType
|
from gateway.platforms.base import MessageEvent, MessageType, merge_pending_message_event
|
||||||
from gateway.run import GatewayRunner, _AGENT_PENDING_SENTINEL
|
from gateway.run import GatewayRunner, _AGENT_PENDING_SENTINEL
|
||||||
from gateway.session import SessionSource, build_session_key
|
from gateway.session import SessionSource, build_session_key
|
||||||
|
|
||||||
@@ -184,6 +184,80 @@ async def test_second_message_during_sentinel_queued_not_duplicate():
|
|||||||
await task1
|
await task1
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_pending_message_event_merges_text_and_photo_followups():
|
||||||
|
pending = {}
|
||||||
|
source = SessionSource(
|
||||||
|
platform=Platform.TELEGRAM,
|
||||||
|
chat_id="12345",
|
||||||
|
chat_type="dm",
|
||||||
|
user_id="u1",
|
||||||
|
)
|
||||||
|
session_key = build_session_key(source)
|
||||||
|
|
||||||
|
text_event = MessageEvent(
|
||||||
|
text="first follow-up",
|
||||||
|
message_type=MessageType.TEXT,
|
||||||
|
source=source,
|
||||||
|
)
|
||||||
|
photo_event = MessageEvent(
|
||||||
|
text="see screenshot",
|
||||||
|
message_type=MessageType.PHOTO,
|
||||||
|
source=source,
|
||||||
|
media_urls=["/tmp/test.png"],
|
||||||
|
media_types=["image/png"],
|
||||||
|
)
|
||||||
|
|
||||||
|
merge_pending_message_event(pending, session_key, text_event, merge_text=True)
|
||||||
|
merge_pending_message_event(pending, session_key, photo_event, merge_text=True)
|
||||||
|
|
||||||
|
merged = pending[session_key]
|
||||||
|
assert merged.message_type == MessageType.PHOTO
|
||||||
|
assert merged.text == "first follow-up\n\nsee screenshot"
|
||||||
|
assert merged.media_urls == ["/tmp/test.png"]
|
||||||
|
assert merged.media_types == ["image/png"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_recent_telegram_text_followup_is_queued_without_interrupt():
|
||||||
|
runner = _make_runner()
|
||||||
|
event = _make_event(text="follow-up")
|
||||||
|
session_key = build_session_key(event.source)
|
||||||
|
|
||||||
|
fake_agent = MagicMock()
|
||||||
|
fake_agent.get_activity_summary.return_value = {"seconds_since_activity": 0}
|
||||||
|
runner._running_agents[session_key] = fake_agent
|
||||||
|
import time as _time
|
||||||
|
runner._running_agents_ts[session_key] = _time.time()
|
||||||
|
|
||||||
|
result = await runner._handle_message(event)
|
||||||
|
|
||||||
|
assert result is None
|
||||||
|
fake_agent.interrupt.assert_not_called()
|
||||||
|
adapter = runner.adapters[Platform.TELEGRAM]
|
||||||
|
assert adapter._pending_messages[session_key].text == "follow-up"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_recent_telegram_followups_append_in_pending_queue():
|
||||||
|
runner = _make_runner()
|
||||||
|
first = _make_event(text="part one")
|
||||||
|
second = _make_event(text="part two")
|
||||||
|
session_key = build_session_key(first.source)
|
||||||
|
|
||||||
|
fake_agent = MagicMock()
|
||||||
|
fake_agent.get_activity_summary.return_value = {"seconds_since_activity": 0}
|
||||||
|
runner._running_agents[session_key] = fake_agent
|
||||||
|
import time as _time
|
||||||
|
runner._running_agents_ts[session_key] = _time.time()
|
||||||
|
|
||||||
|
await runner._handle_message(first)
|
||||||
|
await runner._handle_message(second)
|
||||||
|
|
||||||
|
fake_agent.interrupt.assert_not_called()
|
||||||
|
adapter = runner.adapters[Platform.TELEGRAM]
|
||||||
|
assert adapter._pending_messages[session_key].text == "part one\npart two"
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Test 5: Sentinel not placed for command messages
|
# Test 5: Sentinel not placed for command messages
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@@ -273,6 +347,7 @@ async def test_stop_hard_kills_running_agent():
|
|||||||
|
|
||||||
# Simulate a running (possibly hung) agent
|
# Simulate a running (possibly hung) agent
|
||||||
fake_agent = MagicMock()
|
fake_agent = MagicMock()
|
||||||
|
fake_agent.get_activity_summary.return_value = {"seconds_since_activity": 0}
|
||||||
runner._running_agents[session_key] = fake_agent
|
runner._running_agents[session_key] = fake_agent
|
||||||
|
|
||||||
# Send /stop
|
# Send /stop
|
||||||
@@ -305,6 +380,7 @@ async def test_stop_clears_pending_messages():
|
|||||||
)
|
)
|
||||||
|
|
||||||
fake_agent = MagicMock()
|
fake_agent = MagicMock()
|
||||||
|
fake_agent.get_activity_summary.return_value = {"seconds_since_activity": 0}
|
||||||
runner._running_agents[session_key] = fake_agent
|
runner._running_agents[session_key] = fake_agent
|
||||||
runner._pending_messages[session_key] = "some queued text"
|
runner._pending_messages[session_key] = "some queued text"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user