diff --git a/backend/autometabuilder/integrations/notifications.py b/backend/autometabuilder/workflow/notification_helpers.py similarity index 88% rename from backend/autometabuilder/integrations/notifications.py rename to backend/autometabuilder/workflow/notification_helpers.py index 2564a66..592603a 100644 --- a/backend/autometabuilder/integrations/notifications.py +++ b/backend/autometabuilder/workflow/notification_helpers.py @@ -1,3 +1,4 @@ +"""Notification helpers for workflow plugins.""" import os import logging from slack_sdk import WebClient @@ -7,7 +8,9 @@ import asyncio logger = logging.getLogger("autometabuilder.notifications") + def send_slack_notification(message: str): + """Send a notification to Slack.""" token = os.environ.get("SLACK_BOT_TOKEN") channel = os.environ.get("SLACK_CHANNEL") if not token or not channel: @@ -21,7 +24,9 @@ def send_slack_notification(message: str): except SlackApiError as e: logger.error(f"Error sending Slack notification: {e}") + async def send_discord_notification_async(message: str): + """Send Discord notification asynchronously.""" token = os.environ.get("DISCORD_BOT_TOKEN") channel_id = os.environ.get("DISCORD_CHANNEL_ID") if not token or not channel_id: @@ -44,12 +49,16 @@ async def send_discord_notification_async(message: str): except Exception as e: logger.error(f"Error sending Discord notification: {e}") + def send_discord_notification(message: str): + """Send a Discord notification.""" try: asyncio.run(send_discord_notification_async(message)) except Exception as e: logger.error(f"Error running Discord notification: {e}") + def notify_all(message: str): + """Send notification to all configured channels.""" send_slack_notification(message) send_discord_notification(message) diff --git a/backend/autometabuilder/workflow/plugins/README.md b/backend/autometabuilder/workflow/plugins/README.md index b829165..9dbefb2 100644 --- a/backend/autometabuilder/workflow/plugins/README.md +++ b/backend/autometabuilder/workflow/plugins/README.md @@ -8,6 +8,7 @@ Plugins are now organized into subdirectories by category: - **backend/** - Backend infrastructure and initialization plugins (12 plugins) - **core/** - Core workflow orchestration plugins (7 plugins) - **tools/** - Tool execution and development plugins (7 plugins) +- **notifications/** - External notification integrations (3 plugins) - **logic/** - Logic and comparison operations (9 plugins) - **list/** - List/array operations (7 plugins) - **dict/** - Dictionary/object operations (6 plugins) @@ -18,13 +19,15 @@ Plugins are now organized into subdirectories by category: - **var/** - Variable management (4 plugins) - **test/** - Unit testing and assertions (5 plugins) - **utils/** - Utility functions (7 plugins) +- **web/** - Web UI and Flask operations (26 plugins) -**Total: 90 plugins** +**Total: 93 plugins** ## Categories - [Core Plugins](#core-plugins) - AI and context management - [Tool Plugins](#tool-plugins) - File system and SDLC operations +- [Notification Plugins](#notification-plugins) - External notification integrations - [Logic Plugins](#logic-plugins) - Boolean logic and comparisons - [List Plugins](#list-plugins) - Collection operations - [Dictionary Plugins](#dictionary-plugins) - Object/map operations @@ -36,6 +39,7 @@ Plugins are now organized into subdirectories by category: - [Test Plugins](#test-plugins) - Unit testing and assertions - [Backend Plugins](#backend-plugins) - System initialization - [Utility Plugins](#utility-plugins) - General utilities +- [Web Plugins](#web-plugins) - Web UI and Flask operations --- @@ -172,6 +176,48 @@ Run command inside Docker container. --- +## Notification Plugins + +### `notifications.slack` +Send notification to Slack. + +**Inputs:** +- `message` - The message to send +- `token` - Optional Slack bot token (defaults to SLACK_BOT_TOKEN env var) +- `channel` - Optional channel (defaults to SLACK_CHANNEL env var) + +**Outputs:** +- `success` - Boolean (true if sent successfully) +- `message` - Status message +- `error` - Error message (if failed) +- `skipped` - Boolean (true if skipped due to missing config) + +### `notifications.discord` +Send notification to Discord. + +**Inputs:** +- `message` - The message to send +- `token` - Optional Discord bot token (defaults to DISCORD_BOT_TOKEN env var) +- `channel_id` - Optional channel ID (defaults to DISCORD_CHANNEL_ID env var) + +**Outputs:** +- `success` - Boolean (true if sent successfully) +- `message` - Status message +- `error` - Error message (if failed) +- `skipped` - Boolean (true if skipped due to missing config) + +### `notifications.all` +Send notification to all configured channels (Slack and Discord). + +**Inputs:** +- `message` - The message to send to all channels + +**Outputs:** +- `success` - Boolean +- `message` - Status message + +--- + ## Logic Plugins ### `logic.and` diff --git a/backend/autometabuilder/workflow/plugins/core/core_append_tool_results.py b/backend/autometabuilder/workflow/plugins/core/core_append_tool_results.py index 5347acc..ea7a4fc 100644 --- a/backend/autometabuilder/workflow/plugins/core/core_append_tool_results.py +++ b/backend/autometabuilder/workflow/plugins/core/core_append_tool_results.py @@ -1,7 +1,7 @@ """Workflow plugin: append tool results.""" import os import re -from ....integrations.notifications import notify_all +from ...notification_helpers import notify_all def _is_mvp_reached() -> bool: diff --git a/backend/autometabuilder/workflow/plugins/core/core_run_tool_calls.py b/backend/autometabuilder/workflow/plugins/core/core_run_tool_calls.py index 41d0683..28daaa8 100644 --- a/backend/autometabuilder/workflow/plugins/core/core_run_tool_calls.py +++ b/backend/autometabuilder/workflow/plugins/core/core_run_tool_calls.py @@ -1,5 +1,5 @@ """Workflow plugin: run tool calls.""" -from ....integrations.notifications import notify_all +from ...notification_helpers import notify_all from ..tool_calls_handler import handle_tool_calls diff --git a/backend/autometabuilder/integrations/__init__.py b/backend/autometabuilder/workflow/plugins/notifications/__init__.py similarity index 100% rename from backend/autometabuilder/integrations/__init__.py rename to backend/autometabuilder/workflow/plugins/notifications/__init__.py diff --git a/backend/autometabuilder/workflow/plugins/notifications/notifications_all.py b/backend/autometabuilder/workflow/plugins/notifications/notifications_all.py new file mode 100644 index 0000000..5a3d699 --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/notifications/notifications_all.py @@ -0,0 +1,80 @@ +"""Workflow plugin: send notification to all channels.""" +import os +import logging +from slack_sdk import WebClient +from slack_sdk.errors import SlackApiError +import discord +import asyncio + +logger = logging.getLogger("autometabuilder.notifications") + + +def _send_slack(message: str): + """Send Slack notification.""" + token = os.environ.get("SLACK_BOT_TOKEN") + channel = os.environ.get("SLACK_CHANNEL") + if not token or not channel: + logger.warning("Slack notification skipped: SLACK_BOT_TOKEN or SLACK_CHANNEL missing.") + return + + client = WebClient(token=token) + try: + client.chat_postMessage(channel=channel, text=message) + logger.info("Slack notification sent successfully.") + except SlackApiError as e: + logger.error(f"Error sending Slack notification: {e}") + + +async def _send_discord_async(message: str): + """Send Discord notification asynchronously.""" + token = os.environ.get("DISCORD_BOT_TOKEN") + channel_id = os.environ.get("DISCORD_CHANNEL_ID") + if not token or not channel_id: + logger.warning("Discord notification skipped: DISCORD_BOT_TOKEN or DISCORD_CHANNEL_ID missing.") + return + + intents = discord.Intents.default() + client = discord.Client(intents=intents) + + @client.event + async def on_ready(): + channel = client.get_channel(int(channel_id)) + if channel: + await channel.send(message) + logger.info("Discord notification sent successfully.") + await client.close() + + try: + await client.start(token) + except Exception as e: + logger.error(f"Error sending Discord notification: {e}") + + +def _send_discord(message: str): + """Send Discord notification.""" + try: + asyncio.run(_send_discord_async(message)) + except Exception as e: + logger.error(f"Error running Discord notification: {e}") + + +def run(runtime, inputs): + """ + Send a notification to all configured channels (Slack and Discord). + + Inputs: + message: The message to send to all channels + + Returns: + dict: Contains success status for all channels + """ + message = inputs.get("message", "") + + # Send to both channels + _send_slack(message) + _send_discord(message) + + return { + "success": True, + "message": "Notifications sent to all channels" + } diff --git a/backend/autometabuilder/workflow/plugins/notifications/notifications_discord.py b/backend/autometabuilder/workflow/plugins/notifications/notifications_discord.py new file mode 100644 index 0000000..a25220f --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/notifications/notifications_discord.py @@ -0,0 +1,59 @@ +"""Workflow plugin: send Discord notification.""" +import os +import logging +import discord +import asyncio + +logger = logging.getLogger("autometabuilder.notifications") + + +async def _send_discord_notification_async(message: str, token: str, channel_id: str): + """Send Discord notification asynchronously.""" + intents = discord.Intents.default() + client = discord.Client(intents=intents) + + @client.event + async def on_ready(): + channel = client.get_channel(int(channel_id)) + if channel: + await channel.send(message) + logger.info("Discord notification sent successfully.") + await client.close() + + try: + await client.start(token) + except Exception as e: + logger.error(f"Error sending Discord notification: {e}") + raise + + +def run(runtime, inputs): + """ + Send a notification to Discord. + + Inputs: + message: The message to send + token: Optional Discord bot token (defaults to DISCORD_BOT_TOKEN env var) + channel_id: Optional channel ID (defaults to DISCORD_CHANNEL_ID env var) + + Returns: + dict: Contains success status and any error message + """ + message = inputs.get("message", "") + token = inputs.get("token") or os.environ.get("DISCORD_BOT_TOKEN") + channel_id = inputs.get("channel_id") or os.environ.get("DISCORD_CHANNEL_ID") + + if not token or not channel_id: + logger.warning("Discord notification skipped: DISCORD_BOT_TOKEN or DISCORD_CHANNEL_ID missing.") + return { + "success": False, + "skipped": True, + "error": "DISCORD_BOT_TOKEN or DISCORD_CHANNEL_ID missing" + } + + try: + asyncio.run(_send_discord_notification_async(message, token, channel_id)) + return {"success": True, "message": "Discord notification sent"} + except Exception as e: + logger.error(f"Error running Discord notification: {e}") + return {"success": False, "error": str(e)} diff --git a/backend/autometabuilder/workflow/plugins/notifications/notifications_slack.py b/backend/autometabuilder/workflow/plugins/notifications/notifications_slack.py new file mode 100644 index 0000000..9125745 --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/notifications/notifications_slack.py @@ -0,0 +1,41 @@ +"""Workflow plugin: send Slack notification.""" +import os +import logging +from slack_sdk import WebClient +from slack_sdk.errors import SlackApiError + +logger = logging.getLogger("autometabuilder.notifications") + + +def run(runtime, inputs): + """ + Send a notification to Slack. + + Inputs: + message: The message to send + token: Optional Slack bot token (defaults to SLACK_BOT_TOKEN env var) + channel: Optional channel (defaults to SLACK_CHANNEL env var) + + Returns: + dict: Contains success status and any error message + """ + message = inputs.get("message", "") + token = inputs.get("token") or os.environ.get("SLACK_BOT_TOKEN") + channel = inputs.get("channel") or os.environ.get("SLACK_CHANNEL") + + if not token or not channel: + logger.warning("Slack notification skipped: SLACK_BOT_TOKEN or SLACK_CHANNEL missing.") + return { + "success": False, + "skipped": True, + "error": "SLACK_BOT_TOKEN or SLACK_CHANNEL missing" + } + + client = WebClient(token=token) + try: + client.chat_postMessage(channel=channel, text=message) + logger.info("Slack notification sent successfully.") + return {"success": True, "message": "Slack notification sent"} + except SlackApiError as e: + logger.error(f"Error sending Slack notification: {e}") + return {"success": False, "error": str(e)}