multiple-changes-made

This commit is contained in:
eweeman
2026-01-14 11:44:13 -08:00
parent 9d31399518
commit 9de6602e0d
39 changed files with 806 additions and 66 deletions

264
main.py Normal file
View File

@@ -0,0 +1,264 @@
# --- 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)