diff --git a/bot/api.py b/bot/api.py index f495987..d336ffb 100644 --- a/bot/api.py +++ b/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) diff --git a/bot/bot.py b/bot/bot.py index 5ded028..8d604ef 100644 --- a/bot/bot.py +++ b/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,10 +65,14 @@ 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 @@ -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) diff --git a/bot/commands/actions.py b/bot/commands/actions.py index be0e8ea..4c01b9d 100644 --- a/bot/commands/actions.py +++ b/bot/commands/actions.py @@ -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 diff --git a/bot/server_manager.py b/bot/server_manager.py index 3b94058..eb9c479 100644 --- a/bot/server_manager.py +++ b/bot/server_manager.py @@ -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() diff --git a/bot/static/index.html b/bot/static/index.html index 62d355d..b1d0bce 100644 --- a/bot/static/index.html +++ b/bot/static/index.html @@ -658,12 +658,13 @@
- - - - - -
+ + + + + + +
diff --git a/bot/static/system-logic.js b/bot/static/system-logic.js new file mode 100644 index 0000000..b3ed5a7 --- /dev/null +++ b/bot/static/system-logic.js @@ -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 ` +
+ + +
+ `; + }).join(''); + + const row = document.createElement('tr'); + row.innerHTML = ` + +
${name}
+
${description}
+ + + + + +
+ ${levelCheckboxes} +
+ + + + ${enabled ? 'Active' : 'Inactive'} + + `; + 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 = ` + + ${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 = '
Loading logs...
'; + + 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 = '
No logs yet for this component
'; + } else { + preview.innerHTML = data.lines.map(line => + `
${escapeHtml(line)}
` + ).join(''); + + preview.scrollTop = preview.scrollHeight; + } + } else { + preview.innerHTML = `
Error: ${data.error}
`; + } + } catch (error) { + preview.innerHTML = `
Error loading logs: ${error.message}
`; + } +} + +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); diff --git a/bot/static/system.html b/bot/static/system.html new file mode 100644 index 0000000..2256c2e --- /dev/null +++ b/bot/static/system.html @@ -0,0 +1,408 @@ + + + + + + 🎛️ System Settings - Logging Configuration + + + +
+
+

🎛️ System Settings - Logging Configuration

+
+ + + +
+
+
+
+

📊 Logging Components

+

+ Enable or disable specific log levels for each component. You can toggle any combination of levels. +

+ +
+

🌍 Global Level Controls

+

+ Quickly enable/disable a log level across all components +

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+

🕐 Timestamp Format

+

+ Control how timestamps appear in logs +

+
+ + +
+
+ + + + + + + + + + + + + +
ComponentEnabledLog LevelsStatus
Loading components...
+ +
+
+

📜 Live Log Preview

+
+
+ + +
+ +
+
+
Select a component to view logs...
+
+
+
+
+ + + \ No newline at end of file diff --git a/bot/utils/autonomous.py b/bot/utils/autonomous.py index 4cca2bf..ff7f403 100644 --- a/bot/utils/autonomous.py +++ b/bot/utils/autonomous.py @@ -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 ========== diff --git a/bot/utils/autonomous_engine.py b/bot/utils/autonomous_engine.py index d346d22..c4789ba 100644 --- a/bot/utils/autonomous_engine.py +++ b/bot/utils/autonomous_engine.py @@ -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 diff --git a/bot/utils/autonomous_persistence.py b/bot/utils/autonomous_persistence.py index eb35dc5..71ed736 100644 --- a/bot/utils/autonomous_persistence.py +++ b/bot/utils/autonomous_persistence.py @@ -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 {}, {} diff --git a/bot/utils/autonomous_v1_legacy.py b/bot/utils/autonomous_v1_legacy.py index d8f1ad7..1170964 100644 --- a/bot/utils/autonomous_v1_legacy.py +++ b/bot/utils/autonomous_v1_legacy.py @@ -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}") diff --git a/bot/utils/bipolar_mode.py b/bot/utils/bipolar_mode.py index 1b075eb..bc1108b 100644 --- a/bot/utils/bipolar_mode.py +++ b/bot/utils/bipolar_mode.py @@ -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)) diff --git a/bot/utils/context_manager.py b/bot/utils/context_manager.py index 9be044b..a461d0b 100644 --- a/bot/utils/context_manager.py +++ b/bot/utils/context_manager.py @@ -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]" diff --git a/bot/utils/core.py b/bot/utils/core.py index 5af8a5c..647b8f0 100644 --- a/bot/utils/core.py +++ b/bot/utils/core.py @@ -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() diff --git a/bot/utils/danbooru_client.py b/bot/utils/danbooru_client.py index 0c8b271..480ff37 100644 --- a/bot/utils/danbooru_client.py +++ b/bot/utils/danbooru_client.py @@ -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 diff --git a/bot/utils/dm_interaction_analyzer.py b/bot/utils/dm_interaction_analyzer.py index 15a4b88..5ee27ba 100644 --- a/bot/utils/dm_interaction_analyzer.py +++ b/bot/utils/dm_interaction_analyzer.py @@ -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) diff --git a/bot/utils/dm_logger.py b/bot/utils/dm_logger.py index b3d09ce..bfb5686 100644 --- a/bot/utils/dm_logger.py +++ b/bot/utils/dm_logger.py @@ -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 diff --git a/bot/utils/evil_mode.py b/bot/utils/evil_mode.py index 3cd9a34..8fd6518 100644 --- a/bot/utils/evil_mode.py +++ b/bot/utils/evil_mode.py @@ -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}") diff --git a/bot/utils/face_detector_manager.py b/bot/utils/face_detector_manager.py index 4fe3c0e..e16f2b9 100644 --- a/bot/utils/face_detector_manager.py +++ b/bot/utils/face_detector_manager.py @@ -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 diff --git a/bot/utils/figurine_notifier.py b/bot/utils/figurine_notifier.py index 68cba06..d1db068 100644 --- a/bot/utils/figurine_notifier.py +++ b/bot/utils/figurine_notifier.py @@ -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 diff --git a/bot/utils/image_generation.py b/bot/utils/image_generation.py index 27e5e43..2c68390 100644 --- a/bot/utils/image_generation.py +++ b/bot/utils/image_generation.py @@ -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: diff --git a/bot/utils/image_handling.py b/bot/utils/image_handling.py index eb982b6..040cf0f 100644 --- a/bot/utils/image_handling.py +++ b/bot/utils/image_handling.py @@ -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)}" diff --git a/bot/utils/kindness.py b/bot/utils/kindness.py index 731e946..612f2f8 100644 --- a/bot/utils/kindness.py +++ b/bot/utils/kindness.py @@ -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}") diff --git a/bot/utils/llm.py b/bot/utils/llm.py index 6bc9a85..cf193be 100644 --- a/bot/utils/llm.py +++ b/bot/utils/llm.py @@ -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 diff --git a/bot/utils/log_config.py b/bot/utils/log_config.py new file mode 100644 index 0000000..ee2189b --- /dev/null +++ b/bot/utils/log_config.py @@ -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 diff --git a/bot/utils/logger.py b/bot/utils/logger.py new file mode 100644 index 0000000..c56b3df --- /dev/null +++ b/bot/utils/logger.py @@ -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() diff --git a/bot/utils/media.py b/bot/utils/media.py index 35b23bd..af5d3c6 100644 --- a/bot/utils/media.py +++ b/bot/utils/media.py @@ -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}") diff --git a/bot/utils/moods.py b/bot/utils/moods.py index 16541e6..a28cc8d 100644 --- a/bot/utils/moods.py +++ b/bot/utils/moods.py @@ -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 diff --git a/bot/utils/persona_dialogue.py b/bot/utils/persona_dialogue.py index 23eff17..d802a88 100644 --- a/bot/utils/persona_dialogue.py +++ b/bot/utils/persona_dialogue.py @@ -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) diff --git a/bot/utils/profile_picture_manager.py b/bot/utils/profile_picture_manager.py index 891ad1a..01f32c7 100644 --- a/bot/utils/profile_picture_manager.py +++ b/bot/utils/profile_picture_manager.py @@ -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 diff --git a/bot/utils/scheduled.py b/bot/utils/scheduled.py index 30f9043..0f9525d 100644 --- a/bot/utils/scheduled.py +++ b/bot/utils/scheduled.py @@ -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() diff --git a/bot/utils/sentiment_analysis.py b/bot/utils/sentiment_analysis.py index b58e42e..dba3948 100644 --- a/bot/utils/sentiment_analysis.py +++ b/bot/utils/sentiment_analysis.py @@ -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 \ No newline at end of file diff --git a/bot/utils/twitter_fetcher.py b/bot/utils/twitter_fetcher.py index 79235b7..00c635e 100644 --- a/bot/utils/twitter_fetcher.py +++ b/bot/utils/twitter_fetcher.py @@ -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 diff --git a/bot/utils/twscrape_fix.py b/bot/utils/twscrape_fix.py index 5664c4e..53dd57a 100644 --- a/bot/utils/twscrape_fix.py +++ b/bot/utils/twscrape_fix.py @@ -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}")