multiple-changes-made
This commit is contained in:
@@ -1,15 +1,15 @@
|
||||
#########################
|
||||
# Slack
|
||||
#########################
|
||||
SLACK_TOKEN=xoxb-...
|
||||
SIGNING_SECRET=...
|
||||
SLACK_TOKEN=xoxb-3919808802802-8695124073830-ukwZtvw0aC7JXAahAzPVJ2f2
|
||||
SIGNING_SECRET=c99484d93a5aa0308c430e0145d66ceb
|
||||
|
||||
#########################
|
||||
# Qdrant (Vector DB)
|
||||
#########################
|
||||
QDRANT_HOST=10.0.0.12
|
||||
QDRANT_HOST=10.10.20.61
|
||||
QDRANT_PORT=6333
|
||||
QDRANT_COLLECTION=abot-slack
|
||||
QDRANT_COLLECTION=abot-slack-dev
|
||||
QDRANT_TIMEOUT=10
|
||||
|
||||
#########################
|
||||
@@ -20,8 +20,8 @@ EMBEDDING_MODEL=all-MiniLM-L6-v2
|
||||
#########################
|
||||
# Local LLM (Remote Machine)
|
||||
#########################
|
||||
LOCAL_LLM_ENDPOINT=http://10.0.0.20:8000/v1/chat/completions
|
||||
LOCAL_LLM_MODEL=llama3
|
||||
LOCAL_LLM_ENDPOINT=https://api.chat.pathcore.org/v1/chat/completions
|
||||
LOCAL_LLM_MODEL=mistral
|
||||
LOCAL_LLM_TIMEOUT=60
|
||||
|
||||
#########################
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"C0D7LT3JA": "techsupport",
|
||||
"C08B9A6RPN1": "abot_channel_bot",
|
||||
"C03U17ER7": "integration_sandbox_bot",
|
||||
"C0DQ40MH8": "sales",
|
||||
"C2RGSA4GL": "billing",
|
||||
"C0DUFQ4BB": "wireless",
|
||||
"C09KNPDT481": "abot_scripting_bot"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
BOT_USER_ID = ""
|
||||
CONVERSATION_TIMEOUT_MINUTES = 15
|
||||
MAX_HISTORY_LENGTH = 10
|
||||
MAX_MESSAGE_LENGTH = 4000
|
||||
WEB_SEARCH_MAX_USES = 5
|
||||
@@ -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 ---
|
||||
@@ -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)")
|
||||
+53
-60
@@ -1,4 +1,4 @@
|
||||
# --- abot.py ---
|
||||
# --- main.py ---
|
||||
|
||||
import os
|
||||
import sys
|
||||
@@ -10,7 +10,10 @@ from typing import Dict, Any
|
||||
from dotenv import load_dotenv
|
||||
from flask import Flask, jsonify
|
||||
from slackeventsapi import SlackEventAdapter
|
||||
import slack
|
||||
#import slack
|
||||
import json
|
||||
from hot_reload import ReloadableRegistry, start_hot_reload
|
||||
|
||||
|
||||
# --------------------------------------------------
|
||||
# Environment & Config
|
||||
@@ -25,7 +28,43 @@ from slack_event_validation import validate_slack_event
|
||||
import message_processor
|
||||
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
|
||||
@@ -44,7 +83,7 @@ root_logger.setLevel(logging.INFO)
|
||||
|
||||
logging.getLogger("slack").setLevel(logging.WARNING)
|
||||
|
||||
logging.info("abot.py logging initialized")
|
||||
logging.info("main.py logging initialized")
|
||||
|
||||
# --------------------------------------------------
|
||||
# Dummy Tool (safe fallback)
|
||||
@@ -62,70 +101,19 @@ class DummyToolModule:
|
||||
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),
|
||||
},
|
||||
}
|
||||
|
||||
GLOBAL_TOOL_REGISTRY = load_tools(BASE_DIR / "tools")
|
||||
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())}")
|
||||
BOT_PROFILES = load_bots(BASE_DIR / "bots")
|
||||
logging.info(f"Channel mappings loaded: {list(CHANNEL_ID_TO_BOT.keys())}")
|
||||
|
||||
# --------------------------------------------------
|
||||
# Flask + Slack Init
|
||||
@@ -194,7 +182,12 @@ def handle_message(event_data):
|
||||
# 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
|
||||
|
||||
if enable_insert and not is_bot_message and not subtype:
|
||||
@@ -241,7 +234,7 @@ def handle_message(event_data):
|
||||
slack_client=slack_client,
|
||||
vector_store=qdrant_functions,
|
||||
bot_profile=profile,
|
||||
tool_registry=GLOBAL_TOOL_REGISTRY,
|
||||
tool_registry=filter_tools_for_bot(GLOBAL_TOOL_REGISTRY, profile),
|
||||
)
|
||||
return jsonify({"status": "processed"}), 200
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,8 +1,11 @@
|
||||
flask
|
||||
slack-sdk
|
||||
slackeventsapi
|
||||
slackfunctions
|
||||
python-dotenv
|
||||
|
||||
qdrant-client
|
||||
sentence-transformers
|
||||
requests
|
||||
watchdog
|
||||
aiohttp
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user