multiple-changes-made
This commit is contained in:
12
.env
12
.env
@@ -1,15 +1,15 @@
|
|||||||
#########################
|
#########################
|
||||||
# Slack
|
# Slack
|
||||||
#########################
|
#########################
|
||||||
SLACK_TOKEN=xoxb-...
|
SLACK_TOKEN=xoxb-3919808802802-8695124073830-ukwZtvw0aC7JXAahAzPVJ2f2
|
||||||
SIGNING_SECRET=...
|
SIGNING_SECRET=c99484d93a5aa0308c430e0145d66ceb
|
||||||
|
|
||||||
#########################
|
#########################
|
||||||
# Qdrant (Vector DB)
|
# Qdrant (Vector DB)
|
||||||
#########################
|
#########################
|
||||||
QDRANT_HOST=10.0.0.12
|
QDRANT_HOST=10.10.20.61
|
||||||
QDRANT_PORT=6333
|
QDRANT_PORT=6333
|
||||||
QDRANT_COLLECTION=abot-slack
|
QDRANT_COLLECTION=abot-slack-dev
|
||||||
QDRANT_TIMEOUT=10
|
QDRANT_TIMEOUT=10
|
||||||
|
|
||||||
#########################
|
#########################
|
||||||
@@ -20,8 +20,8 @@ EMBEDDING_MODEL=all-MiniLM-L6-v2
|
|||||||
#########################
|
#########################
|
||||||
# Local LLM (Remote Machine)
|
# Local LLM (Remote Machine)
|
||||||
#########################
|
#########################
|
||||||
LOCAL_LLM_ENDPOINT=http://10.0.0.20:8000/v1/chat/completions
|
LOCAL_LLM_ENDPOINT=https://api.chat.pathcore.org/v1/chat/completions
|
||||||
LOCAL_LLM_MODEL=llama3
|
LOCAL_LLM_MODEL=mistral
|
||||||
LOCAL_LLM_TIMEOUT=60
|
LOCAL_LLM_TIMEOUT=60
|
||||||
|
|
||||||
#########################
|
#########################
|
||||||
|
|||||||
0
__init__.py
Normal file
0
__init__.py
Normal file
BIN
__pycache__/abot.cpython-312.pyc
Normal file
BIN
__pycache__/abot.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/bot_loader.cpython-312.pyc
Normal file
BIN
__pycache__/bot_loader.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/config.cpython-312.pyc
Normal file
BIN
__pycache__/config.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/conversation_history.cpython-312.pyc
Normal file
BIN
__pycache__/conversation_history.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/hot_reload.cpython-312.pyc
Normal file
BIN
__pycache__/hot_reload.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/local_llm_client.cpython-312.pyc
Normal file
BIN
__pycache__/local_llm_client.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/message_processor.cpython-312.pyc
Normal file
BIN
__pycache__/message_processor.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/slack_event_validation.cpython-312.pyc
Normal file
BIN
__pycache__/slack_event_validation.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/slack_functions.cpython-312.pyc
Normal file
BIN
__pycache__/slack_functions.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/tool_loader.cpython-312.pyc
Normal file
BIN
__pycache__/tool_loader.cpython-312.pyc
Normal file
Binary file not shown.
35
bot_loader.py
Normal file
35
bot_loader.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import importlib
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
def load_bots(bots_path: Path):
|
||||||
|
bots = {}
|
||||||
|
|
||||||
|
for file in bots_path.glob("*.py"):
|
||||||
|
if file.name.startswith("_"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip files that are clearly tools, not bot profiles
|
||||||
|
if file.name.endswith("_tool.py"):
|
||||||
|
logging.info(f"Skipping tool file in bots directory: {file.name}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
module_name = f"bots.{file.stem}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
module = importlib.import_module(module_name)
|
||||||
|
|
||||||
|
# Check if BOT_IDENTIFIER exists
|
||||||
|
if not hasattr(module, "BOT_IDENTIFIER"):
|
||||||
|
logging.warning(f"Bot {file.name} missing BOT_IDENTIFIER, skipping")
|
||||||
|
continue
|
||||||
|
|
||||||
|
identifier = module.BOT_IDENTIFIER
|
||||||
|
bots[identifier] = module
|
||||||
|
|
||||||
|
logging.info(f"Loaded bot profile: {identifier}")
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
logging.error(f"Failed to load bot {file.name}", exc_info=True)
|
||||||
|
|
||||||
|
return bots
|
||||||
BIN
bots/__pycache__/abot_channel_bot.cpython-312.pyc
Normal file
BIN
bots/__pycache__/abot_channel_bot.cpython-312.pyc
Normal file
Binary file not shown.
BIN
bots/__pycache__/abot_scripting_bot.cpython-312.pyc
Normal file
BIN
bots/__pycache__/abot_scripting_bot.cpython-312.pyc
Normal file
Binary file not shown.
BIN
bots/__pycache__/billing_bot.cpython-312.pyc
Normal file
BIN
bots/__pycache__/billing_bot.cpython-312.pyc
Normal file
Binary file not shown.
BIN
bots/__pycache__/imail_tool.cpython-312.pyc
Normal file
BIN
bots/__pycache__/imail_tool.cpython-312.pyc
Normal file
Binary file not shown.
BIN
bots/__pycache__/integration_sandbox_bot.cpython-312.pyc
Normal file
BIN
bots/__pycache__/integration_sandbox_bot.cpython-312.pyc
Normal file
Binary file not shown.
BIN
bots/__pycache__/sales_bot.cpython-312.pyc
Normal file
BIN
bots/__pycache__/sales_bot.cpython-312.pyc
Normal file
Binary file not shown.
BIN
bots/__pycache__/techsupport_bot.cpython-312.pyc
Normal file
BIN
bots/__pycache__/techsupport_bot.cpython-312.pyc
Normal file
Binary file not shown.
BIN
bots/__pycache__/wireless_bot.cpython-312.pyc
Normal file
BIN
bots/__pycache__/wireless_bot.cpython-312.pyc
Normal file
Binary file not shown.
9
channel_map.json
Normal file
9
channel_map.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"C0D7LT3JA": "techsupport",
|
||||||
|
"C08B9A6RPN1": "abot_channel_bot",
|
||||||
|
"C03U17ER7": "integration_sandbox_bot",
|
||||||
|
"C0DQ40MH8": "sales",
|
||||||
|
"C2RGSA4GL": "billing",
|
||||||
|
"C0DUFQ4BB": "wireless",
|
||||||
|
"C09KNPDT481": "abot_scripting_bot"
|
||||||
|
}
|
||||||
5
config.py
Normal file
5
config.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
BOT_USER_ID = ""
|
||||||
|
CONVERSATION_TIMEOUT_MINUTES = 15
|
||||||
|
MAX_HISTORY_LENGTH = 10
|
||||||
|
MAX_MESSAGE_LENGTH = 4000
|
||||||
|
WEB_SEARCH_MAX_USES = 5
|
||||||
142
conversation_history.py
Normal file
142
conversation_history.py
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
# --- START OF FILE conversation_history.py ---
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
from collections import defaultdict, deque
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Optional, Dict, Any, List
|
||||||
|
import logging # Added logging
|
||||||
|
# Import global config defaults, channel specific config might override lengths elsewhere
|
||||||
|
from config import BOT_USER_ID, CONVERSATION_TIMEOUT_MINUTES, MAX_HISTORY_LENGTH as GLOBAL_MAX_HISTORY_LENGTH
|
||||||
|
|
||||||
|
# Conversation (messages section of the prompt) history storage
|
||||||
|
# Keyed by channel_id
|
||||||
|
conversation_histories = defaultdict(lambda: deque(maxlen=GLOBAL_MAX_HISTORY_LENGTH)) # Use deque for efficiency
|
||||||
|
|
||||||
|
# Last message times for each channel, for conversation timeout
|
||||||
|
last_message_times = {}
|
||||||
|
|
||||||
|
def update_llm_conversation_history(inputmessage, bot_identifier="unknown", channel_max_length: Optional[int] = None):
|
||||||
|
"""
|
||||||
|
Update the conversation history for a specific channel.
|
||||||
|
Handles both regular text messages and tool-related messages with structured content.
|
||||||
|
Uses global timeout and default max length, but deque handles automatic trimming.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
inputmessage (dict): Message object from the user or LLM.
|
||||||
|
bot_identifier (str): Identifier for debug logging (e.g., "techsupport").
|
||||||
|
"""
|
||||||
|
channel = inputmessage.get('channel')
|
||||||
|
if not channel:
|
||||||
|
logging.error("Attempted to update history with no channel ID.")
|
||||||
|
return None # Cannot proceed without channel
|
||||||
|
|
||||||
|
user = inputmessage.get('user')
|
||||||
|
current_time = datetime.now()
|
||||||
|
|
||||||
|
# Check for timeout and clear history if needed
|
||||||
|
if channel in last_message_times:
|
||||||
|
last_time = last_message_times[channel]
|
||||||
|
if (current_time - last_time) > timedelta(minutes=CONVERSATION_TIMEOUT_MINUTES):
|
||||||
|
logging.info(f"[{bot_identifier}/{channel}] Conversation timed out. Clearing history.")
|
||||||
|
conversation_histories[channel].clear() # Clear the deque
|
||||||
|
|
||||||
|
# Update last message time
|
||||||
|
last_message_times[channel] = current_time
|
||||||
|
|
||||||
|
# Determine the role based on the user ID
|
||||||
|
role = "assistant" if user == BOT_USER_ID else "user"
|
||||||
|
|
||||||
|
new_message = None # Initialize
|
||||||
|
|
||||||
|
# Check message structure to determine format
|
||||||
|
content = inputmessage.get('content')
|
||||||
|
if isinstance(content, list):
|
||||||
|
# This could be a tool_use request from assistant OR tool_result from user simulation
|
||||||
|
# The role should already be correctly determined above based on 'user' field
|
||||||
|
new_message = {
|
||||||
|
"role": role,
|
||||||
|
"content": content # Keep the list structure
|
||||||
|
}
|
||||||
|
elif isinstance(inputmessage.get('text'), str):
|
||||||
|
# Regular text message from user or assistant
|
||||||
|
text = inputmessage.get('text', '')
|
||||||
|
new_message = {
|
||||||
|
"role": role,
|
||||||
|
"content": text
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
logging.warning(f"[{bot_identifier}/{channel}] Unrecognized message format for history: {inputmessage}")
|
||||||
|
# Optionally create a placeholder or skip
|
||||||
|
new_message = {
|
||||||
|
"role": role,
|
||||||
|
"content": "[Unrecognized message format]"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- START NEW TRIM LOGIC ---
|
||||||
|
# Use the specific channel length if provided, otherwise fallback to global default (though ideally it's always provided now)
|
||||||
|
current_maxlen = channel_max_length if channel_max_length is not None else GLOBAL_MAX_HISTORY_LENGTH
|
||||||
|
|
||||||
|
# Ensure deque uses the correct maxlen (it might have been created with the default)
|
||||||
|
# This seems inefficient to do every time, but deque doesn't easily allow changing maxlen.
|
||||||
|
# A potential optimization is to store deques with specific maxlens from the start,
|
||||||
|
# but let's try the simpler approach first.
|
||||||
|
# We'll manually trim *before* appending if needed.
|
||||||
|
while len(conversation_histories[channel]) >= current_maxlen:
|
||||||
|
try:
|
||||||
|
conversation_histories[channel].popleft() # Remove the oldest message
|
||||||
|
logging.debug(f"[{bot_identifier}/{channel}] History limit ({current_maxlen}) reached. Popped oldest message.")
|
||||||
|
except IndexError: # Should not happen with deque, but safety first
|
||||||
|
break
|
||||||
|
# --- END NEW TRIM LOGIC ---
|
||||||
|
|
||||||
|
# Add new message to history deque (automatically handles max length)
|
||||||
|
if new_message:
|
||||||
|
conversation_histories[channel].append(new_message)
|
||||||
|
logging.debug(f"[{bot_identifier}/{channel}] History updated. New length: {len(conversation_histories[channel])}")
|
||||||
|
|
||||||
|
|
||||||
|
# write the updated conversation history to a file for debugging.
|
||||||
|
debug_dir = "debug"
|
||||||
|
os.makedirs(debug_dir, exist_ok=True) # Ensure debug directory exists
|
||||||
|
debug_history_filename = os.path.join(debug_dir, f'conversation_history-{bot_identifier}.txt')
|
||||||
|
try:
|
||||||
|
with open(debug_history_filename, 'w', encoding='utf-8') as f:
|
||||||
|
# Convert deque to list for JSON serialization
|
||||||
|
history_list = list(conversation_histories[channel])
|
||||||
|
f.write(json.dumps(history_list, indent=2))
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"[{bot_identifier}/{channel}] Failed to write debug history file {debug_history_filename}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
# Return the current history as a list
|
||||||
|
return list(conversation_histories[channel])
|
||||||
|
|
||||||
|
def get_conversation_history(channel_id):
|
||||||
|
"""
|
||||||
|
Get the current conversation history for a specific channel as a list.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
channel_id (str): The channel ID to get conversation history for
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: The conversation history for the specified channel (empty list if none)
|
||||||
|
"""
|
||||||
|
return list(conversation_histories.get(channel_id, []))
|
||||||
|
|
||||||
|
def clear_conversation_history(channel_id):
|
||||||
|
"""
|
||||||
|
Clear the conversation history for a specific channel.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
channel_id (str): The channel ID to clear conversation history for
|
||||||
|
"""
|
||||||
|
if channel_id in conversation_histories:
|
||||||
|
conversation_histories[channel_id].clear()
|
||||||
|
logging.info(f"Conversation history cleared for channel {channel_id}")
|
||||||
|
if channel_id in last_message_times:
|
||||||
|
del last_message_times[channel_id] # Reset timeout timer too
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
# --- END OF FILE conversation_history.py ---
|
||||||
57
hot_reload.py
Normal file
57
hot_reload.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import threading
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from watchdog.observers import Observer
|
||||||
|
from watchdog.events import FileSystemEventHandler
|
||||||
|
|
||||||
|
from tool_loader import load_tools
|
||||||
|
from bot_loader import load_bots
|
||||||
|
|
||||||
|
class ReloadableRegistry:
|
||||||
|
def __init__(self, base_dir):
|
||||||
|
self.base_dir = Path(base_dir)
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
|
||||||
|
self.tools = {}
|
||||||
|
self.bots = {}
|
||||||
|
|
||||||
|
self.reload_all()
|
||||||
|
|
||||||
|
def reload_all(self):
|
||||||
|
with self.lock:
|
||||||
|
logging.info("Reloading bots and tools...")
|
||||||
|
self.tools = load_tools(self.base_dir / "tools")
|
||||||
|
self.bots = load_bots(self.base_dir / "bots")
|
||||||
|
|
||||||
|
def get_tools(self):
|
||||||
|
with self.lock:
|
||||||
|
return self.tools.copy()
|
||||||
|
|
||||||
|
def get_bots(self):
|
||||||
|
with self.lock:
|
||||||
|
return self.bots.copy()
|
||||||
|
|
||||||
|
|
||||||
|
class ReloadHandler(FileSystemEventHandler):
|
||||||
|
def __init__(self, registry):
|
||||||
|
self.registry = registry
|
||||||
|
|
||||||
|
def on_any_event(self, event):
|
||||||
|
if event.is_directory:
|
||||||
|
return
|
||||||
|
if event.src_path.endswith(".py"):
|
||||||
|
logging.info(f"Detected change: {event.src_path}")
|
||||||
|
self.registry.reload_all()
|
||||||
|
|
||||||
|
|
||||||
|
def start_hot_reload(registry):
|
||||||
|
observer = Observer()
|
||||||
|
handler = ReloadHandler(registry)
|
||||||
|
|
||||||
|
observer.schedule(handler, str(registry.base_dir / "tools"), recursive=True)
|
||||||
|
observer.schedule(handler, str(registry.base_dir / "bots"), recursive=True)
|
||||||
|
|
||||||
|
observer.daemon = True
|
||||||
|
observer.start()
|
||||||
|
|
||||||
|
logging.info("🔥 Hot reload enabled (tools + bots)")
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# --- abot.py ---
|
# --- main.py ---
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
@@ -10,7 +10,10 @@ from typing import Dict, Any
|
|||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from flask import Flask, jsonify
|
from flask import Flask, jsonify
|
||||||
from slackeventsapi import SlackEventAdapter
|
from slackeventsapi import SlackEventAdapter
|
||||||
import slack
|
#import slack
|
||||||
|
import json
|
||||||
|
from hot_reload import ReloadableRegistry, start_hot_reload
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
# Environment & Config
|
# Environment & Config
|
||||||
@@ -25,7 +28,43 @@ from slack_event_validation import validate_slack_event
|
|||||||
import message_processor
|
import message_processor
|
||||||
import conversation_history
|
import conversation_history
|
||||||
|
|
||||||
import qdrant_functions # Vector DB (RAG)
|
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
|
# Logging
|
||||||
@@ -44,7 +83,7 @@ root_logger.setLevel(logging.INFO)
|
|||||||
|
|
||||||
logging.getLogger("slack").setLevel(logging.WARNING)
|
logging.getLogger("slack").setLevel(logging.WARNING)
|
||||||
|
|
||||||
logging.info("abot.py logging initialized")
|
logging.info("main.py logging initialized")
|
||||||
|
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
# Dummy Tool (safe fallback)
|
# Dummy Tool (safe fallback)
|
||||||
@@ -62,70 +101,19 @@ class DummyToolModule:
|
|||||||
logging.error("Dummy tool invoked", extra={"kwargs": kwargs})
|
logging.error("Dummy tool invoked", extra={"kwargs": kwargs})
|
||||||
return {"error": "Tool unavailable"}
|
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
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
|
|
||||||
GLOBAL_TOOL_REGISTRY: Dict[str, Dict[str, Any]] = {
|
GLOBAL_TOOL_REGISTRY = load_tools(BASE_DIR / "tools")
|
||||||
"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())}")
|
logging.info(f"Registered tools: {list(GLOBAL_TOOL_REGISTRY.keys())}")
|
||||||
|
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
# Bot Profiles
|
# Bot Profiles
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
|
|
||||||
import abot_channel_bot
|
BOT_PROFILES = load_bots(BASE_DIR / "bots")
|
||||||
import techsupport_bot
|
logging.info(f"Channel mappings loaded: {list(CHANNEL_ID_TO_BOT.keys())}")
|
||||||
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
|
# Flask + Slack Init
|
||||||
@@ -194,7 +182,12 @@ def handle_message(event_data):
|
|||||||
# RAG Insert (profile controlled)
|
# RAG Insert (profile controlled)
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
|
|
||||||
profile = CHANNEL_BOT_MAPPING.get(channel)
|
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
|
enable_insert = getattr(profile, "ENABLE_RAG_INSERT", False) if profile else False
|
||||||
|
|
||||||
if enable_insert and not is_bot_message and not subtype:
|
if enable_insert and not is_bot_message and not subtype:
|
||||||
@@ -241,7 +234,7 @@ def handle_message(event_data):
|
|||||||
slack_client=slack_client,
|
slack_client=slack_client,
|
||||||
vector_store=qdrant_functions,
|
vector_store=qdrant_functions,
|
||||||
bot_profile=profile,
|
bot_profile=profile,
|
||||||
tool_registry=GLOBAL_TOOL_REGISTRY,
|
tool_registry=filter_tools_for_bot(GLOBAL_TOOL_REGISTRY, profile),
|
||||||
)
|
)
|
||||||
return jsonify({"status": "processed"}), 200
|
return jsonify({"status": "processed"}), 200
|
||||||
|
|
||||||
0
rag/__init__.py
Normal file
0
rag/__init__.py
Normal file
BIN
rag/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
rag/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
rag/__pycache__/qdrant_functions.cpython-312.pyc
Normal file
BIN
rag/__pycache__/qdrant_functions.cpython-312.pyc
Normal file
Binary file not shown.
@@ -1,8 +1,11 @@
|
|||||||
flask
|
flask
|
||||||
slack-sdk
|
slack-sdk
|
||||||
slackeventsapi
|
slackeventsapi
|
||||||
|
slackfunctions
|
||||||
python-dotenv
|
python-dotenv
|
||||||
|
|
||||||
qdrant-client
|
qdrant-client
|
||||||
sentence-transformers
|
sentence-transformers
|
||||||
requests
|
requests
|
||||||
|
watchdog
|
||||||
|
aiohttp
|
||||||
|
|||||||
71
slack_event_validation.py
Normal file
71
slack_event_validation.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
def validate_slack_event(event_data, max_message_length, valid_event_types=None):
|
||||||
|
"""
|
||||||
|
Validate incoming Slack event to ensure it's a legitimate message.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event_data (dict): The incoming event data from Slack
|
||||||
|
max_message_length (int): Maximum allowed length for messages
|
||||||
|
valid_event_types (list, optional): List of valid event types. Defaults to ['message', 'app_mention', 'app_home_opened', 'event_callback']
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the event is valid, False otherwise
|
||||||
|
"""
|
||||||
|
# Set default valid_event_types if none provided
|
||||||
|
if valid_event_types is None:
|
||||||
|
valid_event_types = ['message', 'app_mention', 'app_home_opened', 'event_callback']
|
||||||
|
|
||||||
|
# Check if event_data is a dictionary
|
||||||
|
if not isinstance(event_data, dict):
|
||||||
|
logging.warning(f"Invalid event: Not a dictionary. Received type: {type(event_data)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check for 'event' key
|
||||||
|
if 'event' not in event_data:
|
||||||
|
logging.warning("Invalid event: Missing 'event' key")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check that event is a dictionary
|
||||||
|
if not isinstance(event_data['event'], dict):
|
||||||
|
logging.warning(f"Invalid event: 'event' is not a dictionary. Received type: {type(event_data['event'])}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check for event ID
|
||||||
|
if 'event_id' not in event_data:
|
||||||
|
logging.warning("Invalid event: Missing 'event_id'")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Validate event type
|
||||||
|
event_type = event_data.get('type')
|
||||||
|
if event_type not in valid_event_types:
|
||||||
|
logging.warning(f"Invalid event type: {event_type}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Basic message validation
|
||||||
|
message = event_data['event']
|
||||||
|
|
||||||
|
# Ensure message has required keys
|
||||||
|
required_keys = ['channel', 'user', 'text', 'ts']
|
||||||
|
for key in required_keys:
|
||||||
|
if key not in message:
|
||||||
|
logging.warning(f"Invalid message: Missing required key '{key}'")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Validate channel and user IDs (basic length and format check)
|
||||||
|
if not (isinstance(message['channel'], str) and len(message['channel']) > 0):
|
||||||
|
logging.warning("Invalid channel ID")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not (isinstance(message['user'], str) and len(message['user']) > 0):
|
||||||
|
logging.warning("Invalid user ID")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check message length to prevent extremely large messages
|
||||||
|
if len(message.get('text', '')) > max_message_length:
|
||||||
|
logging.warning(f"Message exceeds maximum length of {max_message_length} characters")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Additional security checks can be added here
|
||||||
|
|
||||||
|
return True
|
||||||
385
slack_functions.py
Normal file
385
slack_functions.py
Normal file
@@ -0,0 +1,385 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from zoneinfo import ZoneInfo # Import ZoneInfo for proper timezone handling
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# User information cache management
|
||||||
|
def load_user_cache(cache_file='user_cache.json'):
|
||||||
|
"""Load user cache from file"""
|
||||||
|
try:
|
||||||
|
with open(cache_file, 'r') as f:
|
||||||
|
return json.load(f)
|
||||||
|
except (FileNotFoundError, json.JSONDecodeError):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def save_user_cache(users, cache_file='user_cache.json'):
|
||||||
|
"""Save user cache to file"""
|
||||||
|
with open(cache_file, 'w') as f:
|
||||||
|
json.dump(users, f, indent=2)
|
||||||
|
|
||||||
|
def get_slack_user_info(slack_client, user_id, bot_user_id=None, users=None):
|
||||||
|
"""
|
||||||
|
Retrieve comprehensive user information, fetching from Slack if not in cache
|
||||||
|
|
||||||
|
Args:
|
||||||
|
slack_client: The initialized Slack client
|
||||||
|
user_id (str): Slack user ID
|
||||||
|
bot_user_id (str, optional): The bot's user ID
|
||||||
|
users (dict, optional): User cache dictionary
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Formatted user name
|
||||||
|
"""
|
||||||
|
# Initialize users if not provided
|
||||||
|
if users is None:
|
||||||
|
users = load_user_cache()
|
||||||
|
|
||||||
|
# Check if user is in cache and not stale (older than 30 days)
|
||||||
|
if user_id in users:
|
||||||
|
cached_user = users[user_id]
|
||||||
|
cache_time = datetime.fromisoformat(cached_user.get('cached_at', '1970-01-01'))
|
||||||
|
if (datetime.now() - cache_time) < timedelta(days=30):
|
||||||
|
# Return preferred name format
|
||||||
|
return (
|
||||||
|
cached_user.get('real_name') or
|
||||||
|
cached_user.get('display_name') or
|
||||||
|
cached_user.get('name') or
|
||||||
|
user_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fetch user info from Slack
|
||||||
|
try:
|
||||||
|
response = slack_client.users_info(user=user_id)
|
||||||
|
if response['ok']:
|
||||||
|
user_info = response['user']
|
||||||
|
profile = user_info.get('profile', {})
|
||||||
|
|
||||||
|
# Extract comprehensive user details
|
||||||
|
user_details = {
|
||||||
|
'id': user_id,
|
||||||
|
'name': user_info.get('name', ''),
|
||||||
|
'real_name': user_info.get('real_name', ''),
|
||||||
|
'display_name': profile.get('display_name', ''),
|
||||||
|
'email': profile.get('email', ''),
|
||||||
|
'title': profile.get('title', ''),
|
||||||
|
'first_name': profile.get('first_name', ''),
|
||||||
|
'last_name': profile.get('last_name', ''),
|
||||||
|
'cached_at': datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update cache
|
||||||
|
users[user_id] = user_details
|
||||||
|
save_user_cache(users)
|
||||||
|
|
||||||
|
# Return preferred name format
|
||||||
|
return (
|
||||||
|
user_details.get('real_name') or
|
||||||
|
user_details.get('display_name') or
|
||||||
|
user_details.get('name') or
|
||||||
|
user_id
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error fetching user info for {user_id}: {e}")
|
||||||
|
return user_id # Fallback to user ID
|
||||||
|
|
||||||
|
def load_channel_cache(cache_file='channel_cache.json'):
|
||||||
|
"""Load channel cache from file"""
|
||||||
|
try:
|
||||||
|
with open(cache_file, 'r') as f:
|
||||||
|
return json.load(f)
|
||||||
|
except (FileNotFoundError, json.JSONDecodeError):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def save_channel_cache(channels, cache_file='channel_cache.json'):
|
||||||
|
"""Save channel cache to file"""
|
||||||
|
with open(cache_file, 'w') as f:
|
||||||
|
json.dump(channels, f, indent=2)
|
||||||
|
|
||||||
|
def get_slack_channel_info(slack_client, channel_id, bot_user_id=None, channels=None):
|
||||||
|
"""
|
||||||
|
Retrieve channel information, fetching from Slack if not in cache
|
||||||
|
|
||||||
|
Args:
|
||||||
|
slack_client: The initialized Slack client
|
||||||
|
channel_id (str): Slack channel ID
|
||||||
|
bot_user_id (str, optional): The bot's user ID
|
||||||
|
channels (dict, optional): Channel cache dictionary
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Channel name
|
||||||
|
"""
|
||||||
|
# Initialize channels if not provided
|
||||||
|
if channels is None:
|
||||||
|
channels = load_channel_cache()
|
||||||
|
|
||||||
|
# Check if channel is in cache and not stale (older than 30 days)
|
||||||
|
if channel_id in channels:
|
||||||
|
cached_channel = channels[channel_id]
|
||||||
|
cache_time = datetime.fromisoformat(cached_channel.get('cached_at', '1970-01-01'))
|
||||||
|
if (datetime.now() - cache_time) < timedelta(days=30):
|
||||||
|
return cached_channel.get('name', channel_id)
|
||||||
|
|
||||||
|
# Fetch channel info from Slack
|
||||||
|
try:
|
||||||
|
# Different API methods for different channel types
|
||||||
|
if channel_id.startswith('C'): # Public channel
|
||||||
|
response = slack_client.conversations_info(channel=channel_id)
|
||||||
|
elif channel_id.startswith('G'): # Private channel or multi-person DM
|
||||||
|
response = slack_client.conversations_info(channel=channel_id)
|
||||||
|
elif channel_id.startswith('D'): # Direct message
|
||||||
|
# For DMs, we'll use a custom name since there's no channel name
|
||||||
|
user_ids = slack_client.conversations_members(channel=channel_id)
|
||||||
|
if user_ids['ok'] and len(user_ids['members']) == 2:
|
||||||
|
# Get the other user's name (not the bot)
|
||||||
|
other_user_id = [uid for uid in user_ids['members'] if uid != bot_user_id][0]
|
||||||
|
other_user_name = get_slack_user_info(slack_client, other_user_id, bot_user_id)
|
||||||
|
|
||||||
|
channel_details = {
|
||||||
|
'id': channel_id,
|
||||||
|
'name': f"dm-{other_user_name}",
|
||||||
|
'is_dm': True,
|
||||||
|
'cached_at': datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
channels[channel_id] = channel_details
|
||||||
|
save_channel_cache(channels)
|
||||||
|
return channel_details['name']
|
||||||
|
return f"dm-channel"
|
||||||
|
|
||||||
|
if response['ok']:
|
||||||
|
channel_info = response['channel']
|
||||||
|
|
||||||
|
# Extract channel details
|
||||||
|
channel_details = {
|
||||||
|
'id': channel_id,
|
||||||
|
'name': channel_info.get('name', ''),
|
||||||
|
'is_private': channel_info.get('is_private', False),
|
||||||
|
'is_dm': False,
|
||||||
|
'cached_at': datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update cache
|
||||||
|
channels[channel_id] = channel_details
|
||||||
|
save_channel_cache(channels)
|
||||||
|
|
||||||
|
return channel_details['name']
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error fetching channel info for {channel_id}: {e}")
|
||||||
|
|
||||||
|
return channel_id # Fallback to channel ID
|
||||||
|
|
||||||
|
def format_slack_timestamp(ts):
|
||||||
|
"""Convert Slack timestamp to human-readable format in Pacific time"""
|
||||||
|
try:
|
||||||
|
timestamp = float(ts)
|
||||||
|
# Create UTC datetime and then convert to Pacific time
|
||||||
|
dt = datetime.fromtimestamp(timestamp, tz=timezone.utc)
|
||||||
|
# Use ZoneInfo for proper timezone handling, including DST transitions
|
||||||
|
pacific_tz = ZoneInfo("America/Los_Angeles")
|
||||||
|
return dt.astimezone(pacific_tz).strftime("%Y-%m-%d %H:%M:%S %Z")
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return "Unknown Time"
|
||||||
|
|
||||||
|
def log_slack_message(slack_client, channel_id, user_id, text, ts, bot_user_id=None):
|
||||||
|
"""
|
||||||
|
Log Slack messages to files organized by date and channel
|
||||||
|
|
||||||
|
Args:
|
||||||
|
slack_client: The initialized Slack client
|
||||||
|
channel_id (str): Slack channel ID
|
||||||
|
user_id (str): Slack user ID
|
||||||
|
text (str): Message text
|
||||||
|
ts (str): Message timestamp
|
||||||
|
bot_user_id (str, optional): The bot's user ID
|
||||||
|
"""
|
||||||
|
# Create logs directory if it doesn't exist
|
||||||
|
log_dir = Path('./slack-logs')
|
||||||
|
log_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
# Get channel name
|
||||||
|
channel_name = get_slack_channel_info(slack_client, channel_id, bot_user_id)
|
||||||
|
|
||||||
|
# Get user name
|
||||||
|
user_name = get_slack_user_info(slack_client, user_id, bot_user_id)
|
||||||
|
|
||||||
|
# Format the timestamp for filename and log entry, using Pacific time with ZoneInfo
|
||||||
|
timestamp = float(ts)
|
||||||
|
dt = datetime.fromtimestamp(timestamp, tz=timezone.utc)
|
||||||
|
# Use ZoneInfo for America/Los_Angeles timezone (Pacific Time)
|
||||||
|
pacific_tz = ZoneInfo("America/Los_Angeles")
|
||||||
|
pacific_dt = dt.astimezone(pacific_tz)
|
||||||
|
date_str = pacific_dt.strftime("%Y-%m-%d")
|
||||||
|
time_str = pacific_dt.strftime("%H:%M:%S")
|
||||||
|
|
||||||
|
# Create filename with date and channel
|
||||||
|
filename = f"{date_str}-{channel_name}-log.txt"
|
||||||
|
log_path = log_dir / filename
|
||||||
|
|
||||||
|
# Format the log entry
|
||||||
|
log_entry = f"[{date_str} {time_str}] {user_name}: {text}\n"
|
||||||
|
|
||||||
|
# Write to log file
|
||||||
|
with open(log_path, 'a', encoding='utf-8') as f:
|
||||||
|
f.write(log_entry)
|
||||||
|
|
||||||
|
def get_recent_channel_history(channel_id, max_messages=100):
|
||||||
|
"""
|
||||||
|
Read recent messages from the channel log file for the current date
|
||||||
|
|
||||||
|
Args:
|
||||||
|
channel_id (str): Slack channel ID
|
||||||
|
max_messages (int): Maximum number of messages to retrieve
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Formatted string of recent messages
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Get channel name
|
||||||
|
channels = load_channel_cache()
|
||||||
|
channel_name = channels.get(channel_id, {}).get('name', channel_id)
|
||||||
|
|
||||||
|
# Get current date in Pacific time
|
||||||
|
pacific_tz = ZoneInfo("America/Los_Angeles")
|
||||||
|
current_date = datetime.now(pacific_tz).strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
# Construct log file path
|
||||||
|
log_dir = Path('./slack-logs')
|
||||||
|
log_file = f"{current_date}-{channel_name}-log.txt"
|
||||||
|
log_path = log_dir / log_file
|
||||||
|
|
||||||
|
# Check if the log file exists
|
||||||
|
if not log_path.exists():
|
||||||
|
return "No recent channel history available."
|
||||||
|
|
||||||
|
# Read the log file and extract the most recent messages
|
||||||
|
with open(log_path, 'r', encoding='utf-8') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
# Get the last max_messages lines
|
||||||
|
recent_messages = lines[-max_messages:] if len(lines) > max_messages else lines
|
||||||
|
|
||||||
|
# Format the messages as a single string
|
||||||
|
return "".join(recent_messages)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error retrieving channel history: {e}")
|
||||||
|
return "Error retrieving channel history."
|
||||||
|
|
||||||
|
|
||||||
|
def handle_slack_attachments(slack_client, message, bot_user_id=None):
|
||||||
|
"""
|
||||||
|
Process file attachments in Slack messages and save them to appropriate directories
|
||||||
|
|
||||||
|
Args:
|
||||||
|
slack_client: The initialized Slack client
|
||||||
|
message (dict): The Slack message object containing file attachments
|
||||||
|
bot_user_id (str, optional): The bot's user ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if attachments were handled, False otherwise
|
||||||
|
"""
|
||||||
|
# Check if the message contains files
|
||||||
|
if 'files' not in message:
|
||||||
|
return False
|
||||||
|
|
||||||
|
channel_id = message.get('channel')
|
||||||
|
user_id = message.get('user')
|
||||||
|
ts = message.get('ts')
|
||||||
|
|
||||||
|
# Get channel and user info
|
||||||
|
channel_name = get_slack_channel_info(slack_client, channel_id, bot_user_id)
|
||||||
|
user_name = get_slack_user_info(slack_client, user_id, bot_user_id)
|
||||||
|
|
||||||
|
# Process each file in the message
|
||||||
|
for file_data in message['files']:
|
||||||
|
try:
|
||||||
|
file_type = file_data.get('filetype', '').lower()
|
||||||
|
file_mimetype = file_data.get('mimetype', '')
|
||||||
|
file_url = file_data.get('url_private') or file_data.get('url_private_download')
|
||||||
|
file_name = file_data.get('name', 'unknown_file')
|
||||||
|
|
||||||
|
if not file_url:
|
||||||
|
logging.warning(f"No download URL found for file: {file_name}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Determine file category and destination directory
|
||||||
|
if file_mimetype.startswith('audio/') or file_type in ['mp3', 'wav', 'ogg', 'm4a']:
|
||||||
|
save_dir = Path('./slack-audio-files')
|
||||||
|
category = 'audio'
|
||||||
|
elif file_mimetype.startswith('image/') or file_type in ['jpg', 'jpeg', 'png', 'gif']:
|
||||||
|
save_dir = Path('./slack-images')
|
||||||
|
category = 'image'
|
||||||
|
else:
|
||||||
|
save_dir = Path('./slack-other-files')
|
||||||
|
category = 'file'
|
||||||
|
|
||||||
|
# Create the directory if it doesn't exist
|
||||||
|
save_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Format timestamp for filename
|
||||||
|
pacific_tz = ZoneInfo("America/Los_Angeles")
|
||||||
|
timestamp = float(ts)
|
||||||
|
dt = datetime.fromtimestamp(timestamp, tz=pacific_tz)
|
||||||
|
date_str = dt.strftime("%Y-%m-%d")
|
||||||
|
time_str = dt.strftime("%H%M%S")
|
||||||
|
|
||||||
|
# Create a unique filename that matches your logging format
|
||||||
|
# Format: date-channel-user-time-originalfilename.extension
|
||||||
|
file_extension = os.path.splitext(file_name)[1]
|
||||||
|
save_filename = f"{date_str}-{channel_name}-{user_name.replace(' ', '_')}-{time_str}{file_extension}"
|
||||||
|
save_path = save_dir / save_filename
|
||||||
|
|
||||||
|
# Download and save the file
|
||||||
|
download_slack_file(slack_client, file_url, save_path)
|
||||||
|
|
||||||
|
# Log the file save
|
||||||
|
logging.info(f"Saved {category} file from {user_name} in {channel_name}: {save_path}")
|
||||||
|
|
||||||
|
# Also log to the channel log file that a file was shared
|
||||||
|
log_file_message(slack_client, channel_id, user_id, f"[Shared {category} file: {file_name}]", ts, bot_user_id)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error processing file attachment: {e}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def download_slack_file(slack_client, file_url, save_path):
|
||||||
|
"""
|
||||||
|
Download a file from Slack using the private URL
|
||||||
|
|
||||||
|
Args:
|
||||||
|
slack_client: The initialized Slack client (for authentication)
|
||||||
|
file_url (str): The private URL of the file to download
|
||||||
|
save_path (Path): Path where the file should be saved
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if download succeeded, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Get the OAuth token from the slack client for authorization
|
||||||
|
token = slack_client.token
|
||||||
|
|
||||||
|
# Download the file with authorization
|
||||||
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
response = requests.get(file_url, headers=headers, stream=True)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
# Save the file
|
||||||
|
with open(save_path, 'wb') as f:
|
||||||
|
for chunk in response.iter_content(chunk_size=8192):
|
||||||
|
f.write(chunk)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error downloading file from Slack: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def log_file_message(slack_client, channel_id, user_id, text, ts, bot_user_id=None):
|
||||||
|
"""
|
||||||
|
A wrapper around log_slack_message to log file attachments in the same format
|
||||||
|
This uses the existing logging mechanism but with a special message indicating a file was shared
|
||||||
|
"""
|
||||||
|
log_slack_message(slack_client, channel_id, user_id, text, ts, bot_user_id)
|
||||||
40
tool_loader.py
Normal file
40
tool_loader.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import importlib
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
def load_tools(tools_path: Path):
|
||||||
|
registry = {}
|
||||||
|
|
||||||
|
for file in tools_path.glob("*.py"):
|
||||||
|
if file.name.startswith("_"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
module_name = f"tools.{file.stem}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
module = importlib.import_module(module_name)
|
||||||
|
|
||||||
|
# Check if TOOL_DEFINITION exists
|
||||||
|
if not hasattr(module, "TOOL_DEFINITION"):
|
||||||
|
logging.warning(f"Tool {file.name} missing TOOL_DEFINITION, skipping")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check if run function exists
|
||||||
|
if not hasattr(module, "run"):
|
||||||
|
logging.warning(f"Tool {file.name} missing 'run' function, skipping")
|
||||||
|
continue
|
||||||
|
|
||||||
|
name = module.TOOL_DEFINITION["name"]
|
||||||
|
func = getattr(module, "run")
|
||||||
|
|
||||||
|
registry[name] = {
|
||||||
|
"definition": module.TOOL_DEFINITION,
|
||||||
|
"function": func,
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.info(f"Loaded tool: {name}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Failed to load tool {file.name}", exc_info=True)
|
||||||
|
|
||||||
|
return registry
|
||||||
BIN
tools/__pycache__/imail_tool.cpython-312.pyc
Normal file
BIN
tools/__pycache__/imail_tool.cpython-312.pyc
Normal file
Binary file not shown.
BIN
tools/__pycache__/mtscripter.cpython-312.pyc
Normal file
BIN
tools/__pycache__/mtscripter.cpython-312.pyc
Normal file
Binary file not shown.
BIN
tools/__pycache__/user_lookup_tool.cpython-312.pyc
Normal file
BIN
tools/__pycache__/user_lookup_tool.cpython-312.pyc
Normal file
Binary file not shown.
BIN
tools/__pycache__/weather_tool.cpython-312.pyc
Normal file
BIN
tools/__pycache__/weather_tool.cpython-312.pyc
Normal file
Binary file not shown.
Reference in New Issue
Block a user