generated from axioma-ai-labs/python-template
-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #80 from axioma-ai-labs/feature/52-slack_integration
Feature/52 Slack integration
- Loading branch information
Showing
7 changed files
with
484 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"} | ||
) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
Oops, something went wrong.