suna/backend/triggers/providers/slack_provider.py

320 lines
14 KiB
Python

"""
Slack trigger provider for agent triggers.
This provider handles Slack app integration for triggering agents based on events.
"""
import httpx
import hmac
import hashlib
import time
from typing import Dict, Any, Optional
from utils.logger import logger
from ..core import TriggerProvider, TriggerType, TriggerEvent, TriggerResult, TriggerConfig, ProviderDefinition
class SlackTriggerProvider(TriggerProvider):
"""Slack trigger provider for agents."""
def __init__(self, provider_definition: Optional[ProviderDefinition] = None):
super().__init__(TriggerType.SLACK, provider_definition)
async def validate_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
"""
Validate Slack trigger configuration.
For OAuth-based triggers: access_token is required
For webhook-based triggers: signing_secret is required
"""
is_oauth = config.get('oauth_installed', False) or config.get('access_token')
if is_oauth:
if not config.get('access_token'):
raise ValueError("access_token is required for OAuth-based Slack triggers")
validated_config = {
'access_token': config['access_token'],
'bot_user_id': config.get('bot_user_id', ''),
'team_id': config.get('team_id', ''),
'team_name': config.get('team_name', ''),
'bot_name': config.get('bot_name', ''),
'allowed_channels': config.get('allowed_channels', []),
'trigger_keywords': config.get('trigger_keywords', []),
'respond_to_mentions': config.get('respond_to_mentions', True),
'respond_to_direct_messages': config.get('respond_to_direct_messages', True),
'oauth_installed': True,
'provider': config.get('provider', 'slack')
}
else:
if not config.get('signing_secret'):
raise ValueError("signing_secret is required for webhook-based Slack triggers")
validated_config = {
'signing_secret': config['signing_secret'],
'bot_token': config.get('bot_token', ''),
'allowed_channels': config.get('allowed_channels', []),
'trigger_keywords': config.get('trigger_keywords', []),
'respond_to_mentions': config.get('respond_to_mentions', True),
'respond_to_direct_messages': config.get('respond_to_direct_messages', True),
}
for list_field in ['allowed_channels', 'trigger_keywords']:
if not isinstance(validated_config[list_field], list):
raise ValueError(f"{list_field} must be a list")
return validated_config
async def setup_trigger(self, trigger_config: TriggerConfig) -> bool:
"""Set up Slack integration for the trigger."""
try:
is_oauth = trigger_config.config.get('oauth_installed', False)
if is_oauth:
# OAuth trigger setup
access_token = trigger_config.config.get('access_token')
if not access_token:
logger.error(f"Invalid access token for OAuth trigger {trigger_config.trigger_id}")
return False
logger.info(f"Successfully set up Slack OAuth trigger {trigger_config.trigger_id}")
else:
# Webhook trigger setup
signing_secret = trigger_config.config.get('signing_secret')
if not signing_secret:
logger.error(f"Invalid signing secret for webhook trigger {trigger_config.trigger_id}")
return False
logger.info(f"Successfully set up Slack webhook trigger {trigger_config.trigger_id}")
return True
except Exception as e:
logger.error(f"Error setting up Slack trigger {trigger_config.trigger_id}: {e}")
return False
async def teardown_trigger(self, trigger_config: TriggerConfig) -> bool:
"""Remove Slack webhook for the trigger."""
try:
logger.info(f"Successfully tore down Slack webhook for trigger {trigger_config.trigger_id}")
return True
except Exception as e:
logger.error(f"Error tearing down Slack trigger {trigger_config.trigger_id}: {e}")
return False
async def process_event(self, event: TriggerEvent) -> TriggerResult:
"""Process Slack webhook event."""
try:
event_data = event.raw_data
if event_data.get('type') == 'url_verification':
return TriggerResult(
success=True,
should_execute_agent=False,
response_data={"challenge": event_data.get('challenge')}
)
slack_event = event_data.get('event', {})
if not slack_event:
return TriggerResult(
success=True,
should_execute_agent=False,
response_data={"message": "No event data in Slack webhook"}
)
default_config = {
'signing_secret': '',
'bot_token': '',
'allowed_channels': [],
'trigger_keywords': [],
'respond_to_mentions': True,
'respond_to_direct_messages': True,
}
should_trigger = await self._should_trigger_agent(slack_event, default_config)
if not should_trigger:
return TriggerResult(
success=True,
should_execute_agent=False,
response_data={"message": "Event did not meet trigger criteria"}
)
agent_prompt = self._create_agent_prompt(slack_event, default_config)
execution_variables = {
'slack_message_text': slack_event.get('text', ''),
'slack_user_id': slack_event.get('user', ''),
'slack_channel_id': slack_event.get('channel', ''),
'slack_timestamp': slack_event.get('ts', ''),
'slack_event_type': slack_event.get('type', ''),
'trigger_type': 'slack',
'trigger_id': event.trigger_id,
'agent_id': event.agent_id
}
return TriggerResult(
success=True,
should_execute_agent=True,
agent_prompt=agent_prompt,
execution_variables=execution_variables,
metadata={
'slack_data': slack_event,
'trigger_config': default_config
}
)
except Exception as e:
logger.error(f"Error processing Slack event for trigger {event.trigger_id}: {e}")
return TriggerResult(
success=False,
error_message=f"Error processing Slack event: {str(e)}"
)
async def health_check(self, trigger_config: TriggerConfig) -> bool:
"""Check if Slack integration is healthy."""
try:
is_oauth = trigger_config.config.get('oauth_installed', False)
if is_oauth:
# Check OAuth trigger health
access_token = trigger_config.config.get('access_token')
return bool(access_token)
else:
# Check webhook trigger health
signing_secret = trigger_config.config.get('signing_secret')
return bool(signing_secret)
except Exception as e:
logger.error(f"Health check failed for Slack trigger {trigger_config.trigger_id}: {e}")
return False
def get_config_schema(self) -> Dict[str, Any]:
"""Get configuration schema for Slack triggers."""
return {
"type": "object",
"properties": {
"signing_secret": {
"type": "string",
"description": "Slack app signing secret for webhook verification (required for webhook triggers)"
},
"access_token": {
"type": "string",
"description": "Slack bot access token (required for OAuth triggers)"
},
"bot_token": {
"type": "string",
"description": "Slack bot token for sending responses"
},
"oauth_installed": {
"type": "boolean",
"description": "Whether this is an OAuth-based trigger",
"default": False
},
"team_id": {
"type": "string",
"description": "Slack team/workspace ID"
},
"team_name": {
"type": "string",
"description": "Slack team/workspace name"
},
"bot_user_id": {
"type": "string",
"description": "Slack bot user ID"
},
"bot_name": {
"type": "string",
"description": "Slack bot name"
},
"allowed_channels": {
"type": "array",
"items": {"type": "string"},
"description": "List of allowed channel IDs (empty = allow all)"
},
"trigger_keywords": {
"type": "array",
"items": {"type": "string"},
"description": "Keywords that trigger the agent"
},
"respond_to_mentions": {
"type": "boolean",
"description": "Whether to respond to mentions",
"default": True
},
"respond_to_direct_messages": {
"type": "boolean",
"description": "Whether to respond to direct messages",
"default": True
}
},
"anyOf": [
{"required": ["signing_secret"]},
{"required": ["access_token"]}
],
"additionalProperties": False
}
def get_webhook_url(self, trigger_id: str, base_url: str) -> str:
"""Get webhook URL for this Slack trigger."""
# Slack requires a single Event Request URL per app
# All Slack events go to the universal webhook endpoint
return f"{base_url}/api/triggers/slack/webhook"
async def _should_trigger_agent(self, event_data: Dict[str, Any], config: Dict[str, Any]) -> bool:
"""Determine if Slack event should trigger the agent."""
logger.info(f"Slack event data: {event_data}")
logger.info(f"Event type: {event_data.get('type')}")
event_type = event_data.get('type')
if event_type not in ['message', 'app_mention']:
logger.info(f"Not a message or app_mention event, skipping. Type: {event_type}")
return False
if event_data.get('bot_id') or event_data.get('subtype'):
logger.info(f"Bot message or subtype message, skipping. Bot ID: {event_data.get('bot_id')}, Subtype: {event_data.get('subtype')}")
return False
allowed_channels = config.get('allowed_channels', [])
channel_id = event_data.get('channel', '')
if allowed_channels and channel_id not in allowed_channels:
logger.info(f"Channel not allowed. Channel: {channel_id}, Allowed: {allowed_channels}")
return False
message_text = event_data.get('text', '').lower()
logger.info(f"Message text: '{message_text}'")
if config.get('respond_to_mentions', True):
if '<@' in message_text:
logger.info("Message contains mention, triggering agent")
return True
if config.get('respond_to_direct_messages', True):
pass
trigger_keywords = config.get('trigger_keywords', [])
if trigger_keywords:
for keyword in trigger_keywords:
if keyword.lower() in message_text:
logger.info(f"Keyword '{keyword}' found in message, triggering agent")
return True
if not trigger_keywords and not allowed_channels:
logger.info("No keywords or channel restrictions, triggering agent")
return True
logger.info("No trigger criteria met, not triggering agent")
return False
def _create_agent_prompt(self, event_data: Dict[str, Any], config: Dict[str, Any]) -> str:
"""Create prompt for the agent based on the Slack event."""
message_text = event_data.get('text', '')
user_id = event_data.get('user', 'Unknown User')
channel_id = event_data.get('channel', '')
prompt = f"You received a message from user {user_id} in Slack channel {channel_id}: \"{message_text}\""
prompt += "\n\nPlease respond appropriately to this message. Your response will be sent back to the Slack channel."
return prompt
async def _get_trigger_config(self, trigger_id: str) -> Optional[TriggerConfig]:
"""Get trigger configuration by ID."""
return None