mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-29 23:41:35 +08:00
Compare commits
1 Commits
fix/plugin
...
hermes/her
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a3f028239 |
@@ -87,6 +87,14 @@ class SlackAdapter(BasePlatformAdapter):
|
|||||||
# Track pending approval message_ts → resolved flag to prevent
|
# Track pending approval message_ts → resolved flag to prevent
|
||||||
# double-clicks on approval buttons.
|
# double-clicks on approval buttons.
|
||||||
self._approval_resolved: Dict[str, bool] = {}
|
self._approval_resolved: Dict[str, bool] = {}
|
||||||
|
# Track timestamps of messages sent by the bot so we can respond
|
||||||
|
# to thread replies even without an explicit @mention.
|
||||||
|
self._bot_message_ts: set = set()
|
||||||
|
self._BOT_TS_MAX = 5000 # cap to avoid unbounded growth
|
||||||
|
# Track threads where the bot has been @mentioned — once mentioned,
|
||||||
|
# respond to ALL subsequent messages in that thread automatically.
|
||||||
|
self._mentioned_threads: set = set()
|
||||||
|
self._MENTIONED_THREADS_MAX = 5000
|
||||||
|
|
||||||
async def connect(self) -> bool:
|
async def connect(self) -> bool:
|
||||||
"""Connect to Slack via Socket Mode."""
|
"""Connect to Slack via Socket Mode."""
|
||||||
@@ -268,9 +276,22 @@ class SlackAdapter(BasePlatformAdapter):
|
|||||||
|
|
||||||
last_result = await self._get_client(chat_id).chat_postMessage(**kwargs)
|
last_result = await self._get_client(chat_id).chat_postMessage(**kwargs)
|
||||||
|
|
||||||
|
# Track the sent message ts so we can auto-respond to thread
|
||||||
|
# replies without requiring @mention.
|
||||||
|
sent_ts = last_result.get("ts") if last_result else None
|
||||||
|
if sent_ts:
|
||||||
|
self._bot_message_ts.add(sent_ts)
|
||||||
|
# Also register the thread root so replies-to-my-replies work
|
||||||
|
if thread_ts:
|
||||||
|
self._bot_message_ts.add(thread_ts)
|
||||||
|
if len(self._bot_message_ts) > self._BOT_TS_MAX:
|
||||||
|
excess = len(self._bot_message_ts) - self._BOT_TS_MAX // 2
|
||||||
|
for old_ts in list(self._bot_message_ts)[:excess]:
|
||||||
|
self._bot_message_ts.discard(old_ts)
|
||||||
|
|
||||||
return SendResult(
|
return SendResult(
|
||||||
success=True,
|
success=True,
|
||||||
message_id=last_result.get("ts") if last_result else None,
|
message_id=sent_ts,
|
||||||
raw_response=last_result,
|
raw_response=last_result,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -778,48 +799,61 @@ class SlackAdapter(BasePlatformAdapter):
|
|||||||
else:
|
else:
|
||||||
thread_ts = event.get("thread_ts") or ts # ts fallback for channels
|
thread_ts = event.get("thread_ts") or ts # ts fallback for channels
|
||||||
|
|
||||||
# In channels, only respond if bot is mentioned OR if this is a
|
# In channels, respond if:
|
||||||
# reply in a thread where the bot has an active session.
|
# 1. The bot is @mentioned in this message, OR
|
||||||
|
# 2. The message is a reply in a thread the bot started/participated in, OR
|
||||||
|
# 3. The message is in a thread where the bot was previously @mentioned, OR
|
||||||
|
# 4. There's an existing session for this thread (survives restarts)
|
||||||
bot_uid = self._team_bot_user_ids.get(team_id, self._bot_user_id)
|
bot_uid = self._team_bot_user_ids.get(team_id, self._bot_user_id)
|
||||||
is_mentioned = bot_uid and f"<@{bot_uid}>" in text
|
is_mentioned = bot_uid and f"<@{bot_uid}>" in text
|
||||||
|
event_thread_ts = event.get("thread_ts")
|
||||||
|
is_thread_reply = bool(event_thread_ts and event_thread_ts != ts)
|
||||||
|
|
||||||
if not is_dm and bot_uid and not is_mentioned:
|
if not is_dm and bot_uid and not is_mentioned:
|
||||||
# Check if this is a thread reply (thread_ts exists and differs from ts)
|
reply_to_bot_thread = (
|
||||||
event_thread_ts = event.get("thread_ts")
|
is_thread_reply and event_thread_ts in self._bot_message_ts
|
||||||
is_thread_reply = event_thread_ts and event_thread_ts != ts
|
)
|
||||||
|
in_mentioned_thread = (
|
||||||
if is_thread_reply and self._has_active_session_for_thread(
|
event_thread_ts is not None
|
||||||
channel_id=channel_id,
|
and event_thread_ts in self._mentioned_threads
|
||||||
thread_ts=event_thread_ts,
|
)
|
||||||
user_id=user_id,
|
has_session = (
|
||||||
):
|
is_thread_reply
|
||||||
# Allow thread replies without mention if there's an active session
|
and self._has_active_session_for_thread(
|
||||||
pass
|
channel_id=channel_id,
|
||||||
else:
|
thread_ts=event_thread_ts,
|
||||||
# Not a thread reply or no active session - ignore
|
user_id=user_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if not reply_to_bot_thread and not in_mentioned_thread and not has_session:
|
||||||
return
|
return
|
||||||
|
|
||||||
if is_mentioned:
|
if is_mentioned:
|
||||||
# Strip the bot mention from the text
|
# Strip the bot mention from the text
|
||||||
text = text.replace(f"<@{bot_uid}>", "").strip()
|
text = text.replace(f"<@{bot_uid}>", "").strip()
|
||||||
|
# Register this thread so all future messages auto-trigger the bot
|
||||||
|
if event_thread_ts:
|
||||||
|
self._mentioned_threads.add(event_thread_ts)
|
||||||
|
if len(self._mentioned_threads) > self._MENTIONED_THREADS_MAX:
|
||||||
|
to_remove = list(self._mentioned_threads)[:self._MENTIONED_THREADS_MAX // 2]
|
||||||
|
for t in to_remove:
|
||||||
|
self._mentioned_threads.discard(t)
|
||||||
|
|
||||||
# When first mentioned in an existing thread, fetch thread context
|
# When entering a thread for the first time (no existing session),
|
||||||
# so the agent understands the conversation it's joining.
|
# fetch thread context so the agent understands the conversation.
|
||||||
event_thread_ts = event.get("thread_ts")
|
if is_thread_reply and not self._has_active_session_for_thread(
|
||||||
is_thread_reply = event_thread_ts and event_thread_ts != ts
|
channel_id=channel_id,
|
||||||
if is_thread_reply and not self._has_active_session_for_thread(
|
thread_ts=event_thread_ts,
|
||||||
|
user_id=user_id,
|
||||||
|
):
|
||||||
|
thread_context = await self._fetch_thread_context(
|
||||||
channel_id=channel_id,
|
channel_id=channel_id,
|
||||||
thread_ts=event_thread_ts,
|
thread_ts=event_thread_ts,
|
||||||
user_id=user_id,
|
current_ts=ts,
|
||||||
):
|
team_id=team_id,
|
||||||
thread_context = await self._fetch_thread_context(
|
)
|
||||||
channel_id=channel_id,
|
if thread_context:
|
||||||
thread_ts=event_thread_ts,
|
text = thread_context + text
|
||||||
current_ts=ts,
|
|
||||||
team_id=team_id,
|
|
||||||
)
|
|
||||||
if thread_context:
|
|
||||||
text = thread_context + text
|
|
||||||
|
|
||||||
# Determine message type
|
# Determine message type
|
||||||
msg_type = MessageType.TEXT
|
msg_type = MessageType.TEXT
|
||||||
|
|||||||
@@ -371,3 +371,56 @@ class TestSessionKeyFix:
|
|||||||
channel_id="C1", thread_ts="1000.0", user_id="U123"
|
channel_id="C1", thread_ts="1000.0", user_id="U123"
|
||||||
)
|
)
|
||||||
assert result is False
|
assert result is False
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# Thread engagement — bot-started threads & mentioned threads
|
||||||
|
# ===========================================================================
|
||||||
|
|
||||||
|
class TestThreadEngagement:
|
||||||
|
"""Test _bot_message_ts and _mentioned_threads tracking."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_send_tracks_bot_message_ts(self):
|
||||||
|
"""Bot's sent messages are tracked so thread replies work without @mention."""
|
||||||
|
adapter = _make_adapter()
|
||||||
|
mock_client = adapter._team_clients["T1"]
|
||||||
|
mock_client.chat_postMessage = AsyncMock(return_value={"ts": "9000.1"})
|
||||||
|
|
||||||
|
await adapter.send(chat_id="C1", content="Hello!", metadata={"thread_id": "8000.0"})
|
||||||
|
|
||||||
|
assert "9000.1" in adapter._bot_message_ts
|
||||||
|
# Thread root should also be tracked
|
||||||
|
assert "8000.0" in adapter._bot_message_ts
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_bot_message_ts_cap(self):
|
||||||
|
"""Verify memory is bounded when many messages are sent."""
|
||||||
|
adapter = _make_adapter()
|
||||||
|
adapter._BOT_TS_MAX = 10 # low cap for testing
|
||||||
|
mock_client = adapter._team_clients["T1"]
|
||||||
|
|
||||||
|
for i in range(20):
|
||||||
|
mock_client.chat_postMessage = AsyncMock(return_value={"ts": f"{i}.0"})
|
||||||
|
await adapter.send(chat_id="C1", content=f"msg {i}")
|
||||||
|
|
||||||
|
assert len(adapter._bot_message_ts) <= 10
|
||||||
|
|
||||||
|
def test_mentioned_threads_populated_on_mention(self):
|
||||||
|
"""When bot is @mentioned in a thread, that thread is tracked."""
|
||||||
|
adapter = _make_adapter()
|
||||||
|
# Simulate what _handle_slack_message does on mention
|
||||||
|
adapter._mentioned_threads.add("1000.0")
|
||||||
|
assert "1000.0" in adapter._mentioned_threads
|
||||||
|
|
||||||
|
def test_mentioned_threads_cap(self):
|
||||||
|
"""Verify _mentioned_threads is bounded."""
|
||||||
|
adapter = _make_adapter()
|
||||||
|
adapter._MENTIONED_THREADS_MAX = 10
|
||||||
|
for i in range(15):
|
||||||
|
adapter._mentioned_threads.add(f"{i}.0")
|
||||||
|
if len(adapter._mentioned_threads) > adapter._MENTIONED_THREADS_MAX:
|
||||||
|
to_remove = list(adapter._mentioned_threads)[:adapter._MENTIONED_THREADS_MAX // 2]
|
||||||
|
for t in to_remove:
|
||||||
|
adapter._mentioned_threads.discard(t)
|
||||||
|
assert len(adapter._mentioned_threads) <= 10
|
||||||
|
|||||||
Reference in New Issue
Block a user