mirror of https://github.com/kortix-ai/suna.git
320 lines
14 KiB
Python
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 |