272 lines
8.1 KiB
Python
272 lines
8.1 KiB
Python
# --- abot.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
|
||
|
||
# --------------------------------------------------
|
||
# 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 qdrant_functions # Vector DB (RAG)
|
||
|
||
# --------------------------------------------------
|
||
# 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("abot.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"}
|
||
|
||
# --------------------------------------------------
|
||
# Tool Imports
|
||
# --------------------------------------------------
|
||
|
||
try:
|
||
import weather_tool
|
||
import user_lookup_tool
|
||
import mtscripter
|
||
import imail_tool
|
||
ALL_TOOLS_IMPORTED = True
|
||
except Exception as e:
|
||
logging.error("Tool import failure", exc_info=True)
|
||
ALL_TOOLS_IMPORTED = False
|
||
|
||
weather_tool = user_lookup_tool = mtscripter = imail_tool = DummyToolModule
|
||
|
||
# --------------------------------------------------
|
||
# Global Tool Registry
|
||
# --------------------------------------------------
|
||
|
||
GLOBAL_TOOL_REGISTRY: Dict[str, Dict[str, Any]] = {
|
||
"get_weather": {
|
||
"definition": getattr(weather_tool, "TOOL_DEFINITION", {}),
|
||
"function": getattr(weather_tool, "get_weather", DummyToolModule.dummy_func),
|
||
},
|
||
"get_user_info": {
|
||
"definition": getattr(user_lookup_tool, "TOOL_DEFINITION", {}),
|
||
"function": getattr(user_lookup_tool, "get_user_info", DummyToolModule.dummy_func),
|
||
},
|
||
"generate_mikrotik_CPE_script": {
|
||
"definition": getattr(mtscripter, "TOOL_DEFINITION", {}),
|
||
"function": getattr(mtscripter, "generate_mikrotik_CPE_script", DummyToolModule.dummy_func),
|
||
},
|
||
"get_imail_password": {
|
||
"definition": getattr(imail_tool, "TOOL_DEFINITION", {}),
|
||
"function": getattr(imail_tool, "get_imail_password", DummyToolModule.dummy_func),
|
||
},
|
||
}
|
||
|
||
logging.info(f"Registered tools: {list(GLOBAL_TOOL_REGISTRY.keys())}")
|
||
|
||
# --------------------------------------------------
|
||
# Bot Profiles
|
||
# --------------------------------------------------
|
||
|
||
import abot_channel_bot
|
||
import techsupport_bot
|
||
import integration_sandbox_bot
|
||
import sales_bot
|
||
import billing_bot
|
||
import wireless_bot
|
||
import abot_scripting_bot
|
||
|
||
CHANNEL_BOT_MAPPING = {
|
||
"C0D7LT3JA": techsupport_bot,
|
||
"C08B9A6RPN1": abot_channel_bot,
|
||
"C03U17ER7": integration_sandbox_bot,
|
||
"C0DQ40MH8": sales_bot,
|
||
"C2RGSA4GL": billing_bot,
|
||
"C0DUFQ4BB": wireless_bot,
|
||
"C09KNPDT481": abot_scripting_bot,
|
||
}
|
||
|
||
logging.info(f"Channel mappings loaded: {list(CHANNEL_BOT_MAPPING.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)
|
||
# --------------------------------------------------
|
||
|
||
profile = CHANNEL_BOT_MAPPING.get(channel)
|
||
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=GLOBAL_TOOL_REGISTRY,
|
||
)
|
||
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)
|