diff --git a/backend/autometabuilder/integrations/__init__.py b/backend/autometabuilder/integrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/autometabuilder/integrations/notifications.py b/backend/autometabuilder/integrations/notifications.py new file mode 100644 index 0000000..2564a66 --- /dev/null +++ b/backend/autometabuilder/integrations/notifications.py @@ -0,0 +1,55 @@ +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_notification(message: str): + 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_notification_async(message: str): + 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_notification(message: str): + 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_slack_notification(message) + send_discord_notification(message) diff --git a/backend/autometabuilder/workflow/notification_helpers.py b/backend/autometabuilder/workflow/notification_helpers.py index 592603a..7764d89 100644 --- a/backend/autometabuilder/workflow/notification_helpers.py +++ b/backend/autometabuilder/workflow/notification_helpers.py @@ -1,39 +1,36 @@ """Notification helpers for workflow plugins.""" 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_notification(message: str): - """Send a notification to Slack.""" - token = os.environ.get("SLACK_BOT_TOKEN") +def send_slack_notification(runtime, message: str): + """Send a notification to Slack using client from runtime context.""" + client = runtime.context.get("slack_client") if runtime else None channel = os.environ.get("SLACK_CHANNEL") - if not token or not channel: - logger.warning("Slack notification skipped: SLACK_BOT_TOKEN or SLACK_CHANNEL missing.") + + if not client: + logger.warning("Slack notification skipped: Slack client not initialized.") + return + + if not channel: + logger.warning("Slack notification skipped: SLACK_CHANNEL missing.") return - client = WebClient(token=token) try: + from slack_sdk.errors import SlackApiError 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_notification_async(message: str): +async def send_discord_notification_async(message: str, token: str, intents, channel_id: 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() + import discord + client = discord.Client(intents=intents) @client.event @@ -50,15 +47,27 @@ async def send_discord_notification_async(message: str): logger.error(f"Error sending Discord notification: {e}") -def send_discord_notification(message: str): - """Send a Discord notification.""" +def send_discord_notification(runtime, message: str): + """Send a Discord notification using config from runtime context.""" + token = runtime.context.get("discord_token") if runtime else None + intents = runtime.context.get("discord_intents") if runtime else None + channel_id = os.environ.get("DISCORD_CHANNEL_ID") + + if not token: + logger.warning("Discord notification skipped: Discord client not initialized.") + return + + if not channel_id: + logger.warning("Discord notification skipped: DISCORD_CHANNEL_ID missing.") + return + try: - asyncio.run(send_discord_notification_async(message)) + asyncio.run(send_discord_notification_async(message, token, intents, channel_id)) except Exception as e: logger.error(f"Error running Discord notification: {e}") -def notify_all(message: str): +def notify_all(runtime, message: str): """Send notification to all configured channels.""" - send_slack_notification(message) - send_discord_notification(message) + send_slack_notification(runtime, message) + send_discord_notification(runtime, message) diff --git a/backend/autometabuilder/workflow/plugins/README.md b/backend/autometabuilder/workflow/plugins/README.md index 9dbefb2..bf1b29a 100644 --- a/backend/autometabuilder/workflow/plugins/README.md +++ b/backend/autometabuilder/workflow/plugins/README.md @@ -5,7 +5,7 @@ This document describes all available workflow plugins for building declarative ## Directory Structure Plugins are now organized into subdirectories by category: -- **backend/** - Backend infrastructure and initialization plugins (12 plugins) +- **backend/** - Backend infrastructure and initialization plugins (14 plugins) - **core/** - Core workflow orchestration plugins (7 plugins) - **tools/** - Tool execution and development plugins (7 plugins) - **notifications/** - External notification integrations (3 plugins) @@ -21,7 +21,7 @@ Plugins are now organized into subdirectories by category: - **utils/** - Utility functions (7 plugins) - **web/** - Web UI and Flask operations (26 plugins) -**Total: 93 plugins** +**Total: 95 plugins** ## Categories @@ -178,12 +178,16 @@ Run command inside Docker container. ## Notification Plugins +**Note:** Notification plugins require the corresponding backend plugins (`backend.create_slack` and/or `backend.create_discord`) to be run first to initialize the clients. + ### `notifications.slack` Send notification to Slack. +**Prerequisites:** +- `backend.create_slack` must be run first to initialize the Slack client + **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:** @@ -195,9 +199,11 @@ Send notification to Slack. ### `notifications.discord` Send notification to Discord. +**Prerequisites:** +- `backend.create_discord` must be run first to initialize the Discord configuration + **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:** @@ -209,6 +215,9 @@ Send notification to Discord. ### `notifications.all` Send notification to all configured channels (Slack and Discord). +**Prerequisites:** +- `backend.create_slack` and `backend.create_discord` should be run first for full functionality + **Inputs:** - `message` - The message to send to all channels @@ -858,6 +867,28 @@ Initialize OpenAI client. - `result` - OpenAI client - `initialized` - Boolean +### `backend.create_slack` +Initialize Slack WebClient. + +**Inputs:** +- `token` - Optional Slack bot token (defaults to SLACK_BOT_TOKEN env var) + +**Outputs:** +- `result` - Slack client +- `initialized` - Boolean +- `error` - Error message (if failed) + +### `backend.create_discord` +Initialize Discord client configuration. + +**Inputs:** +- `token` - Optional Discord bot token (defaults to DISCORD_BOT_TOKEN env var) + +**Outputs:** +- `result` - Discord token +- `initialized` - Boolean +- `error` - Error message (if failed) + ### `backend.load_metadata` Load metadata.json. diff --git a/backend/autometabuilder/workflow/plugins/backend/backend_create_discord.py b/backend/autometabuilder/workflow/plugins/backend/backend_create_discord.py new file mode 100644 index 0000000..005df8c --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/backend/backend_create_discord.py @@ -0,0 +1,36 @@ +"""Workflow plugin: create Discord client.""" +import os +import logging +import discord + +logger = logging.getLogger("autometabuilder") + + +def run(runtime, inputs): + """ + Initialize Discord Client (without starting the connection). + + Note: Discord client needs to be started asynchronously. This plugin + just stores the token and intents configuration for later use by + notification plugins. + + Inputs: + token: Optional Discord bot token (defaults to DISCORD_BOT_TOKEN env var) + + Returns: + dict: Contains initialization status and configuration + """ + token = inputs.get("token") or os.environ.get("DISCORD_BOT_TOKEN") + + if not token: + logger.warning("Discord client not initialized: DISCORD_BOT_TOKEN missing.") + runtime.context["discord_token"] = None + return {"result": None, "initialized": False, "error": "DISCORD_BOT_TOKEN missing"} + + # Store token and intents configuration in context + # Discord client must be created per-use due to its async nature + runtime.context["discord_token"] = token + runtime.context["discord_intents"] = discord.Intents.default() + + logger.info("Discord configuration initialized successfully.") + return {"result": token, "initialized": True} diff --git a/backend/autometabuilder/workflow/plugins/backend/backend_create_slack.py b/backend/autometabuilder/workflow/plugins/backend/backend_create_slack.py new file mode 100644 index 0000000..a87ae54 --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/backend/backend_create_slack.py @@ -0,0 +1,33 @@ +"""Workflow plugin: create Slack client.""" +import os +import logging +from slack_sdk import WebClient + +logger = logging.getLogger("autometabuilder") + + +def run(runtime, inputs): + """ + Initialize Slack WebClient. + + Inputs: + token: Optional Slack bot token (defaults to SLACK_BOT_TOKEN env var) + + Returns: + dict: Contains the Slack client in result and initialized status + """ + token = inputs.get("token") or os.environ.get("SLACK_BOT_TOKEN") + + if not token: + logger.warning("Slack client not initialized: SLACK_BOT_TOKEN missing.") + runtime.context["slack_client"] = None + return {"result": None, "initialized": False, "error": "SLACK_BOT_TOKEN missing"} + + # Create Slack client + client = WebClient(token=token) + + # Store in context for other plugins to use + runtime.context["slack_client"] = client + + logger.info("Slack client initialized successfully.") + return {"result": client, "initialized": True} 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 ea7a4fc..6c75a5b 100644 --- a/backend/autometabuilder/workflow/plugins/core/core_append_tool_results.py +++ b/backend/autometabuilder/workflow/plugins/core/core_append_tool_results.py @@ -47,6 +47,6 @@ def run(runtime, inputs): if runtime.context["args"].yolo and _is_mvp_reached(): runtime.logger.info("MVP reached. Stopping YOLO loop.") - notify_all("AutoMetabuilder YOLO loop stopped: MVP reached.") + notify_all(runtime, "AutoMetabuilder YOLO loop stopped: MVP reached.") return {"messages": messages} 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 28daaa8..6088791 100644 --- a/backend/autometabuilder/workflow/plugins/core/core_run_tool_calls.py +++ b/backend/autometabuilder/workflow/plugins/core/core_run_tool_calls.py @@ -19,7 +19,7 @@ def run(runtime, inputs): runtime.logger ) if not tool_calls and resp_msg.content: - notify_all(f"AutoMetabuilder task complete: {resp_msg.content[:100]}...") + notify_all(runtime, f"AutoMetabuilder task complete: {resp_msg.content[:100]}...") return { "tool_results": tool_results, "no_tool_calls": not bool(tool_calls) diff --git a/backend/autometabuilder/workflow/plugins/notifications/notifications_all.py b/backend/autometabuilder/workflow/plugins/notifications/notifications_all.py index 5a3d699..3ea3574 100644 --- a/backend/autometabuilder/workflow/plugins/notifications/notifications_all.py +++ b/backend/autometabuilder/workflow/plugins/notifications/notifications_all.py @@ -1,39 +1,29 @@ """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.") +def _send_slack(client, message: str, channel: str): + """Send Slack notification using provided client.""" + if not client or not channel: + logger.warning("Slack notification skipped: client or channel missing.") return - client = WebClient(token=token) try: + from slack_sdk.errors import SlackApiError 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): +async def _send_discord_async(message: str, token: str, intents, channel_id: 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 + import discord - intents = discord.Intents.default() client = discord.Client(intents=intents) @client.event @@ -50,10 +40,14 @@ async def _send_discord_async(message: str): logger.error(f"Error sending Discord notification: {e}") -def _send_discord(message: str): +def _send_discord(message: str, token: str, intents, channel_id: str): """Send Discord notification.""" + if not token or not channel_id: + logger.warning("Discord notification skipped: token or channel_id missing.") + return + try: - asyncio.run(_send_discord_async(message)) + asyncio.run(_send_discord_async(message, token, intents, channel_id)) except Exception as e: logger.error(f"Error running Discord notification: {e}") @@ -70,9 +64,18 @@ def run(runtime, inputs): """ message = inputs.get("message", "") + # Get Slack client from runtime context + slack_client = runtime.context.get("slack_client") + slack_channel = os.environ.get("SLACK_CHANNEL") + + # Get Discord config from runtime context + discord_token = runtime.context.get("discord_token") + discord_intents = runtime.context.get("discord_intents") + discord_channel_id = os.environ.get("DISCORD_CHANNEL_ID") + # Send to both channels - _send_slack(message) - _send_discord(message) + _send_slack(slack_client, message, slack_channel) + _send_discord(message, discord_token, discord_intents, discord_channel_id) return { "success": True, diff --git a/backend/autometabuilder/workflow/plugins/notifications/notifications_discord.py b/backend/autometabuilder/workflow/plugins/notifications/notifications_discord.py index a25220f..10de22f 100644 --- a/backend/autometabuilder/workflow/plugins/notifications/notifications_discord.py +++ b/backend/autometabuilder/workflow/plugins/notifications/notifications_discord.py @@ -1,15 +1,16 @@ """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): +async def _send_discord_notification_async(message: str, token: str, intents, channel_id: str): """Send Discord notification asynchronously.""" - intents = discord.Intents.default() + # Import discord here to avoid loading it at module level + import discord + client = discord.Client(intents=intents) @client.event @@ -33,26 +34,36 @@ def run(runtime, inputs): 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.") + # Get Discord token and intents from runtime context (initialized by backend.create_discord) + token = runtime.context.get("discord_token") + intents = runtime.context.get("discord_intents") + + if not token: + logger.warning("Discord notification skipped: Discord client not initialized.") return { "success": False, "skipped": True, - "error": "DISCORD_BOT_TOKEN or DISCORD_CHANNEL_ID missing" + "error": "Discord client not initialized" + } + + if not channel_id: + logger.warning("Discord notification skipped: DISCORD_CHANNEL_ID missing.") + return { + "success": False, + "skipped": True, + "error": "DISCORD_CHANNEL_ID missing" } try: - asyncio.run(_send_discord_notification_async(message, token, channel_id)) + asyncio.run(_send_discord_notification_async(message, token, intents, channel_id)) return {"success": True, "message": "Discord notification sent"} except Exception as e: logger.error(f"Error running Discord notification: {e}") diff --git a/backend/autometabuilder/workflow/plugins/notifications/notifications_slack.py b/backend/autometabuilder/workflow/plugins/notifications/notifications_slack.py index 9125745..dc3b1d3 100644 --- a/backend/autometabuilder/workflow/plugins/notifications/notifications_slack.py +++ b/backend/autometabuilder/workflow/plugins/notifications/notifications_slack.py @@ -1,8 +1,6 @@ """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") @@ -13,26 +11,36 @@ def run(runtime, inputs): 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.") + # Get Slack client from runtime context (initialized by backend.create_slack) + client = runtime.context.get("slack_client") + + if not client: + logger.warning("Slack notification skipped: Slack client not initialized.") return { "success": False, "skipped": True, - "error": "SLACK_BOT_TOKEN or SLACK_CHANNEL missing" + "error": "Slack client not initialized" + } + + if not channel: + logger.warning("Slack notification skipped: SLACK_CHANNEL missing.") + return { + "success": False, + "skipped": True, + "error": "SLACK_CHANNEL missing" } - client = WebClient(token=token) try: + # Import SlackApiError here to handle errors from the client + from slack_sdk.errors import SlackApiError client.chat_postMessage(channel=channel, text=message) logger.info("Slack notification sent successfully.") return {"success": True, "message": "Slack notification sent"}