Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions src/google/adk/integrations/slack/slack_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from __future__ import annotations

import logging
import re
from typing import Any

from google.adk.runners import Runner
Expand All @@ -31,6 +32,13 @@

logger = logging.getLogger("google_adk." + __name__)

_SESSION_ID_UNSAFE_CHARS = re.compile(r"[^A-Za-z0-9_-]")


def _to_session_id_part(value: str) -> str:
"""Converts Slack identifiers into ADK session-id-safe strings."""
return _SESSION_ID_UNSAFE_CHARS.sub("_", value)


class SlackRunner:
"""Runner for ADK agents on Slack."""
Expand Down Expand Up @@ -73,8 +81,14 @@ async def _handle_message(self, event: dict[str, Any], say: Any) -> None:
if not text or not user_id or not channel_id:
return

# In Slack, we can use the channel_id (and optionally thread_ts) as a session ID.
session_id = f"{channel_id}-{thread_ts}" if thread_ts else channel_id
# In Slack, we can use the channel_id (and optionally thread_ts) as a
# session ID. Slack timestamps contain "." but managed session services only
# accept letters, numbers, "_" and "-".
session_id = (
f"{_to_session_id_part(channel_id)}-{_to_session_id_part(thread_ts)}"
if thread_ts
else _to_session_id_part(channel_id)
)

new_message = types.Content(role="user", parts=[types.Part(text=text)])

Expand Down
33 changes: 33 additions & 0 deletions tests/unittests/integrations/slack/test_slack_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,39 @@ async def mock_run_async(*args, **kwargs):
text="Hi user!",
)

@patch("google.adk.integrations.slack.slack_runner.logger")
async def test_handle_message_sanitizes_slack_thread_ts_session_id(
self, mock_logger
):
mock_say = AsyncMock()
mock_say.return_value = {"ts": "thinking_ts"}
event = {
"text": "Hello bot",
"user": "U12345",
"channel": "C67890",
"thread_ts": "1234567890.123456",
"ts": "1234567899.999999",
}

async def mock_run_async(*args, **kwargs):
return
yield

self.mock_runner.run_async.side_effect = mock_run_async

await self.slack_runner._handle_message(event, mock_say)

self.mock_runner.run_async.assert_called_once_with(
user_id="U12345",
session_id="C67890-1234567890_123456",
new_message=types.Content(
role="user", parts=[types.Part(text="Hello bot")]
),
)
mock_say.assert_called_once_with(
text="_Thinking..._", thread_ts="1234567890.123456"
)

@patch("google.adk.integrations.slack.slack_runner.logger")
async def test_handle_message_multi_turn(self, mock_logger):
# Setup mocks
Expand Down
Loading