feat: Implement comprehensive non-hierarchical logging system
- Created new logging infrastructure with per-component filtering - Added 6 log levels: DEBUG, INFO, API, WARNING, ERROR, CRITICAL - Implemented non-hierarchical level control (any combination can be enabled) - Migrated 917 print() statements across 31 files to structured logging - Created web UI (system.html) for runtime configuration with dark theme - Added global level controls to enable/disable levels across all components - Added timestamp format control (off/time/date/datetime options) - Implemented log rotation (10MB per file, 5 backups) - Added API endpoints for dynamic log configuration - Configured HTTP request logging with filtering via api.requests component - Intercepted APScheduler logs with proper formatting - Fixed persistence paths to use /app/memory for Docker volume compatibility - Fixed checkbox display bug in web UI (enabled_levels now properly shown) - Changed System Settings button to open in same tab instead of new window Components: bot, api, api.requests, autonomous, persona, vision, llm, conversation, mood, dm, scheduled, gpu, media, server, commands, sentiment, core, apscheduler All settings persist across container restarts via JSON config.
This commit is contained in:
457
bot/api.py
457
bot/api.py
@@ -50,8 +50,25 @@ from utils.figurine_notifier import (
|
||||
send_figurine_dm_to_single_user
|
||||
)
|
||||
from utils.dm_logger import dm_logger
|
||||
from utils.logger import get_logger, list_components, get_component_stats
|
||||
from utils.log_config import (
|
||||
load_config as load_log_config,
|
||||
save_config as save_log_config,
|
||||
update_component,
|
||||
update_global_level,
|
||||
update_timestamp_format,
|
||||
update_api_filters,
|
||||
reset_to_defaults,
|
||||
reload_all_loggers
|
||||
)
|
||||
import time
|
||||
from fnmatch import fnmatch
|
||||
nest_asyncio.apply()
|
||||
|
||||
# Initialize API logger
|
||||
logger = get_logger('api')
|
||||
api_requests_logger = get_logger('api.requests')
|
||||
|
||||
# ========== GPU Selection Helper ==========
|
||||
def get_current_gpu_url():
|
||||
"""Get the URL for the currently selected GPU"""
|
||||
@@ -70,6 +87,58 @@ def get_current_gpu_url():
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# ========== Logging Middleware ==========
|
||||
@app.middleware("http")
|
||||
async def log_requests(request: Request, call_next):
|
||||
"""Middleware to log HTTP requests based on configuration."""
|
||||
start_time = time.time()
|
||||
|
||||
# Get logging config
|
||||
log_config = load_log_config()
|
||||
api_config = log_config.get('components', {}).get('api.requests', {})
|
||||
|
||||
# Check if API request logging is enabled
|
||||
if not api_config.get('enabled', False):
|
||||
return await call_next(request)
|
||||
|
||||
# Get filters
|
||||
filters = api_config.get('filters', {})
|
||||
exclude_paths = filters.get('exclude_paths', [])
|
||||
exclude_status = filters.get('exclude_status', [])
|
||||
include_slow_requests = filters.get('include_slow_requests', True)
|
||||
slow_threshold_ms = filters.get('slow_threshold_ms', 1000)
|
||||
|
||||
# Process request
|
||||
response = await call_next(request)
|
||||
|
||||
# Calculate duration
|
||||
duration_ms = (time.time() - start_time) * 1000
|
||||
|
||||
# Check if path should be excluded
|
||||
path = request.url.path
|
||||
for pattern in exclude_paths:
|
||||
if fnmatch(path, pattern):
|
||||
return response
|
||||
|
||||
# Check if status should be excluded (unless it's a slow request)
|
||||
is_slow = duration_ms >= slow_threshold_ms
|
||||
if response.status_code in exclude_status and not (include_slow_requests and is_slow):
|
||||
return response
|
||||
|
||||
# Log the request
|
||||
log_msg = f"{request.method} {path} - {response.status_code} ({duration_ms:.2f}ms)"
|
||||
|
||||
if is_slow:
|
||||
api_requests_logger.warning(f"SLOW REQUEST: {log_msg}")
|
||||
elif response.status_code >= 500:
|
||||
api_requests_logger.error(log_msg)
|
||||
elif response.status_code >= 400:
|
||||
api_requests_logger.warning(log_msg)
|
||||
else:
|
||||
api_requests_logger.api(log_msg)
|
||||
|
||||
return response
|
||||
|
||||
# Serve static folder
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
|
||||
@@ -354,7 +423,7 @@ def trigger_argument(data: BipolarTriggerRequest):
|
||||
channel_id, message_id, globals.client, data.context
|
||||
)
|
||||
if not success:
|
||||
print(f"⚠️ Failed to trigger argument from message: {error}")
|
||||
logger.error(f"Failed to trigger argument from message: {error}")
|
||||
|
||||
globals.client.loop.create_task(trigger_from_message())
|
||||
|
||||
@@ -419,17 +488,17 @@ def trigger_dialogue(data: dict):
|
||||
continue
|
||||
|
||||
if not message:
|
||||
print(f"⚠️ Message {message_id} not found")
|
||||
logger.error(f"Message {message_id} not found")
|
||||
return
|
||||
|
||||
# Check if there's already an argument or dialogue in progress
|
||||
dialogue_manager = get_dialogue_manager()
|
||||
if dialogue_manager.is_dialogue_active(message.channel.id):
|
||||
print(f"⚠️ Dialogue already active in channel {message.channel.id}")
|
||||
logger.error(f"Dialogue already active in channel {message.channel.id}")
|
||||
return
|
||||
|
||||
if is_argument_in_progress(message.channel.id):
|
||||
print(f"⚠️ Argument already in progress in channel {message.channel.id}")
|
||||
logger.error(f"Argument already in progress in channel {message.channel.id}")
|
||||
return
|
||||
|
||||
# Determine current persona from the message author
|
||||
@@ -441,12 +510,12 @@ def trigger_dialogue(data: dict):
|
||||
current_persona = "evil" if globals.EVIL_MODE else "miku"
|
||||
else:
|
||||
# User message - can't trigger dialogue from user messages
|
||||
print(f"⚠️ Cannot trigger dialogue from user message")
|
||||
logger.error(f"Cannot trigger dialogue from user message")
|
||||
return
|
||||
|
||||
opposite_persona = "evil" if current_persona == "miku" else "miku"
|
||||
|
||||
print(f"🎭 [Manual Trigger] Forcing {opposite_persona} to start dialogue on message {message_id}")
|
||||
logger.info(f"[Manual Trigger] Forcing {opposite_persona} to start dialogue on message {message_id}")
|
||||
|
||||
# Force start the dialogue (bypass interjection check)
|
||||
dialogue_manager.start_dialogue(message.channel.id)
|
||||
@@ -459,7 +528,7 @@ def trigger_dialogue(data: dict):
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error triggering dialogue: {e}")
|
||||
logger.error(f"Error triggering dialogue: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
@@ -514,8 +583,6 @@ def get_gpu_status():
|
||||
@app.post("/gpu-select")
|
||||
async def select_gpu(request: Request):
|
||||
"""Select which GPU to use for inference"""
|
||||
from utils.gpu_preload import preload_amd_models
|
||||
|
||||
data = await request.json()
|
||||
gpu = data.get("gpu", "nvidia").lower()
|
||||
|
||||
@@ -532,16 +599,10 @@ async def select_gpu(request: Request):
|
||||
with open(gpu_state_file, "w") as f:
|
||||
json.dump(state, f, indent=2)
|
||||
|
||||
print(f"🎮 GPU Selection: Switched to {gpu.upper()} GPU")
|
||||
|
||||
# Preload models on AMD GPU (16GB VRAM - can hold both text + vision)
|
||||
if gpu == "amd":
|
||||
asyncio.create_task(preload_amd_models())
|
||||
print("🔧 Preloading text and vision models on AMD GPU...")
|
||||
|
||||
logger.info(f"GPU Selection: Switched to {gpu.upper()} GPU")
|
||||
return {"status": "ok", "message": f"Switched to {gpu.upper()} GPU", "gpu": gpu}
|
||||
except Exception as e:
|
||||
print(f"🎮 GPU Selection Error: {e}")
|
||||
logger.error(f"GPU Selection Error: {e}")
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
@app.get("/bipolar-mode/arguments")
|
||||
@@ -574,17 +635,17 @@ async def set_server_mood_endpoint(guild_id: int, data: MoodSetRequest):
|
||||
|
||||
# Check if server exists
|
||||
if guild_id not in server_manager.servers:
|
||||
print(f"🎭 API: Server {guild_id} not found in server_manager.servers")
|
||||
logger.warning(f"Server {guild_id} not found in server_manager.servers")
|
||||
return {"status": "error", "message": "Server not found"}
|
||||
|
||||
# Check if mood is valid
|
||||
from utils.moods import MOOD_EMOJIS
|
||||
if data.mood not in MOOD_EMOJIS:
|
||||
print(f"🎭 API: Mood '{data.mood}' not found in MOOD_EMOJIS. Available moods: {list(MOOD_EMOJIS.keys())}")
|
||||
logger.warning(f"Mood '{data.mood}' not found in MOOD_EMOJIS. Available moods: {list(MOOD_EMOJIS.keys())}")
|
||||
return {"status": "error", "message": f"Mood '{data.mood}' not recognized. Available moods: {', '.join(MOOD_EMOJIS.keys())}"}
|
||||
|
||||
success = server_manager.set_server_mood(guild_id, data.mood)
|
||||
print(f"🎭 API: Server mood set result: {success}")
|
||||
logger.debug(f"Server mood set result: {success}")
|
||||
|
||||
if success:
|
||||
# V2: Notify autonomous engine of mood change
|
||||
@@ -592,30 +653,30 @@ async def set_server_mood_endpoint(guild_id: int, data: MoodSetRequest):
|
||||
from utils.autonomous import on_mood_change
|
||||
on_mood_change(guild_id, data.mood)
|
||||
except Exception as e:
|
||||
print(f"⚠️ API: Failed to notify autonomous engine of mood change: {e}")
|
||||
logger.error(f"Failed to notify autonomous engine of mood change: {e}")
|
||||
|
||||
# Update the nickname for this server
|
||||
from utils.moods import update_server_nickname
|
||||
print(f"🎭 API: Updating nickname for server {guild_id}")
|
||||
logger.debug(f"Updating nickname for server {guild_id}")
|
||||
globals.client.loop.create_task(update_server_nickname(guild_id))
|
||||
return {"status": "ok", "new_mood": data.mood, "guild_id": guild_id}
|
||||
|
||||
print(f"🎭 API: set_server_mood returned False for unknown reason")
|
||||
logger.warning(f"set_server_mood returned False for unknown reason")
|
||||
return {"status": "error", "message": "Failed to set server mood"}
|
||||
|
||||
@app.post("/servers/{guild_id}/mood/reset")
|
||||
async def reset_server_mood_endpoint(guild_id: int):
|
||||
"""Reset mood to neutral for a specific server"""
|
||||
print(f"🎭 API: Resetting mood for server {guild_id} to neutral")
|
||||
logger.debug(f"Resetting mood for server {guild_id} to neutral")
|
||||
|
||||
# Check if server exists
|
||||
if guild_id not in server_manager.servers:
|
||||
print(f"🎭 API: Server {guild_id} not found in server_manager.servers")
|
||||
logger.warning(f"Server {guild_id} not found in server_manager.servers")
|
||||
return {"status": "error", "message": "Server not found"}
|
||||
|
||||
print(f"🎭 API: Server validation passed, calling set_server_mood")
|
||||
logger.debug(f"Server validation passed, calling set_server_mood")
|
||||
success = server_manager.set_server_mood(guild_id, "neutral")
|
||||
print(f"🎭 API: Server mood reset result: {success}")
|
||||
logger.debug(f"Server mood reset result: {success}")
|
||||
|
||||
if success:
|
||||
# V2: Notify autonomous engine of mood change
|
||||
@@ -623,15 +684,15 @@ async def reset_server_mood_endpoint(guild_id: int):
|
||||
from utils.autonomous import on_mood_change
|
||||
on_mood_change(guild_id, "neutral")
|
||||
except Exception as e:
|
||||
print(f"⚠️ API: Failed to notify autonomous engine of mood reset: {e}")
|
||||
logger.error(f"Failed to notify autonomous engine of mood reset: {e}")
|
||||
|
||||
# Update the nickname for this server
|
||||
from utils.moods import update_server_nickname
|
||||
print(f"🎭 API: Updating nickname for server {guild_id}")
|
||||
logger.debug(f"Updating nickname for server {guild_id}")
|
||||
globals.client.loop.create_task(update_server_nickname(guild_id))
|
||||
return {"status": "ok", "new_mood": "neutral", "guild_id": guild_id}
|
||||
|
||||
print(f"🎭 API: set_server_mood returned False for unknown reason")
|
||||
logger.warning(f"set_server_mood returned False for unknown reason")
|
||||
return {"status": "error", "message": "Failed to reset server mood"}
|
||||
|
||||
@app.get("/servers/{guild_id}/mood/state")
|
||||
@@ -788,22 +849,22 @@ async def trigger_autonomous_reaction(guild_id: int = None):
|
||||
async def trigger_detect_and_join_conversation(guild_id: int = None):
|
||||
# If guild_id is provided, detect and join conversation only for that server
|
||||
# If no guild_id, trigger for all servers
|
||||
print(f"🔍 [API] Join conversation endpoint called with guild_id={guild_id}")
|
||||
logger.debug(f"Join conversation endpoint called with guild_id={guild_id}")
|
||||
if globals.client and globals.client.loop and globals.client.loop.is_running():
|
||||
if guild_id is not None:
|
||||
# Trigger for specific server only (force=True to bypass checks when manually triggered)
|
||||
print(f"🔍 [API] Importing and calling miku_detect_and_join_conversation_for_server({guild_id}, force=True)")
|
||||
logger.debug(f"Importing and calling miku_detect_and_join_conversation_for_server({guild_id}, force=True)")
|
||||
from utils.autonomous import miku_detect_and_join_conversation_for_server
|
||||
globals.client.loop.create_task(miku_detect_and_join_conversation_for_server(guild_id, force=True))
|
||||
return {"status": "ok", "message": f"Detect and join conversation queued for server {guild_id}"}
|
||||
else:
|
||||
# Trigger for all servers (force=True to bypass checks when manually triggered)
|
||||
print(f"🔍 [API] Importing and calling miku_detect_and_join_conversation() for all servers")
|
||||
logger.debug(f"Importing and calling miku_detect_and_join_conversation() for all servers")
|
||||
from utils.autonomous import miku_detect_and_join_conversation
|
||||
globals.client.loop.create_task(miku_detect_and_join_conversation(force=True))
|
||||
return {"status": "ok", "message": "Detect and join conversation queued for all servers"}
|
||||
else:
|
||||
print(f"⚠️ [API] Bot not ready: client={globals.client}, loop={globals.client.loop if globals.client else None}")
|
||||
logger.error(f"Bot not ready: client={globals.client}, loop={globals.client.loop if globals.client else None}")
|
||||
return {"status": "error", "message": "Bot not ready"}
|
||||
|
||||
@app.post("/profile-picture/change")
|
||||
@@ -834,7 +895,7 @@ async def trigger_profile_picture_change(
|
||||
custom_image_bytes = None
|
||||
if file:
|
||||
custom_image_bytes = await file.read()
|
||||
print(f"🖼️ Received custom image upload ({len(custom_image_bytes)} bytes)")
|
||||
logger.info(f"Received custom image upload ({len(custom_image_bytes)} bytes)")
|
||||
|
||||
# Change profile picture
|
||||
result = await profile_picture_manager.change_profile_picture(
|
||||
@@ -858,7 +919,7 @@ async def trigger_profile_picture_change(
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error in profile picture API: {e}")
|
||||
logger.error(f"Error in profile picture API: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return {"status": "error", "message": f"Unexpected error: {str(e)}"}
|
||||
@@ -955,7 +1016,7 @@ async def manual_send(
|
||||
'content': file_content
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to read file {file.filename}: {e}")
|
||||
logger.error(f"Failed to read file {file.filename}: {e}")
|
||||
return {"status": "error", "message": f"Failed to read file {file.filename}: {e}"}
|
||||
|
||||
# Use create_task to avoid timeout context manager error
|
||||
@@ -967,28 +1028,28 @@ async def manual_send(
|
||||
try:
|
||||
reference_message = await channel.fetch_message(int(reply_to_message_id))
|
||||
except Exception as e:
|
||||
print(f"⚠️ Could not fetch message {reply_to_message_id} for reply: {e}")
|
||||
logger.error(f"Could not fetch message {reply_to_message_id} for reply: {e}")
|
||||
return
|
||||
|
||||
# Send the main message
|
||||
if message.strip():
|
||||
if reference_message:
|
||||
await channel.send(message, reference=reference_message, mention_author=mention_author)
|
||||
print(f"✅ Manual message sent as reply to #{channel.name}")
|
||||
logger.info(f"Manual message sent as reply to #{channel.name}")
|
||||
else:
|
||||
await channel.send(message)
|
||||
print(f"✅ Manual message sent to #{channel.name}")
|
||||
logger.info(f"Manual message sent to #{channel.name}")
|
||||
|
||||
# Send files if any
|
||||
for file_info in file_data:
|
||||
try:
|
||||
await channel.send(file=discord.File(io.BytesIO(file_info['content']), filename=file_info['filename']))
|
||||
print(f"✅ File {file_info['filename']} sent to #{channel.name}")
|
||||
logger.info(f"File {file_info['filename']} sent to #{channel.name}")
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to send file {file_info['filename']}: {e}")
|
||||
logger.error(f"Failed to send file {file_info['filename']}: {e}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to send message: {e}")
|
||||
logger.error(f"Failed to send message: {e}")
|
||||
|
||||
globals.client.loop.create_task(send_message_and_files())
|
||||
return {"status": "ok", "message": "Message and files queued for sending"}
|
||||
@@ -1028,7 +1089,7 @@ async def manual_send_webhook(
|
||||
'content': file_content
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to read file {file.filename}: {e}")
|
||||
logger.error(f"Failed to read file {file.filename}: {e}")
|
||||
return {"status": "error", "message": f"Failed to read file {file.filename}: {e}"}
|
||||
|
||||
# Use create_task to avoid timeout context manager error
|
||||
@@ -1037,7 +1098,7 @@ async def manual_send_webhook(
|
||||
# Get or create webhooks for this channel (inside the task)
|
||||
webhooks = await get_or_create_webhooks_for_channel(channel)
|
||||
if not webhooks:
|
||||
print(f"❌ Failed to create webhooks for channel #{channel.name}")
|
||||
logger.error(f"Failed to create webhooks for channel #{channel.name}")
|
||||
return
|
||||
|
||||
# Select the appropriate webhook
|
||||
@@ -1065,10 +1126,10 @@ async def manual_send_webhook(
|
||||
)
|
||||
|
||||
persona_name = "Evil Miku" if persona == "evil" else "Hatsune Miku"
|
||||
print(f"✅ Manual webhook message sent as {persona_name} to #{channel.name}")
|
||||
logger.info(f"Manual webhook message sent as {persona_name} to #{channel.name}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to send webhook message: {e}")
|
||||
logger.error(f"Failed to send webhook message: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
@@ -1196,7 +1257,7 @@ async def delete_figurine_subscriber(user_id: str):
|
||||
async def figurines_send_now(tweet_url: str = Form(None)):
|
||||
"""Trigger immediate figurine DM send to all subscribers, optionally with specific tweet URL"""
|
||||
if globals.client and globals.client.loop and globals.client.loop.is_running():
|
||||
print(f"🚀 API: Sending figurine DMs to all subscribers, tweet_url: {tweet_url}")
|
||||
logger.info(f"Sending figurine DMs to all subscribers, tweet_url: {tweet_url}")
|
||||
globals.client.loop.create_task(send_figurine_dm_to_all_subscribers(globals.client, tweet_url=tweet_url))
|
||||
return {"status": "ok", "message": "Figurine DMs queued"}
|
||||
return {"status": "error", "message": "Bot not ready"}
|
||||
@@ -1205,24 +1266,24 @@ async def figurines_send_now(tweet_url: str = Form(None)):
|
||||
@app.post("/figurines/send_to_user")
|
||||
async def figurines_send_to_user(user_id: str = Form(...), tweet_url: str = Form(None)):
|
||||
"""Send figurine DM to a specific user, optionally with specific tweet URL"""
|
||||
print(f"🎯 API: Received figurine send request - user_id: '{user_id}', tweet_url: '{tweet_url}'")
|
||||
logger.debug(f"Received figurine send request - user_id: '{user_id}', tweet_url: '{tweet_url}'")
|
||||
|
||||
if not globals.client or not globals.client.loop or not globals.client.loop.is_running():
|
||||
print("❌ API: Bot not ready")
|
||||
logger.error("Bot not ready")
|
||||
return {"status": "error", "message": "Bot not ready"}
|
||||
|
||||
try:
|
||||
user_id_int = int(user_id)
|
||||
print(f"✅ API: Parsed user_id as {user_id_int}")
|
||||
logger.debug(f"Parsed user_id as {user_id_int}")
|
||||
except ValueError:
|
||||
print(f"❌ API: Invalid user ID: '{user_id}'")
|
||||
logger.error(f"Invalid user ID: '{user_id}'")
|
||||
return {"status": "error", "message": "Invalid user ID"}
|
||||
|
||||
# Clean up tweet URL if it's empty string
|
||||
if tweet_url == "":
|
||||
tweet_url = None
|
||||
|
||||
print(f"🎯 API: Sending figurine DM to user {user_id_int}, tweet_url: {tweet_url}")
|
||||
logger.info(f"Sending figurine DM to user {user_id_int}, tweet_url: {tweet_url}")
|
||||
|
||||
# Queue the DM send task in the bot's event loop
|
||||
globals.client.loop.create_task(send_figurine_dm_to_single_user(globals.client, user_id_int, tweet_url=tweet_url))
|
||||
@@ -1233,22 +1294,22 @@ async def figurines_send_to_user(user_id: str = Form(...), tweet_url: str = Form
|
||||
@app.get("/servers")
|
||||
def get_servers():
|
||||
"""Get all configured servers"""
|
||||
print(f"🎭 API: /servers endpoint called")
|
||||
print(f"🎭 API: server_manager.servers keys: {list(server_manager.servers.keys())}")
|
||||
print(f"🎭 API: server_manager.servers count: {len(server_manager.servers)}")
|
||||
logger.debug("/servers endpoint called")
|
||||
logger.debug(f"server_manager.servers keys: {list(server_manager.servers.keys())}")
|
||||
logger.debug(f"server_manager.servers count: {len(server_manager.servers)}")
|
||||
|
||||
# Debug: Check config file directly
|
||||
config_file = server_manager.config_file
|
||||
print(f"🎭 API: Config file path: {config_file}")
|
||||
logger.debug(f"Config file path: {config_file}")
|
||||
if os.path.exists(config_file):
|
||||
try:
|
||||
with open(config_file, "r", encoding="utf-8") as f:
|
||||
config_data = json.load(f)
|
||||
print(f"🎭 API: Config file contains: {list(config_data.keys())}")
|
||||
logger.debug(f"Config file contains: {list(config_data.keys())}")
|
||||
except Exception as e:
|
||||
print(f"🎭 API: Failed to read config file: {e}")
|
||||
logger.error(f"Failed to read config file: {e}")
|
||||
else:
|
||||
print(f"🎭 API: Config file does not exist")
|
||||
logger.warning("Config file does not exist")
|
||||
|
||||
servers = []
|
||||
for server in server_manager.get_all_servers():
|
||||
@@ -1260,10 +1321,10 @@ def get_servers():
|
||||
server_data['guild_id'] = str(server_data['guild_id'])
|
||||
|
||||
servers.append(server_data)
|
||||
print(f"🎭 API: Adding server to response: {server_data['guild_id']} - {server_data['guild_name']}")
|
||||
print(f"🎭 API: Server data type check - guild_id: {type(server_data['guild_id'])}, value: {server_data['guild_id']}")
|
||||
logger.debug(f"Adding server to response: {server_data['guild_id']} - {server_data['guild_name']}")
|
||||
logger.debug(f"Server data type check - guild_id: {type(server_data['guild_id'])}, value: {server_data['guild_id']}")
|
||||
|
||||
print(f"🎭 API: Returning {len(servers)} servers")
|
||||
logger.debug(f"Returning {len(servers)} servers")
|
||||
|
||||
# Debug: Show exact JSON being sent
|
||||
import json
|
||||
@@ -1316,7 +1377,7 @@ def update_server(guild_id: int, data: dict):
|
||||
@app.post("/servers/{guild_id}/bedtime-range")
|
||||
def update_server_bedtime_range(guild_id: int, data: dict):
|
||||
"""Update server bedtime range configuration"""
|
||||
print(f"⏰ API: Updating bedtime range for server {guild_id}: {data}")
|
||||
logger.debug(f"Updating bedtime range for server {guild_id}: {data}")
|
||||
|
||||
# Validate the data
|
||||
required_fields = ['bedtime_hour', 'bedtime_minute', 'bedtime_hour_end', 'bedtime_minute_end']
|
||||
@@ -1346,7 +1407,7 @@ def update_server_bedtime_range(guild_id: int, data: dict):
|
||||
# Update just the bedtime job for this server (avoid restarting all schedulers)
|
||||
job_success = server_manager.update_server_bedtime_job(guild_id, globals.client)
|
||||
if job_success:
|
||||
print(f"✅ API: Bedtime range updated for server {guild_id}")
|
||||
logger.info(f"Bedtime range updated for server {guild_id}")
|
||||
return {
|
||||
"status": "ok",
|
||||
"message": f"Bedtime range updated: {bedtime_hour:02d}:{bedtime_minute:02d} - {bedtime_hour_end:02d}:{bedtime_minute_end:02d}"
|
||||
@@ -1413,14 +1474,14 @@ async def send_custom_prompt_dm(user_id: str, req: CustomPromptRequest):
|
||||
try:
|
||||
response = await query_llama(req.prompt, user_id=user_id, guild_id=None, response_type="dm_response")
|
||||
await user.send(response)
|
||||
print(f"✅ Custom DM prompt sent to user {user_id}: {req.prompt[:50]}...")
|
||||
logger.info(f"Custom DM prompt sent to user {user_id}: {req.prompt[:50]}...")
|
||||
|
||||
# Log to DM history
|
||||
from utils.dm_logger import dm_logger
|
||||
dm_logger.log_conversation(user_id, req.prompt, response)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to send custom DM prompt to user {user_id}: {e}")
|
||||
logger.error(f"Failed to send custom DM prompt to user {user_id}: {e}")
|
||||
|
||||
# Use create_task to avoid timeout context manager error
|
||||
globals.client.loop.create_task(send_dm_custom_prompt())
|
||||
@@ -1456,7 +1517,7 @@ async def send_manual_message_dm(
|
||||
'content': file_content
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to read file {file.filename}: {e}")
|
||||
logger.error(f"Failed to read file {file.filename}: {e}")
|
||||
return {"status": "error", "message": f"Failed to read file {file.filename}: {e}"}
|
||||
|
||||
async def send_dm_message_and_files():
|
||||
@@ -1468,32 +1529,32 @@ async def send_manual_message_dm(
|
||||
dm_channel = user.dm_channel or await user.create_dm()
|
||||
reference_message = await dm_channel.fetch_message(int(reply_to_message_id))
|
||||
except Exception as e:
|
||||
print(f"⚠️ Could not fetch DM message {reply_to_message_id} for reply: {e}")
|
||||
logger.error(f"Could not fetch DM message {reply_to_message_id} for reply: {e}")
|
||||
return
|
||||
|
||||
# Send the main message
|
||||
if message.strip():
|
||||
if reference_message:
|
||||
await user.send(message, reference=reference_message, mention_author=mention_author)
|
||||
print(f"✅ Manual DM reply message sent to user {user_id}")
|
||||
logger.info(f"Manual DM reply message sent to user {user_id}")
|
||||
else:
|
||||
await user.send(message)
|
||||
print(f"✅ Manual DM message sent to user {user_id}")
|
||||
logger.info(f"Manual DM message sent to user {user_id}")
|
||||
|
||||
# Send files if any
|
||||
for file_info in file_data:
|
||||
try:
|
||||
await user.send(file=discord.File(io.BytesIO(file_info['content']), filename=file_info['filename']))
|
||||
print(f"✅ File {file_info['filename']} sent via DM to user {user_id}")
|
||||
logger.info(f"File {file_info['filename']} sent via DM to user {user_id}")
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to send file {file_info['filename']} via DM: {e}")
|
||||
logger.error(f"Failed to send file {file_info['filename']} via DM: {e}")
|
||||
|
||||
# Log to DM history (user message = manual override trigger, miku response = the message sent)
|
||||
from utils.dm_logger import dm_logger
|
||||
dm_logger.log_conversation(user_id, "[Manual Override Trigger]", message, attachments=[f['filename'] for f in file_data])
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to send manual DM to user {user_id}: {e}")
|
||||
logger.error(f"Failed to send manual DM to user {user_id}: {e}")
|
||||
|
||||
# Use create_task to avoid timeout context manager error
|
||||
globals.client.loop.create_task(send_dm_message_and_files())
|
||||
@@ -1559,7 +1620,7 @@ async def test_image_detection(req: dict):
|
||||
async def view_generated_image(filename: str):
|
||||
"""Serve generated images from ComfyUI output directory"""
|
||||
try:
|
||||
print(f"🖼️ Image view request for: {filename}")
|
||||
logger.debug(f"Image view request for: {filename}")
|
||||
|
||||
# Try multiple possible paths for ComfyUI output
|
||||
possible_paths = [
|
||||
@@ -1572,13 +1633,13 @@ async def view_generated_image(filename: str):
|
||||
for path in possible_paths:
|
||||
if os.path.exists(path):
|
||||
image_path = path
|
||||
print(f"✅ Found image at: {path}")
|
||||
logger.debug(f"Found image at: {path}")
|
||||
break
|
||||
else:
|
||||
print(f"❌ Not found at: {path}")
|
||||
logger.debug(f"Not found at: {path}")
|
||||
|
||||
if not image_path:
|
||||
print(f"❌ Image not found anywhere: {filename}")
|
||||
logger.warning(f"Image not found anywhere: {filename}")
|
||||
return {"status": "error", "message": f"Image not found: {filename}"}
|
||||
|
||||
# Determine content type based on file extension
|
||||
@@ -1591,11 +1652,11 @@ async def view_generated_image(filename: str):
|
||||
elif ext == "webp":
|
||||
content_type = "image/webp"
|
||||
|
||||
print(f"📤 Serving image: {image_path} as {content_type}")
|
||||
logger.info(f"Serving image: {image_path} as {content_type}")
|
||||
return FileResponse(image_path, media_type=content_type)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error serving image: {e}")
|
||||
logger.error(f"Error serving image: {e}")
|
||||
return {"status": "error", "message": f"Error serving image: {e}"}
|
||||
|
||||
@app.post("/servers/{guild_id}/autonomous/tweet")
|
||||
@@ -1638,36 +1699,36 @@ def get_available_moods():
|
||||
@app.post("/test/mood/{guild_id}")
|
||||
async def test_mood_change(guild_id: int, data: MoodSetRequest):
|
||||
"""Test endpoint for debugging mood changes"""
|
||||
print(f"🧪 TEST: Testing mood change for server {guild_id} to {data.mood}")
|
||||
logger.debug(f"TEST: Testing mood change for server {guild_id} to {data.mood}")
|
||||
|
||||
# Check if server exists
|
||||
if guild_id not in server_manager.servers:
|
||||
return {"status": "error", "message": f"Server {guild_id} not found"}
|
||||
|
||||
server_config = server_manager.get_server_config(guild_id)
|
||||
print(f"🧪 TEST: Server config found: {server_config.guild_name if server_config else 'None'}")
|
||||
logger.debug(f"TEST: Server config found: {server_config.guild_name if server_config else 'None'}")
|
||||
|
||||
# Try to set mood
|
||||
success = server_manager.set_server_mood(guild_id, data.mood)
|
||||
print(f"🧪 TEST: Mood set result: {success}")
|
||||
logger.debug(f"TEST: Mood set result: {success}")
|
||||
|
||||
if success:
|
||||
# V2: Notify autonomous engine of mood change
|
||||
try:
|
||||
from utils.autonomous import on_mood_change
|
||||
on_mood_change(guild_id, data.mood)
|
||||
print(f"🧪 TEST: Notified autonomous engine of mood change")
|
||||
logger.debug(f"TEST: Notified autonomous engine of mood change")
|
||||
except Exception as e:
|
||||
print(f"⚠️ TEST: Failed to notify autonomous engine: {e}")
|
||||
logger.error(f"TEST: Failed to notify autonomous engine: {e}")
|
||||
|
||||
# Try to update nickname
|
||||
from utils.moods import update_server_nickname
|
||||
print(f"🧪 TEST: Attempting nickname update...")
|
||||
logger.debug(f"TEST: Attempting nickname update...")
|
||||
try:
|
||||
await update_server_nickname(guild_id)
|
||||
print(f"🧪 TEST: Nickname update completed")
|
||||
logger.debug(f"TEST: Nickname update completed")
|
||||
except Exception as e:
|
||||
print(f"🧪 TEST: Nickname update failed: {e}")
|
||||
logger.error(f"TEST: Nickname update failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
@@ -1707,10 +1768,10 @@ def get_dm_conversations(user_id: str, limit: int = 50):
|
||||
from utils.dm_logger import dm_logger
|
||||
# Convert string user_id to int for internal processing
|
||||
user_id_int = int(user_id)
|
||||
print(f"🔍 API: Loading conversations for user {user_id_int}, limit: {limit}")
|
||||
logger.debug(f"Loading conversations for user {user_id_int}, limit: {limit}")
|
||||
|
||||
logs = dm_logger._load_user_logs(user_id_int)
|
||||
print(f"🔍 API: Loaded logs for user {user_id_int}: {len(logs.get('conversations', []))} conversations")
|
||||
logger.debug(f"Loaded logs for user {user_id_int}: {len(logs.get('conversations', []))} conversations")
|
||||
|
||||
conversations = logs["conversations"][-limit:] if limit > 0 else logs["conversations"]
|
||||
|
||||
@@ -1719,20 +1780,20 @@ def get_dm_conversations(user_id: str, limit: int = 50):
|
||||
if "message_id" in conv:
|
||||
conv["message_id"] = str(conv["message_id"])
|
||||
|
||||
print(f"🔍 API: Returning {len(conversations)} conversations")
|
||||
logger.debug(f"Returning {len(conversations)} conversations")
|
||||
|
||||
# Debug: Show message IDs being returned
|
||||
for i, conv in enumerate(conversations):
|
||||
msg_id = conv.get("message_id", "")
|
||||
is_bot = conv.get("is_bot_message", False)
|
||||
content_preview = conv.get("content", "")[:30] + "..." if conv.get("content", "") else "[No content]"
|
||||
print(f"🔍 API: Conv {i}: id={msg_id} (type: {type(msg_id)}), is_bot={is_bot}, content='{content_preview}'")
|
||||
logger.debug(f"Conv {i}: id={msg_id} (type: {type(msg_id)}), is_bot={is_bot}, content='{content_preview}'")
|
||||
|
||||
return {"status": "ok", "conversations": conversations}
|
||||
except ValueError:
|
||||
return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
|
||||
except Exception as e:
|
||||
print(f"❌ API: Failed to get conversations for user {user_id}: {e}")
|
||||
logger.error(f"Failed to get conversations for user {user_id}: {e}")
|
||||
return {"status": "error", "message": f"Failed to get conversations: {e}"}
|
||||
|
||||
@app.get("/dms/users/{user_id}/search")
|
||||
@@ -1792,7 +1853,7 @@ def get_blocked_users():
|
||||
blocked_users = dm_logger.get_blocked_users()
|
||||
return {"status": "ok", "blocked_users": blocked_users}
|
||||
except Exception as e:
|
||||
print(f"❌ API: Failed to get blocked users: {e}")
|
||||
logger.error(f"Failed to get blocked users: {e}")
|
||||
return {"status": "error", "message": f"Failed to get blocked users: {e}"}
|
||||
|
||||
@app.post("/dms/users/{user_id}/block")
|
||||
@@ -1808,7 +1869,7 @@ def block_user(user_id: str):
|
||||
success = dm_logger.block_user(user_id_int, username)
|
||||
|
||||
if success:
|
||||
print(f"🚫 API: User {user_id} ({username}) blocked")
|
||||
logger.info(f"User {user_id} ({username}) blocked")
|
||||
return {"status": "ok", "message": f"User {username} has been blocked"}
|
||||
else:
|
||||
return {"status": "error", "message": f"User {username} is already blocked"}
|
||||
@@ -1816,7 +1877,7 @@ def block_user(user_id: str):
|
||||
except ValueError:
|
||||
return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
|
||||
except Exception as e:
|
||||
print(f"❌ API: Failed to block user {user_id}: {e}")
|
||||
logger.error(f"Failed to block user {user_id}: {e}")
|
||||
return {"status": "error", "message": f"Failed to block user: {e}"}
|
||||
|
||||
@app.post("/dms/users/{user_id}/unblock")
|
||||
@@ -1827,7 +1888,7 @@ def unblock_user(user_id: str):
|
||||
success = dm_logger.unblock_user(user_id_int)
|
||||
|
||||
if success:
|
||||
print(f"✅ API: User {user_id} unblocked")
|
||||
logger.info(f"User {user_id} unblocked")
|
||||
return {"status": "ok", "message": f"User has been unblocked"}
|
||||
else:
|
||||
return {"status": "error", "message": f"User is not blocked"}
|
||||
@@ -1835,7 +1896,7 @@ def unblock_user(user_id: str):
|
||||
except ValueError:
|
||||
return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
|
||||
except Exception as e:
|
||||
print(f"❌ API: Failed to unblock user {user_id}: {e}")
|
||||
logger.error(f"Failed to unblock user {user_id}: {e}")
|
||||
return {"status": "error", "message": f"Failed to unblock user: {e}"}
|
||||
|
||||
@app.post("/dms/users/{user_id}/conversations/{conversation_id}/delete")
|
||||
@@ -1853,13 +1914,13 @@ def delete_conversation(user_id: str, conversation_id: str):
|
||||
|
||||
# For now, return success immediately since we can't await in FastAPI sync endpoint
|
||||
# The actual deletion happens asynchronously
|
||||
print(f"🗑️ API: Queued deletion of conversation {conversation_id} for user {user_id}")
|
||||
logger.info(f"Queued deletion of conversation {conversation_id} for user {user_id}")
|
||||
return {"status": "ok", "message": "Message deletion queued (will delete from both Discord and logs)"}
|
||||
|
||||
except ValueError:
|
||||
return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
|
||||
except Exception as e:
|
||||
print(f"❌ API: Failed to queue conversation deletion {conversation_id}: {e}")
|
||||
logger.error(f"Failed to queue conversation deletion {conversation_id}: {e}")
|
||||
return {"status": "error", "message": f"Failed to delete conversation: {e}"}
|
||||
|
||||
@app.post("/dms/users/{user_id}/conversations/delete-all")
|
||||
@@ -1876,13 +1937,13 @@ def delete_all_conversations(user_id: str):
|
||||
success = globals.client.loop.create_task(do_delete_all())
|
||||
|
||||
# Return success immediately since we can't await in FastAPI sync endpoint
|
||||
print(f"🗑️ API: Queued bulk deletion of all conversations for user {user_id}")
|
||||
logger.info(f"Queued bulk deletion of all conversations for user {user_id}")
|
||||
return {"status": "ok", "message": "Bulk deletion queued (will delete all Miku messages from Discord and clear logs)"}
|
||||
|
||||
except ValueError:
|
||||
return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
|
||||
except Exception as e:
|
||||
print(f"❌ API: Failed to queue bulk conversation deletion for user {user_id}: {e}")
|
||||
logger.error(f"Failed to queue bulk conversation deletion for user {user_id}: {e}")
|
||||
return {"status": "error", "message": f"Failed to delete conversations: {e}"}
|
||||
|
||||
@app.post("/dms/users/{user_id}/delete-completely")
|
||||
@@ -1893,7 +1954,7 @@ def delete_user_completely(user_id: str):
|
||||
success = dm_logger.delete_user_completely(user_id_int)
|
||||
|
||||
if success:
|
||||
print(f"🗑️ API: Completely deleted user {user_id}")
|
||||
logger.info(f"Completely deleted user {user_id}")
|
||||
return {"status": "ok", "message": "User data deleted completely"}
|
||||
else:
|
||||
return {"status": "error", "message": "No user data found"}
|
||||
@@ -1901,7 +1962,7 @@ def delete_user_completely(user_id: str):
|
||||
except ValueError:
|
||||
return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
|
||||
except Exception as e:
|
||||
print(f"❌ API: Failed to completely delete user {user_id}: {e}")
|
||||
logger.error(f"Failed to completely delete user {user_id}: {e}")
|
||||
return {"status": "error", "message": f"Failed to delete user: {e}"}
|
||||
|
||||
# ========== DM Interaction Analysis Endpoints ==========
|
||||
@@ -1923,7 +1984,7 @@ def run_dm_analysis():
|
||||
|
||||
return {"status": "ok", "message": "DM analysis started"}
|
||||
except Exception as e:
|
||||
print(f"❌ API: Failed to run DM analysis: {e}")
|
||||
logger.error(f"Failed to run DM analysis: {e}")
|
||||
return {"status": "error", "message": f"Failed to run DM analysis: {e}"}
|
||||
|
||||
@app.post("/dms/users/{user_id}/analyze")
|
||||
@@ -1949,7 +2010,7 @@ def analyze_user_interaction(user_id: str):
|
||||
except ValueError:
|
||||
return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
|
||||
except Exception as e:
|
||||
print(f"❌ API: Failed to analyze user {user_id}: {e}")
|
||||
logger.error(f"Failed to analyze user {user_id}: {e}")
|
||||
return {"status": "error", "message": f"Failed to analyze user: {e}"}
|
||||
|
||||
@app.get("/dms/analysis/reports")
|
||||
@@ -1974,11 +2035,11 @@ def get_analysis_reports(limit: int = 20):
|
||||
report['filename'] = filename
|
||||
reports.append(report)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to load report {filename}: {e}")
|
||||
logger.warning(f"Failed to load report {filename}: {e}")
|
||||
|
||||
return {"status": "ok", "reports": reports}
|
||||
except Exception as e:
|
||||
print(f"❌ API: Failed to get reports: {e}")
|
||||
logger.error(f"Failed to get reports: {e}")
|
||||
return {"status": "error", "message": f"Failed to get reports: {e}"}
|
||||
|
||||
@app.get("/dms/analysis/reports/{user_id}")
|
||||
@@ -2005,13 +2066,13 @@ def get_user_reports(user_id: str, limit: int = 10):
|
||||
report['filename'] = filename
|
||||
reports.append(report)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to load report {filename}: {e}")
|
||||
logger.warning(f"Failed to load report {filename}: {e}")
|
||||
|
||||
return {"status": "ok", "reports": reports}
|
||||
except ValueError:
|
||||
return {"status": "error", "message": f"Invalid user ID format: {user_id}"}
|
||||
except Exception as e:
|
||||
print(f"❌ API: Failed to get user reports: {e}")
|
||||
logger.error(f"Failed to get user reports: {e}")
|
||||
return {"status": "error", "message": f"Failed to get user reports: {e}"}
|
||||
|
||||
# ========== Message Reaction Endpoint ==========
|
||||
@@ -2043,15 +2104,15 @@ async def add_reaction_to_message(
|
||||
try:
|
||||
message = await channel.fetch_message(msg_id)
|
||||
await message.add_reaction(emoji)
|
||||
print(f"✅ Added reaction {emoji} to message {msg_id} in channel #{channel.name}")
|
||||
logger.info(f"Added reaction {emoji} to message {msg_id} in channel #{channel.name}")
|
||||
except discord.NotFound:
|
||||
print(f"❌ Message {msg_id} not found in channel #{channel.name}")
|
||||
logger.error(f"Message {msg_id} not found in channel #{channel.name}")
|
||||
except discord.Forbidden:
|
||||
print(f"❌ Bot doesn't have permission to add reactions in channel #{channel.name}")
|
||||
logger.error(f"Bot doesn't have permission to add reactions in channel #{channel.name}")
|
||||
except discord.HTTPException as e:
|
||||
print(f"❌ Failed to add reaction: {e}")
|
||||
logger.error(f"Failed to add reaction: {e}")
|
||||
except Exception as e:
|
||||
print(f"❌ Unexpected error adding reaction: {e}")
|
||||
logger.error(f"Unexpected error adding reaction: {e}")
|
||||
|
||||
globals.client.loop.create_task(add_reaction_task())
|
||||
|
||||
@@ -2061,7 +2122,7 @@ async def add_reaction_to_message(
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ API: Failed to add reaction: {e}")
|
||||
logger.error(f"Failed to add reaction: {e}")
|
||||
return {"status": "error", "message": f"Failed to add reaction: {e}"}
|
||||
|
||||
# ========== Autonomous V2 Endpoints ==========
|
||||
@@ -2290,7 +2351,7 @@ Be detailed but conversational. React to what you see with Miku's cheerful, play
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error in chat stream: {str(e)}"
|
||||
print(f"❌ {error_msg}")
|
||||
logger.error(error_msg)
|
||||
yield f"data: {json.dumps({'error': error_msg})}\n\n"
|
||||
|
||||
return StreamingResponse(
|
||||
@@ -2303,6 +2364,168 @@ Be detailed but conversational. React to what you see with Miku's cheerful, play
|
||||
}
|
||||
)
|
||||
|
||||
# ========== Log Management API ==========
|
||||
class LogConfigUpdateRequest(BaseModel):
|
||||
component: Optional[str] = None
|
||||
enabled: Optional[bool] = None
|
||||
enabled_levels: Optional[List[str]] = None
|
||||
|
||||
class LogFilterUpdateRequest(BaseModel):
|
||||
exclude_paths: Optional[List[str]] = None
|
||||
exclude_status: Optional[List[int]] = None
|
||||
include_slow_requests: Optional[bool] = None
|
||||
slow_threshold_ms: Optional[int] = None
|
||||
|
||||
@app.get("/api/log/config")
|
||||
async def get_log_config():
|
||||
"""Get current logging configuration."""
|
||||
try:
|
||||
config = load_log_config()
|
||||
logger.debug("Log config requested")
|
||||
return {"success": True, "config": config}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get log config: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
@app.post("/api/log/config")
|
||||
async def update_log_config(request: LogConfigUpdateRequest):
|
||||
"""Update logging configuration."""
|
||||
try:
|
||||
if request.component:
|
||||
success = update_component(
|
||||
request.component,
|
||||
enabled=request.enabled,
|
||||
enabled_levels=request.enabled_levels
|
||||
)
|
||||
if not success:
|
||||
return {"success": False, "error": f"Failed to update component {request.component}"}
|
||||
|
||||
logger.info(f"Log config updated: component={request.component}, enabled_levels={request.enabled_levels}, enabled={request.enabled}")
|
||||
return {"success": True, "message": "Configuration updated"}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update log config: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
@app.get("/api/log/components")
|
||||
async def get_log_components():
|
||||
"""Get list of all logging components with their descriptions."""
|
||||
try:
|
||||
components = list_components()
|
||||
stats = get_component_stats()
|
||||
logger.debug("Log components list requested")
|
||||
return {
|
||||
"success": True,
|
||||
"components": components,
|
||||
"stats": stats
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get log components: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
@app.post("/api/log/reload")
|
||||
async def reload_log_config():
|
||||
"""Reload logging configuration from file."""
|
||||
try:
|
||||
success = reload_all_loggers()
|
||||
if success:
|
||||
logger.info("Log configuration reloaded")
|
||||
return {"success": True, "message": "Configuration reloaded"}
|
||||
else:
|
||||
return {"success": False, "error": "Failed to reload configuration"}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to reload log config: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
@app.post("/api/log/filters")
|
||||
async def update_log_filters(request: LogFilterUpdateRequest):
|
||||
"""Update API request filtering configuration."""
|
||||
try:
|
||||
success = update_api_filters(
|
||||
exclude_paths=request.exclude_paths,
|
||||
exclude_status=request.exclude_status,
|
||||
include_slow_requests=request.include_slow_requests,
|
||||
slow_threshold_ms=request.slow_threshold_ms
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info(f"API filters updated: {request.dict(exclude_none=True)}")
|
||||
return {"success": True, "message": "Filters updated"}
|
||||
else:
|
||||
return {"success": False, "error": "Failed to update filters"}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update filters: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
@app.post("/api/log/reset")
|
||||
async def reset_log_config():
|
||||
"""Reset logging configuration to defaults."""
|
||||
try:
|
||||
success = reset_to_defaults()
|
||||
if success:
|
||||
logger.info("Log configuration reset to defaults")
|
||||
return {"success": True, "message": "Configuration reset to defaults"}
|
||||
else:
|
||||
return {"success": False, "error": "Failed to reset configuration"}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to reset log config: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
@app.post("/api/log/global-level")
|
||||
async def update_global_level_endpoint(level: str, enabled: bool):
|
||||
"""Enable or disable a specific log level across all components."""
|
||||
try:
|
||||
from utils.log_config import update_global_level
|
||||
success = update_global_level(level, enabled)
|
||||
if success:
|
||||
action = "enabled" if enabled else "disabled"
|
||||
logger.info(f"Global level {level} {action} across all components")
|
||||
return {"success": True, "message": f"Level {level} {action} globally"}
|
||||
else:
|
||||
return {"success": False, "error": f"Failed to update global level {level}"}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update global level: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
@app.post("/api/log/timestamp-format")
|
||||
async def update_timestamp_format_endpoint(format_type: str):
|
||||
"""Update timestamp format for all log outputs."""
|
||||
try:
|
||||
success = update_timestamp_format(format_type)
|
||||
if success:
|
||||
logger.info(f"Timestamp format updated to: {format_type}")
|
||||
return {"success": True, "message": f"Timestamp format set to: {format_type}"}
|
||||
else:
|
||||
return {"success": False, "error": f"Invalid timestamp format: {format_type}"}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update timestamp format: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
@app.get("/api/log/files/{component}")
|
||||
async def get_log_file(component: str, lines: int = 100):
|
||||
"""Get last N lines from a component's log file."""
|
||||
try:
|
||||
from pathlib import Path
|
||||
log_dir = Path('/app/memory/logs')
|
||||
log_file = log_dir / f'{component.replace(".", "_")}.log'
|
||||
|
||||
if not log_file.exists():
|
||||
return {"success": False, "error": "Log file not found"}
|
||||
|
||||
with open(log_file, 'r', encoding='utf-8') as f:
|
||||
all_lines = f.readlines()
|
||||
last_lines = all_lines[-lines:] if len(all_lines) >= lines else all_lines
|
||||
|
||||
logger.debug(f"Log file requested: {component} ({lines} lines)")
|
||||
return {
|
||||
"success": True,
|
||||
"component": component,
|
||||
"lines": last_lines,
|
||||
"total_lines": len(all_lines)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to read log file for {component}: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
def start_api():
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=3939)
|
||||
|
||||
138
bot/bot.py
138
bot/bot.py
@@ -46,9 +46,13 @@ from utils.autonomous import (
|
||||
)
|
||||
from utils.dm_logger import dm_logger
|
||||
from utils.dm_interaction_analyzer import init_dm_analyzer
|
||||
from utils.logger import get_logger
|
||||
|
||||
import globals
|
||||
|
||||
# Initialize bot logger
|
||||
logger = get_logger('bot')
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s %(levelname)s: %(message)s",
|
||||
@@ -61,11 +65,15 @@ logging.basicConfig(
|
||||
|
||||
@globals.client.event
|
||||
async def on_ready():
|
||||
print(f'🎤 MikuBot connected as {globals.client.user}')
|
||||
print(f'💬 DM support enabled - users can message Miku directly!')
|
||||
logger.info(f'🎤 MikuBot connected as {globals.client.user}')
|
||||
logger.info(f'💬 DM support enabled - users can message Miku directly!')
|
||||
|
||||
globals.BOT_USER = globals.client.user
|
||||
|
||||
# Intercept external library loggers (APScheduler, etc.)
|
||||
from utils.logger import intercept_external_loggers
|
||||
intercept_external_loggers()
|
||||
|
||||
# Restore evil mode state from previous session (if any)
|
||||
from utils.evil_mode import restore_evil_mode_on_startup
|
||||
restore_evil_mode_on_startup()
|
||||
@@ -77,7 +85,7 @@ async def on_ready():
|
||||
# Initialize DM interaction analyzer
|
||||
if globals.OWNER_USER_ID and globals.OWNER_USER_ID != 0:
|
||||
init_dm_analyzer(globals.OWNER_USER_ID)
|
||||
print(f"📊 DM Interaction Analyzer initialized for owner ID: {globals.OWNER_USER_ID}")
|
||||
logger.info(f"📊 DM Interaction Analyzer initialized for owner ID: {globals.OWNER_USER_ID}")
|
||||
|
||||
# Schedule daily DM analysis (runs at 2 AM every day)
|
||||
from utils.scheduled import run_daily_dm_analysis
|
||||
@@ -88,9 +96,9 @@ async def on_ready():
|
||||
minute=0,
|
||||
id='daily_dm_analysis'
|
||||
)
|
||||
print("⏰ Scheduled daily DM analysis at 2:00 AM")
|
||||
logger.info("⏰ Scheduled daily DM analysis at 2:00 AM")
|
||||
else:
|
||||
print("⚠️ OWNER_USER_ID not set, DM analysis feature disabled")
|
||||
logger.warning("OWNER_USER_ID not set, DM analysis feature disabled")
|
||||
|
||||
# Setup autonomous speaking (now handled by server manager)
|
||||
setup_autonomous_speaking()
|
||||
@@ -146,7 +154,7 @@ async def on_message(message):
|
||||
await replied_msg.reply(file=discord.File(output_video))
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error processing video: {e}")
|
||||
logger.error(f"Error processing video: {e}")
|
||||
await message.channel.send("Sorry, something went wrong while generating the video.")
|
||||
return
|
||||
|
||||
@@ -159,11 +167,11 @@ async def on_message(message):
|
||||
miku_addressed = await is_miku_addressed(message)
|
||||
|
||||
if is_dm:
|
||||
print(f"💌 DM from {message.author.display_name}: {message.content[:50]}{'...' if len(message.content) > 50 else ''}")
|
||||
logger.info(f"💌 DM from {message.author.display_name}: {message.content[:50]}{'...' if len(message.content) > 50 else ''}")
|
||||
|
||||
# Check if user is blocked
|
||||
if dm_logger.is_user_blocked(message.author.id):
|
||||
print(f"🚫 Blocked user {message.author.display_name} ({message.author.id}) tried to send DM - ignoring")
|
||||
logger.info(f"🚫 Blocked user {message.author.display_name} ({message.author.id}) tried to send DM - ignoring")
|
||||
return
|
||||
|
||||
# Log the user's DM message
|
||||
@@ -185,7 +193,7 @@ async def on_message(message):
|
||||
# Add reply context marker to the prompt
|
||||
prompt = f'[Replying to your message: "{replied_content}"] {prompt}'
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to fetch replied message for context: {e}")
|
||||
logger.error(f"Failed to fetch replied message for context: {e}")
|
||||
|
||||
async with message.channel.typing():
|
||||
# If message has an image, video, or GIF attachment
|
||||
@@ -212,9 +220,9 @@ async def on_message(message):
|
||||
)
|
||||
|
||||
if is_dm:
|
||||
print(f"💌 DM image response to {message.author.display_name} (using DM mood: {globals.DM_MOOD})")
|
||||
logger.info(f"💌 DM image response to {message.author.display_name} (using DM mood: {globals.DM_MOOD})")
|
||||
else:
|
||||
print(f"💬 Server image response to {message.author.display_name} in {message.guild.name} (using server mood)")
|
||||
logger.info(f"💬 Server image response to {message.author.display_name} in {message.guild.name} (using server mood)")
|
||||
|
||||
response_message = await message.channel.send(miku_reply)
|
||||
|
||||
@@ -229,7 +237,7 @@ async def on_message(message):
|
||||
current_persona = "evil" if globals.EVIL_MODE else "miku"
|
||||
asyncio.create_task(check_for_interjection(response_message, current_persona))
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error checking for persona interjection: {e}")
|
||||
logger.error(f"Error checking for persona interjection: {e}")
|
||||
|
||||
return
|
||||
|
||||
@@ -239,7 +247,7 @@ async def on_message(message):
|
||||
is_gif = attachment.filename.lower().endswith('.gif')
|
||||
media_type = "gif" if is_gif else "video"
|
||||
|
||||
print(f"🎬 Processing {media_type}: {attachment.filename}")
|
||||
logger.debug(f"🎬 Processing {media_type}: {attachment.filename}")
|
||||
|
||||
# Download the media
|
||||
media_bytes_b64 = await download_and_encode_media(attachment.url)
|
||||
@@ -253,13 +261,13 @@ async def on_message(message):
|
||||
|
||||
# If it's a GIF, convert to MP4 for better processing
|
||||
if is_gif:
|
||||
print(f"🔄 Converting GIF to MP4 for processing...")
|
||||
logger.debug(f"🔄 Converting GIF to MP4 for processing...")
|
||||
mp4_bytes = await convert_gif_to_mp4(media_bytes)
|
||||
if mp4_bytes:
|
||||
media_bytes = mp4_bytes
|
||||
print(f"✅ GIF converted to MP4")
|
||||
logger.info(f"✅ GIF converted to MP4")
|
||||
else:
|
||||
print(f"⚠️ GIF conversion failed, trying direct processing")
|
||||
logger.warning(f"GIF conversion failed, trying direct processing")
|
||||
|
||||
# Extract frames
|
||||
frames = await extract_video_frames(media_bytes, num_frames=6)
|
||||
@@ -268,7 +276,7 @@ async def on_message(message):
|
||||
await message.channel.send(f"I couldn't extract frames from that {media_type}, sorry!")
|
||||
return
|
||||
|
||||
print(f"📹 Extracted {len(frames)} frames from {attachment.filename}")
|
||||
logger.debug(f"📹 Extracted {len(frames)} frames from {attachment.filename}")
|
||||
|
||||
# Analyze the video/GIF with appropriate media type
|
||||
video_description = await analyze_video_with_vision(frames, media_type=media_type)
|
||||
@@ -284,9 +292,9 @@ async def on_message(message):
|
||||
)
|
||||
|
||||
if is_dm:
|
||||
print(f"💌 DM {media_type} response to {message.author.display_name} (using DM mood: {globals.DM_MOOD})")
|
||||
logger.info(f"💌 DM {media_type} response to {message.author.display_name} (using DM mood: {globals.DM_MOOD})")
|
||||
else:
|
||||
print(f"💬 Server video response to {message.author.display_name} in {message.guild.name} (using server mood)")
|
||||
logger.info(f"💬 Server video response to {message.author.display_name} in {message.guild.name} (using server mood)")
|
||||
|
||||
response_message = await message.channel.send(miku_reply)
|
||||
|
||||
@@ -301,7 +309,7 @@ async def on_message(message):
|
||||
current_persona = "evil" if globals.EVIL_MODE else "miku"
|
||||
asyncio.create_task(check_for_interjection(response_message, current_persona))
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error checking for persona interjection: {e}")
|
||||
logger.error(f"Error checking for persona interjection: {e}")
|
||||
|
||||
return
|
||||
|
||||
@@ -310,7 +318,7 @@ async def on_message(message):
|
||||
for embed in message.embeds:
|
||||
# Handle Tenor GIF embeds specially (Discord uses these for /gif command)
|
||||
if embed.type == 'gifv' and embed.url and 'tenor.com' in embed.url:
|
||||
print(f"🎭 Processing Tenor GIF from embed: {embed.url}")
|
||||
logger.info(f"🎭 Processing Tenor GIF from embed: {embed.url}")
|
||||
|
||||
# Extract the actual GIF URL from Tenor
|
||||
gif_url = await extract_tenor_gif_url(embed.url)
|
||||
@@ -322,7 +330,7 @@ async def on_message(message):
|
||||
gif_url = embed.thumbnail.url
|
||||
|
||||
if not gif_url:
|
||||
print(f"⚠️ Could not extract GIF URL from Tenor embed")
|
||||
logger.warning(f"Could not extract GIF URL from Tenor embed")
|
||||
continue
|
||||
|
||||
# Download the GIF
|
||||
@@ -336,13 +344,13 @@ async def on_message(message):
|
||||
media_bytes = base64.b64decode(media_bytes_b64)
|
||||
|
||||
# Convert GIF to MP4
|
||||
print(f"🔄 Converting Tenor GIF to MP4 for processing...")
|
||||
logger.debug(f"Converting Tenor GIF to MP4 for processing...")
|
||||
mp4_bytes = await convert_gif_to_mp4(media_bytes)
|
||||
if not mp4_bytes:
|
||||
print(f"⚠️ GIF conversion failed, trying direct frame extraction")
|
||||
logger.warning(f"GIF conversion failed, trying direct frame extraction")
|
||||
mp4_bytes = media_bytes
|
||||
else:
|
||||
print(f"✅ Tenor GIF converted to MP4")
|
||||
logger.debug(f"Tenor GIF converted to MP4")
|
||||
|
||||
# Extract frames
|
||||
frames = await extract_video_frames(mp4_bytes, num_frames=6)
|
||||
@@ -351,7 +359,7 @@ async def on_message(message):
|
||||
await message.channel.send("I couldn't extract frames from that GIF, sorry!")
|
||||
return
|
||||
|
||||
print(f"📹 Extracted {len(frames)} frames from Tenor GIF")
|
||||
logger.info(f"📹 Extracted {len(frames)} frames from Tenor GIF")
|
||||
|
||||
# Analyze the GIF with tenor_gif media type
|
||||
video_description = await analyze_video_with_vision(frames, media_type="tenor_gif")
|
||||
@@ -366,9 +374,9 @@ async def on_message(message):
|
||||
)
|
||||
|
||||
if is_dm:
|
||||
print(f"💌 DM Tenor GIF response to {message.author.display_name} (using DM mood: {globals.DM_MOOD})")
|
||||
logger.info(f"💌 DM Tenor GIF response to {message.author.display_name} (using DM mood: {globals.DM_MOOD})")
|
||||
else:
|
||||
print(f"💬 Server Tenor GIF response to {message.author.display_name} in {message.guild.name} (using server mood)")
|
||||
logger.info(f"💬 Server Tenor GIF response to {message.author.display_name} in {message.guild.name} (using server mood)")
|
||||
|
||||
response_message = await message.channel.send(miku_reply)
|
||||
|
||||
@@ -383,19 +391,19 @@ async def on_message(message):
|
||||
current_persona = "evil" if globals.EVIL_MODE else "miku"
|
||||
asyncio.create_task(check_for_interjection(response_message, current_persona))
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error checking for persona interjection: {e}")
|
||||
logger.error(f"Error checking for persona interjection: {e}")
|
||||
|
||||
return
|
||||
|
||||
# Handle other types of embeds (rich, article, image, video, link)
|
||||
elif embed.type in ['rich', 'article', 'image', 'video', 'link']:
|
||||
print(f"📰 Processing {embed.type} embed")
|
||||
logger.error(f"Processing {embed.type} embed")
|
||||
|
||||
# Extract content from embed
|
||||
embed_content = await extract_embed_content(embed)
|
||||
|
||||
if not embed_content['has_content']:
|
||||
print(f"⚠️ Embed has no extractable content, skipping")
|
||||
logger.warning(f"Embed has no extractable content, skipping")
|
||||
continue
|
||||
|
||||
# Build context string with embed text
|
||||
@@ -406,28 +414,28 @@ async def on_message(message):
|
||||
# Process images from embed
|
||||
if embed_content['images']:
|
||||
for img_url in embed_content['images']:
|
||||
print(f"🖼️ Processing image from embed: {img_url}")
|
||||
logger.error(f"Processing image from embed: {img_url}")
|
||||
try:
|
||||
base64_img = await download_and_encode_image(img_url)
|
||||
if base64_img:
|
||||
print(f"✅ Image downloaded, analyzing with vision model...")
|
||||
logger.info(f"Image downloaded, analyzing with vision model...")
|
||||
# Analyze image
|
||||
qwen_description = await analyze_image_with_qwen(base64_img)
|
||||
truncated = (qwen_description[:50] + "...") if len(qwen_description) > 50 else qwen_description
|
||||
print(f"📝 Vision analysis result: {truncated}")
|
||||
logger.error(f"Vision analysis result: {truncated}")
|
||||
if qwen_description and qwen_description.strip():
|
||||
embed_context_parts.append(f"[Embedded image shows: {qwen_description}]")
|
||||
else:
|
||||
print(f"❌ Failed to download image from embed")
|
||||
logger.error(f"Failed to download image from embed")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error processing embedded image: {e}")
|
||||
logger.error(f"Error processing embedded image: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
# Process videos from embed
|
||||
if embed_content['videos']:
|
||||
for video_url in embed_content['videos']:
|
||||
print(f"🎬 Processing video from embed: {video_url}")
|
||||
logger.info(f"🎬 Processing video from embed: {video_url}")
|
||||
try:
|
||||
media_bytes_b64 = await download_and_encode_media(video_url)
|
||||
if media_bytes_b64:
|
||||
@@ -435,17 +443,17 @@ async def on_message(message):
|
||||
media_bytes = base64.b64decode(media_bytes_b64)
|
||||
frames = await extract_video_frames(media_bytes, num_frames=6)
|
||||
if frames:
|
||||
print(f"📹 Extracted {len(frames)} frames, analyzing with vision model...")
|
||||
logger.info(f"📹 Extracted {len(frames)} frames, analyzing with vision model...")
|
||||
video_description = await analyze_video_with_vision(frames, media_type="video")
|
||||
print(f"📝 Video analysis result: {video_description[:100]}...")
|
||||
logger.info(f"Video analysis result: {video_description[:100]}...")
|
||||
if video_description and video_description.strip():
|
||||
embed_context_parts.append(f"[Embedded video shows: {video_description}]")
|
||||
else:
|
||||
print(f"❌ Failed to extract frames from video")
|
||||
logger.error(f"Failed to extract frames from video")
|
||||
else:
|
||||
print(f"❌ Failed to download video from embed")
|
||||
logger.error(f"Failed to download video from embed")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error processing embedded video: {e}")
|
||||
logger.error(f"Error processing embedded video: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
@@ -468,9 +476,9 @@ async def on_message(message):
|
||||
)
|
||||
|
||||
if is_dm:
|
||||
print(f"💌 DM embed response to {message.author.display_name} (using DM mood: {globals.DM_MOOD})")
|
||||
logger.info(f"💌 DM embed response to {message.author.display_name} (using DM mood: {globals.DM_MOOD})")
|
||||
else:
|
||||
print(f"💬 Server embed response to {message.author.display_name} in {message.guild.name}")
|
||||
logger.info(f"💬 Server embed response to {message.author.display_name} in {message.guild.name}")
|
||||
|
||||
response_message = await message.channel.send(response)
|
||||
|
||||
@@ -485,7 +493,7 @@ async def on_message(message):
|
||||
current_persona = "evil" if globals.EVIL_MODE else "miku"
|
||||
asyncio.create_task(check_for_interjection(response_message, current_persona))
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error checking for persona interjection: {e}")
|
||||
logger.error(f"Error checking for persona interjection: {e}")
|
||||
|
||||
return
|
||||
|
||||
@@ -494,7 +502,7 @@ async def on_message(message):
|
||||
is_image_request, image_prompt = await detect_image_request(prompt)
|
||||
|
||||
if is_image_request and image_prompt:
|
||||
print(f"🎨 Image generation request detected: '{image_prompt}' from {message.author.display_name}")
|
||||
logger.info(f"🎨 Image generation request detected: '{image_prompt}' from {message.author.display_name}")
|
||||
|
||||
# Handle the image generation workflow
|
||||
success = await handle_image_generation_request(message, image_prompt)
|
||||
@@ -502,7 +510,7 @@ async def on_message(message):
|
||||
return # Image generation completed successfully
|
||||
|
||||
# If image generation failed, fall back to normal response
|
||||
print(f"⚠️ Image generation failed, falling back to normal response")
|
||||
logger.warning(f"Image generation failed, falling back to normal response")
|
||||
|
||||
# If message is just a prompt, no image
|
||||
# For DMs, pass None as guild_id to use DM mood
|
||||
@@ -518,9 +526,9 @@ async def on_message(message):
|
||||
)
|
||||
|
||||
if is_dm:
|
||||
print(f"💌 DM response to {message.author.display_name} (using DM mood: {globals.DM_MOOD})")
|
||||
logger.info(f"💌 DM response to {message.author.display_name} (using DM mood: {globals.DM_MOOD})")
|
||||
else:
|
||||
print(f"💬 Server response to {message.author.display_name} in {message.guild.name} (using server mood)")
|
||||
logger.info(f"💬 Server response to {message.author.display_name} in {message.guild.name} (using server mood)")
|
||||
|
||||
response_message = await message.channel.send(response)
|
||||
|
||||
@@ -530,15 +538,15 @@ async def on_message(message):
|
||||
|
||||
# For server messages, check if opposite persona should interject (persona dialogue system)
|
||||
if not is_dm and globals.BIPOLAR_MODE:
|
||||
print(f"🔧 [DEBUG] Attempting to check for interjection (is_dm={is_dm}, BIPOLAR_MODE={globals.BIPOLAR_MODE})")
|
||||
logger.debug(f"Attempting to check for interjection (is_dm={is_dm}, BIPOLAR_MODE={globals.BIPOLAR_MODE})")
|
||||
try:
|
||||
from utils.persona_dialogue import check_for_interjection
|
||||
current_persona = "evil" if globals.EVIL_MODE else "miku"
|
||||
print(f"🔧 [DEBUG] Creating interjection check task for persona: {current_persona}")
|
||||
logger.debug(f"Creating interjection check task for persona: {current_persona}")
|
||||
# Pass the bot's response message for analysis
|
||||
asyncio.create_task(check_for_interjection(response_message, current_persona))
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error checking for persona interjection: {e}")
|
||||
logger.error(f"Error checking for persona interjection: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
@@ -557,11 +565,11 @@ async def on_message(message):
|
||||
|
||||
detected = detect_mood_shift(response, server_context)
|
||||
if detected and detected != server_config.current_mood_name:
|
||||
print(f"🔄 Auto mood detection for server {message.guild.name}: {server_config.current_mood_name} -> {detected}")
|
||||
logger.info(f"🔄 Auto mood detection for server {message.guild.name}: {server_config.current_mood_name} -> {detected}")
|
||||
|
||||
# Block direct transitions to asleep unless from sleepy
|
||||
if detected == "asleep" and server_config.current_mood_name != "sleepy":
|
||||
print("❌ Ignoring asleep mood; server wasn't sleepy before.")
|
||||
logger.warning("Ignoring asleep mood; server wasn't sleepy before.")
|
||||
else:
|
||||
# Update server mood
|
||||
server_manager.set_server_mood(message.guild.id, detected)
|
||||
@@ -570,7 +578,7 @@ async def on_message(message):
|
||||
from utils.moods import update_server_nickname
|
||||
globals.client.loop.create_task(update_server_nickname(message.guild.id))
|
||||
|
||||
print(f"🔄 Server mood auto-updated to: {detected}")
|
||||
logger.info(f"🔄 Server mood auto-updated to: {detected}")
|
||||
|
||||
if detected == "asleep":
|
||||
server_manager.set_server_sleep_state(message.guild.id, True)
|
||||
@@ -580,15 +588,15 @@ async def on_message(message):
|
||||
server_manager.set_server_sleep_state(message.guild.id, False)
|
||||
server_manager.set_server_mood(message.guild.id, "neutral")
|
||||
await update_server_nickname(message.guild.id)
|
||||
print(f"🌅 Server {message.guild.name} woke up from auto-sleep")
|
||||
logger.info(f"🌅 Server {message.guild.name} woke up from auto-sleep")
|
||||
|
||||
globals.client.loop.create_task(delayed_wakeup())
|
||||
else:
|
||||
print(f"⚠️ No server config found for guild {message.guild.id}, skipping mood detection")
|
||||
logger.error(f"No server config found for guild {message.guild.id}, skipping mood detection")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error in server mood detection: {e}")
|
||||
logger.error(f"Error in server mood detection: {e}")
|
||||
elif is_dm:
|
||||
print("💌 DM message - no mood detection (DM mood only changes via auto-rotation)")
|
||||
logger.debug("DM message - no mood detection (DM mood only changes via auto-rotation)")
|
||||
|
||||
# V2: Track message for autonomous engine (non-blocking, no LLM calls)
|
||||
# IMPORTANT: Only call this if the message was NOT addressed to Miku
|
||||
@@ -645,7 +653,7 @@ async def on_raw_reaction_add(payload):
|
||||
)
|
||||
|
||||
reactor_type = "🤖 Miku" if is_bot_reactor else f"👤 {user.display_name}"
|
||||
print(f"➕ DM reaction added: {emoji_str} by {reactor_type} on message {payload.message_id}")
|
||||
logger.debug(f"DM reaction added: {emoji_str} by {reactor_type} on message {payload.message_id}")
|
||||
|
||||
@globals.client.event
|
||||
async def on_raw_reaction_remove(payload):
|
||||
@@ -683,7 +691,7 @@ async def on_raw_reaction_remove(payload):
|
||||
)
|
||||
|
||||
reactor_type = "🤖 Miku" if user.id == globals.client.user.id else f"👤 {user.display_name}"
|
||||
print(f"➖ DM reaction removed: {emoji_str} by {reactor_type} from message {payload.message_id}")
|
||||
logger.debug(f"DM reaction removed: {emoji_str} by {reactor_type} from message {payload.message_id}")
|
||||
|
||||
@globals.client.event
|
||||
async def on_presence_update(before, after):
|
||||
@@ -698,16 +706,18 @@ async def on_member_join(member):
|
||||
autonomous_member_join(member)
|
||||
|
||||
def start_api():
|
||||
uvicorn.run(app, host="0.0.0.0", port=3939, log_level="info")
|
||||
# Set log_level to "critical" to silence uvicorn's access logs
|
||||
# Our custom api.requests middleware handles HTTP logging with better formatting and filtering
|
||||
uvicorn.run(app, host="0.0.0.0", port=3939, log_level="critical")
|
||||
|
||||
def save_autonomous_state():
|
||||
"""Save autonomous context on shutdown"""
|
||||
try:
|
||||
from utils.autonomous import autonomous_engine
|
||||
autonomous_engine.save_context()
|
||||
print("💾 Saved autonomous context on shutdown")
|
||||
logger.info("💾 Saved autonomous context on shutdown")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to save autonomous context on shutdown: {e}")
|
||||
logger.error(f"Failed to save autonomous context on shutdown: {e}")
|
||||
|
||||
# Register shutdown handlers
|
||||
atexit.register(save_autonomous_state)
|
||||
|
||||
@@ -4,17 +4,20 @@ import asyncio
|
||||
import globals
|
||||
from utils.moods import load_mood_description
|
||||
from utils.scheduled import send_bedtime_reminder
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger('commands')
|
||||
|
||||
|
||||
def set_mood(new_mood: str) -> bool:
|
||||
"""Set mood (legacy function - now handled per-server or DM)"""
|
||||
print("⚠️ set_mood called - this function is deprecated. Use server-specific mood endpoints instead.")
|
||||
logger.warning("set_mood called - this function is deprecated. Use server-specific mood endpoints instead.")
|
||||
return False
|
||||
|
||||
|
||||
def reset_mood() -> str:
|
||||
"""Reset mood to neutral (legacy function - now handled per-server or DM)"""
|
||||
print("⚠️ reset_mood called - this function is deprecated. Use server-specific mood endpoints instead.")
|
||||
logger.warning("reset_mood called - this function is deprecated. Use server-specific mood endpoints instead.")
|
||||
return "neutral"
|
||||
|
||||
|
||||
@@ -24,7 +27,7 @@ def check_mood():
|
||||
|
||||
def calm_miku() -> str:
|
||||
"""Calm Miku down (legacy function - now handled per-server or DM)"""
|
||||
print("⚠️ calm_miku called - this function is deprecated. Use server-specific mood endpoints instead.")
|
||||
logger.warning("calm_miku called - this function is deprecated. Use server-specific mood endpoints instead.")
|
||||
return "neutral"
|
||||
|
||||
|
||||
@@ -34,14 +37,14 @@ def reset_conversation(user_id):
|
||||
|
||||
async def force_sleep() -> str:
|
||||
"""Force Miku to sleep (legacy function - now handled per-server or DM)"""
|
||||
print("⚠️ force_sleep called - this function is deprecated. Use server-specific mood endpoints instead.")
|
||||
logger.warning("force_sleep called - this function is deprecated. Use server-specific mood endpoints instead.")
|
||||
return "asleep"
|
||||
|
||||
|
||||
async def wake_up(set_sleep_state=None):
|
||||
reset_mood()
|
||||
# Note: DMs don't have sleep states, so this is deprecated
|
||||
print("⚠️ wake_up called - this function is deprecated. Use server-specific mood endpoints instead.")
|
||||
logger.warning("wake_up called - this function is deprecated. Use server-specific mood endpoints instead.")
|
||||
|
||||
if set_sleep_state:
|
||||
await set_sleep_state(False)
|
||||
@@ -59,5 +62,5 @@ async def update_profile_picture(mood: str = "neutral"):
|
||||
success = await update_profile_picture(globals.client, mood=mood)
|
||||
return success
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error updating profile picture: {e}")
|
||||
logger.error(f"Error updating profile picture: {e}")
|
||||
return False
|
||||
|
||||
@@ -13,6 +13,9 @@ from apscheduler.triggers.cron import CronTrigger
|
||||
from apscheduler.triggers.date import DateTrigger
|
||||
import random
|
||||
from datetime import datetime, timedelta
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger('server')
|
||||
|
||||
@dataclass
|
||||
class ServerConfig:
|
||||
@@ -58,7 +61,7 @@ class ServerConfig:
|
||||
features_list = [f.strip().strip("'\"") for f in features_str.split(',') if f.strip()]
|
||||
data['enabled_features'] = set(features_list)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to parse enabled_features string '{data['enabled_features']}': {e}")
|
||||
logger.warning(f"Failed to parse enabled_features string '{data['enabled_features']}': {e}")
|
||||
# Fallback to default features
|
||||
data['enabled_features'] = {"autonomous", "bedtime", "monday_video"}
|
||||
return cls(**data)
|
||||
@@ -83,12 +86,12 @@ class ServerManager:
|
||||
guild_id = int(guild_id_str)
|
||||
self.servers[guild_id] = ServerConfig.from_dict(server_data)
|
||||
self.server_memories[guild_id] = {}
|
||||
print(f"📋 Loaded config for server: {server_data['guild_name']} (ID: {guild_id})")
|
||||
logger.info(f"Loaded config for server: {server_data['guild_name']} (ID: {guild_id})")
|
||||
|
||||
# After loading, check if we need to repair the config
|
||||
self.repair_config()
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to load server config: {e}")
|
||||
logger.error(f"Failed to load server config: {e}")
|
||||
self._create_default_config()
|
||||
else:
|
||||
self._create_default_config()
|
||||
@@ -101,21 +104,21 @@ class ServerManager:
|
||||
# Check if enabled_features is a string (corrupted)
|
||||
if isinstance(server.enabled_features, str):
|
||||
needs_repair = True
|
||||
print(f"🔧 Repairing corrupted enabled_features for server: {server.guild_name}")
|
||||
logger.info(f"Repairing corrupted enabled_features for server: {server.guild_name}")
|
||||
# Re-parse the features
|
||||
try:
|
||||
features_str = server.enabled_features.strip('{}')
|
||||
features_list = [f.strip().strip("'\"") for f in features_str.split(',') if f.strip()]
|
||||
server.enabled_features = set(features_list)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to repair enabled_features for {server.guild_name}: {e}")
|
||||
logger.warning(f"Failed to repair enabled_features for {server.guild_name}: {e}")
|
||||
server.enabled_features = {"autonomous", "bedtime", "monday_video"}
|
||||
|
||||
if needs_repair:
|
||||
print("🔧 Saving repaired configuration...")
|
||||
logger.info("Saving repaired configuration...")
|
||||
self.save_config()
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to repair config: {e}")
|
||||
logger.error(f"Failed to repair config: {e}")
|
||||
|
||||
def _create_default_config(self):
|
||||
"""Create default configuration for backward compatibility"""
|
||||
@@ -132,7 +135,7 @@ class ServerManager:
|
||||
self.servers[default_server.guild_id] = default_server
|
||||
self.server_memories[default_server.guild_id] = {}
|
||||
self.save_config()
|
||||
print("📋 Created default server configuration")
|
||||
logger.info("Created default server configuration")
|
||||
|
||||
def save_config(self):
|
||||
"""Save server configurations to file"""
|
||||
@@ -150,14 +153,14 @@ class ServerManager:
|
||||
with open(self.config_file, "w", encoding="utf-8") as f:
|
||||
json.dump(config_data, f, indent=2)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to save server config: {e}")
|
||||
logger.error(f"Failed to save server config: {e}")
|
||||
|
||||
def add_server(self, guild_id: int, guild_name: str, autonomous_channel_id: int,
|
||||
autonomous_channel_name: str, bedtime_channel_ids: List[int] = None,
|
||||
enabled_features: Set[str] = None) -> bool:
|
||||
"""Add a new server configuration"""
|
||||
if guild_id in self.servers:
|
||||
print(f"⚠️ Server {guild_id} already exists")
|
||||
logger.info(f"Server {guild_id} already exists")
|
||||
return False
|
||||
|
||||
if bedtime_channel_ids is None:
|
||||
@@ -178,7 +181,7 @@ class ServerManager:
|
||||
self.servers[guild_id] = server
|
||||
self.server_memories[guild_id] = {}
|
||||
self.save_config()
|
||||
print(f"✅ Added new server: {guild_name} (ID: {guild_id})")
|
||||
logger.info(f"Added new server: {guild_name} (ID: {guild_id})")
|
||||
return True
|
||||
|
||||
def remove_server(self, guild_id: int) -> bool:
|
||||
@@ -199,7 +202,7 @@ class ServerManager:
|
||||
del self.server_memories[guild_id]
|
||||
|
||||
self.save_config()
|
||||
print(f"🗑️ Removed server: {server_name} (ID: {guild_id})")
|
||||
logger.info(f"Removed server: {server_name} (ID: {guild_id})")
|
||||
return True
|
||||
|
||||
def get_server_config(self, guild_id: int) -> Optional[ServerConfig]:
|
||||
@@ -221,7 +224,7 @@ class ServerManager:
|
||||
setattr(server, key, value)
|
||||
|
||||
self.save_config()
|
||||
print(f"✅ Updated config for server: {server.guild_name}")
|
||||
logger.info(f"Updated config for server: {server.guild_name}")
|
||||
return True
|
||||
|
||||
def get_server_memory(self, guild_id: int, key: str = None):
|
||||
@@ -267,12 +270,12 @@ class ServerManager:
|
||||
from utils.moods import load_mood_description
|
||||
server.current_mood_description = load_mood_description(mood_name)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to load mood description for {mood_name}: {e}")
|
||||
logger.error(f"Failed to load mood description for {mood_name}: {e}")
|
||||
server.current_mood_description = f"I'm feeling {mood_name} today."
|
||||
|
||||
self.save_config()
|
||||
print(f"😊 Server {server.guild_name} mood changed to: {mood_name}")
|
||||
print(f"😊 Mood description: {server.current_mood_description[:100]}{'...' if len(server.current_mood_description) > 100 else ''}")
|
||||
logger.info(f"Server {server.guild_name} mood changed to: {mood_name}")
|
||||
logger.debug(f"Mood description: {server.current_mood_description[:100]}{'...' if len(server.current_mood_description) > 100 else ''}")
|
||||
return True
|
||||
|
||||
def get_server_sleep_state(self, guild_id: int) -> bool:
|
||||
@@ -323,7 +326,7 @@ class ServerManager:
|
||||
def setup_server_scheduler(self, guild_id: int, client: discord.Client):
|
||||
"""Setup independent scheduler for a specific server"""
|
||||
if guild_id not in self.servers:
|
||||
print(f"⚠️ Cannot setup scheduler for unknown server: {guild_id}")
|
||||
logger.warning(f"Cannot setup scheduler for unknown server: {guild_id}")
|
||||
return
|
||||
|
||||
server_config = self.servers[guild_id]
|
||||
@@ -363,8 +366,8 @@ class ServerManager:
|
||||
|
||||
# Add bedtime reminder job
|
||||
if "bedtime" in server_config.enabled_features:
|
||||
print(f"⏰ Setting up bedtime scheduler for server {server_config.guild_name}")
|
||||
print(f" Random time range: {server_config.bedtime_hour:02d}:{server_config.bedtime_minute:02d} - {server_config.bedtime_hour_end:02d}:{server_config.bedtime_minute_end:02d}")
|
||||
logger.info(f"Setting up bedtime scheduler for server {server_config.guild_name}")
|
||||
logger.debug(f" Random time range: {server_config.bedtime_hour:02d}:{server_config.bedtime_minute:02d} - {server_config.bedtime_hour_end:02d}:{server_config.bedtime_minute_end:02d}")
|
||||
scheduler.add_job(
|
||||
self._schedule_random_bedtime_for_server,
|
||||
CronTrigger(hour=server_config.bedtime_hour, minute=server_config.bedtime_minute),
|
||||
@@ -382,11 +385,11 @@ class ServerManager:
|
||||
|
||||
self.schedulers[guild_id] = scheduler
|
||||
scheduler.start()
|
||||
print(f"⏰ Started scheduler for server: {server_config.guild_name}")
|
||||
logger.info(f"Started scheduler for server: {server_config.guild_name}")
|
||||
|
||||
def start_all_schedulers(self, client: discord.Client):
|
||||
"""Start schedulers for all servers"""
|
||||
print("🚀 Starting all server schedulers...")
|
||||
logger.info("Starting all server schedulers...")
|
||||
|
||||
for guild_id in self.servers:
|
||||
self.setup_server_scheduler(guild_id, client)
|
||||
@@ -396,42 +399,42 @@ class ServerManager:
|
||||
# Start Figurine DM scheduler
|
||||
self.setup_figurine_updates_scheduler(client)
|
||||
|
||||
print(f"✅ Started {len(self.servers)} server schedulers + DM mood scheduler")
|
||||
logger.info(f"Started {len(self.servers)} server schedulers + DM mood scheduler")
|
||||
|
||||
def update_server_bedtime_job(self, guild_id: int, client: discord.Client):
|
||||
"""Update just the bedtime job for a specific server without restarting all schedulers"""
|
||||
server_config = self.servers.get(guild_id)
|
||||
if not server_config:
|
||||
print(f"⚠️ No server config found for guild {guild_id}")
|
||||
logger.warning(f"No server config found for guild {guild_id}")
|
||||
return False
|
||||
|
||||
scheduler = self.schedulers.get(guild_id)
|
||||
if not scheduler:
|
||||
print(f"⚠️ No scheduler found for guild {guild_id}")
|
||||
logger.warning(f"No scheduler found for guild {guild_id}")
|
||||
return False
|
||||
|
||||
# Remove existing bedtime job if it exists
|
||||
bedtime_job_id = f"bedtime_schedule_{guild_id}"
|
||||
try:
|
||||
scheduler.remove_job(bedtime_job_id)
|
||||
print(f"🗑️ Removed old bedtime job for server {guild_id}")
|
||||
logger.info(f"Removed old bedtime job for server {guild_id}")
|
||||
except Exception as e:
|
||||
print(f"ℹ️ No existing bedtime job to remove for server {guild_id}: {e}")
|
||||
logger.debug(f"No existing bedtime job to remove for server {guild_id}: {e}")
|
||||
|
||||
# Add new bedtime job with updated configuration
|
||||
if "bedtime" in server_config.enabled_features:
|
||||
print(f"⏰ Updating bedtime scheduler for server {server_config.guild_name}")
|
||||
print(f" New random time range: {server_config.bedtime_hour:02d}:{server_config.bedtime_minute:02d} - {server_config.bedtime_hour_end:02d}:{server_config.bedtime_minute_end:02d}")
|
||||
logger.info(f"Updating bedtime scheduler for server {server_config.guild_name}")
|
||||
logger.debug(f" New random time range: {server_config.bedtime_hour:02d}:{server_config.bedtime_minute:02d} - {server_config.bedtime_hour_end:02d}:{server_config.bedtime_minute_end:02d}")
|
||||
scheduler.add_job(
|
||||
self._schedule_random_bedtime_for_server,
|
||||
CronTrigger(hour=server_config.bedtime_hour, minute=server_config.bedtime_minute),
|
||||
args=[guild_id, client],
|
||||
id=bedtime_job_id
|
||||
)
|
||||
print(f"✅ Updated bedtime job for server {server_config.guild_name}")
|
||||
logger.info(f"Updated bedtime job for server {server_config.guild_name}")
|
||||
return True
|
||||
else:
|
||||
print(f"ℹ️ Bedtime feature not enabled for server {guild_id}")
|
||||
logger.info(f"Bedtime feature not enabled for server {guild_id}")
|
||||
return True
|
||||
|
||||
def setup_dm_mood_scheduler(self, client: discord.Client):
|
||||
@@ -449,10 +452,10 @@ class ServerManager:
|
||||
|
||||
scheduler.start()
|
||||
self.schedulers["dm_mood"] = scheduler
|
||||
print("🔄 DM mood rotation scheduler started (every 2 hours)")
|
||||
logger.info("DM mood rotation scheduler started (every 2 hours)")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to setup DM mood scheduler: {e}")
|
||||
logger.error(f"Failed to setup DM mood scheduler: {e}")
|
||||
|
||||
def _enqueue_figurine_send(self, client: discord.Client):
|
||||
"""Enqueue the figurine DM send task in the client's loop."""
|
||||
@@ -460,11 +463,11 @@ class ServerManager:
|
||||
from utils.figurine_notifier import send_figurine_dm_to_all_subscribers
|
||||
if client.loop and client.loop.is_running():
|
||||
client.loop.create_task(send_figurine_dm_to_all_subscribers(client))
|
||||
print("✅ Figurine DM send task queued")
|
||||
logger.debug("Figurine DM send task queued")
|
||||
else:
|
||||
print("⚠️ Client loop not available for figurine DM send")
|
||||
logger.warning("Client loop not available for figurine DM send")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error enqueuing figurine DM: {e}")
|
||||
logger.error(f"Error enqueuing figurine DM: {e}")
|
||||
|
||||
def _schedule_one_figurine_send_today(self, scheduler: AsyncIOScheduler, client: discord.Client):
|
||||
"""Schedule one figurine DM send at a random non-evening time today (or tomorrow if time passed)."""
|
||||
@@ -475,7 +478,7 @@ class ServerManager:
|
||||
target_time = now.replace(hour=random_hour, minute=random_minute, second=0, microsecond=0)
|
||||
if target_time <= now:
|
||||
target_time = target_time + timedelta(days=1)
|
||||
print(f"🗓️ Scheduling figurine DM at {target_time.strftime('%Y-%m-%d %H:%M')} (random non-evening)")
|
||||
logger.info(f"Scheduling figurine DM at {target_time.strftime('%Y-%m-%d %H:%M')} (random non-evening)")
|
||||
scheduler.add_job(
|
||||
self._enqueue_figurine_send,
|
||||
DateTrigger(run_date=target_time),
|
||||
@@ -499,22 +502,22 @@ class ServerManager:
|
||||
self._schedule_one_figurine_send_today(scheduler, client)
|
||||
scheduler.start()
|
||||
self.schedulers["figurine_dm"] = scheduler
|
||||
print("🗓️ Figurine updates scheduler started")
|
||||
logger.info("Figurine updates scheduler started")
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to setup figurine updates scheduler: {e}")
|
||||
logger.error(f"Failed to setup figurine updates scheduler: {e}")
|
||||
|
||||
def stop_all_schedulers(self):
|
||||
"""Stop all schedulers"""
|
||||
print("🛑 Stopping all schedulers...")
|
||||
logger.info("Stopping all schedulers...")
|
||||
|
||||
for scheduler in self.schedulers.values():
|
||||
try:
|
||||
scheduler.shutdown()
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error stopping scheduler: {e}")
|
||||
logger.warning(f"Error stopping scheduler: {e}")
|
||||
|
||||
self.schedulers.clear()
|
||||
print("✅ All schedulers stopped")
|
||||
logger.info("All schedulers stopped")
|
||||
|
||||
# Implementation of autonomous functions - these integrate with the autonomous system
|
||||
def _run_autonomous_for_server(self, guild_id: int, client: discord.Client):
|
||||
@@ -525,11 +528,11 @@ class ServerManager:
|
||||
# Create an async task in the client's event loop
|
||||
if client.loop and client.loop.is_running():
|
||||
client.loop.create_task(autonomous_tick(guild_id))
|
||||
print(f"✅ [V2] Autonomous tick queued for server {guild_id}")
|
||||
logger.debug(f"[V2] Autonomous tick queued for server {guild_id}")
|
||||
else:
|
||||
print(f"⚠️ Client loop not available for autonomous tick in server {guild_id}")
|
||||
logger.warning(f"Client loop not available for autonomous tick in server {guild_id}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error in autonomous tick for server {guild_id}: {e}")
|
||||
logger.error(f"Error in autonomous tick for server {guild_id}: {e}")
|
||||
|
||||
def _run_autonomous_reaction_for_server(self, guild_id: int, client: discord.Client):
|
||||
"""Run autonomous reaction for a specific server - called by APScheduler"""
|
||||
@@ -539,11 +542,11 @@ class ServerManager:
|
||||
# Create an async task in the client's event loop
|
||||
if client.loop and client.loop.is_running():
|
||||
client.loop.create_task(autonomous_reaction_tick(guild_id))
|
||||
print(f"✅ [V2] Autonomous reaction queued for server {guild_id}")
|
||||
logger.debug(f"[V2] Autonomous reaction queued for server {guild_id}")
|
||||
else:
|
||||
print(f"⚠️ Client loop not available for autonomous reaction in server {guild_id}")
|
||||
logger.warning(f"Client loop not available for autonomous reaction in server {guild_id}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error in autonomous reaction for server {guild_id}: {e}")
|
||||
logger.error(f"Error in autonomous reaction for server {guild_id}: {e}")
|
||||
|
||||
def _run_conversation_detection_for_server(self, guild_id: int, client: discord.Client):
|
||||
"""Run conversation detection for a specific server - called by APScheduler"""
|
||||
@@ -552,11 +555,11 @@ class ServerManager:
|
||||
# Create an async task in the client's event loop
|
||||
if client.loop and client.loop.is_running():
|
||||
client.loop.create_task(miku_detect_and_join_conversation_for_server(guild_id))
|
||||
print(f"✅ Conversation detection queued for server {guild_id}")
|
||||
logger.debug(f"Conversation detection queued for server {guild_id}")
|
||||
else:
|
||||
print(f"⚠️ Client loop not available for conversation detection in server {guild_id}")
|
||||
logger.warning(f"Client loop not available for conversation detection in server {guild_id}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error in conversation detection for server {guild_id}: {e}")
|
||||
logger.error(f"Error in conversation detection for server {guild_id}: {e}")
|
||||
|
||||
def _send_monday_video_for_server(self, guild_id: int, client: discord.Client):
|
||||
"""Send Monday video for a specific server - called by APScheduler"""
|
||||
@@ -565,35 +568,35 @@ class ServerManager:
|
||||
# Create an async task in the client's event loop
|
||||
if client.loop and client.loop.is_running():
|
||||
client.loop.create_task(send_monday_video_for_server(guild_id))
|
||||
print(f"✅ Monday video queued for server {guild_id}")
|
||||
logger.debug(f"Monday video queued for server {guild_id}")
|
||||
else:
|
||||
print(f"⚠️ Client loop not available for Monday video in server {guild_id}")
|
||||
logger.warning(f"Client loop not available for Monday video in server {guild_id}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error in Monday video for server {guild_id}: {e}")
|
||||
logger.error(f"Error in Monday video for server {guild_id}: {e}")
|
||||
|
||||
def _schedule_random_bedtime_for_server(self, guild_id: int, client: discord.Client):
|
||||
"""Schedule bedtime reminder for a specific server at a random time within the configured range"""
|
||||
print(f"⏰ Bedtime scheduler triggered for server {guild_id} at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
logger.info(f"Bedtime scheduler triggered for server {guild_id} at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
|
||||
# Get server config to determine the random time range
|
||||
server_config = self.servers.get(guild_id)
|
||||
if not server_config:
|
||||
print(f"⚠️ No server config found for guild {guild_id}")
|
||||
logger.warning(f"No server config found for guild {guild_id}")
|
||||
return
|
||||
|
||||
# Calculate random time within the bedtime range
|
||||
start_minutes = server_config.bedtime_hour * 60 + server_config.bedtime_minute
|
||||
end_minutes = server_config.bedtime_hour_end * 60 + server_config.bedtime_minute_end
|
||||
|
||||
print(f"🕐 Bedtime range calculation: {server_config.bedtime_hour:02d}:{server_config.bedtime_minute:02d} ({start_minutes} min) to {server_config.bedtime_hour_end:02d}:{server_config.bedtime_minute_end:02d} ({end_minutes} min)")
|
||||
logger.debug(f"Bedtime range calculation: {server_config.bedtime_hour:02d}:{server_config.bedtime_minute:02d} ({start_minutes} min) to {server_config.bedtime_hour_end:02d}:{server_config.bedtime_minute_end:02d} ({end_minutes} min)")
|
||||
|
||||
# Handle case where end time is next day (e.g., 23:30 to 00:30)
|
||||
if end_minutes <= start_minutes:
|
||||
end_minutes += 24 * 60 # Add 24 hours
|
||||
print(f"🌙 Cross-midnight range detected, adjusted end to {end_minutes} minutes")
|
||||
logger.debug(f"Cross-midnight range detected, adjusted end to {end_minutes} minutes")
|
||||
|
||||
random_minutes = random.randint(start_minutes, end_minutes)
|
||||
print(f"🎲 Random time selected: {random_minutes} minutes from midnight")
|
||||
logger.debug(f"Random time selected: {random_minutes} minutes from midnight")
|
||||
|
||||
# Convert back to hours and minutes
|
||||
random_hour = (random_minutes // 60) % 24
|
||||
@@ -609,7 +612,7 @@ class ServerManager:
|
||||
|
||||
delay_seconds = (target_time - now).total_seconds()
|
||||
|
||||
print(f"🎲 Random bedtime for server {server_config.guild_name}: {random_hour:02d}:{random_minute:02d} (in {delay_seconds/60:.1f} minutes)")
|
||||
logger.info(f"Random bedtime for server {server_config.guild_name}: {random_hour:02d}:{random_minute:02d} (in {delay_seconds/60:.1f} minutes)")
|
||||
|
||||
# Schedule the actual bedtime reminder
|
||||
try:
|
||||
@@ -618,9 +621,9 @@ class ServerManager:
|
||||
def send_bedtime_delayed():
|
||||
if client.loop and client.loop.is_running():
|
||||
client.loop.create_task(send_bedtime_reminder_for_server(guild_id, client))
|
||||
print(f"✅ Random bedtime reminder sent for server {guild_id}")
|
||||
logger.info(f"Random bedtime reminder sent for server {guild_id}")
|
||||
else:
|
||||
print(f"⚠️ Client loop not available for bedtime reminder in server {guild_id}")
|
||||
logger.warning(f"Client loop not available for bedtime reminder in server {guild_id}")
|
||||
|
||||
# Use the scheduler to schedule the delayed bedtime reminder
|
||||
scheduler = self.schedulers.get(guild_id)
|
||||
@@ -630,12 +633,12 @@ class ServerManager:
|
||||
DateTrigger(run_date=target_time),
|
||||
id=f"bedtime_reminder_{guild_id}_{int(target_time.timestamp())}"
|
||||
)
|
||||
print(f"✅ Bedtime reminder scheduled for server {guild_id} at {target_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
logger.info(f"Bedtime reminder scheduled for server {guild_id} at {target_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
else:
|
||||
print(f"⚠️ No scheduler found for server {guild_id}")
|
||||
logger.warning(f"No scheduler found for server {guild_id}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error scheduling bedtime reminder for server {guild_id}: {e}")
|
||||
logger.error(f"Error scheduling bedtime reminder for server {guild_id}: {e}")
|
||||
|
||||
def _rotate_server_mood(self, guild_id: int, client: discord.Client):
|
||||
"""Rotate mood for a specific server - called by APScheduler"""
|
||||
@@ -644,11 +647,11 @@ class ServerManager:
|
||||
# Create an async task in the client's event loop
|
||||
if client.loop and client.loop.is_running():
|
||||
client.loop.create_task(rotate_server_mood(guild_id))
|
||||
print(f"✅ Mood rotation queued for server {guild_id}")
|
||||
logger.debug(f"Mood rotation queued for server {guild_id}")
|
||||
else:
|
||||
print(f"⚠️ Client loop not available for mood rotation in server {guild_id}")
|
||||
logger.warning(f"Client loop not available for mood rotation in server {guild_id}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error in mood rotation for server {guild_id}: {e}")
|
||||
logger.error(f"Error in mood rotation for server {guild_id}: {e}")
|
||||
|
||||
# Global instance
|
||||
server_manager = ServerManager()
|
||||
|
||||
@@ -658,12 +658,13 @@
|
||||
<div class="tab-container">
|
||||
<div class="tab-buttons">
|
||||
<button class="tab-button active" onclick="switchTab('tab1')">Server Management</button>
|
||||
<button class="tab-button" onclick="switchTab('tab2')">Actions</button>
|
||||
<button class="tab-button" onclick="switchTab('tab3')">Status</button>
|
||||
<button class="tab-button" onclick="switchTab('tab4')">🎨 Image Generation</button>
|
||||
<button class="tab-button" onclick="switchTab('tab5')">📊 Autonomous Stats</button>
|
||||
<button class="tab-button" onclick="switchTab('tab6')">💬 Chat with LLM</button>
|
||||
</div>
|
||||
<button class="tab-button" onclick="switchTab('tab2')">Actions</button>
|
||||
<button class="tab-button" onclick="switchTab('tab3')">Status</button>
|
||||
<button class="tab-button" onclick="switchTab('tab4')">🎨 Image Generation</button>
|
||||
<button class="tab-button" onclick="switchTab('tab5')">📊 Autonomous Stats</button>
|
||||
<button class="tab-button" onclick="switchTab('tab6')">💬 Chat with LLM</button>
|
||||
<button class="tab-button" onclick="window.location.href='/static/system.html'">🎛️ System Settings</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab 1 Content -->
|
||||
<div id="tab1" class="tab-content active">
|
||||
|
||||
415
bot/static/system-logic.js
Normal file
415
bot/static/system-logic.js
Normal file
@@ -0,0 +1,415 @@
|
||||
let currentConfig = null;
|
||||
let componentsData = null;
|
||||
|
||||
// Load configuration on page load
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
loadConfiguration();
|
||||
loadComponents();
|
||||
});
|
||||
|
||||
async function loadConfiguration() {
|
||||
try {
|
||||
const response = await fetch('/api/log/config');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
currentConfig = data.config;
|
||||
|
||||
// Load timestamp format setting
|
||||
const timestampFormat = data.config.formatting?.timestamp_format || 'datetime';
|
||||
const timestampSelect = document.getElementById('timestampFormat');
|
||||
if (timestampSelect) {
|
||||
timestampSelect.value = timestampFormat;
|
||||
}
|
||||
} else {
|
||||
showNotification('Failed to load configuration', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showNotification('Error loading configuration: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadComponents() {
|
||||
try {
|
||||
const response = await fetch('/api/log/components');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
componentsData = data;
|
||||
renderComponentsTable();
|
||||
populatePreviewSelect();
|
||||
} else {
|
||||
showNotification('Failed to load components', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showNotification('Error loading components: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function renderComponentsTable() {
|
||||
const tbody = document.getElementById('componentsTable');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
for (const [name, description] of Object.entries(componentsData.components)) {
|
||||
const stats = componentsData.stats[name] || {};
|
||||
const enabled = stats.enabled !== undefined ? stats.enabled : true;
|
||||
const enabledLevels = stats.enabled_levels || ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'];
|
||||
|
||||
const allLevels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'];
|
||||
if (name === 'api.requests') {
|
||||
allLevels.push('API');
|
||||
}
|
||||
|
||||
const levelCheckboxes = allLevels.map(level => {
|
||||
const emoji = {'DEBUG': '🔍', 'INFO': 'ℹ️', 'WARNING': '⚠️', 'ERROR': '❌', 'CRITICAL': '🔥', 'API': '🌐'}[level];
|
||||
const checked = enabledLevels.includes(level) ? 'checked' : '';
|
||||
return `
|
||||
<div class="level-checkbox">
|
||||
<input type="checkbox"
|
||||
id="level_${name}_${level}"
|
||||
${checked}
|
||||
onchange="updateComponentLevels('${name}')">
|
||||
<label for="level_${name}_${level}">${emoji} ${level}</label>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td>
|
||||
<div style="color: #61dafb; font-weight: bold;">${name}</div>
|
||||
<div class="component-description">${description}</div>
|
||||
</td>
|
||||
<td>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" id="enabled_${name}" ${enabled ? 'checked' : ''} onchange="updateComponentEnabled('${name}')">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</td>
|
||||
<td>
|
||||
<div class="level-checkboxes">
|
||||
${levelCheckboxes}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="status-indicator ${enabled ? 'status-active' : 'status-inactive'}"></span>
|
||||
${enabled ? 'Active' : 'Inactive'}
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
|
||||
if (name === 'api.requests') {
|
||||
document.getElementById('enabled_' + name).addEventListener('change', (e) => {
|
||||
document.getElementById('apiFilters').style.display = e.target.checked ? 'block' : 'none';
|
||||
});
|
||||
|
||||
if (enabled) {
|
||||
document.getElementById('apiFilters').style.display = 'block';
|
||||
loadApiFilters();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update global level checkboxes based on current state
|
||||
updateGlobalLevelCheckboxes();
|
||||
}
|
||||
|
||||
function updateGlobalLevelCheckboxes() {
|
||||
const allLevels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL', 'API'];
|
||||
|
||||
for (const level of allLevels) {
|
||||
let allComponentsHaveLevel = true;
|
||||
|
||||
// Check if ALL components have this level enabled
|
||||
for (const [name, description] of Object.entries(componentsData.components)) {
|
||||
const stats = componentsData.stats[name] || {};
|
||||
const enabledLevels = stats.enabled_levels || [];
|
||||
|
||||
// Skip API level for non-api.requests components
|
||||
if (level === 'API' && name !== 'api.requests') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!enabledLevels.includes(level)) {
|
||||
allComponentsHaveLevel = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const checkbox = document.getElementById('global_' + level);
|
||||
if (checkbox) {
|
||||
checkbox.checked = allComponentsHaveLevel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function populatePreviewSelect() {
|
||||
const select = document.getElementById('previewComponent');
|
||||
select.innerHTML = '';
|
||||
|
||||
for (const name of Object.keys(componentsData.components)) {
|
||||
const option = document.createElement('option');
|
||||
option.value = name;
|
||||
option.textContent = name;
|
||||
select.appendChild(option);
|
||||
}
|
||||
|
||||
loadLogPreview();
|
||||
}
|
||||
|
||||
async function updateComponentEnabled(component) {
|
||||
const enabled = document.getElementById('enabled_' + component).checked;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/log/config', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
component: component,
|
||||
enabled: enabled
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showNotification(`${enabled ? 'Enabled' : 'Disabled'} ${component}`, 'success');
|
||||
|
||||
const row = document.getElementById('enabled_' + component).closest('tr');
|
||||
const statusCell = row.querySelector('td:last-child');
|
||||
statusCell.innerHTML = `
|
||||
<span class="status-indicator ${enabled ? 'status-active' : 'status-inactive'}"></span>
|
||||
${enabled ? 'Active' : 'Inactive'}
|
||||
`;
|
||||
} else {
|
||||
showNotification('Failed to update ' + component + ': ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showNotification('Error updating component: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function updateGlobalLevel(level, enabled) {
|
||||
try {
|
||||
const response = await fetch(`/api/log/global-level?level=${level}&enabled=${enabled}`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
const action = enabled ? 'enabled' : 'disabled';
|
||||
showNotification(`${level} ${action} globally across all components`, 'success');
|
||||
|
||||
// Reload components to reflect changes
|
||||
await loadComponents();
|
||||
} else {
|
||||
showNotification('Failed to update global level: ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showNotification('Error updating global level: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function updateTimestampFormat(format) {
|
||||
try {
|
||||
const response = await fetch(`/api/log/timestamp-format?format_type=${format}`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showNotification(`Timestamp format updated: ${format}`, 'success');
|
||||
|
||||
// Reload all loggers to apply the change
|
||||
await fetch('/api/log/reload', { method: 'POST' });
|
||||
} else {
|
||||
showNotification('Failed to update timestamp format: ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showNotification('Error updating timestamp format: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function updateComponentLevels(component) {
|
||||
const allLevels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'];
|
||||
if (component === 'api.requests') {
|
||||
allLevels.push('API');
|
||||
}
|
||||
|
||||
const enabledLevels = allLevels.filter(level => {
|
||||
const checkbox = document.getElementById(`level_${component}_${level}`);
|
||||
return checkbox && checkbox.checked;
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/log/config', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
component: component,
|
||||
enabled_levels: enabledLevels
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showNotification(`Updated levels for ${component}: ${enabledLevels.join(', ')}`, 'success');
|
||||
|
||||
// Update global level checkboxes to reflect current state
|
||||
updateGlobalLevelCheckboxes();
|
||||
} else {
|
||||
showNotification('Failed to update ' + component + ': ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showNotification('Error updating component: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadApiFilters() {
|
||||
if (!currentConfig || !currentConfig.components['api.requests']) return;
|
||||
|
||||
const filters = currentConfig.components['api.requests'].filters || {};
|
||||
document.getElementById('excludePaths').value = (filters.exclude_paths || []).join(', ');
|
||||
document.getElementById('excludeStatus').value = (filters.exclude_status || []).join(', ');
|
||||
document.getElementById('includeSlowRequests').checked = filters.include_slow_requests !== false;
|
||||
document.getElementById('slowThreshold').value = filters.slow_threshold_ms || 1000;
|
||||
}
|
||||
|
||||
async function saveApiFilters() {
|
||||
const excludePaths = document.getElementById('excludePaths').value
|
||||
.split(',')
|
||||
.map(s => s.trim())
|
||||
.filter(s => s.length > 0);
|
||||
|
||||
const excludeStatus = document.getElementById('excludeStatus').value
|
||||
.split(',')
|
||||
.map(s => parseInt(s.trim()))
|
||||
.filter(n => !isNaN(n));
|
||||
|
||||
const includeSlowRequests = document.getElementById('includeSlowRequests').checked;
|
||||
const slowThreshold = parseInt(document.getElementById('slowThreshold').value);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/log/filters', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
exclude_paths: excludePaths,
|
||||
exclude_status: excludeStatus,
|
||||
include_slow_requests: includeSlowRequests,
|
||||
slow_threshold_ms: slowThreshold
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showNotification('API filters saved', 'success');
|
||||
} else {
|
||||
showNotification('Failed to save filters: ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showNotification('Error saving filters: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function saveAllSettings() {
|
||||
try {
|
||||
const response = await fetch('/api/log/reload', {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showNotification('All settings saved and reloaded', 'success');
|
||||
await loadConfiguration();
|
||||
await loadComponents();
|
||||
} else {
|
||||
showNotification('Failed to reload settings: ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showNotification('Error saving settings: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function resetToDefaults() {
|
||||
if (!confirm('Are you sure you want to reset all logging settings to defaults?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/log/reset', {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showNotification('Settings reset to defaults', 'success');
|
||||
await loadConfiguration();
|
||||
await loadComponents();
|
||||
} else {
|
||||
showNotification('Failed to reset settings: ' + data.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showNotification('Error resetting settings: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLogPreview() {
|
||||
const component = document.getElementById('previewComponent').value;
|
||||
const preview = document.getElementById('logPreview');
|
||||
|
||||
preview.innerHTML = '<div class="loading">Loading logs...</div>';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/log/files/${component}?lines=50`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
if (data.lines.length === 0) {
|
||||
preview.innerHTML = '<div class="loading">No logs yet for this component</div>';
|
||||
} else {
|
||||
preview.innerHTML = data.lines.map(line =>
|
||||
`<div class="log-line">${escapeHtml(line)}</div>`
|
||||
).join('');
|
||||
|
||||
preview.scrollTop = preview.scrollHeight;
|
||||
}
|
||||
} else {
|
||||
preview.innerHTML = `<div class="loading">Error: ${data.error}</div>`;
|
||||
}
|
||||
} catch (error) {
|
||||
preview.innerHTML = `<div class="loading">Error loading logs: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function showNotification(message, type) {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `notification notification-${type}`;
|
||||
notification.textContent = message;
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.remove();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Auto-refresh log preview every 5 seconds
|
||||
setInterval(() => {
|
||||
if (document.getElementById('previewComponent').value) {
|
||||
loadLogPreview();
|
||||
}
|
||||
}, 5000);
|
||||
408
bot/static/system.html
Normal file
408
bot/static/system.html
Normal file
@@ -0,0 +1,408 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>🎛️ System Settings - Logging Configuration</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: monospace;
|
||||
background-color: #121212;
|
||||
color: #fff;
|
||||
}
|
||||
.container {
|
||||
padding: 2rem;
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 2px solid #333;
|
||||
}
|
||||
h1 {
|
||||
color: #61dafb;
|
||||
margin: 0;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
h2 {
|
||||
color: #61dafb;
|
||||
font-size: 1.3rem;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
button, select {
|
||||
padding: 0.4rem 0.8rem;
|
||||
background: #333;
|
||||
color: #fff;
|
||||
border: 1px solid #555;
|
||||
font-family: monospace;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
button:hover, select:hover {
|
||||
background: #444;
|
||||
border-color: #666;
|
||||
}
|
||||
.btn-primary {
|
||||
background: #61dafb;
|
||||
color: #000;
|
||||
border-color: #61dafb;
|
||||
font-weight: bold;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background: #4fa8c5;
|
||||
}
|
||||
.btn-secondary {
|
||||
background: #555;
|
||||
border-color: #666;
|
||||
}
|
||||
.btn-danger {
|
||||
background: #d32f2f;
|
||||
border-color: #d32f2f;
|
||||
}
|
||||
.btn-danger:hover {
|
||||
background: #b71c1c;
|
||||
}
|
||||
.content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2rem;
|
||||
}
|
||||
.card {
|
||||
background: #1e1e1e;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
.components-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.components-table th {
|
||||
background: #2a2a2a;
|
||||
color: #61dafb;
|
||||
padding: 0.8rem;
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
border-bottom: 2px solid #444;
|
||||
}
|
||||
.components-table td {
|
||||
padding: 0.8rem;
|
||||
border-bottom: 1px solid #2a2a2a;
|
||||
vertical-align: top;
|
||||
}
|
||||
.components-table tr:hover {
|
||||
background: #252525;
|
||||
}
|
||||
.component-description {
|
||||
font-size: 0.8rem;
|
||||
color: #999;
|
||||
margin-top: 0.3rem;
|
||||
}
|
||||
.toggle {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 24px;
|
||||
}
|
||||
.toggle input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #555;
|
||||
transition: 0.3s;
|
||||
border-radius: 24px;
|
||||
}
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background-color: white;
|
||||
transition: 0.3s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
input:checked + .slider {
|
||||
background-color: #61dafb;
|
||||
}
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(26px);
|
||||
}
|
||||
.level-checkboxes {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.level-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
.level-checkbox input[type="checkbox"] {
|
||||
width: auto;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.level-checkbox label {
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.status-indicator {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-right: 0.3rem;
|
||||
}
|
||||
.status-active {
|
||||
background-color: #4CAF50;
|
||||
}
|
||||
.status-inactive {
|
||||
background-color: #555;
|
||||
}
|
||||
.api-filters {
|
||||
background: #2a2a2a;
|
||||
border: 1px solid #444;
|
||||
padding: 1rem;
|
||||
margin-top: 1rem;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.api-filters h3 {
|
||||
color: #61dafb;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
.filter-row {
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
.filter-row label {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.3rem;
|
||||
color: #ccc;
|
||||
}
|
||||
.setting-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
.setting-row label {
|
||||
font-weight: bold;
|
||||
color: #ccc;
|
||||
}
|
||||
input[type="text"], input[type="number"] {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
background: #333;
|
||||
color: #fff;
|
||||
border: 1px solid #555;
|
||||
font-family: monospace;
|
||||
}
|
||||
.log-preview {
|
||||
background: #000;
|
||||
color: #0f0;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
font-family: monospace;
|
||||
font-size: 0.85rem;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
.log-preview-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.log-line {
|
||||
margin-bottom: 3px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.notification {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
background-color: #222;
|
||||
color: #fff;
|
||||
padding: 1rem;
|
||||
border: 1px solid #555;
|
||||
border-radius: 8px;
|
||||
opacity: 0.95;
|
||||
z-index: 1000;
|
||||
font-size: 0.9rem;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
.notification-success {
|
||||
border-color: #4CAF50;
|
||||
background: #1b4d1b;
|
||||
}
|
||||
.notification-error {
|
||||
border-color: #d32f2f;
|
||||
background: #4d1b1b;
|
||||
}
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(400px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #999;
|
||||
}
|
||||
@media (max-width: 1200px) {
|
||||
.content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🎛️ System Settings - Logging Configuration</h1>
|
||||
<div class="header-actions">
|
||||
<button class="btn-secondary" onclick="window.location.href='/'">← Back to Dashboard</button>
|
||||
<button class="btn-primary" onclick="saveAllSettings()">💾 Save All</button>
|
||||
<button class="btn-danger" onclick="resetToDefaults()">🔄 Reset to Defaults</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="card">
|
||||
<h2>📊 Logging Components</h2>
|
||||
<p style="color: #999; margin-bottom: 1rem; font-size: 0.9rem;">
|
||||
Enable or disable specific log levels for each component. You can toggle any combination of levels.
|
||||
</p>
|
||||
|
||||
<div class="api-filters" style="margin-bottom: 1.5rem;">
|
||||
<h3>🌍 Global Level Controls</h3>
|
||||
<p style="color: #999; font-size: 0.85rem; margin-bottom: 0.8rem;">
|
||||
Quickly enable/disable a log level across all components
|
||||
</p>
|
||||
<div class="level-checkboxes">
|
||||
<div class="level-checkbox">
|
||||
<input type="checkbox" id="global_DEBUG" checked onchange="updateGlobalLevel('DEBUG', this.checked)">
|
||||
<label for="global_DEBUG">🔍 DEBUG</label>
|
||||
</div>
|
||||
<div class="level-checkbox">
|
||||
<input type="checkbox" id="global_INFO" checked onchange="updateGlobalLevel('INFO', this.checked)">
|
||||
<label for="global_INFO">ℹ️ INFO</label>
|
||||
</div>
|
||||
<div class="level-checkbox">
|
||||
<input type="checkbox" id="global_WARNING" checked onchange="updateGlobalLevel('WARNING', this.checked)">
|
||||
<label for="global_WARNING">⚠️ WARNING</label>
|
||||
</div>
|
||||
<div class="level-checkbox">
|
||||
<input type="checkbox" id="global_ERROR" checked onchange="updateGlobalLevel('ERROR', this.checked)">
|
||||
<label for="global_ERROR">❌ ERROR</label>
|
||||
</div>
|
||||
<div class="level-checkbox">
|
||||
<input type="checkbox" id="global_CRITICAL" checked onchange="updateGlobalLevel('CRITICAL', this.checked)">
|
||||
<label for="global_CRITICAL">🔥 CRITICAL</label>
|
||||
</div>
|
||||
<div class="level-checkbox">
|
||||
<input type="checkbox" id="global_API" checked onchange="updateGlobalLevel('API', this.checked)">
|
||||
<label for="global_API">🌐 API</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="api-filters" style="margin-bottom: 1.5rem;">
|
||||
<h3>🕐 Timestamp Format</h3>
|
||||
<p style="color: #999; font-size: 0.85rem; margin-bottom: 0.8rem;">
|
||||
Control how timestamps appear in logs
|
||||
</p>
|
||||
<div class="setting-row">
|
||||
<label>Format:</label>
|
||||
<select id="timestampFormat" onchange="updateTimestampFormat(this.value)">
|
||||
<option value="datetime">Date + Time (2026-01-10 20:30:45)</option>
|
||||
<option value="time">Time Only (20:30:45)</option>
|
||||
<option value="date">Date Only (2026-01-10)</option>
|
||||
<option value="off">No Timestamp</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="components-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Component</th>
|
||||
<th>Enabled</th>
|
||||
<th>Log Levels</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="componentsTable">
|
||||
<tr><td colspan="4" class="loading">Loading components...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div id="apiFilters" class="api-filters" style="display: none;">
|
||||
<h3>🌐 API Request Filters</h3>
|
||||
<div class="filter-row">
|
||||
<label>Exclude Paths (comma-separated):</label>
|
||||
<input type="text" id="excludePaths" placeholder="/health, /static/*">
|
||||
</div>
|
||||
<div class="filter-row">
|
||||
<label>Exclude Status Codes (comma-separated):</label>
|
||||
<input type="text" id="excludeStatus" placeholder="200, 304">
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label>Log Slow Requests (>1000ms):</label>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" id="includeSlowRequests" checked>
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="filter-row">
|
||||
<label>Slow Request Threshold (ms):</label>
|
||||
<input type="number" id="slowThreshold" value="1000" min="100" step="100">
|
||||
</div>
|
||||
<button class="btn-primary" onclick="saveApiFilters()" style="margin-top: 0.5rem;">Save API Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>📜 Live Log Preview</h2>
|
||||
<div class="log-preview-header">
|
||||
<div>
|
||||
<label>Component: </label>
|
||||
<select id="previewComponent" onchange="loadLogPreview()"><option value="bot">Bot</option></select>
|
||||
</div>
|
||||
<button class="btn-secondary" onclick="loadLogPreview()">🔄 Refresh</button>
|
||||
</div>
|
||||
<div class="log-preview" id="logPreview">
|
||||
<div class="loading">Select a component to view logs...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/system-logic.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -9,6 +9,9 @@ import time
|
||||
from utils.autonomous_engine import autonomous_engine
|
||||
from server_manager import server_manager
|
||||
import globals
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger('autonomous')
|
||||
|
||||
# Rate limiting: Track last action time per server to prevent rapid-fire
|
||||
_last_action_execution = {} # guild_id -> timestamp
|
||||
@@ -25,7 +28,7 @@ async def autonomous_tick_v2(guild_id: int):
|
||||
if guild_id in _last_action_execution:
|
||||
time_since_last = now - _last_action_execution[guild_id]
|
||||
if time_since_last < _MIN_ACTION_INTERVAL:
|
||||
print(f"⏱️ [V2] Rate limit: Only {time_since_last:.0f}s since last action (need {_MIN_ACTION_INTERVAL}s)")
|
||||
logger.debug(f"[V2] Rate limit: Only {time_since_last:.0f}s since last action (need {_MIN_ACTION_INTERVAL}s)")
|
||||
return
|
||||
|
||||
# Ask the engine if Miku should act (with optional debug logging)
|
||||
@@ -35,7 +38,7 @@ async def autonomous_tick_v2(guild_id: int):
|
||||
# Engine decided not to act
|
||||
return
|
||||
|
||||
print(f"🤖 [V2] Autonomous engine decided to: {action_type} for server {guild_id}")
|
||||
logger.info(f"[V2] Autonomous engine decided to: {action_type} for server {guild_id}")
|
||||
|
||||
# Execute the action using legacy functions
|
||||
from utils.autonomous_v1_legacy import (
|
||||
@@ -58,12 +61,12 @@ async def autonomous_tick_v2(guild_id: int):
|
||||
elif action_type == "change_profile_picture":
|
||||
# Get current mood for this server
|
||||
mood, _ = server_manager.get_server_mood(guild_id)
|
||||
print(f"🎨 [V2] Changing profile picture (mood: {mood})")
|
||||
logger.info(f"[V2] Changing profile picture (mood: {mood})")
|
||||
result = await profile_picture_manager.change_profile_picture(mood=mood, debug=True)
|
||||
if result["success"]:
|
||||
print(f"✅ Profile picture changed successfully!")
|
||||
logger.info(f"Profile picture changed successfully!")
|
||||
else:
|
||||
print(f"⚠️ Profile picture change failed: {result.get('error')}")
|
||||
logger.warning(f"Profile picture change failed: {result.get('error')}")
|
||||
|
||||
# Record that action was taken
|
||||
autonomous_engine.record_action(guild_id)
|
||||
@@ -84,10 +87,10 @@ async def autonomous_tick_v2(guild_id: int):
|
||||
if channel:
|
||||
await maybe_trigger_argument(channel, globals.client, "Triggered after an autonomous action")
|
||||
except Exception as bipolar_err:
|
||||
print(f"⚠️ Bipolar check error: {bipolar_err}")
|
||||
logger.warning(f"Bipolar check error: {bipolar_err}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error executing autonomous action: {e}")
|
||||
logger.error(f"Error executing autonomous action: {e}")
|
||||
|
||||
|
||||
async def autonomous_reaction_tick_v2(guild_id: int):
|
||||
@@ -101,7 +104,7 @@ async def autonomous_reaction_tick_v2(guild_id: int):
|
||||
if not should_react:
|
||||
return
|
||||
|
||||
print(f"🤖 [V2] Scheduled reaction check triggered for server {guild_id}")
|
||||
logger.debug(f"[V2] Scheduled reaction check triggered for server {guild_id}")
|
||||
|
||||
try:
|
||||
from utils.autonomous_v1_legacy import miku_autonomous_reaction_for_server
|
||||
@@ -112,7 +115,7 @@ async def autonomous_reaction_tick_v2(guild_id: int):
|
||||
autonomous_engine.record_action(guild_id)
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error executing scheduled reaction: {e}")
|
||||
logger.error(f"Error executing scheduled reaction: {e}")
|
||||
|
||||
|
||||
def on_message_event(message):
|
||||
@@ -160,7 +163,7 @@ async def _check_and_react(guild_id: int, message):
|
||||
should_react = autonomous_engine.should_react_to_message(guild_id, message_age)
|
||||
|
||||
if should_react:
|
||||
print(f"🎯 [V2] Real-time reaction triggered for message from {message.author.display_name}")
|
||||
logger.info(f"[V2] Real-time reaction triggered for message from {message.author.display_name}")
|
||||
from utils.autonomous_v1_legacy import miku_autonomous_reaction_for_server
|
||||
await miku_autonomous_reaction_for_server(guild_id, force_message=message)
|
||||
|
||||
@@ -186,7 +189,7 @@ async def _check_and_act(guild_id: int):
|
||||
action_type = autonomous_engine.should_take_action(guild_id, triggered_by_message=True)
|
||||
|
||||
if action_type:
|
||||
print(f"🎯 [V2] Message triggered autonomous action: {action_type}")
|
||||
logger.info(f"[V2] Message triggered autonomous action: {action_type}")
|
||||
|
||||
# Execute the action directly (don't call autonomous_tick_v2 which would check again)
|
||||
from utils.autonomous_v1_legacy import (
|
||||
@@ -209,12 +212,12 @@ async def _check_and_act(guild_id: int):
|
||||
elif action_type == "change_profile_picture":
|
||||
# Get current mood for this server
|
||||
mood, _ = server_manager.get_server_mood(guild_id)
|
||||
print(f"🎨 [V2] Changing profile picture (mood: {mood})")
|
||||
logger.info(f"[V2] Changing profile picture (mood: {mood})")
|
||||
result = await profile_picture_manager.change_profile_picture(mood=mood, debug=True)
|
||||
if result["success"]:
|
||||
print(f"✅ Profile picture changed successfully!")
|
||||
logger.info(f"Profile picture changed successfully!")
|
||||
else:
|
||||
print(f"⚠️ Profile picture change failed: {result.get('error')}")
|
||||
logger.warning(f"Profile picture change failed: {result.get('error')}")
|
||||
|
||||
# Record that action was taken
|
||||
autonomous_engine.record_action(guild_id)
|
||||
@@ -232,10 +235,10 @@ async def _check_and_act(guild_id: int):
|
||||
if channel:
|
||||
await maybe_trigger_argument(channel, globals.client, "Triggered after message-based action")
|
||||
except Exception as bipolar_err:
|
||||
print(f"⚠️ Bipolar check error: {bipolar_err}")
|
||||
logger.warning(f"Bipolar check error: {bipolar_err}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error executing message-triggered action: {e}")
|
||||
logger.error(f"Error executing message-triggered action: {e}")
|
||||
|
||||
|
||||
def on_presence_update(member, before, after):
|
||||
@@ -256,7 +259,7 @@ def on_presence_update(member, before, after):
|
||||
# Track status changes
|
||||
if before.status != after.status:
|
||||
autonomous_engine.track_user_event(guild_id, "status_changed")
|
||||
print(f"👤 [V2] {member.display_name} status changed: {before.status} → {after.status}")
|
||||
logger.debug(f"[V2] {member.display_name} status changed: {before.status} → {after.status}")
|
||||
|
||||
# Track activity changes
|
||||
if before.activities != after.activities:
|
||||
@@ -272,7 +275,7 @@ def on_presence_update(member, before, after):
|
||||
"activity_started",
|
||||
{"activity_name": activity_name}
|
||||
)
|
||||
print(f"🎮 [V2] {member.display_name} started activity: {activity_name}")
|
||||
logger.debug(f"[V2] {member.display_name} started activity: {activity_name}")
|
||||
|
||||
|
||||
def on_member_join(member):
|
||||
@@ -310,17 +313,17 @@ async def periodic_decay_task():
|
||||
try:
|
||||
autonomous_engine.decay_events(guild_id)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error decaying events for guild {guild_id}: {e}")
|
||||
logger.warning(f"Error decaying events for guild {guild_id}: {e}")
|
||||
|
||||
# Save context to disk periodically
|
||||
try:
|
||||
autonomous_engine.save_context()
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error saving autonomous context: {e}")
|
||||
logger.error(f"Error saving autonomous context: {e}")
|
||||
|
||||
uptime_hours = (time.time() - task_start_time) / 3600
|
||||
print(f"🧹 [V2] Decay task completed (iteration #{iteration_count}, uptime: {uptime_hours:.1f}h)")
|
||||
print(f" └─ Processed {len(guild_ids)} servers")
|
||||
logger.debug(f"[V2] Decay task completed (iteration #{iteration_count}, uptime: {uptime_hours:.1f}h)")
|
||||
logger.debug(f" └─ Processed {len(guild_ids)} servers")
|
||||
|
||||
|
||||
def initialize_v2_system(client):
|
||||
@@ -328,7 +331,7 @@ def initialize_v2_system(client):
|
||||
Initialize the V2 autonomous system.
|
||||
Call this from bot.py on startup.
|
||||
"""
|
||||
print("🚀 Initializing Autonomous V2 System...")
|
||||
logger.debug("Initializing Autonomous V2 System...")
|
||||
|
||||
# Initialize mood states for all servers
|
||||
for guild_id, server_config in server_manager.servers.items():
|
||||
@@ -337,7 +340,7 @@ def initialize_v2_system(client):
|
||||
# Start decay task
|
||||
client.loop.create_task(periodic_decay_task())
|
||||
|
||||
print("✅ Autonomous V2 System initialized")
|
||||
logger.info("Autonomous V2 System initialized")
|
||||
|
||||
|
||||
# ========== Legacy Function Wrappers ==========
|
||||
|
||||
@@ -12,6 +12,9 @@ from typing import Dict, List, Optional
|
||||
from collections import deque
|
||||
import discord
|
||||
from .autonomous_persistence import save_autonomous_context, load_autonomous_context, apply_context_to_signals
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger('autonomous')
|
||||
|
||||
@dataclass
|
||||
class ContextSignals:
|
||||
@@ -238,13 +241,13 @@ class AutonomousEngine:
|
||||
time_since_startup = time.time() - self.bot_startup_time
|
||||
if time_since_startup < 120: # 2 minutes
|
||||
if debug:
|
||||
print(f"⏳ [V2 Debug] Startup cooldown active ({time_since_startup:.0f}s / 120s)")
|
||||
logger.debug(f"[V2 Debug] Startup cooldown active ({time_since_startup:.0f}s / 120s)")
|
||||
return None
|
||||
|
||||
# Never act when asleep
|
||||
if ctx.current_mood == "asleep":
|
||||
if debug:
|
||||
print(f"💤 [V2 Debug] Mood is 'asleep' - no action taken")
|
||||
logger.debug(f"[V2 Debug] Mood is 'asleep' - no action taken")
|
||||
return None
|
||||
|
||||
# Get mood personality
|
||||
@@ -254,14 +257,14 @@ class AutonomousEngine:
|
||||
self._update_activity_metrics(guild_id)
|
||||
|
||||
if debug:
|
||||
print(f"\n🔍 [V2 Debug] Decision Check for Guild {guild_id}")
|
||||
print(f" Triggered by message: {triggered_by_message}")
|
||||
print(f" Mood: {ctx.current_mood} (energy={profile['energy']:.2f}, sociability={profile['sociability']:.2f}, impulsiveness={profile['impulsiveness']:.2f})")
|
||||
print(f" Momentum: {ctx.conversation_momentum:.2f}")
|
||||
print(f" Messages (5min/1hr): {ctx.messages_last_5min}/{ctx.messages_last_hour}")
|
||||
print(f" Messages since appearance: {ctx.messages_since_last_appearance}")
|
||||
print(f" Time since last action: {ctx.time_since_last_action:.0f}s")
|
||||
print(f" Active activities: {len(ctx.users_started_activity)}")
|
||||
logger.debug(f"\n[V2 Debug] Decision Check for Guild {guild_id}")
|
||||
logger.debug(f" Triggered by message: {triggered_by_message}")
|
||||
logger.debug(f" Mood: {ctx.current_mood} (energy={profile['energy']:.2f}, sociability={profile['sociability']:.2f}, impulsiveness={profile['impulsiveness']:.2f})")
|
||||
logger.debug(f" Momentum: {ctx.conversation_momentum:.2f}")
|
||||
logger.debug(f" Messages (5min/1hr): {ctx.messages_last_5min}/{ctx.messages_last_hour}")
|
||||
logger.debug(f" Messages since appearance: {ctx.messages_since_last_appearance}")
|
||||
logger.debug(f" Time since last action: {ctx.time_since_last_action:.0f}s")
|
||||
logger.debug(f" Active activities: {len(ctx.users_started_activity)}")
|
||||
|
||||
# --- Decision Logic ---
|
||||
|
||||
@@ -272,7 +275,7 @@ class AutonomousEngine:
|
||||
# 1. CONVERSATION JOIN (high priority when momentum is high)
|
||||
if self._should_join_conversation(ctx, profile, debug):
|
||||
if debug:
|
||||
print(f"✅ [V2 Debug] DECISION: join_conversation")
|
||||
logger.debug(f"[V2 Debug] DECISION: join_conversation")
|
||||
return "join_conversation"
|
||||
|
||||
# 2. USER ENGAGEMENT (someone interesting appeared)
|
||||
@@ -280,17 +283,17 @@ class AutonomousEngine:
|
||||
if triggered_by_message:
|
||||
# Convert to join_conversation when message-triggered
|
||||
if debug:
|
||||
print(f"✅ [V2 Debug] DECISION: join_conversation (engage_user converted due to message trigger)")
|
||||
logger.debug(f"[V2 Debug] DECISION: join_conversation (engage_user converted due to message trigger)")
|
||||
return "join_conversation"
|
||||
if debug:
|
||||
print(f"✅ [V2 Debug] DECISION: engage_user")
|
||||
logger.debug(f"[V2 Debug] DECISION: engage_user")
|
||||
return "engage_user"
|
||||
|
||||
# 3. FOMO RESPONSE (lots of activity without her)
|
||||
# When FOMO triggers, join the conversation instead of saying something random
|
||||
if self._should_respond_to_fomo(ctx, profile, debug):
|
||||
if debug:
|
||||
print(f"✅ [V2 Debug] DECISION: join_conversation (FOMO)")
|
||||
logger.debug(f"[V2 Debug] DECISION: join_conversation (FOMO)")
|
||||
return "join_conversation" # Jump in and respond to what's being said
|
||||
|
||||
# 4. BORED/LONELY (quiet for too long, depending on mood)
|
||||
@@ -299,29 +302,29 @@ class AutonomousEngine:
|
||||
if self._should_break_silence(ctx, profile, debug):
|
||||
if triggered_by_message:
|
||||
if debug:
|
||||
print(f"✅ [V2 Debug] DECISION: join_conversation (break silence, but message just sent)")
|
||||
logger.debug(f"[V2 Debug] DECISION: join_conversation (break silence, but message just sent)")
|
||||
return "join_conversation" # Respond to the message instead of random general statement
|
||||
else:
|
||||
if debug:
|
||||
print(f"✅ [V2 Debug] DECISION: general (break silence)")
|
||||
logger.debug(f"[V2 Debug] DECISION: general (break silence)")
|
||||
return "general"
|
||||
|
||||
# 5. SHARE TWEET (low activity, wants to share something)
|
||||
# Skip this entirely when triggered by message - would be inappropriate to ignore user's message
|
||||
if not triggered_by_message and self._should_share_content(ctx, profile, debug):
|
||||
if debug:
|
||||
print(f"✅ [V2 Debug] DECISION: share_tweet")
|
||||
logger.debug(f"[V2 Debug] DECISION: share_tweet")
|
||||
return "share_tweet"
|
||||
|
||||
# 6. CHANGE PROFILE PICTURE (very rare, once per day)
|
||||
# Skip this entirely when triggered by message
|
||||
if not triggered_by_message and self._should_change_profile_picture(ctx, profile, debug):
|
||||
if debug:
|
||||
print(f"✅ [V2 Debug] DECISION: change_profile_picture")
|
||||
logger.debug(f"[V2 Debug] DECISION: change_profile_picture")
|
||||
return "change_profile_picture"
|
||||
|
||||
if debug:
|
||||
print(f"❌ [V2 Debug] DECISION: None (no conditions met)")
|
||||
logger.debug(f"[V2 Debug] DECISION: None (no conditions met)")
|
||||
|
||||
return None
|
||||
|
||||
@@ -341,10 +344,10 @@ class AutonomousEngine:
|
||||
result = all(conditions.values())
|
||||
|
||||
if debug:
|
||||
print(f" [Join Conv] momentum={ctx.conversation_momentum:.2f} > {mood_adjusted:.2f}? {conditions['momentum_check']}")
|
||||
print(f" [Join Conv] messages={ctx.messages_since_last_appearance} >= 5? {conditions['messages_check']}")
|
||||
print(f" [Join Conv] cooldown={ctx.time_since_last_action:.0f}s > 300s? {conditions['cooldown_check']}")
|
||||
print(f" [Join Conv] impulsive roll? {conditions['impulsiveness_roll']} | Result: {result}")
|
||||
logger.debug(f" [Join Conv] momentum={ctx.conversation_momentum:.2f} > {mood_adjusted:.2f}? {conditions['momentum_check']}")
|
||||
logger.debug(f" [Join Conv] messages={ctx.messages_since_last_appearance} >= 5? {conditions['messages_check']}")
|
||||
logger.debug(f" [Join Conv] cooldown={ctx.time_since_last_action:.0f}s > 300s? {conditions['cooldown_check']}")
|
||||
logger.debug(f" [Join Conv] impulsive roll? {conditions['impulsiveness_roll']} | Result: {result}")
|
||||
|
||||
return result
|
||||
|
||||
@@ -361,8 +364,8 @@ class AutonomousEngine:
|
||||
|
||||
if debug and has_activities:
|
||||
activities = [name for name, ts in ctx.users_started_activity]
|
||||
print(f" [Engage] activities={activities}, cooldown={ctx.time_since_last_action:.0f}s > 1800s? {cooldown_ok}")
|
||||
print(f" [Engage] roll={roll:.2f} < {threshold:.2f}? {roll_ok} | Result: {result}")
|
||||
logger.debug(f" [Engage] activities={activities}, cooldown={ctx.time_since_last_action:.0f}s > 1800s? {cooldown_ok}")
|
||||
logger.debug(f" [Engage] roll={roll:.2f} < {threshold:.2f}? {roll_ok} | Result: {result}")
|
||||
|
||||
return result
|
||||
|
||||
@@ -378,9 +381,9 @@ class AutonomousEngine:
|
||||
result = msgs_check and momentum_check and cooldown_check
|
||||
|
||||
if debug:
|
||||
print(f" [FOMO] messages={ctx.messages_since_last_appearance} > {fomo_threshold:.0f}? {msgs_check}")
|
||||
print(f" [FOMO] momentum={ctx.conversation_momentum:.2f} > 0.3? {momentum_check}")
|
||||
print(f" [FOMO] cooldown={ctx.time_since_last_action:.0f}s > 900s? {cooldown_check} | Result: {result}")
|
||||
logger.debug(f" [FOMO] messages={ctx.messages_since_last_appearance} > {fomo_threshold:.0f}? {msgs_check}")
|
||||
logger.debug(f" [FOMO] momentum={ctx.conversation_momentum:.2f} > 0.3? {momentum_check}")
|
||||
logger.debug(f" [FOMO] cooldown={ctx.time_since_last_action:.0f}s > 900s? {cooldown_check} | Result: {result}")
|
||||
|
||||
return result
|
||||
|
||||
@@ -397,9 +400,9 @@ class AutonomousEngine:
|
||||
result = quiet_check and silence_check and energy_ok
|
||||
|
||||
if debug:
|
||||
print(f" [Silence] msgs_last_hour={ctx.messages_last_hour} < 5? {quiet_check}")
|
||||
print(f" [Silence] time={ctx.time_since_last_action:.0f}s > {min_silence:.0f}s? {silence_check}")
|
||||
print(f" [Silence] energy roll={energy_roll:.2f} < {profile['energy']:.2f}? {energy_ok} | Result: {result}")
|
||||
logger.debug(f" [Silence] msgs_last_hour={ctx.messages_last_hour} < 5? {quiet_check}")
|
||||
logger.debug(f" [Silence] time={ctx.time_since_last_action:.0f}s > {min_silence:.0f}s? {silence_check}")
|
||||
logger.debug(f" [Silence] energy roll={energy_roll:.2f} < {profile['energy']:.2f}? {energy_ok} | Result: {result}")
|
||||
|
||||
return result
|
||||
|
||||
@@ -416,10 +419,10 @@ class AutonomousEngine:
|
||||
result = quiet_check and cooldown_check and energy_ok and mood_ok
|
||||
|
||||
if debug:
|
||||
print(f" [Share] msgs_last_hour={ctx.messages_last_hour} < 10? {quiet_check}")
|
||||
print(f" [Share] cooldown={ctx.time_since_last_action:.0f}s > 3600s? {cooldown_check}")
|
||||
print(f" [Share] energy roll={energy_roll:.2f} < {energy_threshold:.2f}? {energy_ok}")
|
||||
print(f" [Share] mood '{ctx.current_mood}' appropriate? {mood_ok} | Result: {result}")
|
||||
logger.debug(f" [Share] msgs_last_hour={ctx.messages_last_hour} < 10? {quiet_check}")
|
||||
logger.debug(f" [Share] cooldown={ctx.time_since_last_action:.0f}s > 3600s? {cooldown_check}")
|
||||
logger.debug(f" [Share] energy roll={energy_roll:.2f} < {energy_threshold:.2f}? {energy_ok}")
|
||||
logger.debug(f" [Share] mood '{ctx.current_mood}' appropriate? {mood_ok} | Result: {result}")
|
||||
|
||||
return result
|
||||
|
||||
@@ -447,11 +450,11 @@ class AutonomousEngine:
|
||||
|
||||
if hours_since_change < 20: # At least 20 hours between changes
|
||||
if debug:
|
||||
print(f" [PFP] Last change {hours_since_change:.1f}h ago, waiting...")
|
||||
logger.debug(f" [PFP] Last change {hours_since_change:.1f}h ago, waiting...")
|
||||
return False
|
||||
except Exception as e:
|
||||
if debug:
|
||||
print(f" [PFP] Error checking last change: {e}")
|
||||
logger.debug(f" [PFP] Error checking last change: {e}")
|
||||
|
||||
# Only consider changing during certain hours (10 AM - 10 PM)
|
||||
hour = ctx.hour_of_day
|
||||
@@ -472,11 +475,11 @@ class AutonomousEngine:
|
||||
result = time_check and quiet_check and cooldown_check and roll_ok
|
||||
|
||||
if debug:
|
||||
print(f" [PFP] hour={hour}, time_ok={time_check}")
|
||||
print(f" [PFP] msgs_last_hour={ctx.messages_last_hour} < 5? {quiet_check}")
|
||||
print(f" [PFP] cooldown={ctx.time_since_last_action:.0f}s > 5400s? {cooldown_check}")
|
||||
print(f" [PFP] mood_boost={mood_boost}, roll={roll:.4f} < {base_chance:.4f}? {roll_ok}")
|
||||
print(f" [PFP] Result: {result}")
|
||||
logger.debug(f" [PFP] hour={hour}, time_ok={time_check}")
|
||||
logger.debug(f" [PFP] msgs_last_hour={ctx.messages_last_hour} < 5? {quiet_check}")
|
||||
logger.debug(f" [PFP] cooldown={ctx.time_since_last_action:.0f}s > 5400s? {cooldown_check}")
|
||||
logger.debug(f" [PFP] mood_boost={mood_boost}, roll={roll:.4f} < {base_chance:.4f}? {roll_ok}")
|
||||
logger.debug(f" [PFP] Result: {result}")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@ import time
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
from datetime import datetime, timezone
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger('autonomous')
|
||||
|
||||
CONTEXT_FILE = Path("memory/autonomous_context.json")
|
||||
|
||||
@@ -48,9 +51,9 @@ def save_autonomous_context(server_contexts: dict, server_last_action: dict):
|
||||
CONTEXT_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(CONTEXT_FILE, 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
print(f"💾 [V2] Saved autonomous context for {len(server_contexts)} servers")
|
||||
logger.info(f"[V2] Saved autonomous context for {len(server_contexts)} servers")
|
||||
except Exception as e:
|
||||
print(f"⚠️ [V2] Failed to save autonomous context: {e}")
|
||||
logger.error(f"[V2] Failed to save autonomous context: {e}")
|
||||
|
||||
|
||||
def load_autonomous_context() -> tuple[Dict[int, dict], Dict[int, float]]:
|
||||
@@ -63,7 +66,7 @@ def load_autonomous_context() -> tuple[Dict[int, dict], Dict[int, float]]:
|
||||
- Timestamps are adjusted for elapsed time
|
||||
"""
|
||||
if not CONTEXT_FILE.exists():
|
||||
print("ℹ️ [V2] No saved context found, starting fresh")
|
||||
logger.info("[V2] No saved context found, starting fresh")
|
||||
return {}, {}
|
||||
|
||||
try:
|
||||
@@ -74,7 +77,7 @@ def load_autonomous_context() -> tuple[Dict[int, dict], Dict[int, float]]:
|
||||
downtime = time.time() - saved_at
|
||||
downtime_minutes = downtime / 60
|
||||
|
||||
print(f"📂 [V2] Loading context from {downtime_minutes:.1f} minutes ago")
|
||||
logger.info(f"[V2] Loading context from {downtime_minutes:.1f} minutes ago")
|
||||
|
||||
context_data = {}
|
||||
last_action = {}
|
||||
@@ -106,13 +109,13 @@ def load_autonomous_context() -> tuple[Dict[int, dict], Dict[int, float]]:
|
||||
if last_action_timestamp > 0:
|
||||
last_action[guild_id] = last_action_timestamp
|
||||
|
||||
print(f"✅ [V2] Restored context for {len(context_data)} servers")
|
||||
print(f" └─ Momentum decay factor: {decay_factor:.3f} (from {downtime_minutes:.1f}min downtime)")
|
||||
logger.info(f"[V2] Restored context for {len(context_data)} servers")
|
||||
logger.debug(f" └─ Momentum decay factor: {decay_factor:.3f} (from {downtime_minutes:.1f}min downtime)")
|
||||
|
||||
return context_data, last_action
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ [V2] Failed to load autonomous context: {e}")
|
||||
logger.error(f"[V2] Failed to load autonomous context: {e}")
|
||||
return {}, {}
|
||||
|
||||
|
||||
|
||||
@@ -23,6 +23,9 @@ from utils.image_handling import (
|
||||
convert_gif_to_mp4
|
||||
)
|
||||
from utils.sleep_responses import SLEEP_RESPONSES
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger('autonomous')
|
||||
|
||||
# Server-specific memory storage
|
||||
_server_autonomous_messages = {} # guild_id -> rotating buffer of last general messages
|
||||
@@ -48,7 +51,7 @@ def save_autonomous_config(config):
|
||||
def setup_autonomous_speaking():
|
||||
"""Setup autonomous speaking for all configured servers"""
|
||||
# This is now handled by the server manager
|
||||
print("🤖 Autonomous Miku setup delegated to server manager!")
|
||||
logger.debug("Autonomous Miku setup delegated to server manager!")
|
||||
|
||||
async def miku_autonomous_tick_for_server(guild_id: int, action_type="general", force=False, force_action=None):
|
||||
"""Run autonomous behavior for a specific server"""
|
||||
@@ -71,12 +74,12 @@ async def miku_say_something_general_for_server(guild_id: int):
|
||||
"""Miku says something general in a specific server"""
|
||||
server_config = server_manager.get_server_config(guild_id)
|
||||
if not server_config:
|
||||
print(f"⚠️ No config found for server {guild_id}")
|
||||
logger.warning(f"No config found for server {guild_id}")
|
||||
return
|
||||
|
||||
channel = globals.client.get_channel(server_config.autonomous_channel_id)
|
||||
if not channel:
|
||||
print(f"⚠️ Autonomous channel not found for server {guild_id}")
|
||||
logger.warning(f"Autonomous channel not found for server {guild_id}")
|
||||
return
|
||||
|
||||
# Check if evil mode is active
|
||||
@@ -123,7 +126,7 @@ async def miku_say_something_general_for_server(guild_id: int):
|
||||
message = await query_llama(prompt, user_id=f"miku-autonomous-{guild_id}", guild_id=guild_id, response_type="autonomous_general")
|
||||
if not is_too_similar(message, _server_autonomous_messages[guild_id]):
|
||||
break
|
||||
print("🔁 Response was too similar to past messages, retrying...")
|
||||
logger.debug("Response was too similar to past messages, retrying...")
|
||||
|
||||
try:
|
||||
await channel.send(message)
|
||||
@@ -131,9 +134,9 @@ async def miku_say_something_general_for_server(guild_id: int):
|
||||
if len(_server_autonomous_messages[guild_id]) > MAX_HISTORY:
|
||||
_server_autonomous_messages[guild_id].pop(0)
|
||||
character_name = "Evil Miku" if evil_mode else "Miku"
|
||||
print(f"💬 {character_name} said something general in #{channel.name} (Server: {server_config.guild_name})")
|
||||
logger.info(f"{character_name} said something general in #{channel.name} (Server: {server_config.guild_name})")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to send autonomous message: {e}")
|
||||
logger.error(f"Failed to send autonomous message: {e}")
|
||||
|
||||
async def miku_engage_random_user_for_server(guild_id: int, user_id: str = None, engagement_type: str = None):
|
||||
"""Miku engages a random user in a specific server
|
||||
@@ -145,17 +148,17 @@ async def miku_engage_random_user_for_server(guild_id: int, user_id: str = None,
|
||||
"""
|
||||
server_config = server_manager.get_server_config(guild_id)
|
||||
if not server_config:
|
||||
print(f"⚠️ No config found for server {guild_id}")
|
||||
logger.warning(f"No config found for server {guild_id}")
|
||||
return
|
||||
|
||||
guild = globals.client.get_guild(guild_id)
|
||||
if not guild:
|
||||
print(f"⚠️ Guild {guild_id} not found.")
|
||||
logger.warning(f"Guild {guild_id} not found.")
|
||||
return
|
||||
|
||||
channel = globals.client.get_channel(server_config.autonomous_channel_id)
|
||||
if not channel:
|
||||
print(f"⚠️ Autonomous channel not found for server {guild_id}")
|
||||
logger.warning(f"Autonomous channel not found for server {guild_id}")
|
||||
return
|
||||
|
||||
# Get target user
|
||||
@@ -164,14 +167,14 @@ async def miku_engage_random_user_for_server(guild_id: int, user_id: str = None,
|
||||
try:
|
||||
target = guild.get_member(int(user_id))
|
||||
if not target:
|
||||
print(f"⚠️ User {user_id} not found in server {guild_id}")
|
||||
logger.warning(f"User {user_id} not found in server {guild_id}")
|
||||
return
|
||||
if target.bot:
|
||||
print(f"⚠️ Cannot engage bot user {user_id}")
|
||||
logger.warning(f"Cannot engage bot user {user_id}")
|
||||
return
|
||||
print(f"🎯 Targeting specific user: {target.display_name} (ID: {user_id})")
|
||||
logger.info(f"Targeting specific user: {target.display_name} (ID: {user_id})")
|
||||
except ValueError:
|
||||
print(f"⚠️ Invalid user ID: {user_id}")
|
||||
logger.warning(f"Invalid user ID: {user_id}")
|
||||
return
|
||||
else:
|
||||
# Pick random user
|
||||
@@ -181,11 +184,11 @@ async def miku_engage_random_user_for_server(guild_id: int, user_id: str = None,
|
||||
]
|
||||
|
||||
if not members:
|
||||
print(f"😴 No available members to talk to in server {guild_id}.")
|
||||
logger.warning(f"No available members to talk to in server {guild_id}.")
|
||||
return
|
||||
|
||||
target = random.choice(members)
|
||||
print(f"🎲 Randomly selected user: {target.display_name}")
|
||||
logger.info(f"Randomly selected user: {target.display_name}")
|
||||
|
||||
time_of_day = get_time_of_day()
|
||||
|
||||
@@ -196,7 +199,7 @@ async def miku_engage_random_user_for_server(guild_id: int, user_id: str = None,
|
||||
now = time.time()
|
||||
last_time = _server_user_engagements[guild_id].get(target.id, 0)
|
||||
if now - last_time < 43200: # 12 hours in seconds
|
||||
print(f"⏱️ Recently engaged {target.display_name} in server {guild_id}, switching to general message.")
|
||||
logger.info(f"Recently engaged {target.display_name} in server {guild_id}, switching to general message.")
|
||||
await miku_say_something_general_for_server(guild_id)
|
||||
return
|
||||
|
||||
@@ -286,7 +289,7 @@ async def miku_engage_random_user_for_server(guild_id: int, user_id: str = None,
|
||||
)
|
||||
|
||||
if engagement_type:
|
||||
print(f"💬 Engagement type: {engagement_type}")
|
||||
logger.debug(f"Engagement type: {engagement_type}")
|
||||
|
||||
try:
|
||||
# Use consistent user_id for engaging users to enable conversation history
|
||||
@@ -294,9 +297,9 @@ async def miku_engage_random_user_for_server(guild_id: int, user_id: str = None,
|
||||
await channel.send(f"{target.mention} {message}")
|
||||
_server_user_engagements[guild_id][target.id] = time.time()
|
||||
character_name = "Evil Miku" if evil_mode else "Miku"
|
||||
print(f"👤 {character_name} engaged {display_name} in server {server_config.guild_name}")
|
||||
logger.info(f"{character_name} engaged {display_name} in server {server_config.guild_name}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to engage user: {e}")
|
||||
logger.error(f"Failed to engage user: {e}")
|
||||
|
||||
async def miku_detect_and_join_conversation_for_server(guild_id: int, force: bool = False):
|
||||
"""Miku detects and joins conversations in a specific server
|
||||
@@ -305,30 +308,30 @@ async def miku_detect_and_join_conversation_for_server(guild_id: int, force: boo
|
||||
guild_id: The server ID
|
||||
force: If True, bypass activity checks and random chance (for manual triggers)
|
||||
"""
|
||||
print(f"🔍 [Join Conv] Called for server {guild_id} (force={force})")
|
||||
logger.debug(f"[Join Conv] Called for server {guild_id} (force={force})")
|
||||
server_config = server_manager.get_server_config(guild_id)
|
||||
if not server_config:
|
||||
print(f"⚠️ No config found for server {guild_id}")
|
||||
logger.warning(f"No config found for server {guild_id}")
|
||||
return
|
||||
|
||||
channel = globals.client.get_channel(server_config.autonomous_channel_id)
|
||||
if not isinstance(channel, TextChannel):
|
||||
print(f"⚠️ Autonomous channel is invalid or not found for server {guild_id}")
|
||||
logger.warning(f"Autonomous channel is invalid or not found for server {guild_id}")
|
||||
return
|
||||
|
||||
# Fetch last 20 messages (for filtering)
|
||||
try:
|
||||
messages = [msg async for msg in channel.history(limit=20)]
|
||||
print(f"📜 [Join Conv] Fetched {len(messages)} messages from history")
|
||||
logger.debug(f"[Join Conv] Fetched {len(messages)} messages from history")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to fetch channel history for server {guild_id}: {e}")
|
||||
logger.error(f"Failed to fetch channel history for server {guild_id}: {e}")
|
||||
return
|
||||
|
||||
# Filter messages based on force mode
|
||||
if force:
|
||||
# When forced, use messages from real users (no time limit) - but limit to last 10
|
||||
recent_msgs = [msg for msg in messages if not msg.author.bot][:10]
|
||||
print(f"📊 [Join Conv] Force mode: Using last {len(recent_msgs)} messages from users (no time limit)")
|
||||
logger.debug(f"[Join Conv] Force mode: Using last {len(recent_msgs)} messages from users (no time limit)")
|
||||
else:
|
||||
# Normal mode: Filter to messages in last 10 minutes from real users (not bots)
|
||||
recent_msgs = [
|
||||
@@ -336,23 +339,23 @@ async def miku_detect_and_join_conversation_for_server(guild_id: int, force: boo
|
||||
if not msg.author.bot
|
||||
and (datetime.now(msg.created_at.tzinfo) - msg.created_at).total_seconds() < 600
|
||||
]
|
||||
print(f"📊 [Join Conv] Found {len(recent_msgs)} recent messages from users (last 10 min)")
|
||||
logger.debug(f"[Join Conv] Found {len(recent_msgs)} recent messages from users (last 10 min)")
|
||||
|
||||
user_ids = set(msg.author.id for msg in recent_msgs)
|
||||
|
||||
if not force:
|
||||
if len(recent_msgs) < 5 or len(user_ids) < 2:
|
||||
# Not enough activity
|
||||
print(f"⚠️ [Join Conv] Not enough activity: {len(recent_msgs)} messages, {len(user_ids)} users (need 5+ messages, 2+ users)")
|
||||
logger.debug(f"[Join Conv] Not enough activity: {len(recent_msgs)} messages, {len(user_ids)} users (need 5+ messages, 2+ users)")
|
||||
return
|
||||
|
||||
if random.random() > 0.5:
|
||||
print(f"🎲 [Join Conv] Random chance failed (50% chance)")
|
||||
logger.debug(f"[Join Conv] Random chance failed (50% chance)")
|
||||
return # 50% chance to engage
|
||||
else:
|
||||
print(f"✅ [Join Conv] Force mode - bypassing activity checks")
|
||||
logger.debug(f"[Join Conv] Force mode - bypassing activity checks")
|
||||
if len(recent_msgs) < 1:
|
||||
print(f"⚠️ [Join Conv] No messages found in channel history")
|
||||
logger.warning(f"[Join Conv] No messages found in channel history")
|
||||
return
|
||||
|
||||
# Use last 10 messages for context (oldest to newest)
|
||||
@@ -386,27 +389,27 @@ async def miku_detect_and_join_conversation_for_server(guild_id: int, force: boo
|
||||
reply = await query_llama(prompt, user_id=f"miku-conversation-{guild_id}", guild_id=guild_id, response_type="conversation_join")
|
||||
await channel.send(reply)
|
||||
character_name = "Evil Miku" if evil_mode else "Miku"
|
||||
print(f"💬 {character_name} joined an ongoing conversation in server {server_config.guild_name}")
|
||||
logger.info(f"{character_name} joined an ongoing conversation in server {server_config.guild_name}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to interject in conversation: {e}")
|
||||
logger.error(f"Failed to interject in conversation: {e}")
|
||||
|
||||
async def share_miku_tweet_for_server(guild_id: int):
|
||||
"""Share a Miku tweet in a specific server"""
|
||||
server_config = server_manager.get_server_config(guild_id)
|
||||
if not server_config:
|
||||
print(f"⚠️ No config found for server {guild_id}")
|
||||
logger.warning(f"No config found for server {guild_id}")
|
||||
return
|
||||
|
||||
channel = globals.client.get_channel(server_config.autonomous_channel_id)
|
||||
tweets = await fetch_miku_tweets(limit=5)
|
||||
if not tweets:
|
||||
print(f"📭 No good tweets found for server {guild_id}")
|
||||
logger.warning(f"No good tweets found for server {guild_id}")
|
||||
return
|
||||
|
||||
fresh_tweets = [t for t in tweets if t["url"] not in LAST_SENT_TWEETS]
|
||||
|
||||
if not fresh_tweets:
|
||||
print(f"⚠️ All fetched tweets were recently sent in server {guild_id}. Reusing tweets.")
|
||||
logger.warning(f"All fetched tweets were recently sent in server {guild_id}. Reusing tweets.")
|
||||
fresh_tweets = tweets
|
||||
|
||||
tweet = random.choice(fresh_tweets)
|
||||
@@ -454,12 +457,12 @@ async def handle_custom_prompt_for_server(guild_id: int, user_prompt: str):
|
||||
"""Handle custom prompt for a specific server"""
|
||||
server_config = server_manager.get_server_config(guild_id)
|
||||
if not server_config:
|
||||
print(f"⚠️ No config found for server {guild_id}")
|
||||
logger.warning(f"No config found for server {guild_id}")
|
||||
return False
|
||||
|
||||
channel = globals.client.get_channel(server_config.autonomous_channel_id)
|
||||
if not channel:
|
||||
print(f"⚠️ Autonomous channel not found for server {guild_id}")
|
||||
logger.warning(f"Autonomous channel not found for server {guild_id}")
|
||||
return False
|
||||
|
||||
mood = server_config.current_mood_name
|
||||
@@ -478,7 +481,7 @@ async def handle_custom_prompt_for_server(guild_id: int, user_prompt: str):
|
||||
# Use consistent user_id for manual prompts to enable conversation history
|
||||
message = await query_llama(prompt, user_id=f"miku-manual-{guild_id}", guild_id=guild_id, response_type="autonomous_general")
|
||||
await channel.send(message)
|
||||
print(f"🎤 Miku responded to custom prompt in server {server_config.guild_name}")
|
||||
logger.info(f"Miku responded to custom prompt in server {server_config.guild_name}")
|
||||
|
||||
# Add to server-specific message history
|
||||
if guild_id not in _server_autonomous_messages:
|
||||
@@ -489,7 +492,7 @@ async def handle_custom_prompt_for_server(guild_id: int, user_prompt: str):
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to send custom autonomous message: {e}")
|
||||
logger.error(f"Failed to send custom autonomous message: {e}")
|
||||
return False
|
||||
|
||||
# Legacy functions for backward compatibility - these now delegate to server-specific versions
|
||||
@@ -542,7 +545,7 @@ def load_last_sent_tweets():
|
||||
with open(LAST_SENT_TWEETS_FILE, "r", encoding="utf-8") as f:
|
||||
LAST_SENT_TWEETS = json.load(f)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to load last sent tweets: {e}")
|
||||
logger.error(f"Failed to load last sent tweets: {e}")
|
||||
LAST_SENT_TWEETS = []
|
||||
else:
|
||||
LAST_SENT_TWEETS = []
|
||||
@@ -552,7 +555,7 @@ def save_last_sent_tweets():
|
||||
with open(LAST_SENT_TWEETS_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(LAST_SENT_TWEETS, f)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to save last sent tweets: {e}")
|
||||
logger.error(f"Failed to save last sent tweets: {e}")
|
||||
|
||||
def get_time_of_day():
|
||||
hour = datetime.now().hour + 3
|
||||
@@ -602,7 +605,7 @@ async def _analyze_message_media(message):
|
||||
try:
|
||||
# Handle images
|
||||
if any(attachment.filename.lower().endswith(ext) for ext in [".jpg", ".jpeg", ".png", ".webp"]):
|
||||
print(f" 📸 Analyzing image for reaction: {attachment.filename}")
|
||||
logger.debug(f" Analyzing image for reaction: {attachment.filename}")
|
||||
base64_img = await download_and_encode_image(attachment.url)
|
||||
if base64_img:
|
||||
description = await analyze_image_with_qwen(base64_img)
|
||||
@@ -612,7 +615,7 @@ async def _analyze_message_media(message):
|
||||
elif any(attachment.filename.lower().endswith(ext) for ext in [".gif", ".mp4", ".webm", ".mov"]):
|
||||
is_gif = attachment.filename.lower().endswith('.gif')
|
||||
media_type = "GIF" if is_gif else "video"
|
||||
print(f" 🎬 Analyzing {media_type} for reaction: {attachment.filename}")
|
||||
logger.debug(f" Analyzing {media_type} for reaction: {attachment.filename}")
|
||||
|
||||
# Download media
|
||||
media_bytes_b64 = await download_and_encode_media(attachment.url)
|
||||
@@ -635,7 +638,7 @@ async def _analyze_message_media(message):
|
||||
return f"[{media_type}: {description}]"
|
||||
|
||||
except Exception as e:
|
||||
print(f" ⚠️ Error analyzing media for reaction: {e}")
|
||||
logger.warning(f" Error analyzing media for reaction: {e}")
|
||||
continue
|
||||
|
||||
return None
|
||||
@@ -650,25 +653,25 @@ async def miku_autonomous_reaction_for_server(guild_id: int, force_message=None,
|
||||
"""
|
||||
# 50% chance to proceed (unless forced or with a specific message)
|
||||
if not force and force_message is None and random.random() > 0.5:
|
||||
print(f"🎲 Autonomous reaction skipped for server {guild_id} (50% chance)")
|
||||
logger.debug(f"Autonomous reaction skipped for server {guild_id} (50% chance)")
|
||||
return
|
||||
|
||||
server_config = server_manager.get_server_config(guild_id)
|
||||
if not server_config:
|
||||
print(f"⚠️ No config found for server {guild_id}")
|
||||
logger.warning(f"No config found for server {guild_id}")
|
||||
return
|
||||
|
||||
server_name = server_config.guild_name
|
||||
|
||||
# Don't react if asleep
|
||||
if server_config.current_mood_name == "asleep" or server_config.is_sleeping:
|
||||
print(f"💤 [{server_name}] Miku is asleep, skipping autonomous reaction")
|
||||
logger.info(f"[{server_name}] Miku is asleep, skipping autonomous reaction")
|
||||
return
|
||||
|
||||
# Get the autonomous channel
|
||||
channel = globals.client.get_channel(server_config.autonomous_channel_id)
|
||||
if not channel:
|
||||
print(f"⚠️ [{server_name}] Autonomous channel not found")
|
||||
logger.warning(f"[{server_name}] Autonomous channel not found")
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -677,9 +680,9 @@ async def miku_autonomous_reaction_for_server(guild_id: int, force_message=None,
|
||||
target_message = force_message
|
||||
# Check if we've already reacted to this message
|
||||
if target_message.id in _reacted_message_ids:
|
||||
print(f"⏭️ [{server_name}] Already reacted to message {target_message.id}, skipping")
|
||||
logger.debug(f"[{server_name}] Already reacted to message {target_message.id}, skipping")
|
||||
return
|
||||
print(f"🎯 [{server_name}] Reacting to new message from {target_message.author.display_name}")
|
||||
logger.info(f"[{server_name}] Reacting to new message from {target_message.author.display_name}")
|
||||
else:
|
||||
# Fetch recent messages (last 50 messages to get more candidates)
|
||||
messages = []
|
||||
@@ -697,14 +700,14 @@ async def miku_autonomous_reaction_for_server(guild_id: int, force_message=None,
|
||||
messages.append(message)
|
||||
|
||||
if not messages:
|
||||
print(f"📭 [{server_name}] No recent unreacted messages to react to")
|
||||
logger.debug(f"[{server_name}] No recent unreacted messages to react to")
|
||||
return
|
||||
|
||||
# Pick a random message from the recent ones
|
||||
target_message = random.choice(messages)
|
||||
|
||||
# Analyze any media in the message
|
||||
print(f"🔍 [{server_name}] Analyzing message for reaction from {target_message.author.display_name}")
|
||||
logger.debug(f"[{server_name}] Analyzing message for reaction from {target_message.author.display_name}")
|
||||
media_description = await _analyze_message_media(target_message)
|
||||
|
||||
# Build message content with media description if present
|
||||
@@ -764,7 +767,7 @@ async def miku_autonomous_reaction_for_server(guild_id: int, force_message=None,
|
||||
emoji = emojis[0]
|
||||
else:
|
||||
# No emoji found in response, use fallback
|
||||
print(f"⚠️ [{server_name}] LLM response contained no emoji: '{original_response[:50]}' - using fallback")
|
||||
logger.warning(f"[{server_name}] LLM response contained no emoji: '{original_response[:50]}' - using fallback")
|
||||
emoji = "💙"
|
||||
|
||||
# Final validation: try adding the reaction
|
||||
@@ -772,7 +775,7 @@ async def miku_autonomous_reaction_for_server(guild_id: int, force_message=None,
|
||||
await target_message.add_reaction(emoji)
|
||||
except discord.HTTPException as e:
|
||||
if "Unknown Emoji" in str(e):
|
||||
print(f"❌ [{server_name}] Invalid emoji from LLM: '{original_response[:50]}' - using fallback")
|
||||
logger.warning(f"[{server_name}] Invalid emoji from LLM: '{original_response[:50]}' - using fallback")
|
||||
emoji = "💙"
|
||||
await target_message.add_reaction(emoji)
|
||||
else:
|
||||
@@ -789,14 +792,14 @@ async def miku_autonomous_reaction_for_server(guild_id: int, force_message=None,
|
||||
for msg_id in ids_to_remove:
|
||||
_reacted_message_ids.discard(msg_id)
|
||||
|
||||
print(f"✅ [{server_name}] Autonomous reaction: Added {emoji} to message from {target_message.author.display_name}")
|
||||
logger.info(f"[{server_name}] Autonomous reaction: Added {emoji} to message from {target_message.author.display_name}")
|
||||
|
||||
except discord.Forbidden:
|
||||
print(f"❌ [{server_name}] Missing permissions to add reactions")
|
||||
logger.error(f"[{server_name}] Missing permissions to add reactions")
|
||||
except discord.HTTPException as e:
|
||||
print(f"❌ [{server_name}] Failed to add reaction: {e}")
|
||||
logger.error(f"[{server_name}] Failed to add reaction: {e}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ [{server_name}] Error in autonomous reaction: {e}")
|
||||
logger.error(f"[{server_name}] Error in autonomous reaction: {e}")
|
||||
|
||||
async def miku_autonomous_reaction(force=False):
|
||||
"""Legacy function - run autonomous reactions for all servers
|
||||
@@ -816,14 +819,14 @@ async def miku_autonomous_reaction_for_dm(user_id: int, force_message=None):
|
||||
"""
|
||||
# 50% chance to proceed (unless forced with a specific message)
|
||||
if force_message is None and random.random() > 0.5:
|
||||
print(f"🎲 DM reaction skipped for user {user_id} (50% chance)")
|
||||
logger.debug(f"DM reaction skipped for user {user_id} (50% chance)")
|
||||
return
|
||||
|
||||
# Get the user object
|
||||
try:
|
||||
user = await globals.client.fetch_user(user_id)
|
||||
if not user:
|
||||
print(f"⚠️ Could not find user {user_id}")
|
||||
logger.warning(f"Could not find user {user_id}")
|
||||
return
|
||||
|
||||
dm_channel = user.dm_channel
|
||||
@@ -833,7 +836,7 @@ async def miku_autonomous_reaction_for_dm(user_id: int, force_message=None):
|
||||
username = user.display_name
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error fetching DM channel for user {user_id}: {e}")
|
||||
logger.error(f"Error fetching DM channel for user {user_id}: {e}")
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -842,9 +845,9 @@ async def miku_autonomous_reaction_for_dm(user_id: int, force_message=None):
|
||||
target_message = force_message
|
||||
# Check if we've already reacted to this message
|
||||
if target_message.id in _reacted_message_ids:
|
||||
print(f"⏭️ [DM: {username}] Already reacted to message {target_message.id}, skipping")
|
||||
logger.debug(f"[DM: {username}] Already reacted to message {target_message.id}, skipping")
|
||||
return
|
||||
print(f"🎯 [DM: {username}] Reacting to new message")
|
||||
logger.info(f"[DM: {username}] Reacting to new message")
|
||||
else:
|
||||
# Fetch recent messages from DM (last 50 messages)
|
||||
messages = []
|
||||
@@ -862,14 +865,14 @@ async def miku_autonomous_reaction_for_dm(user_id: int, force_message=None):
|
||||
messages.append(message)
|
||||
|
||||
if not messages:
|
||||
print(f"📭 [DM: {username}] No recent unreacted messages to react to")
|
||||
logger.debug(f"[DM: {username}] No recent unreacted messages to react to")
|
||||
return
|
||||
|
||||
# Pick a random message from the recent ones
|
||||
target_message = random.choice(messages)
|
||||
|
||||
# Analyze any media in the message
|
||||
print(f"🔍 [DM: {username}] Analyzing message for reaction")
|
||||
logger.debug(f"[DM: {username}] Analyzing message for reaction")
|
||||
media_description = await _analyze_message_media(target_message)
|
||||
|
||||
# Build message content with media description if present
|
||||
@@ -929,7 +932,7 @@ async def miku_autonomous_reaction_for_dm(user_id: int, force_message=None):
|
||||
emoji = emojis[0]
|
||||
else:
|
||||
# No emoji found in response, use fallback
|
||||
print(f"⚠️ [DM: {username}] LLM response contained no emoji: '{original_response[:50]}' - using fallback")
|
||||
logger.warning(f"[DM: {username}] LLM response contained no emoji: '{original_response[:50]}' - using fallback")
|
||||
emoji = "💙"
|
||||
|
||||
# Final validation: try adding the reaction
|
||||
@@ -937,7 +940,7 @@ async def miku_autonomous_reaction_for_dm(user_id: int, force_message=None):
|
||||
await target_message.add_reaction(emoji)
|
||||
except discord.HTTPException as e:
|
||||
if "Unknown Emoji" in str(e):
|
||||
print(f"❌ [DM: {username}] Invalid emoji from LLM: '{original_response[:50]}' - using fallback")
|
||||
logger.warning(f"[DM: {username}] Invalid emoji from LLM: '{original_response[:50]}' - using fallback")
|
||||
emoji = "💙"
|
||||
await target_message.add_reaction(emoji)
|
||||
else:
|
||||
@@ -954,14 +957,14 @@ async def miku_autonomous_reaction_for_dm(user_id: int, force_message=None):
|
||||
for msg_id in ids_to_remove:
|
||||
_reacted_message_ids.discard(msg_id)
|
||||
|
||||
print(f"✅ [DM: {username}] Autonomous reaction: Added {emoji} to message")
|
||||
logger.info(f"[DM: {username}] Autonomous reaction: Added {emoji} to message")
|
||||
|
||||
except discord.Forbidden:
|
||||
print(f"❌ [DM: {username}] Missing permissions to add reactions")
|
||||
logger.error(f"[DM: {username}] Missing permissions to add reactions")
|
||||
except discord.HTTPException as e:
|
||||
print(f"❌ [DM: {username}] Failed to add reaction: {e}")
|
||||
logger.error(f"[DM: {username}] Failed to add reaction: {e}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ [DM: {username}] Error in autonomous reaction: {e}")
|
||||
logger.error(f"[DM: {username}] Error in autonomous reaction: {e}")
|
||||
|
||||
|
||||
async def miku_update_profile_picture_for_server(guild_id: int):
|
||||
@@ -973,18 +976,18 @@ async def miku_update_profile_picture_for_server(guild_id: int):
|
||||
|
||||
# Check if enough time has passed
|
||||
if not should_update_profile_picture():
|
||||
print(f"📸 [Server: {guild_id}] Profile picture not ready for update yet")
|
||||
logger.debug(f"[Server: {guild_id}] Profile picture not ready for update yet")
|
||||
return
|
||||
|
||||
# Get server config to use current mood
|
||||
server_config = server_manager.get_server_config(guild_id)
|
||||
if not server_config:
|
||||
print(f"⚠️ No config found for server {guild_id}")
|
||||
logger.warning(f"No config found for server {guild_id}")
|
||||
return
|
||||
|
||||
mood = server_config.current_mood_name
|
||||
|
||||
print(f"📸 [Server: {guild_id}] Attempting profile picture update (mood: {mood})")
|
||||
logger.info(f"[Server: {guild_id}] Attempting profile picture update (mood: {mood})")
|
||||
|
||||
try:
|
||||
success = await update_profile_picture(globals.client, mood=mood)
|
||||
@@ -1001,9 +1004,9 @@ async def miku_update_profile_picture_for_server(guild_id: int):
|
||||
"*updates avatar* Time for a fresh look! ✨"
|
||||
]
|
||||
await channel.send(random.choice(messages))
|
||||
print(f"✅ [Server: {guild_id}] Profile picture updated and announced!")
|
||||
logger.info(f"[Server: {guild_id}] Profile picture updated and announced!")
|
||||
else:
|
||||
print(f"⚠️ [Server: {guild_id}] Profile picture update failed")
|
||||
logger.warning(f"[Server: {guild_id}] Profile picture update failed")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ [Server: {guild_id}] Error updating profile picture: {e}")
|
||||
logger.error(f"[Server: {guild_id}] Error updating profile picture: {e}")
|
||||
|
||||
@@ -11,6 +11,9 @@ import random
|
||||
import asyncio
|
||||
import discord
|
||||
import globals
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger('persona')
|
||||
|
||||
# ============================================================================
|
||||
# CONSTANTS
|
||||
@@ -38,26 +41,26 @@ def save_bipolar_state():
|
||||
}
|
||||
with open(BIPOLAR_STATE_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(state, f, indent=2)
|
||||
print(f"💾 Saved bipolar mode state: enabled={globals.BIPOLAR_MODE}")
|
||||
logger.info(f"Saved bipolar mode state: enabled={globals.BIPOLAR_MODE}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to save bipolar mode state: {e}")
|
||||
logger.error(f"Failed to save bipolar mode state: {e}")
|
||||
|
||||
|
||||
def load_bipolar_state():
|
||||
"""Load bipolar mode state from JSON file"""
|
||||
try:
|
||||
if not os.path.exists(BIPOLAR_STATE_FILE):
|
||||
print("ℹ️ No bipolar mode state file found, using defaults")
|
||||
logger.info("No bipolar mode state file found, using defaults")
|
||||
return False
|
||||
|
||||
with open(BIPOLAR_STATE_FILE, "r", encoding="utf-8") as f:
|
||||
state = json.load(f)
|
||||
|
||||
bipolar_mode = state.get("bipolar_mode_enabled", False)
|
||||
print(f"📂 Loaded bipolar mode state: enabled={bipolar_mode}")
|
||||
logger.info(f"Loaded bipolar mode state: enabled={bipolar_mode}")
|
||||
return bipolar_mode
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to load bipolar mode state: {e}")
|
||||
logger.error(f"Failed to load bipolar mode state: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -71,16 +74,16 @@ def save_webhooks():
|
||||
|
||||
with open(BIPOLAR_WEBHOOKS_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(webhooks_data, f, indent=2)
|
||||
print(f"💾 Saved bipolar webhooks for {len(webhooks_data)} server(s)")
|
||||
logger.info(f"Saved bipolar webhooks for {len(webhooks_data)} server(s)")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to save bipolar webhooks: {e}")
|
||||
logger.error(f"Failed to save bipolar webhooks: {e}")
|
||||
|
||||
|
||||
def load_webhooks():
|
||||
"""Load webhook URLs from JSON file"""
|
||||
try:
|
||||
if not os.path.exists(BIPOLAR_WEBHOOKS_FILE):
|
||||
print("ℹ️ No bipolar webhooks file found")
|
||||
logger.info("No bipolar webhooks file found")
|
||||
return {}
|
||||
|
||||
with open(BIPOLAR_WEBHOOKS_FILE, "r", encoding="utf-8") as f:
|
||||
@@ -91,10 +94,10 @@ def load_webhooks():
|
||||
for guild_id_str, webhook_data in webhooks_data.items():
|
||||
webhooks[int(guild_id_str)] = webhook_data
|
||||
|
||||
print(f"📂 Loaded bipolar webhooks for {len(webhooks)} server(s)")
|
||||
logger.info(f"Loaded bipolar webhooks for {len(webhooks)} server(s)")
|
||||
return webhooks
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to load bipolar webhooks: {e}")
|
||||
logger.error(f"Failed to load bipolar webhooks: {e}")
|
||||
return {}
|
||||
|
||||
|
||||
@@ -105,8 +108,8 @@ def restore_bipolar_mode_on_startup():
|
||||
globals.BIPOLAR_WEBHOOKS = load_webhooks()
|
||||
|
||||
if bipolar_mode:
|
||||
print("🔄 Bipolar mode restored from previous session")
|
||||
print("💬 Persona dialogue system enabled (natural conversations + arguments)")
|
||||
logger.info("Bipolar mode restored from previous session")
|
||||
logger.info("Persona dialogue system enabled (natural conversations + arguments)")
|
||||
|
||||
return bipolar_mode
|
||||
|
||||
@@ -124,7 +127,7 @@ def load_scoreboard() -> dict:
|
||||
with open(BIPOLAR_SCOREBOARD_FILE, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to load scoreboard: {e}")
|
||||
logger.error(f"Failed to load scoreboard: {e}")
|
||||
return {"miku": 0, "evil": 0, "history": []}
|
||||
|
||||
|
||||
@@ -134,9 +137,9 @@ def save_scoreboard(scoreboard: dict):
|
||||
os.makedirs(os.path.dirname(BIPOLAR_SCOREBOARD_FILE), exist_ok=True)
|
||||
with open(BIPOLAR_SCOREBOARD_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(scoreboard, f, indent=2)
|
||||
print(f"💾 Saved scoreboard: Miku {scoreboard['miku']} - {scoreboard['evil']} Evil Miku")
|
||||
logger.info(f"Saved scoreboard: Miku {scoreboard['miku']} - {scoreboard['evil']} Evil Miku")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to save scoreboard: {e}")
|
||||
logger.error(f"Failed to save scoreboard: {e}")
|
||||
|
||||
|
||||
def record_argument_result(winner: str, exchanges: int, reasoning: str = ""):
|
||||
@@ -205,7 +208,7 @@ def enable_bipolar_mode():
|
||||
"""Enable bipolar mode"""
|
||||
globals.BIPOLAR_MODE = True
|
||||
save_bipolar_state()
|
||||
print("🔄 Bipolar mode enabled!")
|
||||
logger.info("Bipolar mode enabled!")
|
||||
|
||||
|
||||
def disable_bipolar_mode():
|
||||
@@ -214,7 +217,7 @@ def disable_bipolar_mode():
|
||||
# Clear any ongoing arguments
|
||||
globals.BIPOLAR_ARGUMENT_IN_PROGRESS.clear()
|
||||
save_bipolar_state()
|
||||
print("🔄 Bipolar mode disabled!")
|
||||
logger.info("Bipolar mode disabled!")
|
||||
|
||||
|
||||
def toggle_bipolar_mode() -> bool:
|
||||
@@ -256,11 +259,11 @@ async def get_or_create_webhooks_for_channel(channel: discord.TextChannel) -> di
|
||||
if miku_webhook and evil_webhook:
|
||||
return {"miku": miku_webhook, "evil_miku": evil_webhook}
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to retrieve cached webhooks: {e}")
|
||||
logger.warning(f"Failed to retrieve cached webhooks: {e}")
|
||||
|
||||
# Create new webhooks
|
||||
try:
|
||||
print(f"🔧 Creating bipolar webhooks for channel #{channel.name}")
|
||||
logger.info(f"Creating bipolar webhooks for channel #{channel.name}")
|
||||
|
||||
# Load avatar images
|
||||
miku_avatar = None
|
||||
@@ -300,14 +303,14 @@ async def get_or_create_webhooks_for_channel(channel: discord.TextChannel) -> di
|
||||
}
|
||||
save_webhooks()
|
||||
|
||||
print(f"✅ Created bipolar webhooks for #{channel.name}")
|
||||
logger.info(f"Created bipolar webhooks for #{channel.name}")
|
||||
return {"miku": miku_webhook, "evil_miku": evil_webhook}
|
||||
|
||||
except discord.Forbidden:
|
||||
print(f"❌ Missing permissions to create webhooks in #{channel.name}")
|
||||
logger.error(f"Missing permissions to create webhooks in #{channel.name}")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to create webhooks: {e}")
|
||||
logger.error(f"Failed to create webhooks: {e}")
|
||||
return None
|
||||
|
||||
|
||||
@@ -322,11 +325,11 @@ async def cleanup_webhooks(client):
|
||||
await webhook.delete(reason="Bipolar mode cleanup")
|
||||
cleaned_count += 1
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to cleanup webhooks in {guild.name}: {e}")
|
||||
logger.warning(f"Failed to cleanup webhooks in {guild.name}: {e}")
|
||||
|
||||
globals.BIPOLAR_WEBHOOKS.clear()
|
||||
save_webhooks()
|
||||
print(f"🧹 Cleaned up {cleaned_count} bipolar webhook(s)")
|
||||
logger.info(f"Cleaned up {cleaned_count} bipolar webhook(s)")
|
||||
return cleaned_count
|
||||
|
||||
|
||||
@@ -602,7 +605,7 @@ async def judge_argument_winner(conversation_log: list, guild_id: int) -> tuple[
|
||||
)
|
||||
|
||||
if not judgment or judgment.startswith("Error"):
|
||||
print("⚠️ Arbiter failed to make judgment, defaulting to draw")
|
||||
logger.warning("Arbiter failed to make judgment, defaulting to draw")
|
||||
return "draw", "The arbiter could not make a decision."
|
||||
|
||||
# Parse the judgment - look at the first line/sentence for the decision
|
||||
@@ -610,37 +613,37 @@ async def judge_argument_winner(conversation_log: list, guild_id: int) -> tuple[
|
||||
first_line = judgment_lines[0].strip().strip('"').strip()
|
||||
first_line_lower = first_line.lower()
|
||||
|
||||
print(f"🔍 Parsing arbiter first line: '{first_line}'")
|
||||
logger.debug(f"Parsing arbiter first line: '{first_line}'")
|
||||
|
||||
# Check the first line for the decision - be very specific
|
||||
# The arbiter should respond with ONLY the name on the first line
|
||||
if first_line_lower == "evil miku":
|
||||
winner = "evil"
|
||||
print("✅ Detected Evil Miku win from first line exact match")
|
||||
logger.debug("Detected Evil Miku win from first line exact match")
|
||||
elif first_line_lower == "hatsune miku":
|
||||
winner = "miku"
|
||||
print("✅ Detected Hatsune Miku win from first line exact match")
|
||||
logger.debug("Detected Hatsune Miku win from first line exact match")
|
||||
elif first_line_lower == "draw":
|
||||
winner = "draw"
|
||||
print("✅ Detected Draw from first line exact match")
|
||||
logger.debug("Detected Draw from first line exact match")
|
||||
elif "evil miku" in first_line_lower and "hatsune" not in first_line_lower:
|
||||
# First line mentions Evil Miku but not Hatsune Miku
|
||||
winner = "evil"
|
||||
print("✅ Detected Evil Miku win from first line (contains 'evil miku' only)")
|
||||
logger.debug("Detected Evil Miku win from first line (contains 'evil miku' only)")
|
||||
elif "hatsune miku" in first_line_lower and "evil" not in first_line_lower:
|
||||
# First line mentions Hatsune Miku but not Evil Miku
|
||||
winner = "miku"
|
||||
print("✅ Detected Hatsune Miku win from first line (contains 'hatsune miku' only)")
|
||||
logger.debug("Detected Hatsune Miku win from first line (contains 'hatsune miku' only)")
|
||||
else:
|
||||
# Fallback: check the whole judgment
|
||||
print(f"⚠️ First line ambiguous, using fallback counting method")
|
||||
logger.debug(f"First line ambiguous, using fallback counting method")
|
||||
judgment_lower = judgment.lower()
|
||||
# Count mentions to break ties
|
||||
evil_count = judgment_lower.count("evil miku")
|
||||
miku_count = judgment_lower.count("hatsune miku")
|
||||
draw_count = judgment_lower.count("draw")
|
||||
|
||||
print(f"📊 Counts - Evil: {evil_count}, Miku: {miku_count}, Draw: {draw_count}")
|
||||
logger.debug(f"Counts - Evil: {evil_count}, Miku: {miku_count}, Draw: {draw_count}")
|
||||
|
||||
if draw_count > 0 and draw_count >= evil_count and draw_count >= miku_count:
|
||||
winner = "draw"
|
||||
@@ -654,7 +657,7 @@ async def judge_argument_winner(conversation_log: list, guild_id: int) -> tuple[
|
||||
return winner, judgment
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error in arbiter judgment: {e}")
|
||||
logger.error(f"Error in arbiter judgment: {e}")
|
||||
return "draw", "An error occurred during judgment."
|
||||
|
||||
|
||||
@@ -756,13 +759,13 @@ async def run_argument(channel: discord.TextChannel, client, trigger_context: st
|
||||
guild_id = channel.guild.id
|
||||
|
||||
if is_argument_in_progress(channel_id):
|
||||
print(f"⚠️ Argument already in progress in #{channel.name}")
|
||||
logger.warning(f"Argument already in progress in #{channel.name}")
|
||||
return
|
||||
|
||||
# Get webhooks for this channel
|
||||
webhooks = await get_or_create_webhooks_for_channel(channel)
|
||||
if not webhooks:
|
||||
print(f"❌ Could not create webhooks for argument in #{channel.name}")
|
||||
logger.error(f"Could not create webhooks for argument in #{channel.name}")
|
||||
return
|
||||
|
||||
# Determine who initiates based on starting_message or inactive persona
|
||||
@@ -773,12 +776,12 @@ async def run_argument(channel: discord.TextChannel, client, trigger_context: st
|
||||
is_evil_message = globals.EVIL_MODE or (starting_message.webhook_id is not None and "Evil" in (starting_message.author.name or ""))
|
||||
initiator = "miku" if is_evil_message else "evil" # Opposite persona responds
|
||||
last_message = starting_message.content
|
||||
print(f"🔄 Starting argument from message, responder: {initiator}")
|
||||
logger.info(f"Starting argument from message, responder: {initiator}")
|
||||
else:
|
||||
# The inactive persona breaks through
|
||||
initiator = get_inactive_persona()
|
||||
last_message = None
|
||||
print(f"🔄 Starting bipolar argument in #{channel.name}, initiated by {initiator}")
|
||||
logger.info(f"Starting bipolar argument in #{channel.name}, initiated by {initiator}")
|
||||
|
||||
start_argument(channel_id, initiator)
|
||||
|
||||
@@ -812,7 +815,7 @@ async def run_argument(channel: discord.TextChannel, client, trigger_context: st
|
||||
globals.EVIL_MODE = original_evil_mode
|
||||
|
||||
if not initial_message or initial_message.startswith("Error") or initial_message.startswith("Sorry"):
|
||||
print("❌ Failed to generate initial argument message")
|
||||
logger.error("Failed to generate initial argument message")
|
||||
end_argument(channel_id)
|
||||
return
|
||||
|
||||
@@ -877,22 +880,22 @@ async def run_argument(channel: discord.TextChannel, client, trigger_context: st
|
||||
if should_end:
|
||||
exchange_count = globals.BIPOLAR_ARGUMENT_IN_PROGRESS.get(channel_id, {}).get("exchange_count", 0)
|
||||
|
||||
print(f"⚖️ Argument complete with {exchange_count} exchanges. Calling arbiter...")
|
||||
logger.info(f"Argument complete with {exchange_count} exchanges. Calling arbiter...")
|
||||
|
||||
# Use arbiter to judge the winner
|
||||
winner, judgment = await judge_argument_winner(conversation_log, guild_id)
|
||||
|
||||
print(f"⚖️ Arbiter decision: {winner}")
|
||||
print(f"📝 Judgment: {judgment}")
|
||||
logger.info(f"Arbiter decision: {winner}")
|
||||
logger.info(f"Judgment: {judgment}")
|
||||
|
||||
# If it's a draw, continue the argument instead of ending
|
||||
if winner == "draw":
|
||||
print("🤝 Arbiter ruled it's still a draw - argument continues...")
|
||||
logger.info("Arbiter ruled it's still a draw - argument continues...")
|
||||
# Reduce the end chance by 5% (but don't go below 5%)
|
||||
current_end_chance = globals.BIPOLAR_ARGUMENT_IN_PROGRESS[channel_id].get("end_chance", 0.1)
|
||||
new_end_chance = max(0.05, current_end_chance - 0.05)
|
||||
globals.BIPOLAR_ARGUMENT_IN_PROGRESS[channel_id]["end_chance"] = new_end_chance
|
||||
print(f"📉 Reduced end chance to {new_end_chance*100:.0f}% - argument continues...")
|
||||
logger.info(f"Reduced end chance to {new_end_chance*100:.0f}% - argument continues...")
|
||||
# Don't end, just continue to the next exchange
|
||||
else:
|
||||
# Clear winner - generate final triumphant message
|
||||
@@ -938,10 +941,10 @@ async def run_argument(channel: discord.TextChannel, client, trigger_context: st
|
||||
# Switch to winner's mode (including role color)
|
||||
from utils.evil_mode import apply_evil_mode_changes, revert_evil_mode_changes
|
||||
if winner == "evil":
|
||||
print("👿 Evil Miku won! Switching to Evil Mode...")
|
||||
logger.info("Evil Miku won! Switching to Evil Mode...")
|
||||
await apply_evil_mode_changes(client, change_username=True, change_pfp=True, change_nicknames=True, change_role_color=True)
|
||||
else:
|
||||
print("💙 Hatsune Miku won! Switching to Normal Mode...")
|
||||
logger.info("Hatsune Miku won! Switching to Normal Mode...")
|
||||
await revert_evil_mode_changes(client, change_username=True, change_pfp=True, change_nicknames=True, change_role_color=True)
|
||||
|
||||
# Clean up argument conversation history
|
||||
@@ -951,7 +954,7 @@ async def run_argument(channel: discord.TextChannel, client, trigger_context: st
|
||||
pass # History cleanup is not critical
|
||||
|
||||
end_argument(channel_id)
|
||||
print(f"✅ Argument ended in #{channel.name}, winner: {winner}")
|
||||
logger.info(f"Argument ended in #{channel.name}, winner: {winner}")
|
||||
return
|
||||
|
||||
# Get current speaker
|
||||
@@ -982,7 +985,7 @@ async def run_argument(channel: discord.TextChannel, client, trigger_context: st
|
||||
globals.EVIL_MODE = original_evil_mode
|
||||
|
||||
if not response or response.startswith("Error") or response.startswith("Sorry"):
|
||||
print(f"❌ Failed to generate argument response")
|
||||
logger.error(f"Failed to generate argument response")
|
||||
end_argument(channel_id)
|
||||
return
|
||||
|
||||
@@ -1021,7 +1024,7 @@ async def run_argument(channel: discord.TextChannel, client, trigger_context: st
|
||||
is_first_response = False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Argument error: {e}")
|
||||
logger.error(f"Argument error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
end_argument(channel_id)
|
||||
@@ -1057,11 +1060,11 @@ async def force_trigger_argument(channel: discord.TextChannel, client, context:
|
||||
starting_message: Optional message to use as the first message in the argument
|
||||
"""
|
||||
if not globals.BIPOLAR_MODE:
|
||||
print("⚠️ Cannot trigger argument - bipolar mode is not enabled")
|
||||
logger.warning("Cannot trigger argument - bipolar mode is not enabled")
|
||||
return False
|
||||
|
||||
if is_argument_in_progress(channel.id):
|
||||
print("⚠️ Argument already in progress in this channel")
|
||||
logger.warning("Argument already in progress in this channel")
|
||||
return False
|
||||
|
||||
asyncio.create_task(run_argument(channel, client, context, starting_message))
|
||||
|
||||
@@ -5,13 +5,18 @@ Replaces the vector search system with organized, complete context.
|
||||
Preserves original content files in their entirety.
|
||||
"""
|
||||
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger('core')
|
||||
|
||||
|
||||
def get_original_miku_lore() -> str:
|
||||
"""Load the complete, unmodified miku_lore.txt file"""
|
||||
try:
|
||||
with open("miku_lore.txt", "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to load miku_lore.txt: {e}")
|
||||
logger.error(f"Failed to load miku_lore.txt: {e}")
|
||||
return "## MIKU LORE\n[File could not be loaded]"
|
||||
|
||||
|
||||
@@ -21,7 +26,7 @@ def get_original_miku_prompt() -> str:
|
||||
with open("miku_prompt.txt", "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to load miku_prompt.txt: {e}")
|
||||
logger.error(f"Failed to load miku_prompt.txt: {e}")
|
||||
return "## MIKU PROMPT\n[File could not be loaded]"
|
||||
|
||||
|
||||
@@ -31,7 +36,7 @@ def get_original_miku_lyrics() -> str:
|
||||
with open("miku_lyrics.txt", "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to load miku_lyrics.txt: {e}")
|
||||
logger.error(f"Failed to load miku_lyrics.txt: {e}")
|
||||
return "## MIKU LYRICS\n[File could not be loaded]"
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@ import globals
|
||||
from langchain_community.vectorstores import FAISS
|
||||
from langchain_text_splitters import CharacterTextSplitter, RecursiveCharacterTextSplitter
|
||||
from langchain_core.documents import Document
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger('core')
|
||||
|
||||
|
||||
# switch_model() removed - llama-swap handles model switching automatically
|
||||
@@ -21,7 +24,7 @@ async def is_miku_addressed(message) -> bool:
|
||||
|
||||
# Safety check: ensure guild and guild.me exist
|
||||
if not message.guild or not message.guild.me:
|
||||
print(f"⚠️ Warning: Invalid guild or guild.me in message from {message.author}")
|
||||
logger.warning(f"Invalid guild or guild.me in message from {message.author}")
|
||||
return False
|
||||
|
||||
# If message contains a ping for Miku, return true
|
||||
@@ -35,7 +38,7 @@ async def is_miku_addressed(message) -> bool:
|
||||
if referenced_msg.author == message.guild.me:
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"⚠️ Could not fetch referenced message: {e}")
|
||||
logger.warning(f"Could not fetch referenced message: {e}")
|
||||
|
||||
cleaned = message.content.strip()
|
||||
|
||||
|
||||
@@ -7,6 +7,10 @@ import aiohttp
|
||||
import random
|
||||
from typing import Optional, List, Dict
|
||||
import asyncio
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger('media')
|
||||
|
||||
|
||||
class DanbooruClient:
|
||||
"""Client for interacting with Danbooru API"""
|
||||
@@ -74,23 +78,23 @@ class DanbooruClient:
|
||||
|
||||
try:
|
||||
url = f"{self.BASE_URL}/posts.json"
|
||||
print(f"🎨 Danbooru request: {url} with params: {params}")
|
||||
logger.debug(f"Danbooru request: {url} with params: {params}")
|
||||
async with self.session.get(url, params=params, timeout=10) as response:
|
||||
if response.status == 200:
|
||||
posts = await response.json()
|
||||
print(f"🎨 Danbooru: Found {len(posts)} posts (page {page})")
|
||||
logger.debug(f"Danbooru: Found {len(posts)} posts (page {page})")
|
||||
return posts
|
||||
else:
|
||||
error_text = await response.text()
|
||||
print(f"⚠️ Danbooru API error: {response.status}")
|
||||
print(f"⚠️ Request URL: {response.url}")
|
||||
print(f"⚠️ Error details: {error_text[:500]}")
|
||||
logger.error(f"Danbooru API error: {response.status}")
|
||||
logger.error(f"Request URL: {response.url}")
|
||||
logger.error(f"Error details: {error_text[:500]}")
|
||||
return []
|
||||
except asyncio.TimeoutError:
|
||||
print(f"⚠️ Danbooru API timeout")
|
||||
logger.error(f"Danbooru API timeout")
|
||||
return []
|
||||
except Exception as e:
|
||||
print(f"⚠️ Danbooru API error: {e}")
|
||||
logger.error(f"Danbooru API error: {e}")
|
||||
return []
|
||||
|
||||
async def get_random_miku_image(
|
||||
@@ -128,7 +132,7 @@ class DanbooruClient:
|
||||
)
|
||||
|
||||
if not posts:
|
||||
print("⚠️ No posts found, trying without mood tags")
|
||||
logger.warning("No posts found, trying without mood tags")
|
||||
# Fallback: try without mood tags
|
||||
posts = await self.search_miku_images(
|
||||
rating=["g", "s"],
|
||||
@@ -146,13 +150,13 @@ class DanbooruClient:
|
||||
]
|
||||
|
||||
if not valid_posts:
|
||||
print("⚠️ No valid posts with sufficient resolution")
|
||||
logger.warning("No valid posts with sufficient resolution")
|
||||
return None
|
||||
|
||||
# Pick a random one
|
||||
selected = random.choice(valid_posts)
|
||||
|
||||
print(f"🎨 Selected Danbooru post #{selected.get('id')} - {selected.get('tag_string_character', 'unknown character')}")
|
||||
logger.info(f"Selected Danbooru post #{selected.get('id')} - {selected.get('tag_string_character', 'unknown character')}")
|
||||
|
||||
return selected
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@ import discord
|
||||
import globals
|
||||
from utils.llm import query_llama
|
||||
from utils.dm_logger import dm_logger
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger('dm')
|
||||
|
||||
# Directories
|
||||
REPORTS_DIR = "memory/dm_reports"
|
||||
@@ -26,7 +29,7 @@ class DMInteractionAnalyzer:
|
||||
"""
|
||||
self.owner_user_id = owner_user_id
|
||||
os.makedirs(REPORTS_DIR, exist_ok=True)
|
||||
print(f"📊 DM Interaction Analyzer initialized for owner: {owner_user_id}")
|
||||
logger.info(f"DM Interaction Analyzer initialized for owner: {owner_user_id}")
|
||||
|
||||
def _load_reported_today(self) -> Dict[str, str]:
|
||||
"""Load the list of users reported today with their dates"""
|
||||
@@ -35,7 +38,7 @@ class DMInteractionAnalyzer:
|
||||
with open(REPORTED_TODAY_FILE, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to load reported_today.json: {e}")
|
||||
logger.error(f"Failed to load reported_today.json: {e}")
|
||||
return {}
|
||||
return {}
|
||||
|
||||
@@ -45,7 +48,7 @@ class DMInteractionAnalyzer:
|
||||
with open(REPORTED_TODAY_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(reported, f, indent=2)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to save reported_today.json: {e}")
|
||||
logger.error(f"Failed to save reported_today.json: {e}")
|
||||
|
||||
def _clean_old_reports(self, reported: Dict[str, str]) -> Dict[str, str]:
|
||||
"""Remove entries from reported_today that are older than 24 hours"""
|
||||
@@ -58,7 +61,7 @@ class DMInteractionAnalyzer:
|
||||
if now - report_date < timedelta(hours=24):
|
||||
cleaned[user_id] = date_str
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to parse date for user {user_id}: {e}")
|
||||
logger.error(f"Failed to parse date for user {user_id}: {e}")
|
||||
|
||||
return cleaned
|
||||
|
||||
@@ -91,7 +94,7 @@ class DMInteractionAnalyzer:
|
||||
if msg_time >= cutoff_time:
|
||||
recent_messages.append(msg)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to parse message timestamp: {e}")
|
||||
logger.error(f"Failed to parse message timestamp: {e}")
|
||||
|
||||
return recent_messages
|
||||
|
||||
@@ -126,14 +129,14 @@ class DMInteractionAnalyzer:
|
||||
recent_messages = self._get_recent_messages(user_id, hours=24)
|
||||
|
||||
if not recent_messages:
|
||||
print(f"📊 No recent messages from user {username} ({user_id})")
|
||||
logger.debug(f"No recent messages from user {username} ({user_id})")
|
||||
return None
|
||||
|
||||
# Count user messages only (not bot responses)
|
||||
user_messages = [msg for msg in recent_messages if not msg.get("is_bot_message", False)]
|
||||
|
||||
if len(user_messages) < 3: # Minimum threshold for analysis
|
||||
print(f"📊 Not enough messages from user {username} ({user_id}) for analysis")
|
||||
logger.info(f"Not enough messages from user {username} ({user_id}) for analysis")
|
||||
return None
|
||||
|
||||
# Format messages for analysis
|
||||
@@ -174,7 +177,7 @@ Respond ONLY with the JSON object, no other text."""
|
||||
response_type="dm_analysis"
|
||||
)
|
||||
|
||||
print(f"📊 Raw LLM response for {username}:\n{response}\n")
|
||||
logger.debug(f"Raw LLM response for {username}:\n{response}\n")
|
||||
|
||||
# Parse JSON response
|
||||
# Remove markdown code blocks if present
|
||||
@@ -192,7 +195,7 @@ Respond ONLY with the JSON object, no other text."""
|
||||
if start_idx != -1 and end_idx != -1:
|
||||
cleaned_response = cleaned_response[start_idx:end_idx+1]
|
||||
|
||||
print(f"📊 Cleaned JSON for {username}:\n{cleaned_response}\n")
|
||||
logger.debug(f"Cleaned JSON for {username}:\n{cleaned_response}\n")
|
||||
|
||||
analysis = json.loads(cleaned_response)
|
||||
|
||||
@@ -205,11 +208,11 @@ Respond ONLY with the JSON object, no other text."""
|
||||
return analysis
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"⚠️ JSON parse error for user {username}: {e}")
|
||||
print(f"⚠️ Failed response: {response}")
|
||||
logger.error(f"JSON parse error for user {username}: {e}")
|
||||
logger.error(f"Failed response: {response}")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to analyze interaction for user {username}: {e}")
|
||||
logger.error(f"Failed to analyze interaction for user {username}: {e}")
|
||||
return None
|
||||
|
||||
def _save_report(self, user_id: int, analysis: Dict) -> str:
|
||||
@@ -221,10 +224,10 @@ Respond ONLY with the JSON object, no other text."""
|
||||
try:
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
json.dump(analysis, f, indent=2, ensure_ascii=False)
|
||||
print(f"💾 Saved report: {filepath}")
|
||||
logger.info(f"Saved report: {filepath}")
|
||||
return filepath
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to save report: {e}")
|
||||
logger.error(f"Failed to save report: {e}")
|
||||
return ""
|
||||
|
||||
async def _send_report_to_owner(self, analysis: Dict):
|
||||
@@ -232,7 +235,7 @@ Respond ONLY with the JSON object, no other text."""
|
||||
try:
|
||||
# Ensure we're using the Discord client's event loop
|
||||
if not globals.client or not globals.client.is_ready():
|
||||
print(f"⚠️ Discord client not ready, cannot send report")
|
||||
logger.warning(f"Discord client not ready, cannot send report")
|
||||
return
|
||||
|
||||
owner = await globals.client.fetch_user(self.owner_user_id)
|
||||
@@ -294,10 +297,10 @@ Respond ONLY with the JSON object, no other text."""
|
||||
)
|
||||
|
||||
await owner.send(embed=embed)
|
||||
print(f"📤 Report sent to owner for user {username}")
|
||||
logger.info(f"Report sent to owner for user {username}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to send report to owner: {e}")
|
||||
logger.error(f"Failed to send report to owner: {e}")
|
||||
|
||||
async def analyze_and_report(self, user_id: int) -> bool:
|
||||
"""
|
||||
@@ -306,12 +309,11 @@ Respond ONLY with the JSON object, no other text."""
|
||||
Returns:
|
||||
True if analysis was performed and reported, False otherwise
|
||||
"""
|
||||
|
||||
# Check if already reported today
|
||||
if self.has_been_reported_today(user_id):
|
||||
print(f"📊 User {user_id} already reported today, skipping")
|
||||
return False
|
||||
|
||||
# Analyze interaction
|
||||
logger.debug(f"User {user_id} already reported today, skipping")
|
||||
return False # Analyze interaction
|
||||
analysis = await self.analyze_user_interaction(user_id)
|
||||
|
||||
if not analysis:
|
||||
@@ -331,13 +333,13 @@ Respond ONLY with the JSON object, no other text."""
|
||||
|
||||
async def run_daily_analysis(self):
|
||||
"""Run analysis on all DM users and report significant interactions"""
|
||||
print("📊 Starting daily DM interaction analysis...")
|
||||
logger.info("Starting daily DM interaction analysis...")
|
||||
|
||||
# Get all DM users
|
||||
all_users = dm_logger.get_all_dm_users()
|
||||
|
||||
if not all_users:
|
||||
print("📊 No DM users to analyze")
|
||||
logger.info("No DM users to analyze")
|
||||
return
|
||||
|
||||
reported_count = 0
|
||||
@@ -363,9 +365,9 @@ Respond ONLY with the JSON object, no other text."""
|
||||
analyzed_count += 1
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to process user {user_summary.get('username', 'Unknown')}: {e}")
|
||||
logger.error(f"Failed to process user {user_summary.get('username', 'Unknown')}: {e}")
|
||||
|
||||
print(f"📊 Daily analysis complete: Analyzed {analyzed_count} users, reported {reported_count}")
|
||||
logger.info(f"Daily analysis complete: Analyzed {analyzed_count} users, reported {reported_count}")
|
||||
|
||||
|
||||
# Global instance (will be initialized with owner ID)
|
||||
|
||||
@@ -9,6 +9,9 @@ import discord
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
import globals
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger('dm')
|
||||
|
||||
# Directory for storing DM logs
|
||||
DM_LOG_DIR = "memory/dms"
|
||||
@@ -19,7 +22,7 @@ class DMLogger:
|
||||
"""Initialize the DM logger and ensure directory exists"""
|
||||
os.makedirs(DM_LOG_DIR, exist_ok=True)
|
||||
os.makedirs("memory", exist_ok=True)
|
||||
print(f"📁 DM Logger initialized: {DM_LOG_DIR}")
|
||||
logger.info(f"DM Logger initialized: {DM_LOG_DIR}")
|
||||
|
||||
def _get_user_log_file(self, user_id: int) -> str:
|
||||
"""Get the log file path for a specific user"""
|
||||
@@ -28,19 +31,19 @@ class DMLogger:
|
||||
def _load_user_logs(self, user_id: int) -> dict:
|
||||
"""Load existing logs for a user, create new if doesn't exist"""
|
||||
log_file = self._get_user_log_file(user_id)
|
||||
print(f"📁 DM Logger: Loading logs from {log_file}")
|
||||
logger.debug(f"DM Logger: Loading logs from {log_file}")
|
||||
|
||||
if os.path.exists(log_file):
|
||||
try:
|
||||
with open(log_file, 'r', encoding='utf-8') as f:
|
||||
logs = json.load(f)
|
||||
print(f"📁 DM Logger: Successfully loaded logs for user {user_id}: {len(logs.get('conversations', []))} conversations")
|
||||
logger.debug(f"DM Logger: Successfully loaded logs for user {user_id}: {len(logs.get('conversations', []))} conversations")
|
||||
return logs
|
||||
except Exception as e:
|
||||
print(f"⚠️ DM Logger: Failed to load DM logs for user {user_id}: {e}")
|
||||
logger.error(f"DM Logger: Failed to load DM logs for user {user_id}: {e}")
|
||||
return {"user_id": user_id, "username": "Unknown", "conversations": []}
|
||||
else:
|
||||
print(f"📁 DM Logger: No log file found for user {user_id}, creating new")
|
||||
logger.debug(f"DM Logger: No log file found for user {user_id}, creating new")
|
||||
return {"user_id": user_id, "username": "Unknown", "conversations": []}
|
||||
|
||||
def _save_user_logs(self, user_id: int, logs: dict):
|
||||
@@ -50,7 +53,7 @@ class DMLogger:
|
||||
with open(log_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(logs, f, indent=2, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to save DM logs for user {user_id}: {e}")
|
||||
logger.error(f"Failed to save DM logs for user {user_id}: {e}")
|
||||
|
||||
def log_user_message(self, user: discord.User, message: discord.Message, is_bot_message: bool = False):
|
||||
"""Log a user message in DMs"""
|
||||
@@ -92,15 +95,15 @@ class DMLogger:
|
||||
# Keep only last 1000 messages to prevent files from getting too large
|
||||
if len(logs["conversations"]) > 1000:
|
||||
logs["conversations"] = logs["conversations"][-1000:]
|
||||
print(f"📝 DM logs for user {username} trimmed to last 1000 messages")
|
||||
logger.info(f"DM logs for user {username} trimmed to last 1000 messages")
|
||||
|
||||
# Save logs
|
||||
self._save_user_logs(user_id, logs)
|
||||
|
||||
if is_bot_message:
|
||||
print(f"🤖 DM logged: Bot -> {username} ({len(message_entry['attachments'])} attachments)")
|
||||
logger.debug(f"DM logged: Bot -> {username} ({len(message_entry['attachments'])} attachments)")
|
||||
else:
|
||||
print(f"💬 DM logged: {username} -> Bot ({len(message_entry['attachments'])} attachments)")
|
||||
logger.debug(f"DM logged: {username} -> Bot ({len(message_entry['attachments'])} attachments)")
|
||||
|
||||
def get_user_conversation_summary(self, user_id: int) -> dict:
|
||||
"""Get a summary of conversations with a user"""
|
||||
@@ -211,10 +214,10 @@ class DMLogger:
|
||||
bot_msg = MockMessage(bot_response, attachments=bot_attachments)
|
||||
self.log_user_message(user, bot_msg, is_bot_message=True)
|
||||
|
||||
print(f"📝 Conversation logged for user {user_id}: user='{user_message[:50]}...', bot='{bot_response[:50]}...'")
|
||||
logger.debug(f"Conversation logged for user {user_id}: user='{user_message[:50]}...', bot='{bot_response[:50]}...'")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to log conversation for user {user_id}: {e}")
|
||||
logger.error(f"Failed to log conversation for user {user_id}: {e}")
|
||||
|
||||
def export_user_conversation(self, user_id: int, format: str = "json") -> str:
|
||||
"""Export all conversations with a user in specified format"""
|
||||
@@ -254,7 +257,7 @@ class DMLogger:
|
||||
with open(BLOCKED_USERS_FILE, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to load blocked users: {e}")
|
||||
logger.error(f"Failed to load blocked users: {e}")
|
||||
return {"blocked_users": []}
|
||||
return {"blocked_users": []}
|
||||
|
||||
@@ -262,9 +265,9 @@ class DMLogger:
|
||||
"""Save the blocked users list"""
|
||||
try:
|
||||
with open(BLOCKED_USERS_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(blocked_data, f, indent=2, ensure_ascii=False)
|
||||
json.dump(blocked_data, f, indent=2)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to save blocked users: {e}")
|
||||
logger.error(f"Failed to save blocked users: {e}")
|
||||
|
||||
def is_user_blocked(self, user_id: int) -> bool:
|
||||
"""Check if a user is blocked"""
|
||||
@@ -289,13 +292,13 @@ class DMLogger:
|
||||
}
|
||||
|
||||
self._save_blocked_users(blocked_data)
|
||||
print(f"🚫 User {user_id} ({username}) has been blocked")
|
||||
logger.info(f"User {user_id} ({username}) has been blocked")
|
||||
return True
|
||||
else:
|
||||
print(f"⚠️ User {user_id} is already blocked")
|
||||
logger.warning(f"User {user_id} is already blocked")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to block user {user_id}: {e}")
|
||||
logger.error(f"Failed to block user {user_id}: {e}")
|
||||
return False
|
||||
|
||||
def unblock_user(self, user_id: int) -> bool:
|
||||
@@ -313,13 +316,13 @@ class DMLogger:
|
||||
username = "Unknown"
|
||||
|
||||
self._save_blocked_users(blocked_data)
|
||||
print(f"✅ User {user_id} ({username}) has been unblocked")
|
||||
logger.info(f"User {user_id} ({username}) has been unblocked")
|
||||
return True
|
||||
else:
|
||||
print(f"⚠️ User {user_id} is not blocked")
|
||||
logger.warning(f"User {user_id} is not blocked")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to unblock user {user_id}: {e}")
|
||||
logger.error(f"Failed to unblock user {user_id}: {e}")
|
||||
return False
|
||||
|
||||
def get_blocked_users(self) -> List[dict]:
|
||||
@@ -368,17 +371,17 @@ class DMLogger:
|
||||
self._save_user_logs(user_id, logs)
|
||||
|
||||
reactor_type = "🤖 Miku" if is_bot_reactor else f"👤 {reactor_name}"
|
||||
print(f"➕ Reaction logged: {emoji} by {reactor_type} on message {message_id}")
|
||||
logger.debug(f"Reaction logged: {emoji} by {reactor_type} on message {message_id}")
|
||||
return True
|
||||
else:
|
||||
print(f"⚠️ Reaction {emoji} by {reactor_name} already exists on message {message_id}")
|
||||
logger.debug(f"Reaction {emoji} by {reactor_name} already exists on message {message_id}")
|
||||
return False
|
||||
|
||||
print(f"⚠️ Message {message_id} not found in user {user_id}'s logs")
|
||||
logger.warning(f"Message {message_id} not found in user {user_id}'s logs")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to log reaction add for user {user_id}, message {message_id}: {e}")
|
||||
logger.error(f"Failed to log reaction add for user {user_id}, message {message_id}: {e}")
|
||||
return False
|
||||
|
||||
async def log_reaction_remove(self, user_id: int, message_id: int, emoji: str, reactor_id: int):
|
||||
@@ -399,20 +402,20 @@ class DMLogger:
|
||||
|
||||
if len(message["reactions"]) < original_count:
|
||||
self._save_user_logs(user_id, logs)
|
||||
print(f"➖ Reaction removed: {emoji} by user/bot {reactor_id} from message {message_id}")
|
||||
logger.debug(f"Reaction removed: {emoji} by user/bot {reactor_id} from message {message_id}")
|
||||
return True
|
||||
else:
|
||||
print(f"⚠️ Reaction {emoji} by {reactor_id} not found on message {message_id}")
|
||||
logger.debug(f"Reaction {emoji} by {reactor_id} not found on message {message_id}")
|
||||
return False
|
||||
else:
|
||||
print(f"⚠️ No reactions on message {message_id}")
|
||||
logger.debug(f"No reactions on message {message_id}")
|
||||
return False
|
||||
|
||||
print(f"⚠️ Message {message_id} not found in user {user_id}'s logs")
|
||||
logger.warning(f"Message {message_id} not found in user {user_id}'s logs")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to log reaction remove for user {user_id}, message {message_id}: {e}")
|
||||
logger.error(f"Failed to log reaction remove for user {user_id}, message {message_id}: {e}")
|
||||
return False
|
||||
|
||||
async def delete_conversation(self, user_id: int, conversation_id: str) -> bool:
|
||||
@@ -420,8 +423,8 @@ class DMLogger:
|
||||
try:
|
||||
logs = self._load_user_logs(user_id)
|
||||
|
||||
print(f"🔍 DM Logger: Looking for bot message ID '{conversation_id}' for user {user_id}")
|
||||
print(f"🔍 DM Logger: Searching through {len(logs['conversations'])} conversations")
|
||||
logger.debug(f"DM Logger: Looking for bot message ID '{conversation_id}' for user {user_id}")
|
||||
logger.debug(f"DM Logger: Searching through {len(logs['conversations'])} conversations")
|
||||
|
||||
# Convert conversation_id to int for comparison if it looks like a Discord message ID
|
||||
conv_id_as_int = None
|
||||
@@ -441,7 +444,7 @@ class DMLogger:
|
||||
break
|
||||
|
||||
if not message_to_delete:
|
||||
print(f"⚠️ No bot message found with ID {conversation_id} for user {user_id}")
|
||||
logger.warning(f"No bot message found with ID {conversation_id} for user {user_id}")
|
||||
return False
|
||||
|
||||
# Try to delete from Discord first
|
||||
@@ -463,13 +466,13 @@ class DMLogger:
|
||||
discord_message = await dm_channel.fetch_message(int(message_id))
|
||||
await discord_message.delete()
|
||||
discord_deleted = True
|
||||
print(f"✅ Deleted Discord message {message_id} from DM with user {user_id}")
|
||||
logger.info(f"Deleted Discord message {message_id} from DM with user {user_id}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Could not delete Discord message {message_id}: {e}")
|
||||
logger.warning(f"Could not delete Discord message {message_id}: {e}")
|
||||
# Continue anyway to delete from logs
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Discord deletion failed: {e}")
|
||||
logger.warning(f"Discord deletion failed: {e}")
|
||||
# Continue anyway to delete from logs
|
||||
|
||||
# Remove from logs regardless of Discord deletion success
|
||||
@@ -488,16 +491,16 @@ class DMLogger:
|
||||
if deleted_count > 0:
|
||||
self._save_user_logs(user_id, logs)
|
||||
if discord_deleted:
|
||||
print(f"🗑️ Deleted bot message from both Discord and logs for user {user_id}")
|
||||
logger.info(f"Deleted bot message from both Discord and logs for user {user_id}")
|
||||
else:
|
||||
print(f"🗑️ Deleted bot message from logs only (Discord deletion failed) for user {user_id}")
|
||||
logger.info(f"Deleted bot message from logs only (Discord deletion failed) for user {user_id}")
|
||||
return True
|
||||
else:
|
||||
print(f"⚠️ No bot message found in logs with ID {conversation_id} for user {user_id}")
|
||||
logger.warning(f"No bot message found in logs with ID {conversation_id} for user {user_id}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to delete conversation {conversation_id} for user {user_id}: {e}")
|
||||
logger.error(f"Failed to delete conversation {conversation_id} for user {user_id}: {e}")
|
||||
return False
|
||||
|
||||
async def delete_all_conversations(self, user_id: int) -> bool:
|
||||
@@ -507,12 +510,12 @@ class DMLogger:
|
||||
conversation_count = len(logs["conversations"])
|
||||
|
||||
if conversation_count == 0:
|
||||
print(f"⚠️ No conversations found for user {user_id}")
|
||||
logger.warning(f"No conversations found for user {user_id}")
|
||||
return False
|
||||
|
||||
# Find all bot messages to delete from Discord
|
||||
bot_messages = [conv for conv in logs["conversations"] if conv.get("is_bot_message", False)]
|
||||
print(f"🔍 Found {len(bot_messages)} bot messages to delete from Discord for user {user_id}")
|
||||
logger.debug(f"Found {len(bot_messages)} bot messages to delete from Discord for user {user_id}")
|
||||
|
||||
# Try to delete all bot messages from Discord
|
||||
discord_deleted_count = 0
|
||||
@@ -534,13 +537,13 @@ class DMLogger:
|
||||
discord_message = await dm_channel.fetch_message(int(message_id))
|
||||
await discord_message.delete()
|
||||
discord_deleted_count += 1
|
||||
print(f"✅ Deleted Discord message {message_id} from DM with user {user_id}")
|
||||
logger.info(f"Deleted Discord message {message_id} from DM with user {user_id}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Could not delete Discord message {message_id}: {e}")
|
||||
logger.error(f"Could not delete Discord message {message_id}: {e}")
|
||||
# Continue with other messages
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Discord bulk deletion failed: {e}")
|
||||
logger.warning(f"Discord bulk deletion failed: {e}")
|
||||
# Continue anyway to delete from logs
|
||||
|
||||
# Delete all conversations from logs regardless of Discord deletion success
|
||||
@@ -548,14 +551,14 @@ class DMLogger:
|
||||
self._save_user_logs(user_id, logs)
|
||||
|
||||
if discord_deleted_count > 0:
|
||||
print(f"🗑️ Deleted {discord_deleted_count} bot messages from Discord and all {conversation_count} conversations from logs for user {user_id}")
|
||||
logger.info(f"Deleted {discord_deleted_count} bot messages from Discord and all {conversation_count} conversations from logs for user {user_id}")
|
||||
else:
|
||||
print(f"🗑️ Deleted all {conversation_count} conversations from logs only (Discord deletion failed) for user {user_id}")
|
||||
logger.info(f"Deleted all {conversation_count} conversations from logs only (Discord deletion failed) for user {user_id}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to delete all conversations for user {user_id}: {e}")
|
||||
logger.error(f"Failed to delete all conversations for user {user_id}: {e}")
|
||||
return False
|
||||
|
||||
def delete_user_completely(self, user_id: int) -> bool:
|
||||
@@ -564,13 +567,13 @@ class DMLogger:
|
||||
log_file = self._get_user_log_file(user_id)
|
||||
if os.path.exists(log_file):
|
||||
os.remove(log_file)
|
||||
print(f"🗑️ Completely deleted log file for user {user_id}")
|
||||
logger.info(f"Completely deleted log file for user {user_id}")
|
||||
return True
|
||||
else:
|
||||
print(f"⚠️ No log file found for user {user_id}")
|
||||
logger.warning(f"No log file found for user {user_id}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to delete user log file {user_id}: {e}")
|
||||
logger.error(f"Failed to delete user log file {user_id}: {e}")
|
||||
return False
|
||||
|
||||
# Global instance
|
||||
|
||||
@@ -9,6 +9,9 @@ import os
|
||||
import random
|
||||
import json
|
||||
import globals
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger('persona')
|
||||
|
||||
# ============================================================================
|
||||
# EVIL MODE PERSISTENCE
|
||||
@@ -40,16 +43,16 @@ def save_evil_mode_state(saved_role_color=None):
|
||||
}
|
||||
with open(EVIL_MODE_STATE_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(state, f, indent=2)
|
||||
print(f"💾 Saved evil mode state: {state}")
|
||||
logger.debug(f"Saved evil mode state: {state}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to save evil mode state: {e}")
|
||||
logger.error(f"Failed to save evil mode state: {e}")
|
||||
|
||||
|
||||
def load_evil_mode_state():
|
||||
"""Load evil mode state from JSON file"""
|
||||
try:
|
||||
if not os.path.exists(EVIL_MODE_STATE_FILE):
|
||||
print(f"ℹ️ No evil mode state file found, using defaults")
|
||||
logger.info(f"No evil mode state file found, using defaults")
|
||||
return False, "evil_neutral", None
|
||||
|
||||
with open(EVIL_MODE_STATE_FILE, "r", encoding="utf-8") as f:
|
||||
@@ -58,10 +61,10 @@ def load_evil_mode_state():
|
||||
evil_mode = state.get("evil_mode_enabled", False)
|
||||
evil_mood = state.get("evil_mood", "evil_neutral")
|
||||
saved_role_color = state.get("saved_role_color")
|
||||
print(f"📂 Loaded evil mode state: evil_mode={evil_mode}, mood={evil_mood}, saved_color={saved_role_color}")
|
||||
logger.debug(f"Loaded evil mode state: evil_mode={evil_mode}, mood={evil_mood}, saved_color={saved_role_color}")
|
||||
return evil_mode, evil_mood, saved_role_color
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to load evil mode state: {e}")
|
||||
logger.error(f"Failed to load evil mode state: {e}")
|
||||
return False, "evil_neutral", None
|
||||
|
||||
|
||||
@@ -70,13 +73,13 @@ def restore_evil_mode_on_startup():
|
||||
evil_mode, evil_mood, saved_role_color = load_evil_mode_state()
|
||||
|
||||
if evil_mode:
|
||||
print("😈 Restoring evil mode from previous session...")
|
||||
logger.debug("Restoring evil mode from previous session...")
|
||||
globals.EVIL_MODE = True
|
||||
globals.EVIL_DM_MOOD = evil_mood
|
||||
globals.EVIL_DM_MOOD_DESCRIPTION = load_evil_mood_description(evil_mood)
|
||||
print(f"😈 Evil mode restored: {evil_mood}")
|
||||
logger.info(f"Evil mode restored: {evil_mood}")
|
||||
else:
|
||||
print("🎤 Normal mode active")
|
||||
logger.info("Normal mode active")
|
||||
|
||||
return evil_mode
|
||||
|
||||
@@ -90,7 +93,7 @@ def get_evil_miku_lore() -> str:
|
||||
with open("evil_miku_lore.txt", "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to load evil_miku_lore.txt: {e}")
|
||||
logger.error(f"Failed to load evil_miku_lore.txt: {e}")
|
||||
return "## EVIL MIKU LORE\n[File could not be loaded]"
|
||||
|
||||
|
||||
@@ -100,7 +103,7 @@ def get_evil_miku_prompt() -> str:
|
||||
with open("evil_miku_prompt.txt", "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to load evil_miku_prompt.txt: {e}")
|
||||
logger.error(f"Failed to load evil_miku_prompt.txt: {e}")
|
||||
return "## EVIL MIKU PROMPT\n[File could not be loaded]"
|
||||
|
||||
|
||||
@@ -110,7 +113,7 @@ def get_evil_miku_lyrics() -> str:
|
||||
with open("evil_miku_lyrics.txt", "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to load evil_miku_lyrics.txt: {e}")
|
||||
logger.error(f"Failed to load evil_miku_lyrics.txt: {e}")
|
||||
return "## EVIL MIKU LYRICS\n[File could not be loaded]"
|
||||
|
||||
|
||||
@@ -178,7 +181,7 @@ def load_evil_mood_description(mood_name: str) -> str:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return f.read().strip()
|
||||
except FileNotFoundError:
|
||||
print(f"⚠️ Evil mood file '{mood_name}' not found. Falling back to evil_neutral.")
|
||||
logger.warning(f"Evil mood file '{mood_name}' not found. Falling back to evil_neutral.")
|
||||
try:
|
||||
with open(os.path.join("moods", "evil", "evil_neutral.txt"), "r", encoding="utf-8") as f:
|
||||
return f.read().strip()
|
||||
@@ -338,13 +341,13 @@ async def get_current_role_color(client) -> str:
|
||||
if role.name.lower() in ["miku color", "miku colour", "miku-color"]:
|
||||
# Convert discord.Color to hex
|
||||
hex_color = f"#{role.color.value:06x}"
|
||||
print(f"🎨 Current role color: {hex_color}")
|
||||
logger.debug(f"Current role color: {hex_color}")
|
||||
return hex_color
|
||||
|
||||
print("⚠️ No 'Miku Color' role found in any server")
|
||||
logger.warning("No 'Miku Color' role found in any server")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to get current role color: {e}")
|
||||
logger.warning(f"Failed to get current role color: {e}")
|
||||
return None
|
||||
|
||||
|
||||
@@ -377,14 +380,14 @@ async def set_role_color(client, hex_color: str):
|
||||
if color_role:
|
||||
await color_role.edit(color=discord_color, reason="Evil mode color change")
|
||||
updated_count += 1
|
||||
print(f" 🎨 Updated role color in {guild.name}: #{hex_color}")
|
||||
logger.debug(f"Updated role color in {guild.name}: #{hex_color}")
|
||||
except Exception as e:
|
||||
print(f" ⚠️ Failed to update role color in {guild.name}: {e}")
|
||||
logger.warning(f"Failed to update role color in {guild.name}: {e}")
|
||||
|
||||
print(f"🎨 Updated role color in {updated_count} server(s) to #{hex_color}")
|
||||
logger.info(f"Updated role color in {updated_count} server(s) to #{hex_color}")
|
||||
return updated_count > 0
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to set role color: {e}")
|
||||
logger.error(f"Failed to set role color: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -398,7 +401,7 @@ async def apply_evil_mode_changes(client, change_username=True, change_pfp=True,
|
||||
change_nicknames: Whether to change server nicknames (default True, but skip on startup restore)
|
||||
change_role_color: Whether to change role color (default True, but skip on startup restore)
|
||||
"""
|
||||
print("😈 Enabling Evil Mode...")
|
||||
logger.info("Enabling Evil Mode...")
|
||||
|
||||
# Save current role color before changing (if we're actually changing it)
|
||||
if change_role_color:
|
||||
@@ -412,9 +415,9 @@ async def apply_evil_mode_changes(client, change_username=True, change_pfp=True,
|
||||
if change_username:
|
||||
try:
|
||||
await client.user.edit(username="Evil Miku")
|
||||
print("✅ Changed bot username to 'Evil Miku'")
|
||||
logger.debug("Changed bot username to 'Evil Miku'")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Could not change bot username: {e}")
|
||||
logger.error(f"Could not change bot username: {e}")
|
||||
|
||||
# Update nicknames in all servers
|
||||
if change_nicknames:
|
||||
@@ -431,7 +434,7 @@ async def apply_evil_mode_changes(client, change_username=True, change_pfp=True,
|
||||
# Save state to file
|
||||
save_evil_mode_state()
|
||||
|
||||
print("😈 Evil Mode enabled!")
|
||||
logger.info("Evil Mode enabled!")
|
||||
|
||||
|
||||
async def revert_evil_mode_changes(client, change_username=True, change_pfp=True, change_nicknames=True, change_role_color=True):
|
||||
@@ -444,16 +447,16 @@ async def revert_evil_mode_changes(client, change_username=True, change_pfp=True
|
||||
change_nicknames: Whether to change server nicknames (default True, but skip on startup restore)
|
||||
change_role_color: Whether to restore role color (default True, but skip on startup restore)
|
||||
"""
|
||||
print("🎤 Disabling Evil Mode...")
|
||||
logger.info("Disabling Evil Mode...")
|
||||
globals.EVIL_MODE = False
|
||||
|
||||
# Change bot username back
|
||||
if change_username:
|
||||
try:
|
||||
await client.user.edit(username="Hatsune Miku")
|
||||
print("✅ Changed bot username back to 'Hatsune Miku'")
|
||||
logger.debug("Changed bot username back to 'Hatsune Miku'")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Could not change bot username: {e}")
|
||||
logger.error(f"Could not change bot username: {e}")
|
||||
|
||||
# Update nicknames in all servers back to normal
|
||||
if change_nicknames:
|
||||
@@ -469,16 +472,16 @@ async def revert_evil_mode_changes(client, change_username=True, change_pfp=True
|
||||
_, _, saved_color = load_evil_mode_state()
|
||||
if saved_color:
|
||||
await set_role_color(client, saved_color)
|
||||
print(f"🎨 Restored role color to {saved_color}")
|
||||
logger.debug(f"Restored role color to {saved_color}")
|
||||
else:
|
||||
print("⚠️ No saved role color found, skipping color restoration")
|
||||
logger.warning("No saved role color found, skipping color restoration")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to restore role color: {e}")
|
||||
logger.error(f"Failed to restore role color: {e}")
|
||||
|
||||
# Save state to file (this will clear saved_role_color since we're back to normal)
|
||||
save_evil_mode_state(saved_role_color=None)
|
||||
|
||||
print("🎤 Evil Mode disabled!")
|
||||
logger.info("Evil Mode disabled!")
|
||||
|
||||
|
||||
async def update_all_evil_nicknames(client):
|
||||
@@ -505,9 +508,9 @@ async def update_evil_server_nickname(client, guild_id: int):
|
||||
me = guild.get_member(client.user.id)
|
||||
if me:
|
||||
await me.edit(nick=nickname)
|
||||
print(f"😈 Changed nickname to '{nickname}' in server {guild.name}")
|
||||
logger.debug(f"Changed nickname to '{nickname}' in server {guild.name}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to update evil nickname in guild {guild_id}: {e}")
|
||||
logger.error(f"Failed to update evil nickname in guild {guild_id}: {e}")
|
||||
|
||||
|
||||
async def revert_all_nicknames(client):
|
||||
@@ -524,7 +527,7 @@ async def set_evil_profile_picture(client):
|
||||
evil_pfp_path = "memory/profile_pictures/evil_pfp.png"
|
||||
|
||||
if not os.path.exists(evil_pfp_path):
|
||||
print(f"⚠️ Evil profile picture not found at {evil_pfp_path}")
|
||||
logger.error(f"Evil profile picture not found at {evil_pfp_path}")
|
||||
return False
|
||||
|
||||
try:
|
||||
@@ -532,10 +535,10 @@ async def set_evil_profile_picture(client):
|
||||
avatar_bytes = f.read()
|
||||
|
||||
await client.user.edit(avatar=avatar_bytes)
|
||||
print("😈 Set evil profile picture")
|
||||
logger.debug("Set evil profile picture")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to set evil profile picture: {e}")
|
||||
logger.error(f"Failed to set evil profile picture: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -554,12 +557,12 @@ async def restore_normal_profile_picture(client):
|
||||
avatar_bytes = f.read()
|
||||
|
||||
await client.user.edit(avatar=avatar_bytes)
|
||||
print(f"🎤 Restored normal profile picture from {path}")
|
||||
logger.debug(f"Restored normal profile picture from {path}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to restore from {path}: {e}")
|
||||
logger.error(f"Failed to restore from {path}: {e}")
|
||||
|
||||
print("⚠️ Could not restore normal profile picture - no backup found")
|
||||
logger.error("Could not restore normal profile picture - no backup found")
|
||||
return False
|
||||
|
||||
|
||||
@@ -602,4 +605,4 @@ async def rotate_evil_mood():
|
||||
globals.EVIL_DM_MOOD_DESCRIPTION = load_evil_mood_description(new_mood)
|
||||
save_evil_mode_state() # Save state when mood rotates
|
||||
|
||||
print(f"😈 Evil mood rotated from {old_mood} to {new_mood}")
|
||||
logger.info(f"Evil mood rotated from {old_mood} to {new_mood}")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# face_detector_manager.py
|
||||
Y# face_detector_manager.py
|
||||
"""
|
||||
Manages on-demand starting/stopping of anime-face-detector container
|
||||
to free up VRAM when not needed.
|
||||
@@ -9,6 +9,9 @@ import aiohttp
|
||||
import subprocess
|
||||
import time
|
||||
from typing import Optional, Dict
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger('gpu')
|
||||
|
||||
|
||||
class FaceDetectorManager:
|
||||
@@ -31,7 +34,7 @@ class FaceDetectorManager:
|
||||
"""
|
||||
try:
|
||||
if debug:
|
||||
print("🚀 Starting anime-face-detector container...")
|
||||
logger.debug("Starting anime-face-detector container...")
|
||||
|
||||
# Start container using docker compose
|
||||
result = subprocess.run(
|
||||
@@ -44,7 +47,7 @@ class FaceDetectorManager:
|
||||
|
||||
if result.returncode != 0:
|
||||
if debug:
|
||||
print(f"⚠️ Failed to start container: {result.stderr}")
|
||||
logger.error(f"Failed to start container: {result.stderr}")
|
||||
return False
|
||||
|
||||
# Wait for API to be ready
|
||||
@@ -53,17 +56,17 @@ class FaceDetectorManager:
|
||||
if await self._check_health():
|
||||
self.is_running = True
|
||||
if debug:
|
||||
print(f"✅ Face detector container started and ready")
|
||||
logger.info(f"Face detector container started and ready")
|
||||
return True
|
||||
await asyncio.sleep(1)
|
||||
|
||||
if debug:
|
||||
print(f"⚠️ Container started but API not ready after {self.STARTUP_TIMEOUT}s")
|
||||
logger.warning(f"Container started but API not ready after {self.STARTUP_TIMEOUT}s")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
if debug:
|
||||
print(f"⚠️ Error starting face detector container: {e}")
|
||||
logger.error(f"Error starting face detector container: {e}")
|
||||
return False
|
||||
|
||||
async def stop_container(self, debug: bool = False) -> bool:
|
||||
@@ -75,7 +78,7 @@ class FaceDetectorManager:
|
||||
"""
|
||||
try:
|
||||
if debug:
|
||||
print("🛑 Stopping anime-face-detector container...")
|
||||
logger.debug("Stopping anime-face-detector container...")
|
||||
|
||||
result = subprocess.run(
|
||||
["docker", "compose", "stop", self.CONTAINER_NAME],
|
||||
@@ -88,16 +91,16 @@ class FaceDetectorManager:
|
||||
if result.returncode == 0:
|
||||
self.is_running = False
|
||||
if debug:
|
||||
print("✅ Face detector container stopped")
|
||||
logger.info("Face detector container stopped")
|
||||
return True
|
||||
else:
|
||||
if debug:
|
||||
print(f"⚠️ Failed to stop container: {result.stderr}")
|
||||
logger.error(f"Failed to stop container: {result.stderr}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
if debug:
|
||||
print(f"⚠️ Error stopping face detector container: {e}")
|
||||
logger.error(f"Error stopping face detector container: {e}")
|
||||
return False
|
||||
|
||||
async def _check_health(self) -> bool:
|
||||
@@ -137,7 +140,7 @@ class FaceDetectorManager:
|
||||
# Step 1: Unload vision model if callback provided
|
||||
if unload_vision_model:
|
||||
if debug:
|
||||
print("📤 Unloading vision model to free VRAM...")
|
||||
logger.debug("Unloading vision model to free VRAM...")
|
||||
await unload_vision_model()
|
||||
await asyncio.sleep(2) # Give time for VRAM to clear
|
||||
|
||||
@@ -145,7 +148,7 @@ class FaceDetectorManager:
|
||||
if not self.is_running:
|
||||
if not await self.start_container(debug=debug):
|
||||
if debug:
|
||||
print("⚠️ Could not start face detector container")
|
||||
logger.error("Could not start face detector container")
|
||||
return None
|
||||
container_was_started = True
|
||||
|
||||
@@ -161,7 +164,7 @@ class FaceDetectorManager:
|
||||
|
||||
if reload_vision_model:
|
||||
if debug:
|
||||
print("📥 Reloading vision model...")
|
||||
logger.debug("Reloading vision model...")
|
||||
await reload_vision_model()
|
||||
|
||||
async def _detect_face_api(self, image_bytes: bytes, debug: bool = False) -> Optional[Dict]:
|
||||
@@ -178,14 +181,14 @@ class FaceDetectorManager:
|
||||
) as response:
|
||||
if response.status != 200:
|
||||
if debug:
|
||||
print(f"⚠️ Face detection API returned status {response.status}")
|
||||
logger.warning(f"Face detection API returned status {response.status}")
|
||||
return None
|
||||
|
||||
result = await response.json()
|
||||
|
||||
if result.get('count', 0) == 0:
|
||||
if debug:
|
||||
print("👤 No faces detected by API")
|
||||
logger.debug("No faces detected by API")
|
||||
return None
|
||||
|
||||
detections = result.get('detections', [])
|
||||
@@ -205,9 +208,9 @@ class FaceDetectorManager:
|
||||
if debug:
|
||||
width = int(x2 - x1)
|
||||
height = int(y2 - y1)
|
||||
print(f"👤 Detected {len(detections)} face(s) via API, using best at ({center_x}, {center_y}) [confidence: {confidence:.2%}]")
|
||||
print(f" Bounding box: x={int(x1)}, y={int(y1)}, w={width}, h={height}")
|
||||
print(f" Keypoints: {len(keypoints)} facial landmarks detected")
|
||||
logger.debug(f"Detected {len(detections)} face(s) via API, using best at ({center_x}, {center_y}) [confidence: {confidence:.2%}]")
|
||||
logger.debug(f" Bounding box: x={int(x1)}, y={int(y1)}, w={width}, h={height}")
|
||||
logger.debug(f" Keypoints: {len(keypoints)} facial landmarks detected")
|
||||
|
||||
return {
|
||||
'center': (center_x, center_y),
|
||||
@@ -219,7 +222,7 @@ class FaceDetectorManager:
|
||||
|
||||
except Exception as e:
|
||||
if debug:
|
||||
print(f"⚠️ Error calling face detection API: {e}")
|
||||
logger.error(f"Error calling face detection API: {e}")
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@@ -10,7 +10,9 @@ import globals
|
||||
from utils.twitter_fetcher import fetch_figurine_tweets_latest
|
||||
from utils.image_handling import analyze_image_with_qwen, download_and_encode_image
|
||||
from utils.llm import query_llama
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger('bot')
|
||||
|
||||
from utils.dm_logger import dm_logger
|
||||
|
||||
@@ -37,14 +39,14 @@ def _ensure_dir(path: str) -> None:
|
||||
def load_subscribers() -> List[int]:
|
||||
try:
|
||||
if os.path.exists(SUBSCRIBERS_FILE):
|
||||
print(f"📁 Figurines: Loading subscribers from {SUBSCRIBERS_FILE}")
|
||||
logger.debug(f"Loading subscribers from {SUBSCRIBERS_FILE}")
|
||||
with open(SUBSCRIBERS_FILE, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
subs = [int(uid) for uid in data.get("subscribers", [])]
|
||||
print(f"📋 Figurines: Loaded {len(subs)} subscribers")
|
||||
logger.debug(f"Loaded {len(subs)} subscribers")
|
||||
return subs
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to load figurine subscribers: {e}")
|
||||
logger.error(f"Failed to load figurine subscribers: {e}")
|
||||
return []
|
||||
|
||||
|
||||
@@ -53,85 +55,85 @@ def save_subscribers(user_ids: List[int]) -> None:
|
||||
_ensure_dir(SUBSCRIBERS_FILE)
|
||||
# Save as strings to be JS-safe in the API layer if needed
|
||||
payload = {"subscribers": [str(uid) for uid in user_ids]}
|
||||
print(f"💾 Figurines: Saving {len(user_ids)} subscribers to {SUBSCRIBERS_FILE}")
|
||||
logger.debug(f"Saving {len(user_ids)} subscribers to {SUBSCRIBERS_FILE}")
|
||||
with open(SUBSCRIBERS_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(payload, f, indent=2)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to save figurine subscribers: {e}")
|
||||
logger.error(f"Failed to save figurine subscribers: {e}")
|
||||
|
||||
|
||||
def add_subscriber(user_id: int) -> bool:
|
||||
print(f"➕ Figurines: Adding subscriber {user_id}")
|
||||
logger.info(f"Adding subscriber {user_id}")
|
||||
subscribers = load_subscribers()
|
||||
if user_id in subscribers:
|
||||
print(f"ℹ️ Figurines: Subscriber {user_id} already present")
|
||||
logger.info(f"Subscriber {user_id} already present")
|
||||
return False
|
||||
subscribers.append(user_id)
|
||||
save_subscribers(subscribers)
|
||||
print(f"✅ Figurines: Subscriber {user_id} added")
|
||||
logger.info(f"Subscriber {user_id} added")
|
||||
return True
|
||||
|
||||
|
||||
def remove_subscriber(user_id: int) -> bool:
|
||||
print(f"🗑️ Figurines: Removing subscriber {user_id}")
|
||||
logger.info(f"Removing subscriber {user_id}")
|
||||
subscribers = load_subscribers()
|
||||
if user_id not in subscribers:
|
||||
print(f"ℹ️ Figurines: Subscriber {user_id} was not present")
|
||||
logger.info(f"Subscriber {user_id} was not present")
|
||||
return False
|
||||
subscribers = [uid for uid in subscribers if uid != user_id]
|
||||
save_subscribers(subscribers)
|
||||
print(f"✅ Figurines: Subscriber {user_id} removed")
|
||||
logger.info(f"Subscriber {user_id} removed")
|
||||
return True
|
||||
|
||||
|
||||
def load_sent_tweets() -> List[str]:
|
||||
try:
|
||||
if os.path.exists(SENT_TWEETS_FILE):
|
||||
print(f"📁 Figurines: Loading sent tweets from {SENT_TWEETS_FILE}")
|
||||
logger.debug(f"Loading sent tweets from {SENT_TWEETS_FILE}")
|
||||
with open(SENT_TWEETS_FILE, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
urls = data.get("urls", [])
|
||||
print(f"📋 Figurines: Loaded {len(urls)} sent tweet URLs")
|
||||
logger.debug(f"Loaded {len(urls)} sent tweet URLs")
|
||||
return urls
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to load figurine sent tweets: {e}")
|
||||
logger.error(f"Failed to load figurine sent tweets: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def save_sent_tweets(urls: List[str]) -> None:
|
||||
try:
|
||||
_ensure_dir(SENT_TWEETS_FILE)
|
||||
print(f"💾 Figurines: Saving {len(urls)} sent tweet URLs to {SENT_TWEETS_FILE}")
|
||||
logger.debug(f"Saving {len(urls)} sent tweet URLs to {SENT_TWEETS_FILE}")
|
||||
with open(SENT_TWEETS_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump({"urls": urls}, f, indent=2)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to save figurine sent tweets: {e}")
|
||||
logger.error(f"Failed to save figurine sent tweets: {e}")
|
||||
|
||||
|
||||
async def choose_random_figurine_tweet() -> Dict[str, Any] | None:
|
||||
"""Fetch figurine tweets from multiple sources, filter out sent, and pick one randomly."""
|
||||
print("🔎 Figurines: Fetching figurine tweets by Latest across sources…")
|
||||
logger.info("Fetching figurine tweets by Latest across sources")
|
||||
tweets = await fetch_figurine_tweets_latest(limit_per_source=10)
|
||||
if not tweets:
|
||||
print("📭 No figurine tweets found across sources")
|
||||
logger.warning("No figurine tweets found across sources")
|
||||
return None
|
||||
|
||||
sent_urls = set(load_sent_tweets())
|
||||
fresh = [t for t in tweets if t.get("url") not in sent_urls]
|
||||
print(f"🧮 Figurines: {len(tweets)} total, {len(fresh)} fresh after filtering sent")
|
||||
logger.debug(f"{len(tweets)} total, {len(fresh)} fresh after filtering sent")
|
||||
if not fresh:
|
||||
print("ℹ️ All figurine tweets have been sent before; allowing reuse")
|
||||
logger.warning("All figurine tweets have been sent before; allowing reuse")
|
||||
fresh = tweets
|
||||
|
||||
chosen = random.choice(fresh)
|
||||
print(f"🎯 Chosen figurine tweet: {chosen.get('url')}")
|
||||
logger.info(f"Chosen figurine tweet: {chosen.get('url')}")
|
||||
return chosen
|
||||
|
||||
|
||||
async def send_figurine_dm_to_user(client: discord.Client, user_id: int, tweet: Dict[str, Any]) -> Tuple[bool, str]:
|
||||
"""Send the figurine tweet to a single subscriber via DM, with analysis and LLM commentary."""
|
||||
try:
|
||||
print(f"✉️ Figurines: Preparing DM to user {user_id}")
|
||||
logger.debug(f"Preparing DM to user {user_id}")
|
||||
user = client.get_user(user_id)
|
||||
if user is None:
|
||||
# Try fetching
|
||||
@@ -169,7 +171,7 @@ async def send_figurine_dm_to_user(client: discord.Client, user_id: int, tweet:
|
||||
img_desc = await analyze_image_with_qwen(base64_img)
|
||||
base_prompt += f"\n\nImage looks like: {img_desc}"
|
||||
except Exception as e:
|
||||
print(f"⚠️ Image analysis failed: {e}")
|
||||
logger.warning(f"Image analysis failed: {e}")
|
||||
|
||||
# Include tweet text too
|
||||
tweet_text = tweet.get("text", "").strip()
|
||||
@@ -190,14 +192,14 @@ async def send_figurine_dm_to_user(client: discord.Client, user_id: int, tweet:
|
||||
# Send the tweet URL first (convert to fxtwitter for better embeds)
|
||||
fx_tweet_url = convert_to_fxtwitter(tweet_url)
|
||||
tweet_message = await dm.send(fx_tweet_url)
|
||||
print(f"✅ Figurines: Tweet URL sent to {user_id}: {fx_tweet_url}")
|
||||
logger.info(f"Tweet URL sent to {user_id}: {fx_tweet_url}")
|
||||
|
||||
# Log the tweet URL message
|
||||
dm_logger.log_user_message(user, tweet_message, is_bot_message=True)
|
||||
|
||||
# Send Miku's comment
|
||||
comment_message = await dm.send(miku_comment)
|
||||
print(f"✅ Figurines: Miku comment sent to {user_id}")
|
||||
logger.info(f"Miku comment sent to {user_id}")
|
||||
|
||||
# Log the comment message
|
||||
dm_logger.log_user_message(user, comment_message, is_bot_message=True)
|
||||
@@ -212,27 +214,27 @@ async def send_figurine_dm_to_user(client: discord.Client, user_id: int, tweet:
|
||||
# Use empty user prompt since this was initiated by Miku
|
||||
globals.conversation_history.setdefault(user_id_str, []).append((tweet_context, miku_comment))
|
||||
|
||||
print(f"📝 Figurines: Messages logged to both DM history and conversation context for user {user_id}")
|
||||
logger.debug(f"Messages logged to both DM history and conversation context for user {user_id}")
|
||||
|
||||
return True, "ok"
|
||||
except Exception as e:
|
||||
print(f"❌ Figurines: Failed DM to {user_id}: {e}")
|
||||
logger.error(f"Failed DM to {user_id}: {e}")
|
||||
return False, f"{e}"
|
||||
|
||||
|
||||
async def send_figurine_dm_to_single_user(client: discord.Client, user_id: int, tweet_url: str = None) -> Dict[str, Any]:
|
||||
"""Send a figurine tweet to a single user, either from search or specific URL."""
|
||||
print(f"🎯 Figurines: Sending DM to single user {user_id}")
|
||||
logger.info(f"Sending DM to single user {user_id}")
|
||||
|
||||
if tweet_url:
|
||||
# Use specific tweet URL
|
||||
print(f"📎 Figurines: Using specific tweet URL: {tweet_url}")
|
||||
logger.info(f"Using specific tweet URL: {tweet_url}")
|
||||
tweet = await fetch_specific_tweet_by_url(tweet_url)
|
||||
if not tweet:
|
||||
return {"status": "error", "message": "Failed to fetch specified tweet"}
|
||||
else:
|
||||
# Search for a random tweet
|
||||
print("🔎 Figurines: Searching for random figurine tweet")
|
||||
logger.info("Searching for random figurine tweet")
|
||||
tweet = await choose_random_figurine_tweet()
|
||||
if not tweet:
|
||||
return {"status": "error", "message": "No figurine tweets found"}
|
||||
@@ -256,7 +258,7 @@ async def send_figurine_dm_to_single_user(client: discord.Client, user_id: int,
|
||||
"failed": [],
|
||||
"tweet": {"url": tweet.get("url", ""), "username": tweet.get("username", "")}
|
||||
}
|
||||
print(f"✅ Figurines: Single user DM sent successfully → {result}")
|
||||
logger.info(f"Single user DM sent successfully → {result}")
|
||||
return result
|
||||
else:
|
||||
result = {
|
||||
@@ -265,27 +267,27 @@ async def send_figurine_dm_to_single_user(client: discord.Client, user_id: int,
|
||||
"failed": [{"user_id": str(user_id), "error": msg}],
|
||||
"message": f"Failed to send DM: {msg}"
|
||||
}
|
||||
print(f"❌ Figurines: Single user DM failed → {result}")
|
||||
logger.error(f"Single user DM failed → {result}")
|
||||
return result
|
||||
|
||||
|
||||
async def fetch_specific_tweet_by_url(tweet_url: str) -> Dict[str, Any] | None:
|
||||
"""Fetch a specific tweet by URL for manual figurine notifications."""
|
||||
try:
|
||||
print(f"🔗 Figurines: Fetching specific tweet from URL: {tweet_url}")
|
||||
logger.debug(f"Fetching specific tweet from URL: {tweet_url}")
|
||||
|
||||
# Extract tweet ID from URL
|
||||
tweet_id = None
|
||||
if "/status/" in tweet_url:
|
||||
try:
|
||||
tweet_id = tweet_url.split("/status/")[1].split("?")[0].split("/")[0]
|
||||
print(f"📋 Figurines: Extracted tweet ID: {tweet_id}")
|
||||
logger.debug(f"Extracted tweet ID: {tweet_id}")
|
||||
except Exception as e:
|
||||
print(f"❌ Figurines: Failed to extract tweet ID from URL: {e}")
|
||||
logger.error(f"Failed to extract tweet ID from URL: {e}")
|
||||
return None
|
||||
|
||||
if not tweet_id:
|
||||
print("❌ Figurines: Could not extract tweet ID from URL")
|
||||
logger.error("Could not extract tweet ID from URL")
|
||||
return None
|
||||
|
||||
# Set up twscrape API (same pattern as existing functions)
|
||||
@@ -313,15 +315,15 @@ async def fetch_specific_tweet_by_url(tweet_url: str) -> Dict[str, Any] | None:
|
||||
|
||||
# Try to fetch the tweet using search instead of tweet_details
|
||||
# Search for the specific tweet ID should return it if accessible
|
||||
print(f"🔍 Figurines: Searching for tweet with ID {tweet_id}")
|
||||
logger.debug(f"Searching for tweet with ID {tweet_id}")
|
||||
search_results = []
|
||||
try:
|
||||
# Search using the tweet ID - this should find the specific tweet
|
||||
from twscrape import gather
|
||||
search_results = await gather(api.search(f"{tweet_id}", limit=1))
|
||||
print(f"🔍 Figurines: Search returned {len(search_results)} results")
|
||||
logger.debug(f"Search returned {len(search_results)} results")
|
||||
except Exception as search_error:
|
||||
print(f"⚠️ Figurines: Search failed: {search_error}")
|
||||
logger.warning(f"Search failed: {search_error}")
|
||||
return None
|
||||
|
||||
# Check if we found the tweet
|
||||
@@ -329,21 +331,21 @@ async def fetch_specific_tweet_by_url(tweet_url: str) -> Dict[str, Any] | None:
|
||||
for tweet in search_results:
|
||||
if str(tweet.id) == str(tweet_id):
|
||||
tweet_data = tweet
|
||||
print(f"✅ Figurines: Found matching tweet with ID {tweet.id}")
|
||||
logger.debug(f"Found matching tweet with ID {tweet.id}")
|
||||
break
|
||||
|
||||
if not tweet_data and search_results:
|
||||
# If no exact match but we have results, use the first one
|
||||
tweet_data = search_results[0]
|
||||
print(f"🔍 Figurines: Using first search result with ID {tweet_data.id}")
|
||||
logger.debug(f"Using first search result with ID {tweet_data.id}")
|
||||
|
||||
if tweet_data:
|
||||
# Extract data using the same pattern as the working search code
|
||||
username = tweet_data.user.username if hasattr(tweet_data, 'user') and tweet_data.user else "unknown"
|
||||
text_content = tweet_data.rawContent if hasattr(tweet_data, 'rawContent') else ""
|
||||
|
||||
print(f"🔍 Figurines: Found tweet from @{username}")
|
||||
print(f"🔍 Figurines: Tweet text: {text_content[:100]}...")
|
||||
logger.debug(f"Found tweet from @{username}")
|
||||
logger.debug(f"Tweet text: {text_content[:100]}...")
|
||||
|
||||
# For media, we'll need to extract it from the tweet_url using the same method as other functions
|
||||
# But for now, let's see if we can get basic tweet data working first
|
||||
@@ -354,37 +356,37 @@ async def fetch_specific_tweet_by_url(tweet_url: str) -> Dict[str, Any] | None:
|
||||
"media": [] # We'll add media extraction later
|
||||
}
|
||||
|
||||
print(f"✅ Figurines: Successfully fetched tweet from @{result['username']}")
|
||||
logger.info(f"Successfully fetched tweet from @{result['username']}")
|
||||
return result
|
||||
else:
|
||||
print("❌ Figurines: No tweet found with the specified ID")
|
||||
logger.error("No tweet found with the specified ID")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Figurines: Error fetching tweet by URL: {e}")
|
||||
logger.error(f"Error fetching tweet by URL: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def send_figurine_dm_to_all_subscribers(client: discord.Client, tweet_url: str = None) -> Dict[str, Any]:
|
||||
"""Pick a figurine tweet and DM it to all subscribers, recording the sent URL."""
|
||||
print("🚀 Figurines: Sending figurine DM to all subscribers…")
|
||||
logger.info("Sending figurine DM to all subscribers")
|
||||
subscribers = load_subscribers()
|
||||
if not subscribers:
|
||||
print("ℹ️ Figurines: No subscribers configured")
|
||||
logger.warning("No subscribers configured")
|
||||
return {"status": "no_subscribers"}
|
||||
|
||||
if tweet_url:
|
||||
# Use specific tweet URL
|
||||
print(f"📎 Figurines: Using specific tweet URL for all subscribers: {tweet_url}")
|
||||
logger.info(f"Using specific tweet URL for all subscribers: {tweet_url}")
|
||||
tweet = await fetch_specific_tweet_by_url(tweet_url)
|
||||
if not tweet:
|
||||
print("ℹ️ Figurines: Failed to fetch specified tweet")
|
||||
logger.warning("Failed to fetch specified tweet")
|
||||
return {"status": "no_tweet", "message": "Failed to fetch specified tweet"}
|
||||
else:
|
||||
# Search for random tweet
|
||||
tweet = await choose_random_figurine_tweet()
|
||||
if tweet is None:
|
||||
print("ℹ️ Figurines: No tweet to send")
|
||||
logger.warning("No tweet to send")
|
||||
return {"status": "no_tweet"}
|
||||
|
||||
results = {"sent": [], "failed": []}
|
||||
@@ -393,7 +395,7 @@ async def send_figurine_dm_to_all_subscribers(client: discord.Client, tweet_url:
|
||||
if ok:
|
||||
results["sent"].append(str(uid))
|
||||
else:
|
||||
print(f"⚠️ Failed to DM user {uid}: {msg}")
|
||||
logger.warning(f"Failed to DM user {uid}: {msg}")
|
||||
results["failed"].append({"user_id": str(uid), "error": msg})
|
||||
|
||||
# Record as sent if at least one success to avoid repeats
|
||||
@@ -407,7 +409,7 @@ async def send_figurine_dm_to_all_subscribers(client: discord.Client, tweet_url:
|
||||
save_sent_tweets(sent_urls)
|
||||
|
||||
summary = {"status": "ok", **results, "tweet": {"url": tweet.get("url", ""), "username": tweet.get("username", "")}}
|
||||
print(f"📦 Figurines: DM send complete → {summary}")
|
||||
logger.info(f"DM send complete → {summary}")
|
||||
return summary
|
||||
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@ import time
|
||||
from typing import Optional, Tuple
|
||||
import globals
|
||||
from utils.llm import query_llama
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger('media')
|
||||
|
||||
# Image generation detection patterns
|
||||
IMAGE_REQUEST_PATTERNS = [
|
||||
@@ -133,11 +136,11 @@ def find_latest_generated_image(prompt_id: str, expected_filename: str = None) -
|
||||
recent_threshold = time.time() - 600 # 10 minutes
|
||||
for file_path in all_files:
|
||||
if os.path.getmtime(file_path) > recent_threshold:
|
||||
print(f"🎨 Found recent image: {file_path}")
|
||||
logger.debug(f"Found recent image: {file_path}")
|
||||
return file_path
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error searching in {output_dir}: {e}")
|
||||
logger.error(f"Error searching in {output_dir}: {e}")
|
||||
continue
|
||||
|
||||
return None
|
||||
@@ -156,7 +159,7 @@ async def generate_image_with_comfyui(prompt: str) -> Optional[str]:
|
||||
# Load the workflow template
|
||||
workflow_path = "Miku_BasicWorkflow.json"
|
||||
if not os.path.exists(workflow_path):
|
||||
print(f"❌ Workflow template not found: {workflow_path}")
|
||||
logger.error(f"Workflow template not found: {workflow_path}")
|
||||
return None
|
||||
|
||||
with open(workflow_path, 'r') as f:
|
||||
@@ -186,29 +189,29 @@ async def generate_image_with_comfyui(prompt: str) -> Optional[str]:
|
||||
async with test_session.get(f"{url}/system_stats", timeout=timeout) as test_response:
|
||||
if test_response.status == 200:
|
||||
comfyui_url = url
|
||||
print(f"✅ ComfyUI found at: {url}")
|
||||
logger.debug(f"ComfyUI found at: {url}")
|
||||
break
|
||||
except:
|
||||
continue
|
||||
|
||||
if not comfyui_url:
|
||||
print(f"❌ ComfyUI not reachable at any of: {comfyui_urls}")
|
||||
logger.error(f"ComfyUI not reachable at any of: {comfyui_urls}")
|
||||
return None
|
||||
async with aiohttp.ClientSession() as session:
|
||||
# Submit the generation request
|
||||
async with session.post(f"{comfyui_url}/prompt", json=payload) as response:
|
||||
if response.status != 200:
|
||||
print(f"❌ ComfyUI request failed: {response.status}")
|
||||
logger.error(f"ComfyUI request failed: {response.status}")
|
||||
return None
|
||||
|
||||
result = await response.json()
|
||||
prompt_id = result.get("prompt_id")
|
||||
|
||||
if not prompt_id:
|
||||
print("❌ No prompt_id received from ComfyUI")
|
||||
logger.error("No prompt_id received from ComfyUI")
|
||||
return None
|
||||
|
||||
print(f"🎨 ComfyUI generation started with prompt_id: {prompt_id}")
|
||||
logger.info(f"ComfyUI generation started with prompt_id: {prompt_id}")
|
||||
|
||||
# Poll for completion (timeout after 5 minutes)
|
||||
timeout = 300 # 5 minutes
|
||||
@@ -242,20 +245,20 @@ async def generate_image_with_comfyui(prompt: str) -> Optional[str]:
|
||||
|
||||
# Verify the file exists before returning
|
||||
if os.path.exists(image_path):
|
||||
print(f"✅ Image generated successfully: {image_path}")
|
||||
logger.info(f"Image generated successfully: {image_path}")
|
||||
return image_path
|
||||
else:
|
||||
# Try alternative paths in case of different mounting
|
||||
alt_path = os.path.join("/app/ComfyUI/output", filename)
|
||||
if os.path.exists(alt_path):
|
||||
print(f"✅ Image generated successfully: {alt_path}")
|
||||
logger.info(f"Image generated successfully: {alt_path}")
|
||||
return alt_path
|
||||
else:
|
||||
print(f"⚠️ Generated image not found at expected paths: {image_path} or {alt_path}")
|
||||
logger.warning(f"Generated image not found at expected paths: {image_path} or {alt_path}")
|
||||
continue
|
||||
|
||||
# If we couldn't find the image via API, try the fallback method
|
||||
print("🔍 Image not found via API, trying fallback method...")
|
||||
logger.debug("Image not found via API, trying fallback method...")
|
||||
fallback_image = find_latest_generated_image(prompt_id)
|
||||
if fallback_image:
|
||||
return fallback_image
|
||||
@@ -263,19 +266,19 @@ async def generate_image_with_comfyui(prompt: str) -> Optional[str]:
|
||||
# Wait before polling again
|
||||
await asyncio.sleep(2)
|
||||
|
||||
print("❌ ComfyUI generation timed out")
|
||||
logger.error("ComfyUI generation timed out")
|
||||
|
||||
# Final fallback: look for the most recent image
|
||||
print("🔍 Trying final fallback: most recent image...")
|
||||
logger.debug("Trying final fallback: most recent image...")
|
||||
fallback_image = find_latest_generated_image(prompt_id)
|
||||
if fallback_image:
|
||||
print(f"✅ Found image via fallback method: {fallback_image}")
|
||||
logger.info(f"Found image via fallback method: {fallback_image}")
|
||||
return fallback_image
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error in generate_image_with_comfyui: {e}")
|
||||
logger.error(f"Error in generate_image_with_comfyui: {e}")
|
||||
return None
|
||||
|
||||
async def handle_image_generation_request(message, prompt: str) -> bool:
|
||||
@@ -307,7 +310,7 @@ async def handle_image_generation_request(message, prompt: str) -> bool:
|
||||
# Start typing to show we're working
|
||||
async with message.channel.typing():
|
||||
# Generate the image
|
||||
print(f"🎨 Starting image generation for prompt: {prompt}")
|
||||
logger.info(f"Starting image generation for prompt: {prompt}")
|
||||
image_path = await generate_image_with_comfyui(prompt)
|
||||
|
||||
if image_path and os.path.exists(image_path):
|
||||
@@ -322,7 +325,7 @@ async def handle_image_generation_request(message, prompt: str) -> bool:
|
||||
|
||||
await message.channel.send(completion_response, file=file)
|
||||
|
||||
print(f"✅ Image sent successfully to {message.author.display_name}")
|
||||
logger.info(f"Image sent successfully to {message.author.display_name}")
|
||||
|
||||
# Log to DM history if it's a DM
|
||||
if is_dm:
|
||||
@@ -336,11 +339,11 @@ async def handle_image_generation_request(message, prompt: str) -> bool:
|
||||
error_response = await query_llama(error_prompt, user_id=user_id, guild_id=guild_id, response_type=response_type)
|
||||
await message.channel.send(error_response)
|
||||
|
||||
print(f"❌ Image generation failed for prompt: {prompt}")
|
||||
logger.error(f"Image generation failed for prompt: {prompt}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error in handle_image_generation_request: {e}")
|
||||
logger.error(f"Error in handle_image_generation_request: {e}")
|
||||
|
||||
# Send error message
|
||||
try:
|
||||
|
||||
@@ -10,6 +10,10 @@ from PIL import Image
|
||||
import re
|
||||
|
||||
import globals
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger('vision')
|
||||
|
||||
# No need for switch_model anymore - llama-swap handles this automatically
|
||||
|
||||
|
||||
@@ -47,7 +51,7 @@ async def extract_tenor_gif_url(tenor_url):
|
||||
match = re.search(r'tenor\.com/(\d+)\.gif', tenor_url)
|
||||
|
||||
if not match:
|
||||
print(f"⚠️ Could not extract Tenor GIF ID from: {tenor_url}")
|
||||
logger.warning(f"Could not extract Tenor GIF ID from: {tenor_url}")
|
||||
return None
|
||||
|
||||
gif_id = match.group(1)
|
||||
@@ -60,7 +64,7 @@ async def extract_tenor_gif_url(tenor_url):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.head(media_url) as resp:
|
||||
if resp.status == 200:
|
||||
print(f"✅ Found Tenor GIF: {media_url}")
|
||||
logger.debug(f"Found Tenor GIF: {media_url}")
|
||||
return media_url
|
||||
|
||||
# If that didn't work, try alternative formats
|
||||
@@ -69,14 +73,14 @@ async def extract_tenor_gif_url(tenor_url):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.head(alt_url) as resp:
|
||||
if resp.status == 200:
|
||||
print(f"✅ Found Tenor GIF (alternative): {alt_url}")
|
||||
logger.debug(f"Found Tenor GIF (alternative): {alt_url}")
|
||||
return alt_url
|
||||
|
||||
print(f"⚠️ Could not find working Tenor media URL for ID: {gif_id}")
|
||||
logger.warning(f"Could not find working Tenor media URL for ID: {gif_id}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error extracting Tenor GIF URL: {e}")
|
||||
logger.error(f"Error extracting Tenor GIF URL: {e}")
|
||||
return None
|
||||
|
||||
|
||||
@@ -114,7 +118,7 @@ async def convert_gif_to_mp4(gif_bytes):
|
||||
with open(temp_mp4_path, 'rb') as f:
|
||||
mp4_bytes = f.read()
|
||||
|
||||
print(f"✅ Converted GIF to MP4 ({len(gif_bytes)} bytes → {len(mp4_bytes)} bytes)")
|
||||
logger.info(f"Converted GIF to MP4 ({len(gif_bytes)} bytes → {len(mp4_bytes)} bytes)")
|
||||
return mp4_bytes
|
||||
|
||||
finally:
|
||||
@@ -125,10 +129,10 @@ async def convert_gif_to_mp4(gif_bytes):
|
||||
os.remove(temp_mp4_path)
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"⚠️ ffmpeg error converting GIF to MP4: {e.stderr.decode()}")
|
||||
logger.error(f"ffmpeg error converting GIF to MP4: {e.stderr.decode()}")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error converting GIF to MP4: {e}")
|
||||
logger.error(f"Error converting GIF to MP4: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
@@ -165,7 +169,7 @@ async def extract_video_frames(video_bytes, num_frames=4):
|
||||
if frames:
|
||||
return frames
|
||||
except Exception as e:
|
||||
print(f"Not a GIF, trying video extraction: {e}")
|
||||
logger.debug(f"Not a GIF, trying video extraction: {e}")
|
||||
|
||||
# For video files (MP4, WebM, etc.), use ffmpeg
|
||||
import subprocess
|
||||
@@ -222,7 +226,7 @@ async def extract_video_frames(video_bytes, num_frames=4):
|
||||
os.remove(temp_video_path)
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error extracting frames: {e}")
|
||||
logger.error(f"Error extracting frames: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
@@ -271,10 +275,10 @@ async def analyze_image_with_vision(base64_img):
|
||||
return data.get("choices", [{}])[0].get("message", {}).get("content", "No description.")
|
||||
else:
|
||||
error_text = await response.text()
|
||||
print(f"❌ Vision API error: {response.status} - {error_text}")
|
||||
logger.error(f"Vision API error: {response.status} - {error_text}")
|
||||
return f"Error analyzing image: {response.status}"
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error in analyze_image_with_vision: {e}")
|
||||
logger.error(f"Error in analyze_image_with_vision: {e}")
|
||||
return f"Error analyzing image: {str(e)}"
|
||||
|
||||
|
||||
@@ -333,10 +337,10 @@ async def analyze_video_with_vision(video_frames, media_type="video"):
|
||||
return data.get("choices", [{}])[0].get("message", {}).get("content", "No description.")
|
||||
else:
|
||||
error_text = await response.text()
|
||||
print(f"❌ Vision API error: {response.status} - {error_text}")
|
||||
logger.error(f"Vision API error: {response.status} - {error_text}")
|
||||
return f"Error analyzing video: {response.status}"
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error in analyze_video_with_vision: {e}")
|
||||
logger.error(f"Error in analyze_video_with_vision: {e}")
|
||||
return f"Error analyzing video: {str(e)}"
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
import random
|
||||
import globals
|
||||
from utils.llm import query_llama # Adjust path as needed
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger('bot')
|
||||
|
||||
|
||||
async def detect_and_react_to_kindness(message, after_reply=False, server_context=None):
|
||||
@@ -19,14 +22,14 @@ async def detect_and_react_to_kindness(message, after_reply=False, server_contex
|
||||
await message.add_reaction(emoji)
|
||||
globals.kindness_reacted_messages.add(message.id)
|
||||
message.kindness_reacted = True # Mark as done
|
||||
print("✅ Kindness detected via keywords. Reacted immediately.")
|
||||
logger.info("Kindness detected via keywords. Reacted immediately.")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error adding reaction: {e}")
|
||||
logger.error(f"Error adding reaction: {e}")
|
||||
return
|
||||
|
||||
# 2. If not after_reply, defer model-based check
|
||||
if not after_reply:
|
||||
print("🗝️ No kindness via keywords. Deferring...")
|
||||
logger.debug("No kindness via keywords. Deferring...")
|
||||
return
|
||||
|
||||
# 3. Model-based detection
|
||||
@@ -42,8 +45,8 @@ async def detect_and_react_to_kindness(message, after_reply=False, server_contex
|
||||
if result.strip().lower().startswith("yes"):
|
||||
await message.add_reaction(emoji)
|
||||
globals.kindness_reacted_messages.add(message.id)
|
||||
print("✅ Kindness detected via model. Reacted.")
|
||||
logger.info("Kindness detected via model. Reacted.")
|
||||
else:
|
||||
print("🧊 No kindness detected.")
|
||||
logger.debug("No kindness detected.")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error during kindness analysis: {e}")
|
||||
logger.error(f"Error during kindness analysis: {e}")
|
||||
|
||||
@@ -10,6 +10,10 @@ import os
|
||||
from utils.context_manager import get_context_for_response_type, get_complete_context
|
||||
from utils.moods import load_mood_description
|
||||
from utils.conversation_history import conversation_history
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger('llm')
|
||||
|
||||
|
||||
def get_current_gpu_url():
|
||||
"""Get the URL for the currently selected GPU for text models"""
|
||||
@@ -23,7 +27,7 @@ def get_current_gpu_url():
|
||||
else:
|
||||
return globals.LLAMA_URL
|
||||
except Exception as e:
|
||||
print(f"⚠️ GPU state read error: {e}, defaulting to NVIDIA")
|
||||
logger.warning(f"GPU state read error: {e}, defaulting to NVIDIA")
|
||||
# Default to NVIDIA if state file doesn't exist
|
||||
return globals.LLAMA_URL
|
||||
|
||||
@@ -102,7 +106,7 @@ async def query_llama(user_prompt, user_id, guild_id=None, response_type="dm_res
|
||||
if model is None:
|
||||
if evil_mode:
|
||||
model = globals.EVIL_TEXT_MODEL # Use DarkIdol uncensored model
|
||||
print(f"😈 Using evil model: {model}")
|
||||
logger.info(f"Using evil model: {model}")
|
||||
else:
|
||||
model = globals.TEXT_MODEL
|
||||
|
||||
@@ -155,7 +159,7 @@ You ARE Miku. Act like it."""
|
||||
is_sleeping = False
|
||||
forced_angry_until = None
|
||||
just_woken_up = False
|
||||
print(f"😈 Using Evil mode with mood: {current_mood_name}")
|
||||
logger.info(f"Using Evil mode with mood: {current_mood_name}")
|
||||
else:
|
||||
current_mood = globals.DM_MOOD_DESCRIPTION # Default to DM mood
|
||||
current_mood_name = globals.DM_MOOD # Default to DM mood name
|
||||
@@ -175,14 +179,14 @@ You ARE Miku. Act like it."""
|
||||
is_sleeping = server_config.is_sleeping
|
||||
forced_angry_until = server_config.forced_angry_until
|
||||
just_woken_up = server_config.just_woken_up
|
||||
print(f"🎭 Using server mood: {current_mood_name} for guild {guild_id}")
|
||||
logger.debug(f"Using server mood: {current_mood_name} for guild {guild_id}")
|
||||
else:
|
||||
print(f"⚠️ No server config found for guild {guild_id}, using DM mood")
|
||||
logger.warning(f"No server config found for guild {guild_id}, using DM mood")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to get server mood for guild {guild_id}, falling back to DM mood: {e}")
|
||||
logger.error(f"Failed to get server mood for guild {guild_id}, falling back to DM mood: {e}")
|
||||
# Fall back to DM mood if server mood fails
|
||||
elif not evil_mode:
|
||||
print(f"🌍 Using DM mood: {globals.DM_MOOD}")
|
||||
logger.debug(f"Using DM mood: {globals.DM_MOOD}")
|
||||
|
||||
# Append angry wake-up note if JUST_WOKEN_UP flag is set (only in non-evil mode)
|
||||
if just_woken_up and not evil_mode:
|
||||
@@ -262,7 +266,7 @@ Please respond in a way that reflects this emotional tone.{pfp_context}"""
|
||||
try:
|
||||
# Get current GPU URL based on user selection
|
||||
llama_url = get_current_gpu_url()
|
||||
print(f"🎮 Using GPU endpoint: {llama_url}")
|
||||
logger.debug(f"Using GPU endpoint: {llama_url}")
|
||||
|
||||
# Add timeout to prevent hanging indefinitely
|
||||
timeout = aiohttp.ClientTimeout(total=300) # 300 second timeout
|
||||
@@ -301,13 +305,13 @@ Please respond in a way that reflects this emotional tone.{pfp_context}"""
|
||||
return reply
|
||||
else:
|
||||
error_text = await response.text()
|
||||
print(f"❌ Error from llama-swap: {response.status} - {error_text}")
|
||||
logger.error(f"Error from llama-swap: {response.status} - {error_text}")
|
||||
# Don't save error responses to conversation history
|
||||
return f"Error: {response.status}"
|
||||
except asyncio.TimeoutError:
|
||||
return "Sorry, the response took too long. Please try again."
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error in query_llama: {e}")
|
||||
logger.error(f"Error in query_llama: {e}")
|
||||
return f"Sorry, there was an error: {str(e)}"
|
||||
|
||||
# Backward compatibility alias for existing code
|
||||
|
||||
286
bot/utils/log_config.py
Normal file
286
bot/utils/log_config.py
Normal file
@@ -0,0 +1,286 @@
|
||||
"""
|
||||
Log Configuration Manager
|
||||
|
||||
Handles runtime configuration updates for the logging system.
|
||||
Provides API for the web UI to update log settings without restarting the bot.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
import json
|
||||
|
||||
try:
|
||||
from utils.logger import get_logger
|
||||
logger = get_logger('core')
|
||||
except Exception:
|
||||
logger = None
|
||||
|
||||
|
||||
CONFIG_FILE = Path('/app/memory/log_settings.json')
|
||||
|
||||
|
||||
def load_config() -> Dict:
|
||||
"""Load log configuration from file."""
|
||||
from utils.logger import get_log_config
|
||||
return get_log_config()
|
||||
|
||||
|
||||
def save_config(config: Dict) -> bool:
|
||||
"""
|
||||
Save log configuration to file.
|
||||
|
||||
Args:
|
||||
config: Configuration dictionary
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
from utils.logger import save_config
|
||||
save_config(config)
|
||||
return True
|
||||
except Exception as e:
|
||||
if logger:
|
||||
logger.error(f"Failed to save log config: {e}")
|
||||
print(f"Failed to save log config: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def update_component(component: str, enabled: bool = None, enabled_levels: List[str] = None) -> bool:
|
||||
"""
|
||||
Update a single component's configuration.
|
||||
|
||||
Args:
|
||||
component: Component name
|
||||
enabled: Enable/disable the component
|
||||
enabled_levels: List of log levels to enable (DEBUG, INFO, WARNING, ERROR, CRITICAL, API)
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
config = load_config()
|
||||
|
||||
if component not in config['components']:
|
||||
return False
|
||||
|
||||
if enabled is not None:
|
||||
config['components'][component]['enabled'] = enabled
|
||||
|
||||
if enabled_levels is not None:
|
||||
valid_levels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL', 'API']
|
||||
# Validate all levels
|
||||
for level in enabled_levels:
|
||||
if level.upper() not in valid_levels:
|
||||
return False
|
||||
config['components'][component]['enabled_levels'] = [l.upper() for l in enabled_levels]
|
||||
|
||||
return save_config(config)
|
||||
except Exception as e:
|
||||
if logger:
|
||||
logger.error(f"Failed to update component {component}: {e}")
|
||||
print(f"Failed to update component {component}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def update_global_level(level: str, enabled: bool) -> bool:
|
||||
"""
|
||||
Enable or disable a specific log level across all components.
|
||||
|
||||
Args:
|
||||
level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL, API)
|
||||
enabled: True to enable, False to disable
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
level = level.upper()
|
||||
valid_levels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL', 'API']
|
||||
|
||||
if level not in valid_levels:
|
||||
return False
|
||||
|
||||
config = load_config()
|
||||
|
||||
# Update all components
|
||||
for component_name in config['components'].keys():
|
||||
current_levels = config['components'][component_name].get('enabled_levels', [])
|
||||
|
||||
if enabled:
|
||||
# Add level if not present
|
||||
if level not in current_levels:
|
||||
current_levels.append(level)
|
||||
else:
|
||||
# Remove level if present
|
||||
if level in current_levels:
|
||||
current_levels.remove(level)
|
||||
|
||||
config['components'][component_name]['enabled_levels'] = current_levels
|
||||
|
||||
return save_config(config)
|
||||
except Exception as e:
|
||||
if logger:
|
||||
logger.error(f"Failed to update global level {level}: {e}")
|
||||
print(f"Failed to update global level {level}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def update_timestamp_format(format_type: str) -> bool:
|
||||
"""
|
||||
Update timestamp format for all log outputs.
|
||||
|
||||
Args:
|
||||
format_type: Format type - 'off', 'time', 'date', or 'datetime'
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
valid_formats = ['off', 'time', 'date', 'datetime']
|
||||
|
||||
if format_type not in valid_formats:
|
||||
return False
|
||||
|
||||
config = load_config()
|
||||
|
||||
if 'formatting' not in config:
|
||||
config['formatting'] = {}
|
||||
|
||||
config['formatting']['timestamp_format'] = format_type
|
||||
|
||||
return save_config(config)
|
||||
except Exception as e:
|
||||
if logger:
|
||||
logger.error(f"Failed to update timestamp format: {e}")
|
||||
print(f"Failed to update timestamp format: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def update_api_filters(
|
||||
exclude_paths: List[str] = None,
|
||||
exclude_status: List[int] = None,
|
||||
include_slow_requests: bool = None,
|
||||
slow_threshold_ms: int = None
|
||||
) -> bool:
|
||||
"""
|
||||
Update API request filtering configuration.
|
||||
|
||||
Args:
|
||||
exclude_paths: List of path patterns to exclude (e.g., ['/health', '/static/*'])
|
||||
exclude_status: List of HTTP status codes to exclude (e.g., [200, 304])
|
||||
include_slow_requests: Whether to log slow requests
|
||||
slow_threshold_ms: Threshold for slow requests in milliseconds
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
config = load_config()
|
||||
|
||||
if 'api.requests' not in config['components']:
|
||||
return False
|
||||
|
||||
filters = config['components']['api.requests'].get('filters', {})
|
||||
|
||||
if exclude_paths is not None:
|
||||
filters['exclude_paths'] = exclude_paths
|
||||
|
||||
if exclude_status is not None:
|
||||
filters['exclude_status'] = exclude_status
|
||||
|
||||
if include_slow_requests is not None:
|
||||
filters['include_slow_requests'] = include_slow_requests
|
||||
|
||||
if slow_threshold_ms is not None:
|
||||
filters['slow_threshold_ms'] = slow_threshold_ms
|
||||
|
||||
config['components']['api.requests']['filters'] = filters
|
||||
|
||||
return save_config(config)
|
||||
except Exception as e:
|
||||
if logger:
|
||||
logger.error(f"Failed to update API filters: {e}")
|
||||
print(f"Failed to update API filters: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def reset_to_defaults() -> bool:
|
||||
"""
|
||||
Reset configuration to defaults.
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
from utils.logger import get_default_config, save_config
|
||||
default_config = get_default_config()
|
||||
save_config(default_config)
|
||||
return True
|
||||
except Exception as e:
|
||||
if logger:
|
||||
logger.error(f"Failed to reset config: {e}")
|
||||
print(f"Failed to reset config: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def get_component_config(component: str) -> Optional[Dict]:
|
||||
"""
|
||||
Get configuration for a specific component.
|
||||
|
||||
Args:
|
||||
component: Component name
|
||||
|
||||
Returns:
|
||||
Component configuration dictionary or None
|
||||
"""
|
||||
try:
|
||||
config = load_config()
|
||||
return config['components'].get(component)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def is_component_enabled(component: str) -> bool:
|
||||
"""
|
||||
Check if a component is enabled.
|
||||
|
||||
Args:
|
||||
component: Component name
|
||||
|
||||
Returns:
|
||||
True if enabled, False otherwise
|
||||
"""
|
||||
component_config = get_component_config(component)
|
||||
if component_config is None:
|
||||
return True # Default to enabled
|
||||
return component_config.get('enabled', True)
|
||||
|
||||
|
||||
def get_component_level(component: str) -> str:
|
||||
"""
|
||||
Get log level for a component.
|
||||
|
||||
Args:
|
||||
component: Component name
|
||||
|
||||
Returns:
|
||||
Log level string (e.g., 'INFO', 'DEBUG')
|
||||
"""
|
||||
component_config = get_component_config(component)
|
||||
if component_config is None:
|
||||
return 'INFO' # Default level
|
||||
return component_config.get('level', 'INFO')
|
||||
|
||||
|
||||
def reload_all_loggers():
|
||||
"""Reload all logger configurations."""
|
||||
try:
|
||||
from utils.logger import reload_config
|
||||
reload_config()
|
||||
return True
|
||||
except Exception as e:
|
||||
if logger:
|
||||
logger.error(f"Failed to reload loggers: {e}")
|
||||
print(f"Failed to reload loggers: {e}")
|
||||
return False
|
||||
395
bot/utils/logger.py
Normal file
395
bot/utils/logger.py
Normal file
@@ -0,0 +1,395 @@
|
||||
"""
|
||||
Centralized Logging System for Miku Discord Bot
|
||||
|
||||
This module provides a robust, component-based logging system with:
|
||||
- Configurable log levels per component
|
||||
- Emoji-based log formatting
|
||||
- Multiple output handlers (console, separate log files per component)
|
||||
- Runtime configuration updates
|
||||
- API request filtering
|
||||
- Docker-compatible output
|
||||
|
||||
Usage:
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger('bot')
|
||||
logger.info("Bot started successfully")
|
||||
logger.error("Failed to connect", exc_info=True)
|
||||
"""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import json
|
||||
|
||||
# Log level emojis
|
||||
LEVEL_EMOJIS = {
|
||||
'DEBUG': '🔍',
|
||||
'INFO': 'ℹ️',
|
||||
'WARNING': '⚠️',
|
||||
'ERROR': '❌',
|
||||
'CRITICAL': '🔥',
|
||||
'API': '🌐',
|
||||
}
|
||||
|
||||
# Custom API log level (between INFO and WARNING)
|
||||
API_LEVEL = 25
|
||||
logging.addLevelName(API_LEVEL, 'API')
|
||||
|
||||
# Component definitions
|
||||
COMPONENTS = {
|
||||
'bot': 'Main bot lifecycle and events',
|
||||
'api': 'FastAPI endpoints (non-HTTP)',
|
||||
'api.requests': 'HTTP request/response logs',
|
||||
'autonomous': 'Autonomous messaging system',
|
||||
'persona': 'Bipolar/persona dialogue system',
|
||||
'vision': 'Image and video processing',
|
||||
'llm': 'LLM API calls and interactions',
|
||||
'conversation': 'Conversation history management',
|
||||
'mood': 'Mood system and state changes',
|
||||
'dm': 'Direct message handling',
|
||||
'scheduled': 'Scheduled tasks and cron jobs',
|
||||
'gpu': 'GPU routing and model management',
|
||||
'media': 'Media processing (audio, video, images)',
|
||||
'server': 'Server management and configuration',
|
||||
'commands': 'Command handling and routing',
|
||||
'sentiment': 'Sentiment analysis',
|
||||
'core': 'Core utilities and helpers',
|
||||
'apscheduler': 'Job scheduler logs (APScheduler)',
|
||||
}
|
||||
|
||||
# Global configuration
|
||||
_log_config: Optional[Dict] = None
|
||||
_loggers: Dict[str, logging.Logger] = {}
|
||||
_handlers_initialized = False
|
||||
|
||||
# Log directory (in mounted volume so logs persist)
|
||||
LOG_DIR = Path(os.getenv('LOG_DIR', '/app/memory/logs'))
|
||||
|
||||
|
||||
class EmojiFormatter(logging.Formatter):
|
||||
"""Custom formatter that adds emojis and colors to log messages."""
|
||||
|
||||
def __init__(self, use_emojis=True, use_colors=False, timestamp_format='datetime', *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.use_emojis = use_emojis
|
||||
self.use_colors = use_colors
|
||||
self.timestamp_format = timestamp_format
|
||||
|
||||
def format(self, record):
|
||||
# Add emoji prefix
|
||||
if self.use_emojis:
|
||||
emoji = LEVEL_EMOJIS.get(record.levelname, '')
|
||||
record.levelname_emoji = f"{emoji} {record.levelname}"
|
||||
else:
|
||||
record.levelname_emoji = record.levelname
|
||||
|
||||
# Format timestamp based on settings
|
||||
if self.timestamp_format == 'off':
|
||||
record.timestamp_formatted = ''
|
||||
elif self.timestamp_format == 'time':
|
||||
record.timestamp_formatted = self.formatTime(record, '%H:%M:%S') + ' '
|
||||
elif self.timestamp_format == 'date':
|
||||
record.timestamp_formatted = self.formatTime(record, '%Y-%m-%d') + ' '
|
||||
elif self.timestamp_format == 'datetime':
|
||||
record.timestamp_formatted = self.formatTime(record, '%Y-%m-%d %H:%M:%S') + ' '
|
||||
else:
|
||||
# Default to datetime if invalid option
|
||||
record.timestamp_formatted = self.formatTime(record, '%Y-%m-%d %H:%M:%S') + ' '
|
||||
|
||||
# Format the message
|
||||
return super().format(record)
|
||||
|
||||
|
||||
class ComponentFilter(logging.Filter):
|
||||
"""Filter logs based on component configuration with individual level toggles."""
|
||||
|
||||
def __init__(self, component_name: str):
|
||||
super().__init__()
|
||||
self.component_name = component_name
|
||||
|
||||
def filter(self, record):
|
||||
"""Check if this log should be output based on enabled levels."""
|
||||
config = get_log_config()
|
||||
|
||||
if not config:
|
||||
return True
|
||||
|
||||
component_config = config.get('components', {}).get(self.component_name, {})
|
||||
|
||||
# Check if component is enabled
|
||||
if not component_config.get('enabled', True):
|
||||
return False
|
||||
|
||||
# Check if specific log level is enabled
|
||||
enabled_levels = component_config.get('enabled_levels', ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL', 'API'])
|
||||
|
||||
# Get the level name for this record
|
||||
level_name = logging.getLevelName(record.levelno)
|
||||
|
||||
return level_name in enabled_levels
|
||||
|
||||
|
||||
def get_log_config() -> Optional[Dict]:
|
||||
"""Get current log configuration."""
|
||||
global _log_config
|
||||
|
||||
if _log_config is None:
|
||||
# Try to load from file
|
||||
config_path = Path('/app/memory/log_settings.json')
|
||||
if config_path.exists():
|
||||
try:
|
||||
with open(config_path, 'r') as f:
|
||||
_log_config = json.load(f)
|
||||
except Exception:
|
||||
_log_config = get_default_config()
|
||||
else:
|
||||
_log_config = get_default_config()
|
||||
|
||||
return _log_config
|
||||
|
||||
|
||||
def get_default_config() -> Dict:
|
||||
"""Get default logging configuration."""
|
||||
# Read from environment variables
|
||||
# Enable api.requests by default (now that uvicorn access logs are disabled)
|
||||
enable_api_requests = os.getenv('LOG_ENABLE_API_REQUESTS', 'true').lower() == 'true'
|
||||
use_emojis = os.getenv('LOG_USE_EMOJIS', 'true').lower() == 'true'
|
||||
|
||||
config = {
|
||||
'version': '1.0',
|
||||
'formatting': {
|
||||
'use_emojis': use_emojis,
|
||||
'use_colors': False,
|
||||
'timestamp_format': 'datetime' # Options: 'off', 'time', 'date', 'datetime'
|
||||
},
|
||||
'components': {}
|
||||
}
|
||||
|
||||
# Set defaults for each component
|
||||
for component in COMPONENTS.keys():
|
||||
if component == 'api.requests':
|
||||
# API requests component defaults to only ERROR and CRITICAL
|
||||
default_levels = ['ERROR', 'CRITICAL'] if not enable_api_requests else ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL', 'API']
|
||||
config['components'][component] = {
|
||||
'enabled': enable_api_requests,
|
||||
'enabled_levels': default_levels,
|
||||
'filters': {
|
||||
'exclude_paths': ['/health', '/static/*'],
|
||||
'exclude_status': [200, 304] if not enable_api_requests else [],
|
||||
'include_slow_requests': True,
|
||||
'slow_threshold_ms': 1000
|
||||
}
|
||||
}
|
||||
elif component == 'apscheduler':
|
||||
# APScheduler defaults to WARNING and above (lots of INFO noise)
|
||||
config['components'][component] = {
|
||||
'enabled': True,
|
||||
'enabled_levels': ['WARNING', 'ERROR', 'CRITICAL']
|
||||
}
|
||||
else:
|
||||
# All other components default to all levels enabled
|
||||
config['components'][component] = {
|
||||
'enabled': True,
|
||||
'enabled_levels': ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
|
||||
}
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def reload_config():
|
||||
"""Reload configuration from file."""
|
||||
global _log_config
|
||||
_log_config = None
|
||||
get_log_config()
|
||||
|
||||
# Update all existing loggers
|
||||
for component_name, logger in _loggers.items():
|
||||
_configure_logger(logger, component_name)
|
||||
|
||||
|
||||
def save_config(config: Dict):
|
||||
"""Save configuration to file."""
|
||||
global _log_config
|
||||
_log_config = config
|
||||
|
||||
config_path = Path('/app/memory/log_settings.json')
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with open(config_path, 'w') as f:
|
||||
json.dump(config, f, indent=2)
|
||||
|
||||
# Reload all loggers
|
||||
reload_config()
|
||||
|
||||
|
||||
def _setup_handlers():
|
||||
"""Set up log handlers (console and file)."""
|
||||
global _handlers_initialized
|
||||
|
||||
if _handlers_initialized:
|
||||
return
|
||||
|
||||
# Create log directory
|
||||
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
_handlers_initialized = True
|
||||
|
||||
|
||||
def _configure_logger(logger: logging.Logger, component_name: str):
|
||||
"""Configure a logger with handlers and filters."""
|
||||
config = get_log_config()
|
||||
formatting = config.get('formatting', {})
|
||||
|
||||
# Clear existing handlers
|
||||
logger.handlers.clear()
|
||||
|
||||
# Set logger level to DEBUG so handlers can filter
|
||||
logger.setLevel(logging.DEBUG)
|
||||
logger.propagate = False
|
||||
|
||||
# Create formatter
|
||||
timestamp_format = formatting.get('timestamp_format', 'datetime') # 'off', 'time', 'date', or 'datetime'
|
||||
use_emojis = formatting.get('use_emojis', True)
|
||||
use_colors = formatting.get('use_colors', False)
|
||||
|
||||
# Console handler - goes to Docker logs
|
||||
console_handler = logging.StreamHandler(sys.stdout)
|
||||
console_formatter = EmojiFormatter(
|
||||
fmt='%(timestamp_formatted)s[%(levelname_emoji)s] [%(name)s] %(message)s',
|
||||
use_emojis=use_emojis,
|
||||
use_colors=use_colors,
|
||||
timestamp_format=timestamp_format
|
||||
)
|
||||
console_handler.setFormatter(console_formatter)
|
||||
console_handler.addFilter(ComponentFilter(component_name))
|
||||
logger.addHandler(console_handler)
|
||||
|
||||
# File handler - separate file per component
|
||||
log_file = LOG_DIR / f'{component_name.replace(".", "_")}.log'
|
||||
file_handler = RotatingFileHandler(
|
||||
log_file,
|
||||
maxBytes=10 * 1024 * 1024, # 10MB
|
||||
backupCount=5,
|
||||
encoding='utf-8'
|
||||
)
|
||||
file_formatter = EmojiFormatter(
|
||||
fmt='%(timestamp_formatted)s[%(levelname)s] [%(name)s] %(message)s',
|
||||
use_emojis=False, # No emojis in file logs
|
||||
use_colors=False,
|
||||
timestamp_format=timestamp_format
|
||||
)
|
||||
file_handler.setFormatter(file_formatter)
|
||||
file_handler.addFilter(ComponentFilter(component_name))
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
|
||||
def get_logger(component: str) -> logging.Logger:
|
||||
"""
|
||||
Get a logger for a specific component.
|
||||
|
||||
Args:
|
||||
component: Component name (e.g., 'bot', 'api', 'autonomous')
|
||||
|
||||
Returns:
|
||||
Configured logger instance
|
||||
|
||||
Example:
|
||||
logger = get_logger('bot')
|
||||
logger.info("Bot started")
|
||||
logger.error("Connection failed", exc_info=True)
|
||||
"""
|
||||
if component not in COMPONENTS:
|
||||
raise ValueError(
|
||||
f"Unknown component '{component}'. "
|
||||
f"Available: {', '.join(COMPONENTS.keys())}"
|
||||
)
|
||||
|
||||
if component in _loggers:
|
||||
return _loggers[component]
|
||||
|
||||
# Setup handlers if not done
|
||||
_setup_handlers()
|
||||
|
||||
# Create logger
|
||||
logger = logging.Logger(component)
|
||||
|
||||
# Add custom API level method
|
||||
def api(self, message, *args, **kwargs):
|
||||
if self.isEnabledFor(API_LEVEL):
|
||||
self._log(API_LEVEL, message, args, **kwargs)
|
||||
|
||||
logger.api = lambda msg, *args, **kwargs: api(logger, msg, *args, **kwargs)
|
||||
|
||||
# Configure logger
|
||||
_configure_logger(logger, component)
|
||||
|
||||
# Cache it
|
||||
_loggers[component] = logger
|
||||
|
||||
return logger
|
||||
|
||||
|
||||
def list_components() -> Dict[str, str]:
|
||||
"""Get list of all available components with descriptions."""
|
||||
return COMPONENTS.copy()
|
||||
|
||||
|
||||
def get_component_stats() -> Dict[str, Dict]:
|
||||
"""Get statistics about each component's logging."""
|
||||
stats = {}
|
||||
|
||||
for component in COMPONENTS.keys():
|
||||
log_file = LOG_DIR / f'{component.replace(".", "_")}.log'
|
||||
|
||||
stats[component] = {
|
||||
'enabled': True, # Will be updated from config
|
||||
'log_file': str(log_file),
|
||||
'file_exists': log_file.exists(),
|
||||
'file_size': log_file.stat().st_size if log_file.exists() else 0,
|
||||
}
|
||||
|
||||
# Update from config
|
||||
config = get_log_config()
|
||||
component_config = config.get('components', {}).get(component, {})
|
||||
stats[component]['enabled'] = component_config.get('enabled', True)
|
||||
stats[component]['level'] = component_config.get('level', 'INFO')
|
||||
stats[component]['enabled_levels'] = component_config.get('enabled_levels', ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'])
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
def intercept_external_loggers():
|
||||
"""
|
||||
Intercept logs from external libraries (APScheduler, etc.) and route them through our system.
|
||||
Call this after initializing your application.
|
||||
"""
|
||||
# Intercept APScheduler loggers
|
||||
apscheduler_loggers = [
|
||||
'apscheduler',
|
||||
'apscheduler.scheduler',
|
||||
'apscheduler.executors',
|
||||
'apscheduler.jobstores',
|
||||
]
|
||||
|
||||
our_logger = get_logger('apscheduler')
|
||||
|
||||
for logger_name in apscheduler_loggers:
|
||||
ext_logger = logging.getLogger(logger_name)
|
||||
# Remove existing handlers
|
||||
ext_logger.handlers.clear()
|
||||
ext_logger.propagate = False
|
||||
|
||||
# Add our handlers
|
||||
for handler in our_logger.handlers:
|
||||
ext_logger.addHandler(handler)
|
||||
|
||||
# Set level
|
||||
ext_logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
# Initialize on import
|
||||
_setup_handlers()
|
||||
@@ -1,6 +1,9 @@
|
||||
# utils/media.py
|
||||
|
||||
import subprocess
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger('media')
|
||||
|
||||
async def overlay_username_with_ffmpeg(base_video_path, output_path, username):
|
||||
font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
|
||||
@@ -65,6 +68,6 @@ async def overlay_username_with_ffmpeg(base_video_path, output_path, username):
|
||||
|
||||
try:
|
||||
subprocess.run(ffmpeg_command, check=True)
|
||||
print("✅ Video processed successfully with username overlays.")
|
||||
logger.info("Video processed successfully with username overlays.")
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"⚠️ FFmpeg error: {e}")
|
||||
logger.error(f"FFmpeg error: {e}")
|
||||
|
||||
@@ -7,6 +7,9 @@ import asyncio
|
||||
from discord.ext import tasks
|
||||
import globals
|
||||
import datetime
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger('mood')
|
||||
|
||||
MOOD_EMOJIS = {
|
||||
"asleep": "💤",
|
||||
@@ -47,7 +50,7 @@ def load_mood_description(mood_name: str) -> str:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return f.read().strip()
|
||||
except FileNotFoundError:
|
||||
print(f"⚠️ Mood file '{mood_name}' not found. Falling back to default.")
|
||||
logger.warning(f"Mood file '{mood_name}' not found. Falling back to default.")
|
||||
# Return a default mood description instead of recursive call
|
||||
return "I'm feeling neutral and balanced today."
|
||||
|
||||
@@ -120,17 +123,17 @@ def detect_mood_shift(response_text, server_context=None):
|
||||
# For server context, check against server's current mood
|
||||
current_mood = server_context.get('current_mood_name', 'neutral')
|
||||
if current_mood != "sleepy":
|
||||
print(f"❎ Mood 'asleep' skipped - server mood isn't 'sleepy', it's '{current_mood}'")
|
||||
logger.debug(f"Mood 'asleep' skipped - server mood isn't 'sleepy', it's '{current_mood}'")
|
||||
continue
|
||||
else:
|
||||
# For DM context, check against DM mood
|
||||
if globals.DM_MOOD != "sleepy":
|
||||
print(f"❎ Mood 'asleep' skipped - DM mood isn't 'sleepy', it's '{globals.DM_MOOD}'")
|
||||
logger.debug(f"Mood 'asleep' skipped - DM mood isn't 'sleepy', it's '{globals.DM_MOOD}'")
|
||||
continue
|
||||
|
||||
for phrase in phrases:
|
||||
if phrase.lower() in response_text.lower():
|
||||
print(f"*️⃣ Mood keyword triggered: {phrase}")
|
||||
logger.info(f"Mood keyword triggered: {phrase}")
|
||||
return mood
|
||||
return None
|
||||
|
||||
@@ -155,13 +158,13 @@ async def rotate_dm_mood():
|
||||
globals.DM_MOOD = new_mood
|
||||
globals.DM_MOOD_DESCRIPTION = load_mood_description(new_mood)
|
||||
|
||||
print(f"🔄 DM mood rotated from {old_mood} to {new_mood}")
|
||||
logger.info(f"DM mood rotated from {old_mood} to {new_mood}")
|
||||
|
||||
# Note: We don't update server nicknames here because servers have their own independent moods.
|
||||
# DM mood only affects direct messages to users.
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Exception in rotate_dm_mood: {e}")
|
||||
logger.error(f"Exception in rotate_dm_mood: {e}")
|
||||
|
||||
async def update_all_server_nicknames():
|
||||
"""
|
||||
@@ -171,8 +174,8 @@ async def update_all_server_nicknames():
|
||||
This function incorrectly used DM mood to update all server nicknames,
|
||||
breaking the independent per-server mood system.
|
||||
"""
|
||||
print("⚠️ WARNING: update_all_server_nicknames() is deprecated and should not be called!")
|
||||
print("⚠️ Use update_server_nickname(guild_id) for per-server nickname updates instead.")
|
||||
logger.warning("WARNING: update_all_server_nicknames() is deprecated and should not be called!")
|
||||
logger.warning("Use update_server_nickname(guild_id) for per-server nickname updates instead.")
|
||||
# Do nothing - this function should not modify nicknames
|
||||
|
||||
async def nickname_mood_emoji(guild_id: int):
|
||||
@@ -182,11 +185,11 @@ async def nickname_mood_emoji(guild_id: int):
|
||||
async def update_server_nickname(guild_id: int):
|
||||
"""Update nickname for a specific server based on its mood"""
|
||||
try:
|
||||
print(f"🎭 Starting nickname update for server {guild_id}")
|
||||
logger.debug(f"Starting nickname update for server {guild_id}")
|
||||
|
||||
# Check if bot is ready
|
||||
if not globals.client.is_ready():
|
||||
print(f"⚠️ Bot not ready yet, deferring nickname update for server {guild_id}")
|
||||
logger.warning(f"Bot not ready yet, deferring nickname update for server {guild_id}")
|
||||
return
|
||||
|
||||
# Check if evil mode is active
|
||||
@@ -196,7 +199,7 @@ async def update_server_nickname(guild_id: int):
|
||||
from server_manager import server_manager
|
||||
server_config = server_manager.get_server_config(guild_id)
|
||||
if not server_config:
|
||||
print(f"⚠️ No server config found for guild {guild_id}")
|
||||
logger.warning(f"No server config found for guild {guild_id}")
|
||||
return
|
||||
|
||||
if evil_mode:
|
||||
@@ -209,29 +212,29 @@ async def update_server_nickname(guild_id: int):
|
||||
emoji = MOOD_EMOJIS.get(mood, "")
|
||||
base_name = "Hatsune Miku"
|
||||
|
||||
print(f"🔍 Server {guild_id} mood is: {mood} (evil_mode={evil_mode})")
|
||||
print(f"🔍 Using emoji: {emoji}")
|
||||
logger.debug(f"Server {guild_id} mood is: {mood} (evil_mode={evil_mode})")
|
||||
logger.debug(f"Using emoji: {emoji}")
|
||||
|
||||
nickname = f"{base_name}{emoji}"
|
||||
print(f"🔍 New nickname will be: {nickname}")
|
||||
logger.debug(f"New nickname will be: {nickname}")
|
||||
|
||||
guild = globals.client.get_guild(guild_id)
|
||||
if guild:
|
||||
print(f"🔍 Found guild: {guild.name}")
|
||||
logger.debug(f"Found guild: {guild.name}")
|
||||
me = guild.get_member(globals.BOT_USER.id)
|
||||
if me is not None:
|
||||
print(f"🔍 Found bot member: {me.display_name}")
|
||||
logger.debug(f"Found bot member: {me.display_name}")
|
||||
try:
|
||||
await me.edit(nick=nickname)
|
||||
print(f"💱 Changed nickname to {nickname} in server {guild.name}")
|
||||
logger.info(f"Changed nickname to {nickname} in server {guild.name}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to update nickname in server {guild.name}: {e}")
|
||||
logger.warning(f"Failed to update nickname in server {guild.name}: {e}")
|
||||
else:
|
||||
print(f"⚠️ Could not find bot member in server {guild.name}")
|
||||
logger.warning(f"Could not find bot member in server {guild.name}")
|
||||
else:
|
||||
print(f"⚠️ Could not find guild {guild_id}")
|
||||
logger.warning(f"Could not find guild {guild_id}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error updating server nickname for guild {guild_id}: {e}")
|
||||
logger.error(f"Error updating server nickname for guild {guild_id}: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
@@ -268,7 +271,7 @@ async def rotate_server_mood(guild_id: int):
|
||||
|
||||
# Block transition to asleep unless coming from sleepy
|
||||
if new_mood_name == "asleep" and old_mood_name != "sleepy":
|
||||
print(f"❌ Cannot rotate to asleep from {old_mood_name}, must be sleepy first")
|
||||
logger.warning(f"Cannot rotate to asleep from {old_mood_name}, must be sleepy first")
|
||||
# Try to get a different mood
|
||||
attempts = 0
|
||||
while (new_mood_name == "asleep" or new_mood_name == old_mood_name) and attempts < 5:
|
||||
@@ -282,7 +285,7 @@ async def rotate_server_mood(guild_id: int):
|
||||
from utils.autonomous import on_mood_change
|
||||
on_mood_change(guild_id, new_mood_name)
|
||||
except Exception as mood_notify_error:
|
||||
print(f"⚠️ Failed to notify autonomous engine of mood change: {mood_notify_error}")
|
||||
logger.error(f"Failed to notify autonomous engine of mood change: {mood_notify_error}")
|
||||
|
||||
# If transitioning to asleep, set up auto-wake
|
||||
if new_mood_name == "asleep":
|
||||
@@ -298,22 +301,22 @@ async def rotate_server_mood(guild_id: int):
|
||||
from utils.autonomous import on_mood_change
|
||||
on_mood_change(guild_id, "neutral")
|
||||
except Exception as mood_notify_error:
|
||||
print(f"⚠️ Failed to notify autonomous engine of wake-up mood change: {mood_notify_error}")
|
||||
logger.error(f"Failed to notify autonomous engine of wake-up mood change: {mood_notify_error}")
|
||||
|
||||
await update_server_nickname(guild_id)
|
||||
print(f"🌅 Server {guild_id} woke up from auto-sleep (mood rotation)")
|
||||
logger.info(f"Server {guild_id} woke up from auto-sleep (mood rotation)")
|
||||
|
||||
globals.client.loop.create_task(delayed_wakeup())
|
||||
print(f"⏰ Scheduled auto-wake for server {guild_id} in 1 hour")
|
||||
logger.info(f"Scheduled auto-wake for server {guild_id} in 1 hour")
|
||||
|
||||
# Update nickname for this specific server
|
||||
await update_server_nickname(guild_id)
|
||||
|
||||
print(f"🔄 Rotated mood for server {guild_id} from {old_mood_name} to {new_mood_name}")
|
||||
logger.info(f"Rotated mood for server {guild_id} from {old_mood_name} to {new_mood_name}")
|
||||
except Exception as e:
|
||||
print(f"❌ Exception in rotate_server_mood for server {guild_id}: {e}")
|
||||
logger.error(f"Exception in rotate_server_mood for server {guild_id}: {e}")
|
||||
|
||||
async def clear_angry_mood_after_delay():
|
||||
"""Clear angry mood after delay (legacy function - now handled per-server)"""
|
||||
print("⚠️ clear_angry_mood_after_delay called - this function is deprecated")
|
||||
logger.warning("clear_angry_mood_after_delay called - this function is deprecated")
|
||||
pass
|
||||
|
||||
@@ -15,6 +15,15 @@ This system is designed to be lightweight on LLM calls:
|
||||
- Only escalates to argument system when tension threshold is reached
|
||||
"""
|
||||
|
||||
import discord
|
||||
import asyncio
|
||||
import time
|
||||
import globals
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger('persona')
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
@@ -38,7 +47,7 @@ ARGUMENT_TENSION_THRESHOLD = 0.75 # Tension level that triggers argument escal
|
||||
# Initial trigger settings
|
||||
INTERJECTION_COOLDOWN_HARD = 180 # 3 minutes hard block
|
||||
INTERJECTION_COOLDOWN_SOFT = 900 # 15 minutes for full recovery
|
||||
INTERJECTION_THRESHOLD = 0.75 # Score needed to trigger interjection (lowered to account for mood multipliers)
|
||||
INTERJECTION_THRESHOLD = 0.5 # Score needed to trigger interjection
|
||||
|
||||
# ============================================================================
|
||||
# INTERJECTION SCORER (Initial Trigger Decision)
|
||||
@@ -62,15 +71,15 @@ class InterjectionScorer:
|
||||
def sentiment_analyzer(self):
|
||||
"""Lazy load sentiment analyzer"""
|
||||
if self._sentiment_analyzer is None:
|
||||
print("🔄 Loading sentiment analyzer for persona dialogue...")
|
||||
logger.debug("Loading sentiment analyzer for persona dialogue...")
|
||||
try:
|
||||
self._sentiment_analyzer = pipeline(
|
||||
"sentiment-analysis",
|
||||
model="distilbert-base-uncased-finetuned-sst-2-english"
|
||||
)
|
||||
print("✅ Sentiment analyzer loaded")
|
||||
logger.info("Sentiment analyzer loaded")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to load sentiment analyzer: {e}")
|
||||
logger.error(f"Failed to load sentiment analyzer: {e}")
|
||||
self._sentiment_analyzer = None
|
||||
return self._sentiment_analyzer
|
||||
|
||||
@@ -97,8 +106,8 @@ class InterjectionScorer:
|
||||
|
||||
opposite_persona = "evil" if current_persona == "miku" else "miku"
|
||||
|
||||
print(f"🔍 [Interjection] Analyzing content: '{message.content[:100]}...'")
|
||||
print(f"🔍 [Interjection] Current persona: {current_persona}, Opposite: {opposite_persona}")
|
||||
logger.debug(f"[Interjection] Analyzing content: '{message.content[:100]}...'")
|
||||
logger.debug(f"[Interjection] Current persona: {current_persona}, Opposite: {opposite_persona}")
|
||||
|
||||
# Calculate score from various factors
|
||||
score = 0.0
|
||||
@@ -106,7 +115,7 @@ class InterjectionScorer:
|
||||
|
||||
# Factor 1: Direct addressing (automatic trigger)
|
||||
if self._mentions_opposite(message.content, opposite_persona):
|
||||
print(f"✅ [Interjection] Direct mention of {opposite_persona} detected!")
|
||||
logger.info(f"[Interjection] Direct mention of {opposite_persona} detected!")
|
||||
return True, "directly_addressed", 1.0
|
||||
|
||||
# Factor 2: Topic relevance
|
||||
@@ -147,8 +156,8 @@ class InterjectionScorer:
|
||||
reason_str = " | ".join(reasons) if reasons else "no_triggers"
|
||||
|
||||
if should_interject:
|
||||
print(f"✅ {opposite_persona.upper()} WILL INTERJECT (score: {score:.2f})")
|
||||
print(f" Reasons: {reason_str}")
|
||||
logger.info(f"{opposite_persona.upper()} WILL INTERJECT (score: {score:.2f})")
|
||||
logger.info(f" Reasons: {reason_str}")
|
||||
|
||||
return should_interject, reason_str, score
|
||||
|
||||
@@ -156,12 +165,12 @@ class InterjectionScorer:
|
||||
"""Fast rejection criteria"""
|
||||
# System messages
|
||||
if message.type != discord.MessageType.default:
|
||||
print(f"❌ [Basic Filter] System message type: {message.type}")
|
||||
logger.debug(f"[Basic Filter] System message type: {message.type}")
|
||||
return False
|
||||
|
||||
# Bipolar mode must be enabled
|
||||
if not globals.BIPOLAR_MODE:
|
||||
print(f"❌ [Basic Filter] Bipolar mode not enabled")
|
||||
logger.debug(f"[Basic Filter] Bipolar mode not enabled")
|
||||
return False
|
||||
|
||||
# Allow bot's own messages (we're checking them for interjections!)
|
||||
@@ -170,10 +179,10 @@ class InterjectionScorer:
|
||||
if message.author.bot and not message.webhook_id:
|
||||
# Check if it's our own bot
|
||||
if message.author.id != globals.client.user.id:
|
||||
print(f"❌ [Basic Filter] Other bot message (not our bot)")
|
||||
logger.debug(f"[Basic Filter] Other bot message (not our bot)")
|
||||
return False
|
||||
|
||||
print(f"✅ [Basic Filter] Passed (bot={message.author.bot}, webhook={message.webhook_id}, our_bot={message.author.id == globals.client.user.id if message.author.bot else 'N/A'})")
|
||||
logger.debug(f"[Basic Filter] Passed (bot={message.author.bot}, webhook={message.webhook_id}, our_bot={message.author.id == globals.client.user.id if message.author.bot else 'N/A'})")
|
||||
return True
|
||||
|
||||
def _mentions_opposite(self, content: str, opposite_persona: str) -> bool:
|
||||
@@ -233,7 +242,7 @@ class InterjectionScorer:
|
||||
|
||||
return min(confidence * 0.6 + intensity_markers, 1.0)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Sentiment analysis error: {e}")
|
||||
logger.error(f"Sentiment analysis error: {e}")
|
||||
return 0.5
|
||||
|
||||
def _detect_personality_clash(self, content: str, opposite_persona: str) -> float:
|
||||
@@ -364,15 +373,15 @@ class PersonaDialogue:
|
||||
}
|
||||
self.active_dialogues[channel_id] = state
|
||||
globals.LAST_PERSONA_DIALOGUE_TIME = time.time()
|
||||
print(f"💬 Started persona dialogue in channel {channel_id}")
|
||||
logger.info(f"Started persona dialogue in channel {channel_id}")
|
||||
return state
|
||||
|
||||
def end_dialogue(self, channel_id: int):
|
||||
"""End a dialogue in a channel"""
|
||||
if channel_id in self.active_dialogues:
|
||||
state = self.active_dialogues[channel_id]
|
||||
print(f"🏁 Ended persona dialogue in channel {channel_id}")
|
||||
print(f" Turns: {state['turn_count']}, Final tension: {state['tension']:.2f}")
|
||||
logger.info(f"Ended persona dialogue in channel {channel_id}")
|
||||
logger.info(f" Turns: {state['turn_count']}, Final tension: {state['tension']:.2f}")
|
||||
del self.active_dialogues[channel_id]
|
||||
|
||||
# ========================================================================
|
||||
@@ -400,7 +409,7 @@ class PersonaDialogue:
|
||||
else:
|
||||
base_delta = -sentiment_score * 0.05
|
||||
except Exception as e:
|
||||
print(f"⚠️ Sentiment analysis error in tension calc: {e}")
|
||||
logger.error(f"Sentiment analysis error in tension calc: {e}")
|
||||
|
||||
text_lower = response_text.lower()
|
||||
|
||||
@@ -557,7 +566,7 @@ On a new line after your response, write:
|
||||
|
||||
# Override: If the response contains a question mark, always continue
|
||||
if '?' in response_text:
|
||||
print(f"⚠️ [Parse Override] Question detected, forcing continue=YES")
|
||||
logger.debug(f"[Parse Override] Question detected, forcing continue=YES")
|
||||
should_continue = True
|
||||
if confidence == "LOW":
|
||||
confidence = "MEDIUM"
|
||||
@@ -605,12 +614,12 @@ You can use emojis naturally! ✨💙"""
|
||||
|
||||
# Safety limits
|
||||
if state["turn_count"] >= MAX_TURNS:
|
||||
print(f"🛑 Dialogue reached {MAX_TURNS} turns, ending")
|
||||
logger.info(f"Dialogue reached {MAX_TURNS} turns, ending")
|
||||
self.end_dialogue(channel_id)
|
||||
return
|
||||
|
||||
if time.time() - state["started_at"] > DIALOGUE_TIMEOUT:
|
||||
print(f"🛑 Dialogue timeout (15 min), ending")
|
||||
logger.info(f"Dialogue timeout (15 min), ending")
|
||||
self.end_dialogue(channel_id)
|
||||
return
|
||||
|
||||
@@ -625,7 +634,7 @@ You can use emojis naturally! ✨💙"""
|
||||
)
|
||||
|
||||
if not response_text:
|
||||
print(f"⚠️ Failed to generate response for {responding_persona}")
|
||||
logger.error(f"Failed to generate response for {responding_persona}")
|
||||
self.end_dialogue(channel_id)
|
||||
return
|
||||
|
||||
@@ -639,11 +648,11 @@ You can use emojis naturally! ✨💙"""
|
||||
"total": state["tension"],
|
||||
})
|
||||
|
||||
print(f"🌡️ Tension: {state['tension']:.2f} (delta: {tension_delta:+.2f})")
|
||||
logger.debug(f"Tension: {state['tension']:.2f} (delta: {tension_delta:+.2f})")
|
||||
|
||||
# Check if we should escalate to argument
|
||||
if state["tension"] >= ARGUMENT_TENSION_THRESHOLD:
|
||||
print(f"🔥 TENSION THRESHOLD REACHED ({state['tension']:.2f}) - ESCALATING TO ARGUMENT")
|
||||
logger.info(f"TENSION THRESHOLD REACHED ({state['tension']:.2f}) - ESCALATING TO ARGUMENT")
|
||||
|
||||
# Send the response that pushed us over
|
||||
await self._send_as_persona(channel, responding_persona, response_text)
|
||||
@@ -659,7 +668,7 @@ You can use emojis naturally! ✨💙"""
|
||||
state["turn_count"] += 1
|
||||
state["last_speaker"] = responding_persona
|
||||
|
||||
print(f"🗣️ Turn {state['turn_count']}: {responding_persona} | Continue: {should_continue} ({confidence}) | Tension: {state['tension']:.2f}")
|
||||
logger.debug(f"Turn {state['turn_count']}: {responding_persona} | Continue: {should_continue} ({confidence}) | Tension: {state['tension']:.2f}")
|
||||
|
||||
# Decide what happens next
|
||||
opposite = "evil" if responding_persona == "miku" else "miku"
|
||||
@@ -677,14 +686,14 @@ You can use emojis naturally! ✨💙"""
|
||||
)
|
||||
else:
|
||||
# Clear signal to end
|
||||
print(f"🏁 Dialogue ended naturally after {state['turn_count']} turns (tension: {state['tension']:.2f})")
|
||||
logger.info(f"Dialogue ended naturally after {state['turn_count']} turns (tension: {state['tension']:.2f})")
|
||||
self.end_dialogue(channel_id)
|
||||
|
||||
async def _next_turn(self, channel: discord.TextChannel, persona: str):
|
||||
"""Queue the next turn"""
|
||||
# Check if dialogue was interrupted
|
||||
if await self._was_interrupted(channel):
|
||||
print(f"💬 Dialogue interrupted by other activity")
|
||||
logger.info(f"Dialogue interrupted by other activity")
|
||||
self.end_dialogue(channel.id)
|
||||
return
|
||||
|
||||
@@ -741,7 +750,7 @@ Don't force a response if you have nothing meaningful to contribute."""
|
||||
return
|
||||
|
||||
if "[DONE]" in response.upper():
|
||||
print(f"🏁 {persona} chose not to respond, dialogue ended (tension: {state['tension']:.2f})")
|
||||
logger.info(f"{persona} chose not to respond, dialogue ended (tension: {state['tension']:.2f})")
|
||||
self.end_dialogue(channel_id)
|
||||
else:
|
||||
clean_response = response.replace("[DONE]", "").strip()
|
||||
@@ -750,11 +759,11 @@ Don't force a response if you have nothing meaningful to contribute."""
|
||||
tension_delta = self.calculate_tension_delta(clean_response, state["tension"])
|
||||
state["tension"] = max(0.0, min(1.0, state["tension"] + tension_delta))
|
||||
|
||||
print(f"🌡️ Last word tension: {state['tension']:.2f} (delta: {tension_delta:+.2f})")
|
||||
logger.debug(f"Last word tension: {state['tension']:.2f} (delta: {tension_delta:+.2f})")
|
||||
|
||||
# Check for argument escalation
|
||||
if state["tension"] >= ARGUMENT_TENSION_THRESHOLD:
|
||||
print(f"🔥 TENSION THRESHOLD REACHED on last word - ESCALATING TO ARGUMENT")
|
||||
logger.info(f"TENSION THRESHOLD REACHED on last word - ESCALATING TO ARGUMENT")
|
||||
await self._send_as_persona(channel, persona, clean_response)
|
||||
await self._escalate_to_argument(channel, persona, clean_response)
|
||||
return
|
||||
@@ -782,7 +791,7 @@ Don't force a response if you have nothing meaningful to contribute."""
|
||||
]
|
||||
|
||||
if all(closing_indicators):
|
||||
print(f"🏁 Dialogue ended after last word, {state['turn_count']} turns total")
|
||||
logger.info(f"Dialogue ended after last word, {state['turn_count']} turns total")
|
||||
self.end_dialogue(channel.id)
|
||||
else:
|
||||
asyncio.create_task(self._next_turn(channel, opposite))
|
||||
@@ -802,7 +811,7 @@ Don't force a response if you have nothing meaningful to contribute."""
|
||||
|
||||
# Don't start if an argument is already going
|
||||
if is_argument_in_progress(channel.id):
|
||||
print(f"⚠️ Argument already in progress, skipping escalation")
|
||||
logger.warning(f"Argument already in progress, skipping escalation")
|
||||
return
|
||||
|
||||
# Build context for the argument
|
||||
@@ -811,7 +820,7 @@ The last thing said was: "{triggering_message}"
|
||||
|
||||
This pushed things over the edge into a full argument."""
|
||||
|
||||
print(f"⚔️ Escalating to argument in #{channel.name}")
|
||||
logger.info(f"Escalating to argument in #{channel.name}")
|
||||
|
||||
# Use the existing argument system
|
||||
# Pass the triggering message so the opposite persona responds to it
|
||||
@@ -839,7 +848,7 @@ This pushed things over the edge into a full argument."""
|
||||
if msg.author.id != globals.client.user.id:
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error checking for interruption: {e}")
|
||||
logger.warning(f"Error checking for interruption: {e}")
|
||||
|
||||
return False
|
||||
|
||||
@@ -853,7 +862,7 @@ This pushed things over the edge into a full argument."""
|
||||
|
||||
messages.reverse()
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error building conversation context: {e}")
|
||||
logger.warning(f"Error building conversation context: {e}")
|
||||
|
||||
return '\n'.join(messages)
|
||||
|
||||
@@ -881,7 +890,7 @@ This pushed things over the edge into a full argument."""
|
||||
|
||||
webhooks = await get_or_create_webhooks_for_channel(channel)
|
||||
if not webhooks:
|
||||
print(f"⚠️ Could not get webhooks for #{channel.name}")
|
||||
logger.warning(f"Could not get webhooks for #{channel.name}")
|
||||
return
|
||||
|
||||
webhook = webhooks["evil_miku"] if persona == "evil" else webhooks["miku"]
|
||||
@@ -890,7 +899,7 @@ This pushed things over the edge into a full argument."""
|
||||
try:
|
||||
await webhook.send(content=content, username=display_name)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error sending as {persona}: {e}")
|
||||
logger.error(f"Error sending as {persona}: {e}")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -929,24 +938,24 @@ async def check_for_interjection(message: discord.Message, current_persona: str)
|
||||
Returns:
|
||||
True if an interjection was triggered, False otherwise
|
||||
"""
|
||||
print(f"🔍 [Persona Dialogue] Checking interjection for message from {current_persona}")
|
||||
logger.debug(f"[Persona Dialogue] Checking interjection for message from {current_persona}")
|
||||
|
||||
scorer = get_interjection_scorer()
|
||||
dialogue_manager = get_dialogue_manager()
|
||||
|
||||
# Don't trigger if dialogue already active
|
||||
if dialogue_manager.is_dialogue_active(message.channel.id):
|
||||
print(f"⏸️ [Persona Dialogue] Dialogue already active in channel {message.channel.id}")
|
||||
logger.debug(f"[Persona Dialogue] Dialogue already active in channel {message.channel.id}")
|
||||
return False
|
||||
|
||||
# Check if we should interject
|
||||
should_interject, reason, score = await scorer.should_interject(message, current_persona)
|
||||
|
||||
print(f"📊 [Persona Dialogue] Interjection check: should_interject={should_interject}, reason={reason}, score={score:.2f}")
|
||||
logger.debug(f"[Persona Dialogue] Interjection check: should_interject={should_interject}, reason={reason}, score={score:.2f}")
|
||||
|
||||
if should_interject:
|
||||
opposite_persona = "evil" if current_persona == "miku" else "miku"
|
||||
print(f"🎭 Triggering {opposite_persona} interjection (reason: {reason}, score: {score:.2f})")
|
||||
logger.info(f"Triggering {opposite_persona} interjection (reason: {reason}, score: {score:.2f})")
|
||||
|
||||
# Start dialogue with the opposite persona responding first
|
||||
dialogue_manager.start_dialogue(message.channel.id)
|
||||
|
||||
@@ -25,8 +25,11 @@ import discord
|
||||
import globals
|
||||
|
||||
from .danbooru_client import danbooru_client
|
||||
from .logger import get_logger
|
||||
import globals
|
||||
|
||||
logger = get_logger('vision')
|
||||
|
||||
|
||||
class ProfilePictureManager:
|
||||
"""Manages Miku's profile picture with intelligent cropping and face detection"""
|
||||
@@ -55,10 +58,10 @@ class ProfilePictureManager:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get("http://anime-face-detector:6078/health", timeout=aiohttp.ClientTimeout(total=5)) as response:
|
||||
if response.status == 200:
|
||||
print("✅ Anime face detector API connected (pre-loaded)")
|
||||
logger.info("Anime face detector API connected (pre-loaded)")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"ℹ️ Face detector not pre-loaded (container not running)")
|
||||
logger.info(f"Face detector not pre-loaded (container not running)")
|
||||
return False
|
||||
|
||||
async def _ensure_vram_available(self, debug: bool = False):
|
||||
@@ -68,7 +71,7 @@ class ProfilePictureManager:
|
||||
"""
|
||||
try:
|
||||
if debug:
|
||||
print("💾 Swapping to text model to free VRAM for face detection...")
|
||||
logger.info("Swapping to text model to free VRAM for face detection...")
|
||||
|
||||
# Make a simple request to text model to trigger swap
|
||||
async with aiohttp.ClientSession() as session:
|
||||
@@ -86,13 +89,13 @@ class ProfilePictureManager:
|
||||
) as response:
|
||||
if response.status == 200:
|
||||
if debug:
|
||||
print("✅ Vision model unloaded, VRAM available")
|
||||
logger.debug("Vision model unloaded, VRAM available")
|
||||
# Give system time to fully release VRAM
|
||||
await asyncio.sleep(3)
|
||||
return True
|
||||
except Exception as e:
|
||||
if debug:
|
||||
print(f"⚠️ Could not swap models: {e}")
|
||||
logger.error(f"Could not swap models: {e}")
|
||||
|
||||
return False
|
||||
|
||||
@@ -100,7 +103,7 @@ class ProfilePictureManager:
|
||||
"""Start the face detector container using Docker socket API"""
|
||||
try:
|
||||
if debug:
|
||||
print("🚀 Starting face detector container...")
|
||||
logger.info("Starting face detector container...")
|
||||
|
||||
# Use Docker socket API to start container
|
||||
import aiofiles
|
||||
@@ -112,7 +115,7 @@ class ProfilePictureManager:
|
||||
# Check if socket exists
|
||||
if not os.path.exists(socket_path):
|
||||
if debug:
|
||||
print("⚠️ Docker socket not available")
|
||||
logger.error("Docker socket not available")
|
||||
return False
|
||||
|
||||
# Use aiohttp UnixConnector to communicate with Docker socket
|
||||
@@ -127,7 +130,7 @@ class ProfilePictureManager:
|
||||
if response.status not in [204, 304]: # 204=started, 304=already running
|
||||
if debug:
|
||||
error_text = await response.text()
|
||||
print(f"⚠️ Failed to start container: {response.status} - {error_text}")
|
||||
logger.error(f"Failed to start container: {response.status} - {error_text}")
|
||||
return False
|
||||
|
||||
# Wait for API to be ready
|
||||
@@ -140,32 +143,32 @@ class ProfilePictureManager:
|
||||
) as response:
|
||||
if response.status == 200:
|
||||
if debug:
|
||||
print(f"✅ Face detector ready (took {i+1}s)")
|
||||
logger.info(f"Face detector ready (took {i+1}s)")
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
await asyncio.sleep(1)
|
||||
|
||||
if debug:
|
||||
print("⚠️ Face detector didn't become ready in time")
|
||||
logger.warning("Face detector didn't become ready in time")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
if debug:
|
||||
print(f"⚠️ Error starting face detector: {e}")
|
||||
logger.error(f"Error starting face detector: {e}")
|
||||
return False
|
||||
|
||||
async def _stop_face_detector(self, debug: bool = False):
|
||||
"""Stop the face detector container using Docker socket API"""
|
||||
try:
|
||||
if debug:
|
||||
print("🛑 Stopping face detector to free VRAM...")
|
||||
logger.info("Stopping face detector to free VRAM...")
|
||||
|
||||
socket_path = "/var/run/docker.sock"
|
||||
|
||||
if not os.path.exists(socket_path):
|
||||
if debug:
|
||||
print("⚠️ Docker socket not available")
|
||||
logger.error("Docker socket not available")
|
||||
return
|
||||
|
||||
from aiohttp import UnixConnector
|
||||
@@ -178,26 +181,26 @@ class ProfilePictureManager:
|
||||
async with session.post(url, params={"t": 10}) as response: # 10 second timeout
|
||||
if response.status in [204, 304]: # 204=stopped, 304=already stopped
|
||||
if debug:
|
||||
print("✅ Face detector stopped")
|
||||
logger.info("Face detector stopped")
|
||||
else:
|
||||
if debug:
|
||||
error_text = await response.text()
|
||||
print(f"⚠️ Failed to stop container: {response.status} - {error_text}")
|
||||
logger.warning(f"Failed to stop container: {response.status} - {error_text}")
|
||||
|
||||
except Exception as e:
|
||||
if debug:
|
||||
print(f"⚠️ Error stopping face detector: {e}")
|
||||
logger.error(f"Error stopping face detector: {e}")
|
||||
|
||||
async def save_current_avatar_as_fallback(self):
|
||||
"""Save the bot's current avatar as fallback (only if fallback doesn't exist)"""
|
||||
try:
|
||||
# Only save if fallback doesn't already exist
|
||||
if os.path.exists(self.FALLBACK_PATH):
|
||||
print("✅ Fallback avatar already exists, skipping save")
|
||||
logger.info("Fallback avatar already exists, skipping save")
|
||||
return True
|
||||
|
||||
if not globals.client or not globals.client.user:
|
||||
print("⚠️ Bot client not ready")
|
||||
logger.warning("Bot client not ready")
|
||||
return False
|
||||
|
||||
avatar_asset = globals.client.user.avatar or globals.client.user.default_avatar
|
||||
@@ -209,11 +212,11 @@ class ProfilePictureManager:
|
||||
with open(self.FALLBACK_PATH, 'wb') as f:
|
||||
f.write(avatar_bytes)
|
||||
|
||||
print(f"✅ Saved current avatar as fallback ({len(avatar_bytes)} bytes)")
|
||||
logger.info(f"Saved current avatar as fallback ({len(avatar_bytes)} bytes)")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error saving fallback avatar: {e}")
|
||||
logger.error(f"Error saving fallback avatar: {e}")
|
||||
return False
|
||||
|
||||
async def change_profile_picture(
|
||||
@@ -251,7 +254,7 @@ class ProfilePictureManager:
|
||||
if custom_image_bytes:
|
||||
# Custom upload - no retry needed
|
||||
if debug:
|
||||
print("🖼️ Using provided custom image")
|
||||
logger.info("Using provided custom image")
|
||||
image_bytes = custom_image_bytes
|
||||
result["source"] = "custom_upload"
|
||||
|
||||
@@ -259,7 +262,7 @@ class ProfilePictureManager:
|
||||
try:
|
||||
image = Image.open(io.BytesIO(image_bytes))
|
||||
if debug:
|
||||
print(f"📐 Original image size: {image.size}")
|
||||
logger.debug(f"Original image size: {image.size}")
|
||||
|
||||
# Check if it's an animated GIF
|
||||
if image.format == 'GIF':
|
||||
@@ -269,11 +272,11 @@ class ProfilePictureManager:
|
||||
is_animated_gif = True
|
||||
image.seek(0) # Reset to first frame
|
||||
if debug:
|
||||
print("🎬 Detected animated GIF - will preserve animation")
|
||||
logger.debug("Detected animated GIF - will preserve animation")
|
||||
except EOFError:
|
||||
# Only one frame, treat as static image
|
||||
if debug:
|
||||
print("🖼️ Single-frame GIF - will process as static image")
|
||||
logger.debug("Single-frame GIF - will process as static image")
|
||||
|
||||
except Exception as e:
|
||||
result["error"] = f"Failed to open image: {e}"
|
||||
@@ -282,11 +285,11 @@ class ProfilePictureManager:
|
||||
else:
|
||||
# Danbooru - retry until we find a valid Miku image
|
||||
if debug:
|
||||
print(f"🎨 Searching Danbooru for Miku image (mood: {mood})")
|
||||
logger.info(f"Searching Danbooru for Miku image (mood: {mood})")
|
||||
|
||||
for attempt in range(max_retries):
|
||||
if attempt > 0 and debug:
|
||||
print(f"🔄 Retry attempt {attempt + 1}/{max_retries}")
|
||||
logger.info(f"Retry attempt {attempt + 1}/{max_retries}")
|
||||
|
||||
post = await danbooru_client.get_random_miku_image(mood=mood)
|
||||
if not post:
|
||||
@@ -302,23 +305,23 @@ class ProfilePictureManager:
|
||||
continue
|
||||
|
||||
if debug:
|
||||
print(f"✅ Downloaded image from Danbooru (post #{danbooru_client.get_post_metadata(post).get('id')})")
|
||||
logger.info(f"Downloaded image from Danbooru (post #{danbooru_client.get_post_metadata(post).get('id')})")
|
||||
|
||||
# Load image with PIL
|
||||
try:
|
||||
temp_image = Image.open(io.BytesIO(temp_image_bytes))
|
||||
if debug:
|
||||
print(f"📐 Original image size: {temp_image.size}")
|
||||
logger.debug(f"Original image size: {temp_image.size}")
|
||||
except Exception as e:
|
||||
if debug:
|
||||
print(f"⚠️ Failed to open image: {e}")
|
||||
logger.warning(f"Failed to open image: {e}")
|
||||
continue
|
||||
|
||||
# Verify it's Miku
|
||||
miku_verification = await self._verify_and_locate_miku(temp_image_bytes, debug=debug)
|
||||
if not miku_verification["is_miku"]:
|
||||
if debug:
|
||||
print(f"❌ Image verification failed: not Miku, trying another...")
|
||||
logger.warning(f"Image verification failed: not Miku, trying another...")
|
||||
continue
|
||||
|
||||
# Success! This image is valid
|
||||
@@ -330,7 +333,7 @@ class ProfilePictureManager:
|
||||
# If multiple characters detected, use LLM's suggested crop region
|
||||
if miku_verification.get("crop_region"):
|
||||
if debug:
|
||||
print(f"🎯 Using LLM-suggested crop region for Miku")
|
||||
logger.debug(f"Using LLM-suggested crop region for Miku")
|
||||
image = self._apply_crop_region(image, miku_verification["crop_region"])
|
||||
|
||||
break
|
||||
@@ -344,11 +347,11 @@ class ProfilePictureManager:
|
||||
# If this is an animated GIF, skip most processing and use raw bytes
|
||||
if is_animated_gif:
|
||||
if debug:
|
||||
print("🎬 Using GIF fast path - skipping face detection and cropping")
|
||||
logger.info("Using GIF fast path - skipping face detection and cropping")
|
||||
|
||||
# Generate description of the animated GIF
|
||||
if debug:
|
||||
print("📝 Generating GIF description using video analysis pipeline...")
|
||||
logger.info("Generating GIF description using video analysis pipeline...")
|
||||
description = await self._generate_gif_description(image_bytes, debug=debug)
|
||||
if description:
|
||||
# Save description to file
|
||||
@@ -358,12 +361,12 @@ class ProfilePictureManager:
|
||||
f.write(description)
|
||||
result["metadata"]["description"] = description
|
||||
if debug:
|
||||
print(f"📝 Saved GIF description ({len(description)} chars)")
|
||||
logger.info(f"Saved GIF description ({len(description)} chars)")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to save description file: {e}")
|
||||
logger.error(f"Failed to save description file: {e}")
|
||||
else:
|
||||
if debug:
|
||||
print("⚠️ GIF description generation returned None")
|
||||
logger.error("GIF description generation returned None")
|
||||
|
||||
# Extract dominant color from first frame
|
||||
dominant_color = self._extract_dominant_color(image, debug=debug)
|
||||
@@ -373,14 +376,14 @@ class ProfilePictureManager:
|
||||
"hex": "#{:02x}{:02x}{:02x}".format(*dominant_color)
|
||||
}
|
||||
if debug:
|
||||
print(f"🎨 Dominant color from first frame: RGB{dominant_color} (#{result['metadata']['dominant_color']['hex'][1:]})")
|
||||
logger.debug(f"Dominant color from first frame: RGB{dominant_color} (#{result['metadata']['dominant_color']['hex'][1:]})")
|
||||
|
||||
# Save the original GIF bytes
|
||||
with open(self.CURRENT_PATH, 'wb') as f:
|
||||
f.write(image_bytes)
|
||||
|
||||
if debug:
|
||||
print(f"💾 Saved animated GIF ({len(image_bytes)} bytes)")
|
||||
logger.info(f"Saved animated GIF ({len(image_bytes)} bytes)")
|
||||
|
||||
# Update Discord avatar with original GIF
|
||||
if globals.client and globals.client.user:
|
||||
@@ -401,7 +404,7 @@ class ProfilePictureManager:
|
||||
# Save metadata
|
||||
self._save_metadata(result["metadata"])
|
||||
|
||||
print(f"✅ Animated profile picture updated successfully!")
|
||||
logger.info(f"Animated profile picture updated successfully!")
|
||||
|
||||
# Update role colors if we have a dominant color
|
||||
if dominant_color:
|
||||
@@ -411,12 +414,13 @@ class ProfilePictureManager:
|
||||
|
||||
except discord.HTTPException as e:
|
||||
result["error"] = f"Discord API error: {e}"
|
||||
print(f"⚠️ Failed to update Discord avatar with GIF: {e}")
|
||||
print(f" Note: Animated avatars require Discord Nitro")
|
||||
logger.warning(f"Failed to update Discord avatar with GIF: {e}")
|
||||
if debug:
|
||||
logger.debug("Note: Animated avatars require Discord Nitro")
|
||||
return result
|
||||
except Exception as e:
|
||||
result["error"] = f"Unexpected error updating avatar: {e}"
|
||||
print(f"⚠️ Unexpected error: {e}")
|
||||
logger.error(f"Unexpected error: {e}")
|
||||
return result
|
||||
else:
|
||||
result["error"] = "Bot client not ready"
|
||||
@@ -425,7 +429,7 @@ class ProfilePictureManager:
|
||||
# === NORMAL STATIC IMAGE PATH ===
|
||||
# Step 2: Generate description of the validated image
|
||||
if debug:
|
||||
print("📝 Generating image description...")
|
||||
logger.info("Generating image description...")
|
||||
description = await self._generate_image_description(image_bytes, debug=debug)
|
||||
if description:
|
||||
# Save description to file
|
||||
@@ -435,12 +439,12 @@ class ProfilePictureManager:
|
||||
f.write(description)
|
||||
result["metadata"]["description"] = description
|
||||
if debug:
|
||||
print(f"📝 Saved image description ({len(description)} chars)")
|
||||
logger.info(f"Saved image description ({len(description)} chars)")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to save description file: {e}")
|
||||
logger.warning(f"Failed to save description file: {e}")
|
||||
else:
|
||||
if debug:
|
||||
print("⚠️ Description generation returned None")
|
||||
logger.warning("Description generation returned None")
|
||||
|
||||
# Step 3: Detect face and crop intelligently
|
||||
cropped_image = await self._intelligent_crop(image, image_bytes, target_size=512, debug=debug)
|
||||
@@ -459,7 +463,7 @@ class ProfilePictureManager:
|
||||
f.write(cropped_bytes)
|
||||
|
||||
if debug:
|
||||
print(f"💾 Saved cropped image ({len(cropped_bytes)} bytes)")
|
||||
logger.info(f"Saved cropped image ({len(cropped_bytes)} bytes)")
|
||||
|
||||
# Step 5: Extract dominant color from saved current.png
|
||||
saved_image = Image.open(self.CURRENT_PATH)
|
||||
@@ -470,7 +474,7 @@ class ProfilePictureManager:
|
||||
"hex": "#{:02x}{:02x}{:02x}".format(*dominant_color)
|
||||
}
|
||||
if debug:
|
||||
print(f"🎨 Dominant color: RGB{dominant_color} (#{result['metadata']['dominant_color']['hex'][1:]})")
|
||||
logger.debug(f"Dominant color: RGB{dominant_color} (#{result['metadata']['dominant_color']['hex'][1:]})")
|
||||
|
||||
# Step 6: Update Discord avatar
|
||||
if globals.client and globals.client.user:
|
||||
@@ -495,7 +499,7 @@ class ProfilePictureManager:
|
||||
# Save metadata
|
||||
self._save_metadata(result["metadata"])
|
||||
|
||||
print(f"✅ Profile picture updated successfully!")
|
||||
logger.info(f"Profile picture updated successfully!")
|
||||
|
||||
# Step 7: Update role colors across all servers
|
||||
if dominant_color:
|
||||
@@ -503,16 +507,16 @@ class ProfilePictureManager:
|
||||
|
||||
except discord.HTTPException as e:
|
||||
result["error"] = f"Discord API error: {e}"
|
||||
print(f"⚠️ Failed to update Discord avatar: {e}")
|
||||
logger.warning(f"Failed to update Discord avatar: {e}")
|
||||
except Exception as e:
|
||||
result["error"] = f"Unexpected error updating avatar: {e}"
|
||||
print(f"⚠️ Unexpected error: {e}")
|
||||
logger.error(f"Unexpected error: {e}")
|
||||
else:
|
||||
result["error"] = "Bot client not ready"
|
||||
|
||||
except Exception as e:
|
||||
result["error"] = f"Unexpected error: {e}"
|
||||
print(f"⚠️ Error in change_profile_picture: {e}")
|
||||
logger.error(f"Error in change_profile_picture: {e}")
|
||||
|
||||
return result
|
||||
|
||||
@@ -524,7 +528,7 @@ class ProfilePictureManager:
|
||||
if response.status == 200:
|
||||
return await response.read()
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error downloading image: {e}")
|
||||
logger.error(f"Error downloading image: {e}")
|
||||
return None
|
||||
|
||||
async def _generate_image_description(self, image_bytes: bytes, debug: bool = False) -> Optional[str]:
|
||||
@@ -544,7 +548,7 @@ class ProfilePictureManager:
|
||||
image_b64 = base64.b64encode(image_bytes).decode('utf-8')
|
||||
|
||||
if debug:
|
||||
print(f"📸 Encoded image: {len(image_b64)} chars, calling vision model...")
|
||||
logger.debug(f"Encoded image: {len(image_b64)} chars, calling vision model...")
|
||||
|
||||
prompt = """This is an image of Hatsune Miku that will be used as a profile picture.
|
||||
Please describe this image in detail, including:
|
||||
@@ -583,7 +587,7 @@ Keep the description conversational and in second-person (referring to Miku as "
|
||||
headers = {"Content-Type": "application/json"}
|
||||
|
||||
if debug:
|
||||
print(f"🌐 Calling {globals.LLAMA_URL}/v1/chat/completions with model {globals.VISION_MODEL}")
|
||||
logger.debug(f"Calling {globals.LLAMA_URL}/v1/chat/completions with model {globals.VISION_MODEL}")
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(f"{globals.LLAMA_URL}/v1/chat/completions", json=payload, headers=headers, timeout=aiohttp.ClientTimeout(total=60)) as resp:
|
||||
@@ -591,8 +595,8 @@ Keep the description conversational and in second-person (referring to Miku as "
|
||||
data = await resp.json()
|
||||
|
||||
if debug:
|
||||
print(f"📦 API Response keys: {data.keys()}")
|
||||
print(f"📦 Choices: {data.get('choices', [])}")
|
||||
logger.debug(f"API Response keys: {data.keys()}")
|
||||
logger.debug(f"Choices: {data.get('choices', [])}")
|
||||
|
||||
# Try to get content from the response
|
||||
choice = data.get("choices", [{}])[0]
|
||||
@@ -607,21 +611,21 @@ Keep the description conversational and in second-person (referring to Miku as "
|
||||
|
||||
if description and description.strip():
|
||||
if debug:
|
||||
print(f"✅ Generated description: {description[:100]}...")
|
||||
logger.info(f"Generated description: {description[:100]}...")
|
||||
return description.strip()
|
||||
else:
|
||||
if debug:
|
||||
print(f"⚠️ Description is empty or None")
|
||||
print(f" Full response: {data}")
|
||||
logger.warning(f"Description is empty or None")
|
||||
logger.warning(f" Full response: {data}")
|
||||
else:
|
||||
print(f"⚠️ Description is empty or None")
|
||||
logger.warning(f"Description is empty or None")
|
||||
return None
|
||||
else:
|
||||
error_text = await resp.text()
|
||||
print(f"❌ Vision API error generating description: {resp.status} - {error_text}")
|
||||
logger.error(f"Vision API error generating description: {resp.status} - {error_text}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error generating image description: {e}")
|
||||
logger.error(f"Error generating image description: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
@@ -642,19 +646,19 @@ Keep the description conversational and in second-person (referring to Miku as "
|
||||
from utils.image_handling import extract_video_frames, analyze_video_with_vision
|
||||
|
||||
if debug:
|
||||
print("🎬 Extracting frames from GIF...")
|
||||
logger.info("Extracting frames from GIF...")
|
||||
|
||||
# Extract frames from the GIF (6 frames for good analysis)
|
||||
frames = await extract_video_frames(gif_bytes, num_frames=6)
|
||||
|
||||
if not frames:
|
||||
if debug:
|
||||
print("⚠️ Failed to extract frames from GIF")
|
||||
logger.warning("Failed to extract frames from GIF")
|
||||
return None
|
||||
|
||||
if debug:
|
||||
print(f"✅ Extracted {len(frames)} frames from GIF")
|
||||
print(f"🌐 Analyzing GIF with vision model...")
|
||||
logger.info(f"Extracted {len(frames)} frames from GIF")
|
||||
logger.info(f"Analyzing GIF with vision model...")
|
||||
|
||||
# Use the existing analyze_video_with_vision function (no timeout issues)
|
||||
# Note: This uses a generic prompt, but it works reliably
|
||||
@@ -662,15 +666,15 @@ Keep the description conversational and in second-person (referring to Miku as "
|
||||
|
||||
if description and description.strip() and not description.startswith("Error"):
|
||||
if debug:
|
||||
print(f"✅ Generated GIF description: {description[:100]}...")
|
||||
logger.info(f"Generated GIF description: {description[:100]}...")
|
||||
return description.strip()
|
||||
else:
|
||||
if debug:
|
||||
print(f"⚠️ GIF description failed or empty: {description}")
|
||||
logger.warning(f"GIF description failed or empty: {description}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error generating GIF description: {e}")
|
||||
logger.error(f"Error generating GIF description: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
@@ -740,11 +744,11 @@ Respond in JSON format:
|
||||
response = data.get("choices", [{}])[0].get("message", {}).get("content", "")
|
||||
else:
|
||||
error_text = await resp.text()
|
||||
print(f"❌ Vision API error: {resp.status} - {error_text}")
|
||||
logger.error(f"Vision API error: {resp.status} - {error_text}")
|
||||
return result
|
||||
|
||||
if debug:
|
||||
print(f"🤖 Vision model response: {response}")
|
||||
logger.debug(f"Vision model response: {response}")
|
||||
|
||||
# Parse JSON response
|
||||
import re
|
||||
@@ -766,7 +770,7 @@ Respond in JSON format:
|
||||
result["is_miku"] = "yes" in response_lower or "miku" in response_lower
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error in vision verification: {e}")
|
||||
logger.warning(f"Error in vision verification: {e}")
|
||||
# Assume it's Miku on error (trust Danbooru tags)
|
||||
result["is_miku"] = True
|
||||
|
||||
@@ -793,7 +797,7 @@ Respond in JSON format:
|
||||
region["vertical"] = "bottom"
|
||||
|
||||
if debug:
|
||||
print(f"📍 Parsed location '{location}' -> {region}")
|
||||
logger.debug(f"Parsed location '{location}' -> {region}")
|
||||
|
||||
return region
|
||||
|
||||
@@ -856,11 +860,11 @@ Respond in JSON format:
|
||||
|
||||
if face_detection and face_detection.get('center'):
|
||||
if debug:
|
||||
print(f"😊 Face detected at {face_detection['center']}")
|
||||
logger.debug(f"Face detected at {face_detection['center']}")
|
||||
crop_center = face_detection['center']
|
||||
else:
|
||||
if debug:
|
||||
print("🎯 No face detected, using saliency detection")
|
||||
logger.debug("No face detected, using saliency detection")
|
||||
# Fallback to saliency detection
|
||||
cv_image = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)
|
||||
crop_center = self._detect_saliency(cv_image, debug=debug)
|
||||
@@ -895,12 +899,12 @@ Respond in JSON format:
|
||||
top = 0
|
||||
# Adjust crop_center for logging
|
||||
if debug:
|
||||
print(f"⚠️ Face too close to top edge, shifted crop to y=0")
|
||||
logger.debug(f"Face too close to top edge, shifted crop to y=0")
|
||||
elif top + crop_size > height:
|
||||
# Face is too close to bottom edge
|
||||
top = height - crop_size
|
||||
if debug:
|
||||
print(f"⚠️ Face too close to bottom edge, shifted crop to y={top}")
|
||||
logger.debug(f"Face too close to bottom edge, shifted crop to y={top}")
|
||||
|
||||
# Crop
|
||||
cropped = image.crop((left, top, left + crop_size, top + crop_size))
|
||||
@@ -909,7 +913,7 @@ Respond in JSON format:
|
||||
cropped = cropped.resize((target_size, target_size), Image.Resampling.LANCZOS)
|
||||
|
||||
if debug:
|
||||
print(f"✂️ Cropped to {target_size}x{target_size} centered at {crop_center}")
|
||||
logger.debug(f"Cropped to {target_size}x{target_size} centered at {crop_center}")
|
||||
|
||||
return cropped
|
||||
|
||||
@@ -933,7 +937,7 @@ Respond in JSON format:
|
||||
# Step 2: Start face detector container
|
||||
if not await self._start_face_detector(debug=debug):
|
||||
if debug:
|
||||
print("⚠️ Could not start face detector")
|
||||
logger.error("Could not start face detector")
|
||||
return None
|
||||
|
||||
face_detector_started = True
|
||||
@@ -951,14 +955,14 @@ Respond in JSON format:
|
||||
) as response:
|
||||
if response.status != 200:
|
||||
if debug:
|
||||
print(f"⚠️ Face detection API returned status {response.status}")
|
||||
logger.error(f"Face detection API returned status {response.status}")
|
||||
return None
|
||||
|
||||
result = await response.json()
|
||||
|
||||
if result.get('count', 0) == 0:
|
||||
if debug:
|
||||
print("👤 No faces detected by API")
|
||||
logger.debug("No faces detected by API")
|
||||
return None
|
||||
|
||||
# Get detections and pick the one with highest confidence
|
||||
@@ -981,9 +985,9 @@ Respond in JSON format:
|
||||
if debug:
|
||||
width = int(x2 - x1)
|
||||
height = int(y2 - y1)
|
||||
print(f"👤 Detected {len(detections)} face(s) via API, using best at ({center_x}, {center_y}) [confidence: {confidence:.2%}]")
|
||||
print(f" Bounding box: x={int(x1)}, y={int(y1)}, w={width}, h={height}")
|
||||
print(f" Keypoints: {len(keypoints)} facial landmarks detected")
|
||||
logger.debug(f"Detected {len(detections)} face(s) via API, using best at ({center_x}, {center_y}) [confidence: {confidence:.2%}]")
|
||||
logger.debug(f" Bounding box: x={int(x1)}, y={int(y1)}, w={width}, h={height}")
|
||||
logger.debug(f" Keypoints: {len(keypoints)} facial landmarks detected")
|
||||
|
||||
return {
|
||||
'center': (center_x, center_y),
|
||||
@@ -995,10 +999,10 @@ Respond in JSON format:
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
if debug:
|
||||
print("⚠️ Face detection API timeout")
|
||||
logger.warning("Face detection API timeout")
|
||||
except Exception as e:
|
||||
if debug:
|
||||
print(f"⚠️ Error calling face detection API: {e}")
|
||||
logger.error(f"Error calling face detection API: {e}")
|
||||
finally:
|
||||
# Always stop face detector to free VRAM
|
||||
if face_detector_started:
|
||||
@@ -1027,12 +1031,12 @@ Respond in JSON format:
|
||||
_, max_val, _, max_loc = cv2.minMaxLoc(saliency_map)
|
||||
|
||||
if debug:
|
||||
print(f"🎯 Saliency peak at {max_loc}")
|
||||
logger.debug(f"Saliency peak at {max_loc}")
|
||||
|
||||
return max_loc
|
||||
except Exception as e:
|
||||
if debug:
|
||||
print(f"⚠️ Saliency detection failed: {e}")
|
||||
logger.error(f"Saliency detection failed: {e}")
|
||||
|
||||
# Ultimate fallback: center of image
|
||||
height, width = cv_image.shape[:2]
|
||||
@@ -1070,7 +1074,7 @@ Respond in JSON format:
|
||||
|
||||
if len(pixels) == 0:
|
||||
if debug:
|
||||
print("⚠️ No valid pixels after filtering, using fallback")
|
||||
logger.warning("No valid pixels after filtering, using fallback")
|
||||
return (200, 200, 200) # Neutral gray fallback
|
||||
|
||||
# Use k-means to find dominant colors
|
||||
@@ -1085,11 +1089,11 @@ Respond in JSON format:
|
||||
counts = np.bincount(labels)
|
||||
|
||||
if debug:
|
||||
print(f"🎨 Found {n_colors} color clusters:")
|
||||
logger.debug(f"Found {n_colors} color clusters:")
|
||||
for i, (color, count) in enumerate(zip(colors, counts)):
|
||||
pct = (count / len(labels)) * 100
|
||||
r, g, b = color.astype(int)
|
||||
print(f" {i+1}. RGB({r}, {g}, {b}) = #{r:02x}{g:02x}{b:02x} ({pct:.1f}%)")
|
||||
logger.debug(f" {i+1}. RGB({r}, {g}, {b}) = #{r:02x}{g:02x}{b:02x} ({pct:.1f}%)")
|
||||
|
||||
# Sort by frequency
|
||||
sorted_indices = np.argsort(-counts)
|
||||
@@ -1108,7 +1112,7 @@ Respond in JSON format:
|
||||
saturation = (max_c - min_c) / max_c if max_c > 0 else 0
|
||||
|
||||
if debug:
|
||||
print(f" Color RGB({r}, {g}, {b}) saturation: {saturation:.2f}")
|
||||
logger.debug(f" Color RGB({r}, {g}, {b}) saturation: {saturation:.2f}")
|
||||
|
||||
# Prefer more saturated colors
|
||||
if saturation > best_saturation:
|
||||
@@ -1118,7 +1122,7 @@ Respond in JSON format:
|
||||
|
||||
if best_color:
|
||||
if debug:
|
||||
print(f"🎨 Selected color: RGB{best_color} (saturation: {best_saturation:.2f})")
|
||||
logger.debug(f"Selected color: RGB{best_color} (saturation: {best_saturation:.2f})")
|
||||
return best_color
|
||||
|
||||
# Fallback to most common color
|
||||
@@ -1126,12 +1130,12 @@ Respond in JSON format:
|
||||
# Convert to native Python ints
|
||||
result = (int(dominant_color[0]), int(dominant_color[1]), int(dominant_color[2]))
|
||||
if debug:
|
||||
print(f"🎨 Using most common color: RGB{result}")
|
||||
logger.debug(f"Using most common color: RGB{result}")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
if debug:
|
||||
print(f"⚠️ Error extracting dominant color: {e}")
|
||||
logger.error(f"Error extracting dominant color: {e}")
|
||||
return None
|
||||
|
||||
async def _update_role_colors(self, color: Tuple[int, int, int], debug: bool = False):
|
||||
@@ -1143,15 +1147,15 @@ Respond in JSON format:
|
||||
debug: Enable debug output
|
||||
"""
|
||||
if debug:
|
||||
print(f"🎨 Starting role color update with RGB{color}")
|
||||
logger.debug(f"Starting role color update with RGB{color}")
|
||||
|
||||
if not globals.client:
|
||||
if debug:
|
||||
print("⚠️ No client available for role updates")
|
||||
logger.error("No client available for role updates")
|
||||
return
|
||||
|
||||
if debug:
|
||||
print(f"🌐 Found {len(globals.client.guilds)} guild(s)")
|
||||
logger.debug(f"Found {len(globals.client.guilds)} guild(s)")
|
||||
|
||||
# Convert RGB to Discord color (integer)
|
||||
discord_color = discord.Color.from_rgb(*color)
|
||||
@@ -1162,20 +1166,20 @@ Respond in JSON format:
|
||||
for guild in globals.client.guilds:
|
||||
try:
|
||||
if debug:
|
||||
print(f"🔍 Checking guild: {guild.name}")
|
||||
logger.debug(f"Checking guild: {guild.name}")
|
||||
|
||||
# Find the bot's top role (usually colored role)
|
||||
member = guild.get_member(globals.client.user.id)
|
||||
if not member:
|
||||
if debug:
|
||||
print(f" ⚠️ Bot not found as member in {guild.name}")
|
||||
logger.warning(f" Bot not found as member in {guild.name}")
|
||||
continue
|
||||
|
||||
# Get the highest role that the bot has (excluding @everyone)
|
||||
roles = [r for r in member.roles if r.name != "@everyone"]
|
||||
if not roles:
|
||||
if debug:
|
||||
print(f" ⚠️ No roles found in {guild.name}")
|
||||
logger.warning(f" No roles found in {guild.name}")
|
||||
continue
|
||||
|
||||
# Look for a dedicated color role first (e.g., "Miku Color")
|
||||
@@ -1191,19 +1195,19 @@ Respond in JSON format:
|
||||
# Use dedicated color role if found, otherwise use top role
|
||||
if color_role:
|
||||
if debug:
|
||||
print(f" 🎨 Found dedicated color role: {color_role.name} (position {color_role.position})")
|
||||
logger.debug(f" Found dedicated color role: {color_role.name} (position {color_role.position})")
|
||||
target_role = color_role
|
||||
else:
|
||||
if debug:
|
||||
print(f" 📝 No 'Miku Color' role found, using top role: {bot_top_role.name} (position {bot_top_role.position})")
|
||||
logger.debug(f" No 'Miku Color' role found, using top role: {bot_top_role.name} (position {bot_top_role.position})")
|
||||
target_role = bot_top_role
|
||||
|
||||
# Check permissions
|
||||
can_manage = guild.me.guild_permissions.manage_roles
|
||||
|
||||
if debug:
|
||||
print(f" 🔑 Manage roles permission: {can_manage}")
|
||||
print(f" 📊 Bot top role: {bot_top_role.name} (pos {bot_top_role.position}), Target: {target_role.name} (pos {target_role.position})")
|
||||
logger.debug(f" Manage roles permission: {can_manage}")
|
||||
logger.debug(f" Bot top role: {bot_top_role.name} (pos {bot_top_role.position}), Target: {target_role.name} (pos {target_role.position})")
|
||||
|
||||
# Only update if we have permission and it's not a special role
|
||||
if can_manage:
|
||||
@@ -1219,28 +1223,28 @@ Respond in JSON format:
|
||||
|
||||
updated_count += 1
|
||||
if debug:
|
||||
print(f" ✅ Updated role color in {guild.name}: {target_role.name}")
|
||||
logger.info(f" Updated role color in {guild.name}: {target_role.name}")
|
||||
else:
|
||||
if debug:
|
||||
print(f" ⚠️ No manage_roles permission in {guild.name}")
|
||||
logger.warning(f" No manage_roles permission in {guild.name}")
|
||||
|
||||
except discord.Forbidden:
|
||||
failed_count += 1
|
||||
if debug:
|
||||
print(f" ❌ Forbidden: No permission to update role in {guild.name}")
|
||||
logger.error(f" Forbidden: No permission to update role in {guild.name}")
|
||||
except Exception as e:
|
||||
failed_count += 1
|
||||
if debug:
|
||||
print(f" ❌ Error updating role in {guild.name}: {e}")
|
||||
logger.error(f" Error updating role in {guild.name}: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
if updated_count > 0:
|
||||
print(f"🎨 Updated role colors in {updated_count} server(s)")
|
||||
logger.info(f"Updated role colors in {updated_count} server(s)")
|
||||
else:
|
||||
print(f"⚠️ No roles were updated (failed: {failed_count})")
|
||||
logger.warning(f"No roles were updated (failed: {failed_count})")
|
||||
if failed_count > 0 and debug:
|
||||
print(f"⚠️ Failed to update {failed_count} server(s)")
|
||||
logger.error(f"Failed to update {failed_count} server(s)")
|
||||
|
||||
async def set_custom_role_color(self, hex_color: str, debug: bool = False) -> Dict:
|
||||
"""
|
||||
@@ -1267,7 +1271,7 @@ Respond in JSON format:
|
||||
}
|
||||
|
||||
if debug:
|
||||
print(f"🎨 Setting custom role color: #{hex_color} RGB{color}")
|
||||
logger.debug(f"Setting custom role color: #{hex_color} RGB{color}")
|
||||
|
||||
await self._update_role_colors(color, debug=debug)
|
||||
|
||||
@@ -1290,7 +1294,7 @@ Respond in JSON format:
|
||||
Dict with success status
|
||||
"""
|
||||
if debug:
|
||||
print(f"🎨 Resetting to fallback color: RGB{self.FALLBACK_ROLE_COLOR}")
|
||||
logger.debug(f"Resetting to fallback color: RGB{self.FALLBACK_ROLE_COLOR}")
|
||||
|
||||
await self._update_role_colors(self.FALLBACK_ROLE_COLOR, debug=debug)
|
||||
|
||||
@@ -1308,7 +1312,7 @@ Respond in JSON format:
|
||||
with open(self.METADATA_PATH, 'w') as f:
|
||||
json.dump(metadata, f, indent=2)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error saving metadata: {e}")
|
||||
logger.error(f"Error saving metadata: {e}")
|
||||
|
||||
def load_metadata(self) -> Optional[Dict]:
|
||||
"""Load metadata about current profile picture"""
|
||||
@@ -1317,14 +1321,14 @@ Respond in JSON format:
|
||||
with open(self.METADATA_PATH, 'r') as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error loading metadata: {e}")
|
||||
logger.error(f"Error loading metadata: {e}")
|
||||
return None
|
||||
|
||||
async def restore_fallback(self) -> bool:
|
||||
"""Restore the fallback profile picture"""
|
||||
try:
|
||||
if not os.path.exists(self.FALLBACK_PATH):
|
||||
print("⚠️ No fallback avatar found")
|
||||
logger.warning("No fallback avatar found")
|
||||
return False
|
||||
|
||||
with open(self.FALLBACK_PATH, 'rb') as f:
|
||||
@@ -1341,11 +1345,11 @@ Respond in JSON format:
|
||||
else:
|
||||
await globals.client.user.edit(avatar=avatar_bytes)
|
||||
|
||||
print("✅ Restored fallback avatar")
|
||||
logger.info("Restored fallback avatar")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error restoring fallback: {e}")
|
||||
logger.error(f"Error restoring fallback: {e}")
|
||||
|
||||
return False
|
||||
|
||||
@@ -1362,7 +1366,7 @@ Respond in JSON format:
|
||||
with open(description_path, 'r', encoding='utf-8') as f:
|
||||
return f.read().strip()
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error reading description: {e}")
|
||||
logger.error(f"Error reading description: {e}")
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@@ -13,6 +13,9 @@ import globals
|
||||
from server_manager import server_manager
|
||||
from utils.llm import query_llama
|
||||
from utils.dm_interaction_analyzer import dm_analyzer
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger('scheduled')
|
||||
|
||||
BEDTIME_TRACKING_FILE = "last_bedtime_targets.json"
|
||||
|
||||
@@ -20,7 +23,7 @@ async def send_monday_video_for_server(guild_id: int):
|
||||
"""Send Monday video for a specific server"""
|
||||
server_config = server_manager.get_server_config(guild_id)
|
||||
if not server_config:
|
||||
print(f"⚠️ No config found for server {guild_id}")
|
||||
logger.warning(f"No config found for server {guild_id}")
|
||||
return
|
||||
|
||||
# No need to switch model - llama-swap handles this automatically
|
||||
@@ -37,7 +40,7 @@ async def send_monday_video_for_server(guild_id: int):
|
||||
for channel_id in target_channel_ids:
|
||||
channel = globals.client.get_channel(channel_id)
|
||||
if channel is None:
|
||||
print(f"❌ Could not find channel with ID {channel_id} in server {server_config.guild_name}")
|
||||
logger.error(f"Could not find channel with ID {channel_id} in server {server_config.guild_name}")
|
||||
continue
|
||||
|
||||
try:
|
||||
@@ -45,9 +48,9 @@ async def send_monday_video_for_server(guild_id: int):
|
||||
# Send video link
|
||||
await channel.send(f"[Happy Miku Monday!]({video_url})")
|
||||
|
||||
print(f"✅ Sent Monday video to channel ID {channel_id} in server {server_config.guild_name}")
|
||||
logger.info(f"Sent Monday video to channel ID {channel_id} in server {server_config.guild_name}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to send video to channel ID {channel_id} in server {server_config.guild_name}: {e}")
|
||||
logger.error(f"Failed to send video to channel ID {channel_id} in server {server_config.guild_name}: {e}")
|
||||
|
||||
async def send_monday_video():
|
||||
"""Legacy function - now sends to all servers"""
|
||||
@@ -61,7 +64,7 @@ def load_last_bedtime_targets():
|
||||
with open(BEDTIME_TRACKING_FILE, "r") as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to load bedtime tracking file: {e}")
|
||||
logger.error(f"Failed to load bedtime tracking file: {e}")
|
||||
return {}
|
||||
|
||||
_last_bedtime_targets = load_last_bedtime_targets()
|
||||
@@ -71,13 +74,13 @@ def save_last_bedtime_targets(data):
|
||||
with open(BEDTIME_TRACKING_FILE, "w") as f:
|
||||
json.dump(data, f)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to save bedtime tracking file: {e}")
|
||||
logger.error(f"Failed to save bedtime tracking file: {e}")
|
||||
|
||||
async def send_bedtime_reminder_for_server(guild_id: int, client=None):
|
||||
"""Send bedtime reminder for a specific server"""
|
||||
server_config = server_manager.get_server_config(guild_id)
|
||||
if not server_config:
|
||||
print(f"⚠️ No config found for server {guild_id}")
|
||||
logger.warning(f"No config found for server {guild_id}")
|
||||
return
|
||||
|
||||
# Use provided client or fall back to globals.client
|
||||
@@ -85,7 +88,7 @@ async def send_bedtime_reminder_for_server(guild_id: int, client=None):
|
||||
client = globals.client
|
||||
|
||||
if client is None:
|
||||
print(f"⚠️ No Discord client available for bedtime reminder in server {guild_id}")
|
||||
logger.error(f"No Discord client available for bedtime reminder in server {guild_id}")
|
||||
return
|
||||
|
||||
# No need to switch model - llama-swap handles this automatically
|
||||
@@ -94,7 +97,7 @@ async def send_bedtime_reminder_for_server(guild_id: int, client=None):
|
||||
for channel_id in server_config.bedtime_channel_ids:
|
||||
channel = client.get_channel(channel_id)
|
||||
if not channel:
|
||||
print(f"⚠️ Channel ID {channel_id} not found in server {server_config.guild_name}")
|
||||
logger.warning(f"Channel ID {channel_id} not found in server {server_config.guild_name}")
|
||||
continue
|
||||
|
||||
guild = channel.guild
|
||||
@@ -112,7 +115,8 @@ async def send_bedtime_reminder_for_server(guild_id: int, client=None):
|
||||
online_members.append(specific_user)
|
||||
|
||||
if not online_members:
|
||||
print(f"😴 No online members to ping in {guild.name}")
|
||||
# TODO: Handle this in a different way in the future
|
||||
logger.debug(f"No online members to ping in {guild.name}")
|
||||
continue
|
||||
|
||||
# Avoid repeating the same person unless they're the only one
|
||||
@@ -162,9 +166,9 @@ async def send_bedtime_reminder_for_server(guild_id: int, client=None):
|
||||
|
||||
try:
|
||||
await channel.send(f"{chosen_one.mention} {bedtime_message}")
|
||||
print(f"🌙 Sent bedtime reminder to {chosen_one.display_name} in server {server_config.guild_name}")
|
||||
logger.info(f"Sent bedtime reminder to {chosen_one.display_name} in server {server_config.guild_name}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to send bedtime reminder in server {server_config.guild_name}: {e}")
|
||||
logger.error(f"Failed to send bedtime reminder in server {server_config.guild_name}: {e}")
|
||||
|
||||
async def send_bedtime_reminder():
|
||||
"""Legacy function - now sends to all servers"""
|
||||
@@ -176,7 +180,7 @@ def schedule_random_bedtime():
|
||||
for guild_id in server_manager.servers:
|
||||
# Schedule bedtime for each server using the async function
|
||||
# This will be called from the server manager's event loop
|
||||
print(f"⏰ Scheduling bedtime for server {guild_id}")
|
||||
logger.info(f"Scheduling bedtime for server {guild_id}")
|
||||
# Note: This function is now called from the server manager's context
|
||||
# which properly handles the async operations
|
||||
|
||||
@@ -188,8 +192,8 @@ async def send_bedtime_now():
|
||||
async def run_daily_dm_analysis():
|
||||
"""Run daily DM interaction analysis - reports one user per day"""
|
||||
if dm_analyzer is None:
|
||||
print("⚠️ DM Analyzer not initialized, skipping daily analysis")
|
||||
logger.warning("DM Analyzer not initialized, skipping daily analysis")
|
||||
return
|
||||
|
||||
print("📊 Running daily DM interaction analysis...")
|
||||
logger.info("Running daily DM interaction analysis...")
|
||||
await dm_analyzer.run_daily_analysis()
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
from utils.llm import query_llama
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger('sentiment')
|
||||
|
||||
async def analyze_sentiment(messages: list) -> tuple[str, float]:
|
||||
"""
|
||||
@@ -40,5 +43,5 @@ Response:"""
|
||||
|
||||
return summary, score
|
||||
except Exception as e:
|
||||
print(f"Error in sentiment analysis: {e}")
|
||||
logger.error(f"Error in sentiment analysis: {e}")
|
||||
return "Error analyzing sentiment", 0.5
|
||||
@@ -11,11 +11,14 @@ apply_twscrape_fix()
|
||||
from twscrape import API, gather, Account
|
||||
from playwright.async_api import async_playwright
|
||||
from pathlib import Path
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger('media')
|
||||
|
||||
COOKIE_PATH = Path(__file__).parent / "x.com.cookies.json"
|
||||
|
||||
async def extract_media_urls(page, tweet_url):
|
||||
print(f"🔍 Visiting tweet page: {tweet_url}")
|
||||
logger.debug(f"Visiting tweet page: {tweet_url}")
|
||||
try:
|
||||
await page.goto(tweet_url, timeout=15000)
|
||||
await page.wait_for_timeout(1000)
|
||||
@@ -29,11 +32,11 @@ async def extract_media_urls(page, tweet_url):
|
||||
cleaned = src.split("&name=")[0] + "&name=large"
|
||||
urls.add(cleaned)
|
||||
|
||||
print(f"🖼️ Found {len(urls)} media URLs on tweet: {tweet_url}")
|
||||
logger.debug(f"Found {len(urls)} media URLs on tweet: {tweet_url}")
|
||||
return list(urls)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Playwright error on {tweet_url}: {e}")
|
||||
logger.error(f"Playwright error on {tweet_url}: {e}")
|
||||
return []
|
||||
|
||||
async def fetch_miku_tweets(limit=5):
|
||||
@@ -53,11 +56,11 @@ async def fetch_miku_tweets(limit=5):
|
||||
)
|
||||
await api.pool.login_all()
|
||||
|
||||
print(f"🔎 Searching for Miku tweets (limit={limit})...")
|
||||
logger.info(f"Searching for Miku tweets (limit={limit})...")
|
||||
query = 'Hatsune Miku OR 初音ミク has:images after:2025'
|
||||
tweets = await gather(api.search(query, limit=limit, kv={"product": "Top"}))
|
||||
|
||||
print(f"📄 Found {len(tweets)} tweets, launching browser...")
|
||||
logger.info(f"Found {len(tweets)} tweets, launching browser...")
|
||||
|
||||
async with async_playwright() as p:
|
||||
browser = await p.firefox.launch(headless=True)
|
||||
@@ -78,7 +81,7 @@ async def fetch_miku_tweets(limit=5):
|
||||
for i, tweet in enumerate(tweets, 1):
|
||||
username = tweet.user.username
|
||||
tweet_url = f"https://twitter.com/{username}/status/{tweet.id}"
|
||||
print(f"🧵 Processing tweet {i}/{len(tweets)} from @{username}")
|
||||
logger.debug(f"Processing tweet {i}/{len(tweets)} from @{username}")
|
||||
media_urls = await extract_media_urls(page, tweet_url)
|
||||
|
||||
if media_urls:
|
||||
@@ -90,7 +93,7 @@ async def fetch_miku_tweets(limit=5):
|
||||
})
|
||||
|
||||
await browser.close()
|
||||
print(f"✅ Finished! Returning {len(results)} tweet(s) with media.")
|
||||
logger.info(f"Finished! Returning {len(results)} tweet(s) with media.")
|
||||
return results
|
||||
|
||||
|
||||
@@ -99,7 +102,7 @@ async def _search_latest(api: API, query: str, limit: int) -> list:
|
||||
try:
|
||||
return await gather(api.search(query, limit=limit, kv={"product": "Latest"}))
|
||||
except Exception as e:
|
||||
print(f"⚠️ Latest search failed for '{query}': {e}")
|
||||
logger.error(f"Latest search failed for '{query}': {e}")
|
||||
return []
|
||||
|
||||
|
||||
@@ -131,13 +134,13 @@ async def fetch_figurine_tweets_latest(limit_per_source: int = 10) -> list:
|
||||
"miku from:OtakuOwletMerch",
|
||||
]
|
||||
|
||||
print("🔎 Searching figurine tweets by Latest across sources...")
|
||||
logger.info("Searching figurine tweets by Latest across sources...")
|
||||
all_tweets = []
|
||||
for q in queries:
|
||||
tweets = await _search_latest(api, q, limit_per_source)
|
||||
all_tweets.extend(tweets)
|
||||
|
||||
print(f"📄 Found {len(all_tweets)} candidate tweets, launching browser to extract media...")
|
||||
logger.info(f"Found {len(all_tweets)} candidate tweets, launching browser to extract media...")
|
||||
|
||||
async with async_playwright() as p:
|
||||
browser = await p.firefox.launch(headless=True)
|
||||
@@ -157,7 +160,7 @@ async def fetch_figurine_tweets_latest(limit_per_source: int = 10) -> list:
|
||||
try:
|
||||
username = tweet.user.username
|
||||
tweet_url = f"https://twitter.com/{username}/status/{tweet.id}"
|
||||
print(f"🧵 Processing tweet {i}/{len(all_tweets)} from @{username}")
|
||||
logger.debug(f"Processing tweet {i}/{len(all_tweets)} from @{username}")
|
||||
media_urls = await extract_media_urls(page, tweet_url)
|
||||
if media_urls:
|
||||
results.append({
|
||||
@@ -167,10 +170,10 @@ async def fetch_figurine_tweets_latest(limit_per_source: int = 10) -> list:
|
||||
"media": media_urls
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error processing tweet: {e}")
|
||||
logger.error(f"Error processing tweet: {e}")
|
||||
|
||||
await browser.close()
|
||||
print(f"✅ Figurine fetch finished. Returning {len(results)} tweet(s) with media.")
|
||||
logger.info(f"Figurine fetch finished. Returning {len(results)} tweet(s) with media.")
|
||||
return results
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@ See: https://github.com/vladkens/twscrape/issues/284
|
||||
|
||||
import json
|
||||
import re
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger('core')
|
||||
|
||||
|
||||
def script_url(k: str, v: str):
|
||||
@@ -36,6 +39,6 @@ def apply_twscrape_fix():
|
||||
try:
|
||||
from twscrape import xclid
|
||||
xclid.get_scripts_list = patched_get_scripts_list
|
||||
print("✅ Applied twscrape monkey patch for 'Failed to parse scripts' fix")
|
||||
logger.info("Applied twscrape monkey patch for 'Failed to parse scripts' fix")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to apply twscrape monkey patch: {e}")
|
||||
logger.error(f"Failed to apply twscrape monkey patch: {e}")
|
||||
|
||||
Reference in New Issue
Block a user