Skip to content

Commit

Permalink
Merge pull request #80 from axioma-ai-labs/feature/52-slack_integration
Browse files Browse the repository at this point in the history
Feature/52 Slack integration
  • Loading branch information
anderlean authored Jan 18, 2025
2 parents ef904dd + d6f7f8e commit a54a00c
Show file tree
Hide file tree
Showing 7 changed files with 484 additions and 5 deletions.
6 changes: 5 additions & 1 deletion .env.dev
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,8 @@ SHOPIFY_PASSWORD=
SHOPIFY_STORE_NAME=

# Tavily settings
TAVILY_API_KEY=
TAVILY_API_KEY=

# Slack settings
SLACK_BOT_TOKEN=
SLACK_APP_TOKEN=
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ python-telegram-bot = "*"
telegramify-markdown = "*"
tweepy = "*"
discord = "*"
slack-sdk = "*"
google-auth-oauthlib = "*"
google-auth = "*"
google-api-python-client = "*"
Expand Down
17 changes: 13 additions & 4 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

101 changes: 101 additions & 0 deletions docs/agent/slack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Slack Integration

## Setup

1. Create a Slack App
- Go to [Slack API Apps page](https://api.slack.com/apps)
- Click "Create New App" > "From scratch"
- Choose an app name and workspace
- Save the app configuration

2. Configure Bot Token and Permissions
- Navigate to "OAuth & Permissions" in your app settings
- Under "Scopes", add these Bot Token Scopes:
- `chat:write` (Send messages)
- `channels:history` (View messages in channels)
- `channels:read` (View basic channel info)
- `im:history` (View direct messages)
- `users:read` (View basic user info)
- Click "Install to Workspace"
- Copy the "Bot User OAuth Token"

3. Configure Environment Variables
Add these to your `.env` file:
```bash
SLACK_BOT_TOKEN=xoxb-your-bot-token-here
SLACK_APP_TOKEN=xapp-your-app-token-here
SLACK_SIGNING_SECRET=your-signing-secret-here
```

### Basic Setup
```python
from src.tools.slack import SlackClient

# Initialize Slack client
slack = SlackClient()

# Send a message to a channel
response = slack.send_message(
channel="#general",
text="Hello from your AI assistant!"
)

# Listen for messages
@slack.event("message")
def handle_message(event):
channel = event["channel"]
text = event["text"]
slack.send_message(channel=channel, text=f"Received: {text}")
```

## Features
- Real-time message handling with event subscriptions
- Send and receive messages in channels and DMs
- Process message threads and replies
- Support for rich message formatting and blocks
- Message history and context management
- User and channel information retrieval
- Efficient caching of API client

## TODOs for Future Enhancements:
- Add support for Slack modals and interactive components
- Implement slash commands
- Add support for message reactions
- Implement file management features
- Add support for user presence tracking
- Implement workspace analytics
- Add support for app home customization
- Implement message scheduling features
- Manage interactive components such as buttons and menus
- Enhance error handling and retry mechanisms for API interactions

## Reference
For implementation details, see: `src/tools/slack.py`

The implementation uses the official Slack Bolt Framework. For more information, refer to:
- [Slack API Documentation](https://api.slack.com/docs)
- [Slack Bolt Python Framework](https://slack.dev/bolt-python/concepts)

### Bot Memory
The Slack integration maintains conversation history through a message store that:
- Tracks message threads and their context
- Stores recent interactions per channel/user
- Maintains conversation state for ongoing dialogues
- Implements memory cleanup for older messages
- Supports context retrieval for follow-up responses

Example of history usage:
```python
# Access conversation history
history = slack.get_conversation_history(channel_id)

# Get context for a specific thread
thread_context = slack.get_thread_context(thread_ts)

# Store custom context
slack.store_context(
channel_id=channel,
thread_ts=thread,
context={"key": "value"}
)
```
5 changes: 5 additions & 0 deletions src/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,11 @@ class Settings(BaseSettings):

TAVILY_API_KEY: str = ""

# --- Slack settings ---

SLACK_BOT_TOKEN: str = ""
SLACK_APP_TOKEN: str = ""

# ==========================
# Validators
# ==========================
Expand Down
169 changes: 169 additions & 0 deletions src/tools/slack.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import asyncio
from datetime import datetime
from typing import Callable, Dict, List, Optional

from slack_sdk.errors import SlackApiError
from slack_sdk.socket_mode.aiohttp import SocketModeClient
from slack_sdk.socket_mode.async_listeners import AsyncSocketModeRequestListener
from slack_sdk.socket_mode.request import SocketModeRequest
from slack_sdk.socket_mode.response import SocketModeResponse
from slack_sdk.web.async_client import AsyncWebClient

from src.core.config import settings


class SlackIntegration:
def __init__(
self, bot_token: str = settings.SLACK_BOT_TOKEN, app_token: str = settings.SLACK_APP_TOKEN
):
"""Initialize the Slack integration with both bot and app tokens.
Args:
bot_token: The bot user OAuth token, defaults to settings.SLACK_BOT_TOKEN
app_token: The app-level token starting with 'xapp-', defaults to settings.SLACK_APP_TOKEN
"""
# Initialize with default SSL context instead of bool
ssl_context = None

self.web_client = AsyncWebClient(token=bot_token, ssl=ssl_context)
self.socket_client = SocketModeClient(app_token=app_token, web_client=self.web_client)
self.message_callback: Optional[Callable] = None

# Message storage (Bot's memory)
self.message_history: List[Dict] = []
self.max_history_size = 1000 # Adjust this value as needed

async def connect(self) -> None:
"""Establish connection to Slack"""
try:
# Test the connection
await self.web_client.auth_test()
print("Successfully connected to Slack!")
except SlackApiError as e:
print(f"Error connecting to Slack: {e.response['error']}")
raise

async def listen_for_messages(self, callback: Callable) -> None:
"""Set up message listening with Socket Mode.
Args:
callback: Function to call when messages are received
"""
self.message_callback = callback

# Type-cast the handler to match expected type

handler: AsyncSocketModeRequestListener = self._handle_socket_message # type: ignore
self.socket_client.socket_mode_request_listeners.append(handler)

# Start the Socket Mode client
await self.socket_client.connect()
print("Listening for messages...")

async def _handle_socket_message(self, client: SocketModeClient, req: SocketModeRequest):
"""Internal handler for socket mode messages"""
print(f"Received request type: {req.type}") # Debug print

if req.type == "events_api":
# Acknowledge the request
response = SocketModeResponse(envelope_id=req.envelope_id)
await client.send_socket_mode_response(response)

# Process the event
event = req.payload["event"]
print(f"Received event type: {event['type']}") # Debug print

if event["type"] == "message" and "subtype" not in event:
print(f"Processing message: {event['text']}") # Debug print
# Store message in history if it's not from a bot
if "bot_id" not in event:
self.add_to_history(event)

# Call the callback if set
if self.message_callback:
await self.message_callback(event)

async def send_message(
self, channel_id: str, message: str, thread_ts: Optional[str] = None
) -> None:
"""Send a message to a Slack channel or thread.
Args:
channel_id: The channel ID to send the message to
message: The message text to send
thread_ts: Optional thread timestamp to reply in a thread
"""
try:
if thread_ts:
await self.web_client.chat_postMessage(
channel=channel_id, text=message, thread_ts=thread_ts
)
else:
await self.web_client.chat_postMessage(channel=channel_id, text=message)
except SlackApiError as e:
print(f"Error sending message: {e.response['error']}")
raise

def add_to_history(self, message_event: Dict) -> None:
"""Store a message in the bot's memory.
Args:
message_event: The Slack message event to store
"""
# Create a message structure record
message_record = {
"text": message_event["text"],
"user": message_event["user"],
"channel": message_event["channel"],
"timestamp": message_event["ts"],
"thread_ts": message_event.get("thread_ts"),
"time": datetime.now().isoformat(),
}

# Add to history
self.message_history.append(message_record)

# Maintain size
if len(self.message_history) > self.max_history_size:
self.message_history.pop(0) # Remove oldest message

def get_user_message_history(self, user_id: str) -> List[Dict]:
"""Get all messages from a specific user.
Args:
user_id: The Slack user ID to filter messages for
Returns:
List of message records from the specified user
"""
return [msg for msg in self.message_history if msg["user"] == user_id]

def get_channel_history(self, channel_id: str) -> List[Dict]:
"""Get all messages from a specific channel.
Args:
channel_id: The Slack channel ID to filter messages for
Returns:
List of message records from the specified channel
"""
return [msg for msg in self.message_history if msg["channel"] == channel_id]

async def close(self):
"""Close the Slack connection and cleanup resources"""
# First disconnect the socket client
if hasattr(self, "socket_client") and self.socket_client:
await self.socket_client.disconnect()
# Cancel any pending tasks
for task in asyncio.all_tasks():
if "process_messages" in str(task):
task.cancel()
try:
await task
except asyncio.CancelledError:
pass

# Then close the web client session
if hasattr(self, "web_client") and self.web_client:
if hasattr(self.web_client, "session") and self.web_client.session:
await self.web_client.session.close()
Loading

0 comments on commit a54a00c

Please # to comment.