# --- main.py --- import os import sys import logging from pathlib import Path from collections import deque from typing import Dict, Any from dotenv import load_dotenv from flask import Flask, jsonify from slackeventsapi import SlackEventAdapter from slack_sdk import WebClient import slack import json from hot_reload import ReloadableRegistry, start_hot_reload # -------------------------------------------------- # Environment & Config # -------------------------------------------------- load_dotenv(dotenv_path=Path(".") / ".env") from config import BOT_USER_ID, MAX_MESSAGE_LENGTH import slack_functions from slack_event_validation import validate_slack_event import message_processor import conversation_history import rag.qdrant_functions as qdrant_functions # Vector DB (RAG) from tool_loader import load_tools from bot_loader import load_bots # -------------------------------------------------- # Static Values # -------------------------------------------------- BASE_DIR = Path(__file__).parent # -------------------------------------------------- # Watchdog for new bots and tools # -------------------------------------------------- REGISTRY = ReloadableRegistry(BASE_DIR) start_hot_reload(REGISTRY) # -------------------------------------------------- # Channel Mapping (JSON) # -------------------------------------------------- CHANNEL_MAP_FILE = BASE_DIR / "channel_map.json" with open(CHANNEL_MAP_FILE, "r") as f: CHANNEL_ID_TO_BOT = json.load(f) # -------------------------------------------------- # Helper Functions # -------------------------------------------------- def filter_tools_for_bot(tool_registry, bot_profile): allowed = set(getattr(bot_profile, "ENABLED_TOOL_NAMES", [])) return { name: tool for name, tool in tool_registry.items() if name in allowed } # -------------------------------------------------- # Logging # -------------------------------------------------- formatter = logging.Formatter( "%(asctime)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s" ) handler = logging.StreamHandler(sys.stderr) handler.setFormatter(formatter) root_logger = logging.getLogger() root_logger.handlers.clear() root_logger.addHandler(handler) root_logger.setLevel(logging.INFO) logging.getLogger("slack").setLevel(logging.WARNING) logging.info("main.py logging initialized") # -------------------------------------------------- # Dummy Tool (safe fallback) # -------------------------------------------------- class DummyToolModule: TOOL_DEFINITION = { "name": "dummy_tool", "description": "Tool failed to load", "input_schema": {} } @staticmethod def dummy_func(**kwargs): logging.error("Dummy tool invoked", extra={"kwargs": kwargs}) return {"error": "Tool unavailable"} # -------------------------------------------------- # Global Tool Registry # -------------------------------------------------- GLOBAL_TOOL_REGISTRY = load_tools(BASE_DIR / "tools") logging.info(f"Registered tools: {list(GLOBAL_TOOL_REGISTRY.keys())}") # -------------------------------------------------- # Bot Profiles # -------------------------------------------------- BOT_PROFILES = load_bots(BASE_DIR / "bots") logging.info(f"Channel mappings loaded: {list(CHANNEL_ID_TO_BOT.keys())}") # -------------------------------------------------- # Flask + Slack Init # -------------------------------------------------- app = Flask(__name__) SIGNING_SECRET = os.getenv("SIGNING_SECRET") SLACK_TOKEN = os.getenv("SLACK_TOKEN") if not SIGNING_SECRET or not SLACK_TOKEN: sys.exit("Missing Slack credentials") slack_event_adapter = SlackEventAdapter(SIGNING_SECRET, "/slack/events", app) slack_client = WebClient(token=SLACK_TOKEN) # -------------------------------------------------- # Deduplication # -------------------------------------------------- processed_event_ids = deque(maxlen=1000) # -------------------------------------------------- # Slack Message Handler # -------------------------------------------------- @slack_event_adapter.on("message") def handle_message(event_data): if not validate_slack_event(event_data, MAX_MESSAGE_LENGTH): return jsonify({"status": "invalid"}), 400 event = event_data.get("event", {}) event_id = event_data.get("event_id") api_app_id = event_data.get("api_app_id") dedupe_key = f"{api_app_id}-{event_id}" if dedupe_key in processed_event_ids: return jsonify({"status": "duplicate"}), 200 processed_event_ids.append(dedupe_key) channel = event.get("channel") user = event.get("user") text = event.get("text", "") ts = event.get("ts") if not all([channel, user, ts]): return jsonify({"status": "ignored"}), 200 is_bot_message = user == BOT_USER_ID subtype = event.get("subtype") # -------------------------------------------------- # Log message # -------------------------------------------------- if not is_bot_message: try: slack_functions.log_slack_message( slack_client, channel, user, text, ts, BOT_USER_ID ) except Exception: logging.warning("Failed to log message") # -------------------------------------------------- # RAG Insert (profile controlled) # -------------------------------------------------- bot_name = CHANNEL_ID_TO_BOT.get(channel) BOT_PROFILES = REGISTRY.get_bots() GLOBAL_TOOL_REGISTRY = REGISTRY.get_tools() profile = BOT_PROFILES.get(bot_name) enable_insert = getattr(profile, "ENABLE_RAG_INSERT", False) if profile else False if enable_insert and not is_bot_message and not subtype: try: qdrant_functions.embed_and_store_slack_message( slack_client, channel, user, text, ts, BOT_USER_ID ) except Exception: logging.error("RAG insert failed", exc_info=True) # -------------------------------------------------- # File attachments # -------------------------------------------------- if "files" in event and not is_bot_message: try: slack_functions.handle_slack_attachments( slack_client, event, BOT_USER_ID ) except Exception: logging.error("Attachment handling failed") # -------------------------------------------------- # Mention routing # -------------------------------------------------- if f"<@{BOT_USER_ID}>" not in text or is_bot_message: return jsonify({"status": "no_mention"}), 200 if not profile: slack_client.chat_postMessage( channel=channel, text="I’m not configured for this channel." ) return jsonify({"status": "unmapped_channel"}), 200 logging.info( f"Routing mention to profile: {getattr(profile, 'BOT_IDENTIFIER', 'unknown')}" ) try: message_processor.process_mention( event_data=event_data, slack_client=slack_client, vector_store=qdrant_functions, bot_profile=profile, tool_registry=filter_tools_for_bot(GLOBAL_TOOL_REGISTRY, profile), ) return jsonify({"status": "processed"}), 200 except Exception as e: logging.error("process_mention failed", exc_info=True) slack_client.chat_postMessage( channel=channel, text="⚠️ An internal error occurred." ) return jsonify({"status": "error"}), 500 # -------------------------------------------------- # Health Endpoint # -------------------------------------------------- @app.route("/") def index(): return "Slack AI Bot Router running (Qdrant + Local LLM)" # -------------------------------------------------- # Run # -------------------------------------------------- if __name__ == "__main__": port = int(os.getenv("PORT", 5150)) logging.info(f"Starting server on port {port}") app.run(host="0.0.0.0", port=port, debug=False)