From c221fed7dcacc8e0ec0a595f7c886f05e15a6a32 Mon Sep 17 00:00:00 2001 From: GitHub Copilot Date: Wed, 2 Sep 2026 17:57:20 +0300 Subject: [PATCH] Add enhanced Slack bot with multi-format summaries, language support, and user preferences MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Features: - Multiple summary formats (bullets, numbered, paragraphs, JSON) - Multi-language support (English, Turkish, Spanish, French, German) - Flexible summary lengths (brief, normal, detailed) - Multiple reaction triggers (šŸ“, šŸ“°, šŸ“Œ) - Message filtering (bots, system messages) - Per-user preference storage - Slash commands for configuration (/summary-settings, /summary-set) - Enhanced error handling and logging Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- CONFIG.md | 122 ++++++++++++++++++ TESTING.md | 82 ++++++++++++ app.py | 363 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 567 insertions(+) create mode 100644 CONFIG.md create mode 100644 TESTING.md create mode 100644 app.py diff --git a/CONFIG.md b/CONFIG.md new file mode 100644 index 000000000..482e6f7b6 --- /dev/null +++ b/CONFIG.md @@ -0,0 +1,122 @@ +# Slack Thread Summarizer - Configuration Guide + +## Features + +### 1. Summary Formats +Users can choose how they want summaries formatted: +- **Bullets** (default): `• Point 1\n• Point 2\n...` +- **Numbered**: `1. Point 1\n2. Point 2\n...` +- **Paragraphs**: Full paragraph summaries +- **JSON**: Structured format with "point" and "explanation" fields + +### 2. Summary Lengths +Summaries can be customized by length: +- **Brief**: 3 key points +- **Normal**: 5 key points (default) +- **Detailed**: 10 key points + +### 3. Multi-Language Support +Summaries can be generated in multiple languages: +- **en**: English +- **tr**: Turkish +- **es**: Spanish +- **fr**: French +- **de**: German + +### 4. Reaction Triggers +Different reactions trigger different default behaviors: +- **šŸ“ memo**: Normal 5-point summary +- **šŸ“° newspaper**: Detailed 10-point summary +- **šŸ“Œ pushpin**: Brief 3-point summary + +### 5. Message Filtering +Users can exclude certain message types: +- **Filter bots**: Skip messages from bot users +- **Filter system messages**: Skip channel join/leave events + +### 6. User Preferences +Each user's preferences are stored in `user_preferences.json`: +```json +{ + "U123456789": { + "default_format": "bullets", + "default_length": "normal", + "default_language": "en", + "filter_bots": true, + "filter_system": true + } +} +``` + +## Commands + +### `/summary-settings` +View your current summary preferences. + +``` +/summary-settings +``` + +Output: +``` +Your Summary Settings: +• Format: bullets (bullets, numbered, paragraphs, json) +• Length: normal (brief, normal, detailed) +• Language: en (en, tr, es, fr, de) +• Filter bots: true +• Filter system messages: true +``` + +### `/summary-set ` +Update a specific setting. + +Examples: +``` +/summary-set format json +/summary-set length detailed +/summary-set language tr +/summary-set filter-bots false +/summary-set filter-system false +``` + +## How to Use + +1. **React to a message** in a thread with one of the configured emojis: + - šŸ“ for standard summary + - šŸ“° for detailed summary + - šŸ“Œ for brief summary + +2. **Bot processes the thread** using your saved preferences + +3. **Summary is posted** as a reply in the same thread + +4. **Customize settings** anytime with `/summary-set` commands + +## Environment Variables + +Required in `.env` file: +``` +SLACK_BOT_TOKEN=xoxb-... +SLACK_APP_TOKEN=xapp-... +CEREBRAS_API_KEY=csk-... +``` + +## Slack App Permissions + +Ensure your Slack app has these permissions: +- `reactions:read` - Read reactions +- `chat:write` - Post messages +- `channels:history` - Read channel messages +- `commands` - Use slash commands + +## Error Handling + +- If no messages remain after filtering, the bot notifies the user +- If Cerebras API fails, the bot posts an error message to the thread +- All errors are logged for debugging + +## File Storage + +- `user_preferences.json`: Stores user configuration +- Created automatically on first run +- In production, consider using a database instead diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 000000000..c5564e030 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,82 @@ +# Example Usage and Test Cases + +## Test Scenario 1: Default Summary (šŸ“) +**Setup:** User hasn't configured preferences +**Action:** User reacts with šŸ“ emoji to a thread message +**Expected:** 5-point bullet-point summary in English + +## Test Scenario 2: Custom Format (Numbered) +**Setup:** User runs `/summary-set format numbered` +**Action:** User reacts with šŸ“ emoji +**Expected:** 5-point numbered summary instead of bullets + +## Test Scenario 3: Different Language +**Setup:** +- `/summary-set language tr` (Turkish) +- `/summary-set length brief` (3 points) +**Action:** User reacts with šŸ“ emoji to a thread +**Expected:** 3-point Turkish summary + +## Test Scenario 4: Detailed Summary (šŸ“°) +**Setup:** Default settings +**Action:** User reacts with šŸ“° emoji (newspaper) +**Expected:** 10-point detailed summary + +## Test Scenario 5: Brief Summary (šŸ“Œ) +**Setup:** Default settings +**Action:** User reacts with šŸ“Œ emoji (pushpin) +**Expected:** 3-point brief summary + +## Test Scenario 6: JSON Format +**Setup:** `/summary-set format json` +**Action:** User reacts with šŸ“ emoji +**Expected:** JSON response with structured point and explanation fields + +## Test Scenario 7: Bot Filtering +**Setup:** `/summary-set filter-bots true` +**Action:** Thread has both user and bot messages, react with šŸ“ +**Expected:** Summary excludes bot messages + +## Test Scenario 8: System Message Filtering +**Setup:** `/summary-set filter-system true` +**Action:** Thread has system messages (channel_join, etc.), react with šŸ“ +**Expected:** Summary excludes system messages + +## Test Scenario 9: View Settings +**Setup:** User has customized multiple settings +**Action:** User runs `/summary-settings` +**Expected:** Shows all current settings + +## Test Scenario 10: Error Handling +**Setup:** Empty thread (all messages filtered out) +**Action:** User reacts with šŸ“ emoji +**Expected:** Bot posts message "No messages found to summarize after filtering." + +## Sample User Preferences File + +```json +{ + "U123456789": { + "default_format": "numbered", + "default_length": "detailed", + "default_language": "tr", + "filter_bots": true, + "filter_system": true + }, + "U987654321": { + "default_format": "json", + "default_length": "brief", + "default_language": "en", + "filter_bots": false, + "filter_system": true + } +} +``` + +## Running Tests + +1. Set up your Slack workspace and bot tokens +2. Deploy the bot +3. Run through each scenario manually +4. Verify `user_preferences.json` is created and updated +5. Check logs for any errors diff --git a/app.py b/app.py new file mode 100644 index 000000000..672822ce1 --- /dev/null +++ b/app.py @@ -0,0 +1,363 @@ +import os +import json +from slack_bolt import App +from slack_bolt.adapter.socket_mode import SocketModeHandler +from dotenv import load_dotenv +from cerebras.cloud.sdk import Cerebras + +# Load .env file +load_dotenv() + +# Initialize clients +app = App(token=os.environ.get("SLACK_BOT_TOKEN")) +cerebras_client = Cerebras(api_key=os.environ.get("CEREBRAS_API_KEY")) + +# User preferences storage (in production, use a database) +USER_PREFERENCES_FILE = "user_preferences.json" + + +def load_user_preferences(): + """Load user preferences from JSON file.""" + if os.path.exists(USER_PREFERENCES_FILE): + with open(USER_PREFERENCES_FILE, "r") as f: + return json.load(f) + return {} + + +def save_user_preferences(preferences): + """Save user preferences to JSON file.""" + with open(USER_PREFERENCES_FILE, "w") as f: + json.dump(preferences, f, indent=2) + + +def get_user_preference(user_id, key, default): + """Get a specific user preference.""" + preferences = load_user_preferences() + return preferences.get(user_id, {}).get(key, default) + + +def set_user_preference(user_id, key, value): + """Set a user preference.""" + preferences = load_user_preferences() + if user_id not in preferences: + preferences[user_id] = {} + preferences[user_id][key] = value + save_user_preferences(preferences) + + +# Reaction to action mapping +REACTION_CONFIG = { + "memo": {"action": "summary", "length": "normal", "description": "5-point summary"}, + "newspaper": {"action": "summary", "length": "detailed", "description": "10-point detailed summary"}, + "pushpin": {"action": "summary", "length": "brief", "description": "3-point brief summary"}, + "mag": {"action": "summary", "length": "normal", "description": "summary with analysis"}, +} + + +def filter_messages(messages, user_id): + """Filter messages based on user preferences.""" + filter_bots = get_user_preference(user_id, "filter_bots", True) + filter_system = get_user_preference(user_id, "filter_system", True) + + filtered = [] + for msg in messages: + # Skip bot messages if filter enabled + if filter_bots and msg.get("bot_id"): + continue + # Skip system messages if filter enabled + if filter_system and msg.get("subtype") in ["channel_join", "channel_leave", "bot_add", "bot_remove"]: + continue + # Skip empty messages + if not msg.get("text"): + continue + filtered.append(msg) + + return filtered + + +def format_thread_text(messages): + """Convert thread messages to readable text.""" + thread_text = "" + for msg in messages: + user = msg.get("user", "Unknown User") + text = msg.get("text", "[Empty message or file]") + thread_text += f"- {user}: {text}\n" + return thread_text + + +def build_prompt(thread_text, summary_length, format_style, language, user_id): + """Build the prompt for Cerebras based on user preferences.""" + length_config = { + "brief": {"count": 3, "descriptor": "3-point"}, + "normal": {"count": 5, "descriptor": "5-point"}, + "detailed": {"count": 10, "descriptor": "10-point"}, + } + + config = length_config.get(summary_length, length_config["normal"]) + count = config["count"] + + format_instructions = { + "bullets": f"Format as bullet points (exactly {count} points)", + "numbered": f"Format as a numbered list (exactly {count} items)", + "paragraphs": f"Format as {count} separate paragraphs", + "json": f'Format as JSON array with {count} objects, each with "point" and "explanation" fields', + } + + format_instruction = format_instructions.get(format_style, format_instructions["bullets"]) + + lang_prefix = { + "en": "English", + "tr": "Turkish", + "es": "Spanish", + "fr": "French", + "de": "German", + } + + language_name = lang_prefix.get(language, language) + + prompt = f"""Summarize the following Slack thread into exactly {count} key points. +{format_instruction} +Write the summary in {language_name}. +Keep each point clear, concise, and professional. Do not include introduction or conclusion text, only the {count} points. + +Thread Content: +{thread_text}""" + + return prompt + + +def post_summary(client, channel_id, root_ts, summary, user_id, logger): + """Post the summary to Slack.""" + try: + client.chat_postMessage( + channel=channel_id, + text=f"šŸ“ *Thread Summary:*\n{summary}", + thread_ts=root_ts + ) + logger.info("Summary posted to Slack successfully!") + except Exception as e: + logger.error(f"Error posting summary: {e}") + client.chat_postMessage( + channel=channel_id, + text="Sorry, an error occurred while posting the summary. Please try again later.", + thread_ts=root_ts + ) + + +@app.event("reaction_added") +def handle_reaction(event, client, logger): + """Handle reaction events and summarize threads.""" + reaction = event["reaction"] + + # Check if this reaction is configured + if reaction not in REACTION_CONFIG: + return + + user_id = event["user"] + channel_id = event["item"]["channel"] + message_ts = event["item"]["ts"] + + # Get the root message ts (handles replies in threads) + root_ts = event["item"].get("thread_ts", message_ts) + + config = REACTION_CONFIG[reaction] + length = get_user_preference(user_id, "default_length", config.get("length", "normal")) + format_style = get_user_preference(user_id, "default_format", "bullets") + language = get_user_preference(user_id, "default_language", "en") + + logger.info(f"šŸ“ Reaction '{reaction}' captured! Channel: {channel_id}, User: {user_id}") + + try: + # 1. Fetch thread messages + result = client.conversations_replies(channel=channel_id, ts=root_ts) + messages = result["messages"] + + # 2. Filter messages based on user preferences + filtered_messages = filter_messages(messages, user_id) + + if not filtered_messages: + client.chat_postMessage( + channel=channel_id, + text="No messages found to summarize after filtering.", + thread_ts=root_ts + ) + return + + # 3. Convert messages to text + thread_text = format_thread_text(filtered_messages) + + # 4. Build prompt + prompt = build_prompt(thread_text, length, format_style, language, user_id) + + logger.info("Sending summarization request to Cerebras...") + + # 5. Send to Cerebras + response = cerebras_client.chat.completions.create( + model="llama3.1-70b", + messages=[ + {"role": "system", "content": "You are a Slack thread summarization assistant. Provide only the requested format with no additional commentary."}, + {"role": "user", "content": prompt} + ], + ) + + summary = response.choices[0].message.content + logger.info("Summary created successfully!") + + # 6. Post summary to Slack + post_summary(client, channel_id, root_ts, summary, user_id, logger) + + except Exception as e: + logger.error(f"Error occurred: {e}") + client.chat_postMessage( + channel=channel_id, + text="Sorry, an error occurred while creating the summary. Please try again later.", + thread_ts=root_ts + ) + + +@app.command("/summary-settings") +def handle_summary_settings(ack, body, client, logger): + """Handle /summary-settings command to configure user preferences.""" + ack() + + user_id = body["user_id"] + channel_id = body["channel_id"] + + preferences = load_user_preferences().get(user_id, {}) + + current_format = preferences.get("default_format", "bullets") + current_length = preferences.get("default_length", "normal") + current_language = preferences.get("default_language", "en") + current_filter_bots = preferences.get("filter_bots", True) + current_filter_system = preferences.get("filter_system", True) + + settings_text = f""" +*Your Summary Settings:* +• Format: `{current_format}` (bullets, numbered, paragraphs, json) +• Length: `{current_length}` (brief, normal, detailed) +• Language: `{current_language}` (en, tr, es, fr, de) +• Filter bots: `{current_filter_bots}` +• Filter system messages: `{current_filter_system}` + +Use `/summary-set format ` to change format +Use `/summary-set length ` to change length +Use `/summary-set language ` to change language +Use `/summary-set filter-bots ` to toggle bot filtering +Use `/summary-set filter-system ` to toggle system message filtering + +*Reaction Guide:* +• šŸ“ memo: Normal 5-point summary +• šŸ“° newspaper: Detailed 10-point summary +• šŸ“Œ pushpin: Brief 3-point summary +""" + + client.chat_postMessage( + channel=channel_id, + text=settings_text + ) + + logger.info(f"Settings displayed for user {user_id}") + + +@app.command("/summary-set") +def handle_summary_set(ack, body, client, logger): + """Handle /summary-set command to update user preferences.""" + ack() + + user_id = body["user_id"] + channel_id = body["channel_id"] + text = body.get("text", "").strip().split(" ", 1) + + if len(text) < 2: + client.chat_postMessage( + channel=channel_id, + text="Usage: `/summary-set `\nExample: `/summary-set format numbered`" + ) + return + + setting, value = text[0], text[1] + + valid_formats = ["bullets", "numbered", "paragraphs", "json"] + valid_lengths = ["brief", "normal", "detailed"] + valid_languages = ["en", "tr", "es", "fr", "de"] + + if setting == "format": + if value in valid_formats: + set_user_preference(user_id, "default_format", value) + client.chat_postMessage( + channel=channel_id, + text=f"āœ… Format updated to `{value}`" + ) + else: + client.chat_postMessage( + channel=channel_id, + text=f"āŒ Invalid format. Choose from: {', '.join(valid_formats)}" + ) + + elif setting == "length": + if value in valid_lengths: + set_user_preference(user_id, "default_length", value) + client.chat_postMessage( + channel=channel_id, + text=f"āœ… Length updated to `{value}`" + ) + else: + client.chat_postMessage( + channel=channel_id, + text=f"āŒ Invalid length. Choose from: {', '.join(valid_lengths)}" + ) + + elif setting == "language": + if value in valid_languages: + set_user_preference(user_id, "default_language", value) + client.chat_postMessage( + channel=channel_id, + text=f"āœ… Language updated to `{value}`" + ) + else: + client.chat_postMessage( + channel=channel_id, + text=f"āŒ Invalid language. Choose from: {', '.join(valid_languages)}" + ) + + elif setting == "filter-bots": + if value.lower() in ["true", "false"]: + bool_value = value.lower() == "true" + set_user_preference(user_id, "filter_bots", bool_value) + client.chat_postMessage( + channel=channel_id, + text=f"āœ… Bot filtering updated to `{bool_value}`" + ) + else: + client.chat_postMessage( + channel=channel_id, + text="āŒ Invalid value. Use `true` or `false`" + ) + + elif setting == "filter-system": + if value.lower() in ["true", "false"]: + bool_value = value.lower() == "true" + set_user_preference(user_id, "filter_system", bool_value) + client.chat_postMessage( + channel=channel_id, + text=f"āœ… System message filtering updated to `{bool_value}`" + ) + else: + client.chat_postMessage( + channel=channel_id, + text="āŒ Invalid value. Use `true` or `false`" + ) + + else: + client.chat_postMessage( + channel=channel_id, + text=f"āŒ Unknown setting. Choose from: format, length, language, filter-bots, filter-system" + ) + + logger.info(f"Setting '{setting}' updated for user {user_id}") + + +if __name__ == "__main__": + handler = SocketModeHandler(app, os.environ.get("SLACK_APP_TOKEN")) + app.logger.info("Bot starting... Ready!") + handler.start()