Files
abot/main.py
2026-01-14 12:11:31 -08:00

265 lines
7.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# --- 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="Im 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)