- gateway/normalize.py: normalize_slack_event -> KonstructMessage (strips bot mention) - gateway/channels/slack.py: register_slack_handlers for app_mention + DM events - rate limit check -> ephemeral rejection on exceeded - idempotency dedup (Slack retry protection) - placeholder 'Thinking...' message posted in-thread before Celery dispatch - auto-follow engaged threads with 30-minute TTL - HTTP 200 returned immediately; all LLM work dispatched to Celery - gateway/main.py: FastAPI on port 8001, /slack/events + /health - router/tenant.py: resolve_tenant workspace_id -> tenant_id (RLS-bypass query) - router/ratelimit.py: check_rate_limit Redis token bucket, RateLimitExceeded exception - router/idempotency.py: is_duplicate + mark_processed (SET NX, 24h TTL) - router/context.py: load_agent_for_tenant with RLS ContextVar setup - orchestrator/tasks.py: handle_message now extracts placeholder_ts/channel_id, calls _update_slack_placeholder via chat.update after LLM response - docker-compose.yml: gateway service on port 8001 - pyproject.toml: added redis, konstruct-router, konstruct-orchestrator deps
101 lines
3.4 KiB
Python
101 lines
3.4 KiB
Python
"""
|
|
Slack event normalization.
|
|
|
|
Converts Slack Events API payloads into KonstructMessage format.
|
|
|
|
All channel adapters produce KonstructMessage — the router and orchestrator
|
|
never inspect Slack-specific fields directly.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import uuid
|
|
from datetime import datetime, timezone
|
|
|
|
from shared.models.message import (
|
|
ChannelType,
|
|
KonstructMessage,
|
|
MessageContent,
|
|
SenderInfo,
|
|
)
|
|
|
|
# Pattern to strip <@BOT_USER_ID> mentions from message text.
|
|
# Slack injects <@U...> tokens for @mentions — we strip the bot mention
|
|
# so the agent sees clean user text, not the mention syntax.
|
|
_BOT_MENTION_RE = re.compile(r"<@[A-Z0-9]+>")
|
|
|
|
|
|
def normalize_slack_event(
|
|
event: dict,
|
|
workspace_id: str,
|
|
bot_user_id: str = "",
|
|
) -> KonstructMessage:
|
|
"""
|
|
Normalize a Slack Events API event payload into a KonstructMessage.
|
|
|
|
Handles both ``app_mention`` events (where the bot is @mentioned in a
|
|
channel) and ``message`` events in DMs (``channel_type == "im"``).
|
|
|
|
The bot mention token (``<@BOT_USER_ID>``) is stripped from the beginning
|
|
of the text for ``app_mention`` events so the agent receives clean input.
|
|
|
|
Args:
|
|
event: The inner ``event`` dict from the Slack Events API payload.
|
|
workspace_id: The Slack workspace ID (team_id from the outer payload).
|
|
bot_user_id: The bot's Slack user ID (used for mention stripping).
|
|
|
|
Returns:
|
|
A fully-populated KonstructMessage. ``tenant_id`` is ``None`` at this
|
|
stage — the Message Router populates it via channel_connections lookup.
|
|
"""
|
|
# Extract and clean user text
|
|
raw_text: str = event.get("text", "") or ""
|
|
# Strip any <@BOT_ID> mention tokens from the message
|
|
clean_text = _BOT_MENTION_RE.sub("", raw_text).strip()
|
|
|
|
# Slack thread_ts is the canonical thread identifier
|
|
thread_ts: str | None = event.get("thread_ts") or event.get("ts")
|
|
|
|
# Timestamp — Slack uses Unix float strings ("1234567890.123456")
|
|
ts_raw = event.get("ts", "0")
|
|
try:
|
|
ts_float = float(ts_raw)
|
|
timestamp = datetime.fromtimestamp(ts_float, tz=timezone.utc)
|
|
except (ValueError, TypeError):
|
|
timestamp = datetime.now(tz=timezone.utc)
|
|
|
|
# User info — Slack provides user_id; display name is enriched later
|
|
sender_user_id: str = event.get("user", "") or ""
|
|
is_bot = bool(event.get("bot_id") or event.get("subtype") == "bot_message")
|
|
|
|
# Build the set of mentions present in the original text
|
|
mentions: list[str] = _BOT_MENTION_RE.findall(raw_text)
|
|
# Strip angle brackets from extracted tokens: <@U123> -> U123
|
|
mentions = [m.strip("<>@") for m in mentions]
|
|
|
|
return KonstructMessage(
|
|
id=str(uuid.uuid4()),
|
|
tenant_id=None, # Populated by Message Router
|
|
channel=ChannelType.SLACK,
|
|
channel_metadata={
|
|
"workspace_id": workspace_id,
|
|
"channel_id": event.get("channel", ""),
|
|
"thread_ts": thread_ts,
|
|
"bot_user_id": bot_user_id,
|
|
"event_ts": ts_raw,
|
|
"channel_type": event.get("channel_type", ""),
|
|
},
|
|
sender=SenderInfo(
|
|
user_id=sender_user_id,
|
|
display_name=sender_user_id, # Enriched later if needed
|
|
is_bot=is_bot,
|
|
),
|
|
content=MessageContent(
|
|
text=clean_text,
|
|
mentions=mentions,
|
|
),
|
|
timestamp=timestamp,
|
|
thread_id=thread_ts,
|
|
)
|