265 lines
7.8 KiB
Python
265 lines
7.8 KiB
Python
# --- 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
|
||
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 = slack.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)
|